C++ - puntatori, array e cast
In questa pagina 6
La memoria vista da vicino: indirizzi, puntatori, array in stile C. È il fondamento di ciò che segue (C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete → e le classi che gestiscono memoria). In C gli stessi concetti sono in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C → e Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →; qui si aggiungono i tipi dei puntatori, le reference e i cast C++. Il motivo per studiarli, in una frase di Stroustrup: chi non capisce come un programma si mappa sulla memoria e sulle operazioni della macchina fatica a capire strutture dati, algoritmi e sistemi operativi. Capire la memoria serve anche a "non credere nella magia" di vector e dei contenitori.
Puntatori e indirizzi
La memoria è indirizzata a byte: ogni byte ha un indirizzo numerico. Anche int var = 17; ha un indirizzo e occupa 4 byte.
Definizione. Un puntatore è una variabile che contiene un indirizzo di memoria, come un
intcontiene un intero. Ha un tipo:T*è un "puntatore aT", quindi sappiamo che tipo di dato si trova all'indirizzo che contiene.
double d; // variabile double
double* p; // variabile puntatore a doubleLa * può stare attaccata al tipo o al nome (double* p1;, double *p2;, double * p3; sono equivalenti). Per dichiarare più puntatori in una riga si ripete * per ciascuno, e la spaziatura non cambia la semantica anche se inganna:
| dichiarazione | significato |
|---|---|
double *p1, *p2; |
due puntatori a double |
double *p3, d1; |
un puntatore e un double |
double* p4, d2; |
un puntatore e un double (la * appartiene al nome) |
Indirizzo, dereference
Se var si trova all'indirizzo , int* ptr = &var; fa contenere a ptr: & è l'operatore indirizzo. L'operatore * (dereference) accede al dato puntato; posto a sinistra di un assegnamento fornisce un lvalue, cioè si può scrivere.
indirizzo: ... 212 ...
var (int): 17 <- ptr contiene 212
ptr (int*): 212#include <iostream>
using namespace std;
int main()
{
int var = 17;
int* ptr = &var; // & = "indirizzo di": ptr contiene l'indirizzo di var
cout << "var = " << var << ", *ptr = " << *ptr << '\n';
cout << "ptr == &var ? " << (ptr == &var) << '\n';
int x = 17;
int* pi = &x;
double e = 2.71828;
double* pd = &e;
cout << "*pi = " << *pi << ", *pd = " << *pd << '\n';
*pi = 27; // *pi e' un lvalue: scrive su x
*pd = 3.14159;
*pd = *pi; // copia il VALORE puntato: 27 -> double 27.0 in e
cout << "x = " << x << ", e = " << e << '\n';
// int i = pi; // errore: int* non e' int
// pi = 7; // errore: int non e' int*
// char* pc = pi; // errore: int* non e' char*
double* p0 = nullptr; // puntatore "a niente"
if (p0 == nullptr) cout << "p0 non punta a niente\n";
if (!p0) cout << "forma equivalente\n";
cout << "sizeof(int) = " << sizeof(int) << ", sizeof(int*) = " << sizeof(int*)
<< ", sizeof(double*) = " << sizeof(double*) << '\n';
double *a, *b; // DUE puntatori
double *c, d; // un puntatore (c) e un double (d)
double* g, h; // idem: la * si lega al nome, non al tipo
a = b = c = g = nullptr; d = h = 0;
cout << "a,b: " << sizeof(a) << ' ' << sizeof(b) << "; c,d: " << sizeof(c) << ' ' << sizeof(d)
<< "; g,h: " << sizeof(g) << ' ' << sizeof(h) << '\n';
return 0;
}var = 17, *ptr = 17
ptr == &var ? 1
*pi = 17, *pd = 2.71828
x = 27, e = 27
p0 non punta a niente
forma equivalente
sizeof(int) = 4, sizeof(int*) = 4, sizeof(double*) = 4
a,b: 4 4; c,d: 4 8; g,h: 4 8*pd = *pi;copia il valore puntato (27, convertito indouble), non l'indirizzo.int i = pi;epi = 7;non compilano:int*eintsono tipi diversi.- Un puntatore è una variabile con la sua dimensione (
sizeof(int*)= 4 qui, perché WebAssembly ha indirizzi a 32 bit; su un PC a 64 bit sono 8). La dimensione dipende dall'implementazione, ma non dal tipo puntato. - L'ultima riga mostra l'insidia delle dichiarazioni multiple:
double* g, h;crea un puntatore (g, 4 byte) e undouble(h, 8 byte).
Ogni tipo ha il suo puntatore
int*, double* e char* sono tipi diversi: ciò che si può fare con il dato puntato è diverso, e il compilatore lo tiene separato. char* pc = pi; non compila (e nemmeno pi = pc). Se invece si potesse, int* pi = &ch3; con quattro char consecutivi in memoria farebbe scrivere quattro byte con *pi = 12345;, sovrascrivendo ch3 e i vicini (anche pi stesso, se fosse lì accanto).
Il puntatore nullo
È comodo avere un valore non valido per segnalare un puntatore che non punta a niente: nullptr (C++11; prima NULL o 0). Il controllo si scrive if (p0 != nullptr) oppure if (p0). Dereferenziare nullptr è un errore.
Con i puntatori si è molto vicini all'hardware: il supporto è poco, ma il controllo dei tipi c'è. Non tutto il codice può essere ad alto livello (sistemi embedded), e questo rende std::vector molto più apprezzabile.
void* e cast
void*è un puntatore a memoria grezza (raw): rappresenta il concetto puro di "indirizzo" senza indicare come si usi quella memoria. Si può assegnare qualsiasi puntatore a unvoid*, ma non si può dereferenziare né usare con[]: non si sa che oggetto ci sia.
Attenzione a non confondere void (nessun valore di ritorno) e void*.
Per usare un void* serve una conversione esplicita: i cast. Il prezzo è la perdita del controllo dei tipi.
I cast servono soprattutto a interfacciarsi con hardware (Register* in = reinterpret_cast<Register*>(0xff);) o con altro codice non modificabile (una funzione che chiede Buffer* quando si ha const Buffer*).
#include <iostream>
using namespace std;
void f(void* pv)
{
void* pv2 = pv; // ok: void* si copia
(void)pv2;
// double* pd = pv; // errore: nessuna conversione implicita da void*
// *pv = 7; // errore: cosa dereferenzio? non si sa il tipo
// pv[2] = 7; // errore: stessa ragione
int* pi = static_cast<int*>(pv); // ok: IO dico che ci sta un int (e perdo il controllo dei tipi)
cout << "*pi = " << *pi << '\n';
}
void legge(const int* p)
{
int* scrivibile = const_cast<int*>(p); // toglie il const: pericoloso se l'oggetto e' davvero const
++*scrivibile;
}
int main()
{
int i = 41;
void* pv1 = &i; // qualsiasi puntatore si assegna a void*
f(pv1);
legge(&i); // i NON e' const: la modifica e' lecita
cout << "i = " << i << '\n';
// reinterpret_cast: guarda gli stessi byte con un altro tipo
unsigned int n = 0x01020304;
unsigned char* bytes = reinterpret_cast<unsigned char*>(&n);
cout << "primo byte = " << int(bytes[0]) << " (little endian)\n";
return 0;
}*pi = 41
i = 42
primo byte = 4 (little endian)const_cast è lecito solo se l'oggetto originale non è davvero const (qui i non lo è); se lo è, scrivere dà comportamento indefinito. L'ultima riga mostra che reinterpret_cast permette di guardare i byte di un unsigned int: il primo byte è 04, ordine little endian.
Puntatori e reference
Una reference è come un puntatore immutabile e dereferenziato automaticamente, oppure un nome alternativo per un oggetto (C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →). Le differenze:
| puntatore | reference | |
|---|---|---|
| assegnamento | cambia dove punta; per cambiare l'oggetto serve *p = 7 (p = 7 è pericoloso) |
cambia l'oggetto (r = 7) |
| origine | si ottiene con & |
dichiarata int& r = y; |
| spostamento | si può spostare (p1 = &x2) |
non si sposta dopo l'inizializzazione |
| accesso al dato | *p oppure [] |
nessun operatore: il nome basta |
| copia | assegnare due puntatori fa puntare allo stesso oggetto (shallow copy) | assegnare due reference copia l'oggetto (deep copy) |
| valore nullo | esiste nullptr |
non esiste una reference non valida |
#include <iostream>
using namespace std;
int main()
{
// puntatore: assegnare cambia DOVE punta
int x1 = 10, x2 = 20;
int* p1 = &x1;
p1 = &x2; // il puntatore si e' spostato su x2
*p1 = 7; // scrivo sull'oggetto puntato (x2)
cout << "x1 = " << x1 << ", x2 = " << x2 << '\n';
// reference: assegnare cambia l'OGGETTO, la reference non si sposta mai
int y1 = 10, y2 = 20;
int& r1 = y1;
r1 = y2; // y1 = y2: copia del valore, r1 e' ancora alias di y1
r1 = 7;
cout << "y1 = " << y1 << ", y2 = " << y2 << '\n';
// assegnare puntatori: due puntatori allo stesso oggetto (copia "shallow")
int i = 1, j = 2;
int *pi = &i, *pj = &j;
pj = pi; // ora pi e pj puntano entrambi a i
*pj = 99;
cout << "i = " << i << ", j = " << j << ", *pi = " << *pi << '\n';
// assegnare reference: copia del contenuto
int a = 1, b = 2;
int &ra = a, &rb = b;
rb = ra; // b = a
rb = 55; // modifica solo b
cout << "a = " << a << ", b = " << b << '\n';
return 0;
}x1 = 10, x2 = 7
y1 = 7, y2 = 20
i = 99, j = 2, *pi = 99
a = 1, b = 55La traccia: p1 = &x2; *p1 = 7; scrive su x2 e x1 resta 10. r1 = y2; invece copia 20 in y1 (la reference non si sposta), poi r1 = 7 scrive di nuovo y1. Dopo pj = pi; *pj = 99; sia i che *pi valgono 99 e j resta 2. rb = ra; copia il contenuto: a e b restano oggetti distinti, rb = 55 cambia solo b.
Quale scegliere per i parametri
Per cambiare una variabile ci sono tre modi (incr_v restituisce, incr_p prende int*, incr_r prende int&). Criteri:
- Restituire il valore è più chiaro e meno soggetto a errori; va bene per oggetti piccoli e, con il costruttore di spostamento, anche per grandi.
- Le reference sono più comode, ma la chiamata
incr_r(x)"sembra innocente"; conincr_p(&x)il&rende esplicito chexpuò cambiare. - Un puntatore può esprimere "input non valido" con
nullptr(e la funzione deve controllarlo); una reference no. Criterio: se no-object è un valore plausibile, puntatore; altrimenti reference o const reference.
Array
Gli std::vector sono vettori dinamici e di alto livello; gli array in stile C sono strutture più semplici e più antiche, necessarie in alcune circostanze (più avanti nel corso).
Definizione. Un array è una sequenza omogenea di oggetti allocati in locazioni di memoria contigue: stesso tipo, nessuno spazio vuoto fra gli elementi. È indicizzato con
[], con accesso casuale, e non c'è controllo sulla lettura e scrittura fuori dai limiti: gli errori sono facili e difficili da trovare.
- La dimensione deve essere una costante nota a tempo di compilazione (literal,
constoconstexpr). I VLA (variable length array) sono dello standard C99, non del C++, anche se GCC li accetta. - Gli array si possono avere come variabili globali, locali (attenzione alla memoria nello stack!), come argomenti di funzioni (trasformati in puntatori) e come membri di classe.
const int max = 100;
int gai[max]; // array globale, sempre disponibile
void f(int n)
{
char lac[20]; // array locale: vive fino all'uscita dallo scope
int lai[60];
double lad[n]; // errore: dimensione non costante (se n non è nota in compilazione)
}Puntatori agli elementi e aritmetica dei puntatori
Si può definire un puntatore a un elemento: double ad[10]; double* p = &ad[5];. Su p valgono * e []: *p = 7; scrive ad[5], p[2] = 6; scrive ad[7], p[-3] = 9; scrive ad[2] (indici negativi leciti se restano nell'array).
Aritmetica dei puntatori. I puntatori supportano la somma e la sottrazione di un intero (operatori
+,-,+=,-=) e la differenza fra due puntatori dello stesso tipo. Sommare a un puntatore lo sposta di slot (elementi) a destra, sottrarre lo sposta a sinistra. Lo slot dipende dal tipo puntato: in byte lo spostamento è .
Quindi se p è un double* all'indirizzo , p + 2 è all'indirizzo . La differenza q - p tra due puntatori allo stesso array è invece il numero di elementi compresi fra i due (non i byte). Un'ulteriore dimostrazione che ciò che fa un operatore dipende dai suoi operandi.
#include <iostream>
using namespace std;
int lunghezza(const char a[]) // "const char a[]" equivale a "const char* a": il vettore decade a puntatore
{
int count = 0;
while (a[count]) ++count; // finche' non trova lo '\0' finale
return count;
}
void dimensione_in_funzione(double a[])
{
cout << "dentro la funzione sizeof(a) = " << sizeof(a) << " (la dimensione di un puntatore)\n";
}
int main()
{
constexpr int n = 10;
double ad[n]; // array: 10 double contigui, la dimensione e' una costante
for (int i = 0; i < n; ++i) ad[i] = i * 1.5;
cout << "sizeof(ad) = " << sizeof(ad) << ", elementi = " << sizeof(ad) / sizeof(ad[0]) << '\n';
dimensione_in_funzione(ad);
double* p = &ad[5]; // puntatore a un elemento
*p = 7; // ad[5]
p[2] = 6; // ad[7]
p[-3] = 9; // ad[2]
cout << ad[5] << ' ' << ad[7] << ' ' << ad[2] << '\n';
double* q = p + 2; // aritmetica: avanza di 2 ELEMENTI (2*sizeof(double) byte)
p += 2; // adesso p e q puntano allo stesso elemento
cout << (p == q) << ' ' << (q - &ad[0]) << '\n'; // la differenza e' in elementi: 7
int v[4] = {0, 1, 2, 3};
int* pv = &v[2];
int* qv = pv + 2; // &v[4]: un elemento oltre la fine, si puo' formare ma non dereferenziare
cout << "qv - pv = " << qv - pv << " elementi = "
<< reinterpret_cast<char*>(qv) - reinterpret_cast<char*>(pv) << " byte\n";
// inizializzazione di array
int ai[] = {1, 2, 3, 4, 5, 6}; // dimensione dedotta: 6
int ai2[8] = {1, 2, 3}; // gli altri a 0
double zeri[4] = {}; // tutti a 0.0
char chars[] = {'a', 'b', 'c'}; // NESSUN terminatore
char ac[] = "Beorn"; // 6 char: 'B','e','o','r','n','\0'
cout << sizeof(ai) / sizeof(int) << ' ' << ai2[5] << ' ' << zeri[3] << ' '
<< sizeof(chars) << ' ' << sizeof(ac) << ' ' << lunghezza(ac) << '\n';
return 0;
}sizeof(ad) = 80, elementi = 10
dentro la funzione sizeof(a) = 4 (la dimensione di un puntatore)
7 6 9
1 7
qv - pv = 2 elementi = 8 byte
6 0 0 3 6 5Si noti, riga per riga:
sizeof(ad)= 80 (10doubleda 8 byte); dentro la funzione,sizeof(a)= 4: il compilatore avverte (sizeof on array function parameter will return size of 'double *'). L'informazione sulla dimensione è andata perduta.p = &ad[5],p[2],p[-3]: scrivonoad[7],ad[2];q = p + 2ep += 2finiscono nello stesso punto;q - &ad[0]vale 7 (elementi).- Con
int v[4],pv = &v[2],qv = pv + 2punta a&v[4], un elemento oltre la fine: è lecito formare quel puntatore (si usa come "fine" nei cicli) ma non dereferenziarlo.qv - pvvale 2 elementi, cioè 8 byte: l'indirizzo inqvèpv + 2*sizeof(int), mentre la differenza in C++ è 2, non2*sizeof(int). - Inizializzazione:
int ai[] = {1,2,...}deduce la dimensione (6);int ai2[8] = {1,2,3}mette 0 negli altri;double zeri[4] = {}tutti a 0.0;char chars[] = {'a','b','c'}ha 3 byte, nessun terminatore;char ac[] = "Beorn"ne ha 6, con lo0finale ('\0', non il carattere'0').
Decadimento di un array
Il nome di un array è un puntatore costante al suo primo elemento, un rvalue (un valore, non una variabile). Quindi non si può usare per la copia: x = y; con due array è un errore, e neanche int z[100] = y;. Per copiare serve un ciclo, std::copy o std::vector.
Passare un array a una funzione significa passare un puntatore: decadimento (array decay), con perdita dell'informazione sulla dimensione. È utile (evita di copiare 100 000 char in strlen(lots), che equivale a strlen(&lots[0])) ma un po' antiquato: deriva dal C e C++ lo mantiene per retrocompatibilità.
Stringhe in stile C
Le stringhe C sono array di char terminati da '\0', manipolabili con funzioni dedicate che assumono la presenza del terminatore: hanno le stesse limitazioni degli array. Sono un retaggio del C; il C++ offre il tipo std::string, di alto livello. La funzione lunghezza del programma conta i caratteri fino allo 0.
Problemi classici di puntatori e array
| errore | codice | cosa succede |
|---|---|---|
accesso con nullptr |
int* p = nullptr; *p = 7; |
errore a run-time |
| puntatore non inizializzato | int* p; *p = 9; |
scrive in un posto casuale; tipico nei puntatori membro dimenticati |
| fuori dai limiti | int a[10]; a[10] = 11; int* p = &a[10]; *p = 11; |
primo e ultimo elemento sono i punti critici; -Warray-bounds lo segnala |
| oggetto uscito dallo scope | int* f() { int x = 7; return &x; } ... *f() = 15; |
x non esiste più (address of stack memory associated with local variable 'x' returned) |
| stesso, con una reference | vector& ff() { vector x(7); return x; } |
x distrutto all'uscita: la reference punta a un oggetto morto |
L'ultimo caso è logicamente analogo al precedente: ritornare una reference o un puntatore a una variabile locale è sempre un errore, perché il record di attivazione sparisce (C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →).
Domande d'esame
1. Definire l'aritmetica dei puntatori: concetto, operatori e operandi, due esempi.
Traccia. Concetto: un puntatore tiene un indirizzo e il tipo dell'oggetto puntato, quindi la somma di un intero sposta di slot, cioè di byte. Operatori: + e - fra puntatore e intero, +=/-=, ++/--, - fra due puntatori (risultato in elementi), confronti; non si sommano due puntatori. Esempi: double* q = p + 2; e int* qv = pv + 2 con v[4] (un oltre la fine); p[2] equivale a *(p+2). Soluzioni complete: Esercizio - Aritmetica dei puntatori (primo appello 2021-22, teoria).
2. int v[4] = {0,1,2,3}; int *p = &v[2]; int *q = p + 2; — errore a run-time? a compile-time? Quanto vale q - p?
Traccia. Nessun errore: q punta a v[4], un elemento oltre la fine, lecito finché non lo si dereferenzia. L'indirizzo in q è p + 2*sizeof(int); q - p vale 2 (elementi), non 2*sizeof(int).
3. Differenze fra puntatore e reference. Traccia. Tabella sopra: spostabile, nullo, dereference esplicito, shallow contro deep copy.
4. Cosa succede passando un array a una funzione? Cosa stampa sizeof dentro la funzione?
Traccia. Decadimento a puntatore: la funzione riceve l'indirizzo del primo elemento e non conosce la dimensione; sizeof restituisce la dimensione del puntatore, non dell'array. L'accesso ai dati è lecito.
Versione ripasso
- Puntatore = variabile con un indirizzo;
T*= puntatore aT(tipo noto).&x= indirizzo dix;*p= dato puntato (lvalue).double *p3, d1;= un puntatore e undouble;double* p4, d2;idem (la*è del nome).nullptr= nessun oggetto;if (p)controlla.sizeofdi un puntatore dipende dall'implementazione. Rimandi: Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →, Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →.
int var = 17; int* ptr = &var; // *ptr == 17
*pd = *pi; // copia il VALORE puntato
int i = pi; pi = 7; char* pc = pi; // ERRORI: tipi diversi- Tipi diversi:
int*,double*,char*non si convertono: seint* pi = &ch3;valesse,*pi = 12345scriverebbe 4 byte sovrascrivendo i vicini. void*= memoria grezza: accetta ogni puntatore, ma non si dereferenzia né si indicizza; si recupera il tipo constatic_cast<int*>(pv). Cast:static_cast(conversione esplicita),reinterpret_cast(tipi indipendenti: hardware, byte),const_cast(toglieconst, lecito solo se l'oggetto non è davveroconst),dynamic_cast(C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →). Servono per hardware e codice non modificabile.- Puntatore o reference: puntatore = si sposta, ha
nullptr,*p/[], assegnare copia l'indirizzo (shallow); reference = non si sposta, sempre valida, nome e basta, assegnare copia l'oggetto (deep). Parametri: restituire il valore;incr_p(&x)è esplicito; se no-object è plausibile puntatore, altrimenti reference. - Array C = sequenza omogenea contigua, dimensione costante (no VLA in C++),
[]a accesso casuale, nessun controllo dei limiti; globali, locali, argomenti (diventano puntatori), membri di classe. Inizializzazione:int ai[] = {1,2,3}(dimensione dedotta),int a[8] = {1,2,3}(resto 0),double z[4] = {},char c[] = {'a','b','c'}(niente terminatore),char s[] = "Beorn"(6 byte con'\0'). - Aritmetica dei puntatori:
p + nep - nspostano di elementi ( byte);p += n;q - p= elementi fra i due;p[i]equivale a*(p+i), anchep[-3]. Esempio:double* p = &ad[5]; p[2] = 6;scrivead[7];q = p + 2eq - &ad[0]= 7. Conint v[4],p = &v[2],q = p + 2:qè&v[4](una oltre la fine, lecito formarlo, non dereferenziarlo);q - p= 2, indirizzo diq=p + 2*sizeof(int). - Decadimento: il nome di un array è un puntatore costante (rvalue) al primo elemento;
x = yfra array è errore; passato a una funzione perde la dimensione (sizeof(a)= dimensione del puntatore, warning del compilatore); evita la copia (strlen(lots)=strlen(&lots[0])). - Stringhe C = array di
charterminati da'\0'(non'0'); funzioni che assumono il terminatore; in C++ megliostd::string. - Problemi classici:
*nullptr; puntatore non inizializzato (anche membro);a[10]suint a[10]; puntatore/reference a variabile locale uscita dallo scope (return &x;,vector& ff());deletedue volte (C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete →). - Programma di prova dei puntatori (uscita):
var = 17, *ptr = 17;*pi = 17, *pd = 2.71828; dopo*pi = 27; *pd = *pi;x = 27, e = 27;p0 == nullptrstampa "p0 non punta a niente";sizeof(int*) = 4(WebAssembly, 8 su 64 bit);double *c, d;4 e 8 byte. - Puntatore o reference, prova:
p1 = &x2; *p1 = 7;x1 = 10, x2 = 7;r1 = y2; r1 = 7;y1 = 7, y2 = 20(la reference resta suy1);pj = pi; *pj = 99;i = 99, j = 2;rb = ra; rb = 55;a = 1, b = 55. - Array, prova:
double ad[10]sizeof(ad) = 80, 10 elementi; in una funzionesizeof(a) = 4;p = &ad[5]; *p = 7; p[2] = 6; p[-3] = 9;ad[5] = 7, ad[7] = 6, ad[2] = 9;q = p + 2; p += 2;p == q,q - &ad[0] = 7;char ac[] = "Beorn"sizeof = 6,lunghezza(ac) = 5;char chars[] = {'a','b','c'}3 byte, nessun terminatore. - Cast, prova:
void* pv = &i; static_cast<int*>(pv)*pi = 41;const_cast<int*>(p)per incrementare (lecito:inon èconst)i = 42;reinterpret_cast<unsigned char*>(&n)conn = 0x01020304primo byte 4 (little endian). - Domande tipiche: definire l'aritmetica (concetto, operatori, 2 esempi); differenze puntatore/reference; che cosa vede una funzione che riceve un array; perché
void*perde il controllo dei tipi. - Tabella di sintesi dei cast:
| cast | quando | rischio |
|---|---|---|
static_cast<T>(x) |
conversioni "ragionevoli": double int, void* int* |
si perde il controllo del compilatore |
reinterpret_cast<T>(x) |
indirizzi fissi dell'hardware (0xff), guardare i byte |
totalmente non portabile |
const_cast<T>(x) |
codice di terzi che chiede un puntatore non const |
indefinito se l'oggetto è davvero const |
dynamic_cast<T>(x) |
downcasting nelle gerarchie con funzioni virtuali | solo con tipi polimorfici; nullptr o bad_cast |
- Parametri: tre modi di cambiare una variabile:
x = incr_v(x)(restituisce),incr_p(&x)(puntatore),incr_r(x)(reference).incr_pdeve gestirenullptr. - Funzione sugli array, codice:
int lunghezza(const char a[]) { int count = 0; while (a[count]) ++count; return count; }(stringa C: si ferma al'\0');strlen(lots)conchar lots[100000]non copia 100 000charperché il nome decade a&lots[0]. - Errori tipici: credere che
p + 1sposti di 1 byte; dire cheq - pvale2*sizeof(int); dereferenziare&a[10]; confonderevoidevoid*; sommare due puntatori; dire chedouble* a, b;dichiara due puntatori; usarereinterpret_castdove bastastatic_cast.
Esercizi su questo argomento
- Esercizio - Allocazione dinamica e layout della memoria (primo appello 2022-23, teoria)
- Esercizio - Aritmetica dei puntatori (primo appello 2021-22, teoria)
- Esercizio - Aritmetica dei puntatori (quarto appello 2022-23, teoria)
- Esercizio - Array stile C, vector e la classe MyArray (terzo appello 2022-23, teoria)
- Esercizio - Passaggio per copia e per riferimento con schema dello stack (secondo appello 2022-23, teoria)
- Esercizio - Puntatori, array e passaggio dei parametri (laboratorio 3)
- Esercizio - Quiz e domande su smart pointer, function object e aritmetica dei puntatori (terzo appello 2023-24, teoria)
- Esercizio - Quiz su contenitori STL, override, sizeof, void e virtual table (secondo appello 2021-22, quiz)
- Esercizio - Quiz su operatore =, dangling pointer, enumerazioni e at() (quarto appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)
- Esercizio - Quiz su scope, riferimenti, conversioni e puntatori (secondo appello 2022-23, quiz)
- Esercizio - Shallow copy e deep copy (quarto appello 2021-22, teoria)