Traccia della lezione Lez11: Lucido 2: In alcuni corsi di architetture si fa riferimento a macchine con celle più piccole (per esempio è spesso usata la così detta macchina MIXAL, nella quale 1 byte corrisponde a 6 bit anziché 8). Le operazioni possono essere compiute su più celle alla volta. Ad esempio, si possono sommare due numeri long con una sola istruzione, anche se i due long sono composti da 4 byte (su alcune macchine anche 8). In genere si definisce "parola di memoria" (word) l'unità massima sulla quale un processore può compiere operazioni. Perché i puntatori, pur essendo interi, non sono esplicitamente definiti int o long? - perché hanno un intervallo di definizione diverso da short, int e long, - perché le operazioni che si compiono sui puntatori sono diverse da quelle che si compiono sui numeri interi (non del tutto diverse, vedremo) - perché si vuole nascondere al programmatore la struttura interna della memoria. - per un altro motivo che vedremo nel lucido 4. Lucido 3: La sintassi dei tipi puntatore è composta da due parti come quella del tipo vettore: - il tipo dell'oggetto puntato (nei vettori, il tipo dell'elemento) - l'asterisco (nei vettori, le parentesi quadre col numero di elementi) Nelle dichiarazioni di più variabili nella stessa istruzione, la prima parte va al principio della riga, la seconda parte va con la variabile (l'asterisco va subito prima della variabile, mentre le parentesi quadre andavano subito dopo). Il codice POINTER.C sottolinea la differenza fra variabili di un tipo (char, int, double) e variabili di tipo puntatore a quel tipo (char *, int *, double *): le prime occupano rispettivamente 1, 4 e 8 byte, mentre le seconde occupano sempre 4 byte (sulla mia macchina a 32 bit). Tutti i tipi puntatore hanno lo stesso insieme di valori possibili, sono soggetti alle stesse operazioni e occupano la stessa quantità di memoria (la codifica interna è identica). Quindi in realtà potremmo definire un solo tipo puntatore. Se ne definiscono molti perché nel momento in cui si vuole risalire dall'indirizzo all'oggetto puntato bisogna sapere quante celle questo occupa. Questo è un altro buon motivo per cui non si usano direttamente i numeri interi come indirizzi. Lucido 4: Il disegno illustra il fatto che una variabile puntatore contiene un generico indirizzo di memoria, ma indica anche quante celle costituiscono l'oggetto puntato: - il puntatore pc contiene l'indirizzo dell'elemento V[1] e indica che l'oggetto puntato è un char, formato da una sola cella - il puntatore pi contiene l'indirizzo della prima cella della variabile i e indica che l'oggetto puntato è un int, formato da 4 celle (supponendo sizeof(int) = 4) - il puntatore pi2 contiene l'indirizzo dell'elemento V[1], che però è del tipo sbagliato (pi2 è int *, mentre V[1] è char). Quindi l'indicazione di dimensione è incoerente. D'altra parte, questo è lecito, perché il C non fa controlli di coerenza. Semplicemente, le quattro celle indicate vengono lette e il loro contenuto è interpretato come un numero intero. - il puntatore pc2 contiene l'indirizzo di una cella vuota, che viene interpretata come se contenesse un char (dato che pc2 e' di tipo char *), cioè il contenuto (qualunque esso sia) viene decodificato come un carattere. Tutti i puntatori occupano lo stesso numero di celle (4 in questo esempio). Si noti anche che pc e pi2 coincidono, ma pc può essere usato correttamente dato che il suo tipo è coerente con il contenuto effettivo della cella puntata, mentre pi2 no. Lucido 5: L'osservazione sulla priorità è importante. p1 = &V[3] significa che i tre operatori =, & e [] vanno considerati in questo ordine: - []: si accede all'elemento di indice 3 del vettore V - &: se ne preleva l'indirizzo - =: si assegna l'indirizzo alla variabile p1 L'intera espressione ha come valore l'indirizzo di V[3]. p2 = &A.x significa che i tre operatori =, & e . vanno considerati in questo ordine: - .: si accede al campo x della struct A - &: se ne preleva l'indirizzo - =: si assegna l'indirizzo alla variabile p2 L'intera espressione ha come valore l'indirizzo di A.x. Il codice ADDRESS.C gioca con indirizzi e dimensioni: dichiara tre vettori Vc, Vi e Vd (rispettivamente, di caratteri, interi e double), e per ognuno: - valuta la dimensione del vettore - valuta la dimensione del primo elemento (indice 0): (int) sizeof(Vc), (int) sizeof(Vi), (int) sizeof(Vd) - valuta l'indirizzo del vettore: &Vc, &Vi, &Vd e lo traduce in decimale - valuta l'indirizzo del primo elemento (indice 0): &Vc[0], &Vi[0], &Vd[0] - valuta l'indirizzo del secondo elemento (indice 1): &Vc[1], &Vi[1], &Vd[1] Risultati (sulla mia macchina): Dimensione di Vc = 10 Dimensione di Vc[0] = 1 Indirizzo di Vc = 0240FF50 che tradotto in decimale è 37814096 Indirizzo di Vc[0] = 0240FF50 Indirizzo di Vc[1] = 0240FF51 Dimensione di Vi = 40 Dimensione di Vi[0] = 4 Indirizzo di Vi = 0240FF20 che tradotto in decimale è 37814048 Indirizzo di Vi[0] = 0240FF20 Indirizzo di Vi[1] = 0240FF24 Dimensione di Vd = 40 Dimensione di Vd[0] = 4 Indirizzo di Vd = 0240FEF0 che tradotto in decimale è 37813968 Indirizzo di Vd[0] = 0240FEF0 Indirizzo di Vd[1] = 0240FEF4 Si noti che gli indirizzi dei vettori e dei loro primi elementi coincidono sempre. Si noti anche che l'indirizzo convertito in intero coincide con l'indirizzo, salvo che è scritto in forma decimale (37814096) anziché esadecimale (0240FF50). Infatti, 37814096 / 16 fa 2363381 con resto 0, che diviso 16 fa 147711 con resto 5, che diviso 16 fa 9231 con resto 15 = F, che diviso 16 fa 576 con resto 15 = F, che diviso 16 fa 36 con resto 0, che diviso 16 fa 2 con resto 4 Recuperando i resti in ordine inverso, si ottiene proprio 240FF50. Infine, si noti che le variabili, ovviamente, non si sovrappongono, dato che il compilatore distanzia ognuna dalla successiva almeno della sua dimensione. Lucido 6: La dereferenziazione di puntatori non inizializzati (e quindi contenenti valori imprecisati) è uno degli errori più tipici. Il compilatore di solito non la segnala come errore, ma come avvertimento. Accedere in lettura porta solo a ricavare un valore casuale, e quindi errato. Accedere in scrittura alla cella puntata da un puntatore non inizializzato può portare a scrivere su celle occupate da altri dati (quindi, valori errati), su celle occupate dal programma (con conseguente modifica del programma stesso e operazioni casuali o blocco del programma), o su celle occupate da altri programmi (con il blocco del programma). Per inciso, si noti che puntare a celle contenenti istruzioni del programma (e persino celle esterne al programma) è perfettamente lecito, se lo si fa correttamente, e viene usato correntemente (ad esempio si può puntare a funzioni, ma questo lo vedremo nell'ultima lezione del corso). Referenziazione e dereferenziazione sono ovviamente operazioni opposte e si elidono, ma bisogna fare attenzione all'ordine con cui si effettuano, per garantire che i passaggi intermedi siano sensati. int i; *&i ha senso ed è un intero di valore i &*i non ha senso, perché *i non ha senso &&i non ha senso perché &i non è una cella, ma un valore; però si può conservare &i in una variabile p (dichiarata come int *p) e determinare l'indirizzo &p, cioe' scrivere int *p = &i; e usare &p. Si consideri invece la scrittura: int *p = &i; *p = &i; La prima riga è corretta, mentre la seconda no, anche se paiono uguali. Il fatto è che nella prima riga * non è un operatore di dereferenziazione, ma fa parte della dichiarazione di tipo: int *p = &i; equivale a int *p; p = &i; cioè si sta assegnando &i a p, che è un puntatore. L'istruzione *p = &i, invece è errata perché *p è un valore, e non può essere un l-value, cioè l'indirizzo di una cella. Questo genere di confusioni si evitano facilmente: - separando dichiarazioni e istruzioni - assegnando nomi ai tipi puntatore con l'istruzione typedef typedef int* pint; pint p = &i; Lucido 7: Supponiamo che: - si dichiarino due variabili intere - si dichiarino due puntatori a interi p e q - si assegni a i il valore 2 e a j il valore 3 - si assegni a p l'indirizzo di i e a q l'indirizzo di j Si osservi ancora l'ambiguità dell'assegnamento iniziale di p e q: in realtà le istruzioni sono: int *p, *q; p = &i; q = &j; 1) si modifica il puntatore q in modo che punti i come p: ora *q vale 2 e j vale 3 si assegna il valore 1 all'oggetto puntato da p: ora i, *p e *q valgono 1 e j vale 3 2) si copia nell'oggetto puntato da q l'oggetto puntato da p: ora j e *q valgono 2 si assegna il valore 1 all'oggetto puntato da p: ora i e *p valgono 1, mentre j e *q valgono 2 Lucido 11: Ha senso confrontare puntatori solo se puntano elementi dello stesso vettore. In ogni altro caso, si otterrano risultati diversi su macchine diverse. Per lo stesso motivo ha senso sommare o sottrarre interi a un puntatore solo per muoversi lungo un vettore. Si noti che nell'aritmetica dei puntatori entra pesantemente il tipo dell'oggetto puntato: l'unità di misura infatti non è la singola cella, ma il blocco di celle che corrisponde a un elemento di tale tipo. Lucido 12: Il codice ARITHMETIC.C illustra l'aritmetica dei puntatori, lavorando su un vettore V di 10 double. Per chiarezza, introduciamo il tipo pdouble, definito come puntatore a double. Va sottolineato il fatto che gli indirizzi sono interi, ma non si comportano come interi, perché l'aritmetica dei puntatori ragiona a blocchi di dimensione determinata dal tipo del puntatore. In questo esempio (e sulla mia macchina) i blocchi sono di 8 byte perché lavoriamo con double. Risultati (sulla mia macchina): Indirizzo di V[0] = 0240FF10 Indirizzo di V[1] = 0240FF18 sizeof(V[0]) = 8 i = (int) &V[0] = 37814032 i = (int) (&V[0] + 1) = 37814040 i = (int) &V[0] + 1 = 37814033 &V[6] - &V[3] = 3 (int) &V[6] - (int) &V[3] = 24 1) L'elemento V[1] ha un indirizzo che sta 8 byte dopo l'indirizzo dell'elemento V[0], perché gli elementi del vettore sono consecutivi. 2) L'indirizzo di V[0] si può tradurre in numero intero. - se si somma 1 all'indirizzo di V[0], si ottiene l'indirizzo di V[1], che ritradotto in intero è 8 byte più alto dell'indirizzo di V[0]; - se invece si somma 1 all'indirizzo tradotto, si ottiene l'intero successivo. 3) Sottraendo due indirizzi, si ottiene il numero di elementi del vettore compresi fra loro. Se si sottraggono gli indirizzi tradotti in interi, si ottiene invece un numero 8 volte maggiore (ogni blocco usa 8 celle). Lucido 13: Si noti che il vettore viene CONVERTITO in puntatore nell'assegnamento e nel passaggio a funzioni, ma non è rigorosamente un puntatore. Infatti, non si può fare l'assegnamento contrario (puntatore a vettore) e alcune funzioni distinguono tra i due casi (ad esempio, sizeof dà risultati diversi se le si passa un vettore o un puntatore). L'equivalenza fra vettori e puntatori ha conseguenze bizzarre: int V[10], *p, i; p = V; /* che è come dire p = &V[0]; */ Ora si possono usare indifferentemente le quattro scritture: 1) p[i] 2) *(p+i) 3) *(i+p) (proprietà commutativa della somma) 4) i[p] Il compilatore le interpreta allo stesso modo. Il codice INC.C analizza più a fondo l'equivalenza fra vettori e puntatori e la relazione fra puntatori e operatori di incremento e decremento. Incrementi e decrementi hanno una priorità superiore alla dereferenziazione (asterisco). Questo rende difficile interpretare le espressioni che li combinano. Meglio evitare tali espressioni e scomporle in espressioni più semplici. Consideriamo alcune espressioni miste e applichiamole a un vettore V contenente i quattro valori [ 100 101 102 103 ] e a un puntatore p che inizialmente contenga l'indirizzo del primo elemento del vettore, cioè equivalga al vettore. *++p significa *(++p): incremento p e poi accedo all'oggetto puntato; quindi *(++V) sposta V e vale il vecchio V[1] (ora V[0]) ++*p significa ++(*p): accedo all'oggetto puntato da p e lo incremento; quindi ++*V incrementa V[0] e vale il nuovo V[0] (1 + vecchio V[0]) *p++ significa *(p++): incremento p, ma siccome il valore dell'espressione è p non incrementato, applico * a p e accedo all'oggetto puntato all'inizio; quindi *V++ vale il vecchio V[0], ma sposta V (quindi vale il nuovo V[-1]) (*p)++ significa: accedo all'oggetto puntato da p e lo incremento; quindi (*V)++ incrementa V[0] e vale il vecchio V[0]+1 (ora V[0]) Con la prima prova, sposta p e accede all'oggetto puntato, che e' V[1]: il "vettore" p ora parte da V[1]. Con la seconda prova, accede a V[0] e lo incrementa, senza spostare p. Con la terza prova, incrementa p, ma accede all'oggetto iniziale: il "vettore" p ora parte da V[1], ma il risultato è 100. Con la quarta prova, accede a V[0] e lo incrementa. Lucido 14: A che serve poter assegnare un vettore a un puntatore e poter usare un puntatore come nome per un vettore? 1) per lavorare su un sottovettore di un vettore come se fosse un vettore a sé (basta usare un puntatore alla posizione iniziale del sottovettore). Questo è utile nella scrittura di funzioni, in particolare ricorsive. Esempi semplici: - fare la somma degli elementi compresi fra s e d (per inciso, N/2+N/2 può non essere N, causa conversione con troncamento da reale a intero) - contare gli spazi bianchi in una sottostringa suffisso (da una certa posizione al terminatore) 2) per ottenere un vettore con estremi diversi da 0 e N-1. Se gli estremi sono S e D, e quindi gli elementi sono D-S+1, bisogna portare il puntatore al vettore in posizione -S, così che la vecchia posizione 0 diventa S. Ovviamente, continua a non esserci alcun controllo sullo sforamento degli indici. 3) per definire vettori la cui dimensione non è nota prima dell'esecuzione, ma dipende dai dati elaborati ogni volta (vettori dinamici) L'equivalenza fra vettori e puntatori spiega anche perché nelle funzioni si dichiarino spesso i dati di tipo vettore con l'asterisco anziché con le parentesi quadre: in questo modo la funzione può accedere a dati di entrambi i tipi. Il vantaggio è che non occorre specificare nel prototipo della funzione la grandezza statica del vettore su cui essa lavora, e quindi si possono scrivere funzioni che lavorano su vettori di qualsiasi grandezza. Tornando all'esempio INC.C, se non esistesse l'equivalenza fra vettori e puntatori la funzione StampaVettore avrebbe dovuto avere l'intestazione void StampaVettore (char *nome, int V[4], int s, int d) e avrebbe potuto operare solo su vettori di 4 elementi. In realtà, il compilatore C trasforma automaticamente i vettori in puntatori quando avviene il passaggio dei parametri: persino definendo il parametro "int V[4]", V all'interno della funzione è interpretato come puntatore.