______________
< void ****Mou >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Anche se array e puntatori possono condividono alcune caratteristiche sono essenzialmente due oggetti completamente diversi. Consideriamo il seguente codice e analizziamolo riga per riga.
#include <stdio.h>
void main()
{
int A[10];
int *P;
printf("sizeof A = %ld\n", sizeof(A));
A[0] = 0;
printf("sizeof P = %ld\n", sizeof(P));
P[0] = 0;
return;
}
Con questa linea di codice è stata definita un’area di memoria sufficiente per contenere 10 elementi di tipo intero.
int A[10];
printf("sizeof A = %ld\n", sizeof(A)); // sizeof A = 40
A questi elementi si può accedere direttamente in scrittura e in lettura, per esempio il primo elemento dell’array si raggiunge con una di queste due sintassi equivalenti: A[0] o *A.
A[0] = 0;
La seguente scrittura definisce invece un puntatore ad interi:
int *P;
Come è già stato fatto notare A non è altro che l’indirizzo del primo dei dieci elementi dell’array (e non una variabile) mentre P è a tutti gli effetti una variabile che può contenere un indirizzo di memoria (che punta ad un intero).
Come per A nel momento della definizione viene allocata memoria anche per P ma solo per un numero di byte sufficienti a contenere un indirizzo della memoria:
printf("sizeof P = %ld\n", sizeof(P)); // sizeof P = 8
Quindi P contiene un indirizzo di memoria; ma anche A è un indirizzo di memoria, quindi la sintassi per accedervi è la stessa ovvero P[0] o *P.
P[0] = 0;
La compilazione viene eseguita senza alcun problema, tutto sembra corretto fino al momento dell’esecuzione!
Errore di segmentazione (core dump creato)
Il motivo è semplice: P contiene sì un indirizzo di memoria, ma a quale memoria dal momento che non è mai stato inizializzato? Conterrà quindi il valore NULL (o un valore assolutamente casuale, a seconda del compilatore, poiché lo standard non prevede nessun tipo di inizializzazione di default per variabili locali). La valorizzazione dell’area puntata da P cerca quindi di scrivere in un’area di memoria che nulla ha a che fare con il nostro programma e provoca risultati indesiderati; nel caso migliore l’esecuzione termina con un core dump. Sottolineiamo che questo è il caso migliore, perché mette immediatamente in evidenza la presenza di un problema che va risolto. In altri casi che analizzeremo avanti il comportamento potrebbe comportare dei malfunzionamenti senza necessariamente alcun tipo di segnalazione!
Analizziamo il seguente codice molto simile al precedente ma essenzialmente differente.
#include <stdio.h>
void main()
{
char s[] = "Hello, world!";
char *p = "Hello to you too!";
printf("sizeof s = %ld\n", sizeof(s)); // sizeof s = 14
printf("s=%s\n", s); // s=Hello, world!
printf("sizeof p = %ld\n", sizeof(p)); // sizeof p = 8
printf("p=%s\n", p); // p=Hello to you too!
// s = p;
p = s;
printf("p=%s\n", p);
p = s + 7;
printf("p=%s\n", p);
printf("%c%c%c%c%c%c\n",p[0],p[1],p[2],p[3],p[4],p[5]);
printf("%c\n", p[-7]);
return;
}
L’array s occupa 14 byte, infatti la stringa “Hello, world!” è composta da 13 caratteri più il carattere terminatore mentre il puntatore p occupa solo 8 byte. Un’ulteriore differenza si evidenzia provando a decommentare questa riga di codice, che a runtime provoca un errore.
s = p; // error: assignment to expression with array type
Mentre il contrario è un’operazione lecita e perfettamente corretta.
p = s;
// p point to 'H'
// |H|e|l|l|o|,| |w|o|r|l|d|!|
printf("p=%s\n", p); // p=Hello, world!
A causa di questa riassegnazione p punterà alla stessa stringa indirizzata da s. Inoltre il puntatore può essere riassegnato in questo modo, facciamo puntare p all’ottavo elemento della stringa.
p = s + 7;
// now p point to 'w'
// |H|e|l|l|o|,| |w|o|r|l|d|!|
printf("p=%s\n", p); // p=world!
printf("%c%c%c%c%c%c\n",p[0],p[1],p[2],p[3],p[4],p[5]);
// world!
La cosa che potrebbe sorprendere è che in questo cosa è possibile usare degli indici negativi: non solo questo è sintatticamente corretto ma anche assolutamente lecito e formalmente valido. Il risultato è la stampa del primo carattere della stringa “Hello, world!”.
printf("%c\n", p[-7]); // p point to 'w', p-7 point to 'H'
L’indice può essere qualunque purché porti ad un area di memoria lecita, non sarebbe corretto ad esempio usare l’indice -10 o 12 porterebbero ad aree di memoria esterne alla stringa.
printf("%c\n", p[-10]);
printf("%c\n", p[12]);
Non comparirà nessun messaggio di errore durante la compilazione né durante l’esecuzione ma stamperà dei valori imprevedibili. L’assegnazione in questa locazione di memoria invece potrebbe creare problemi a runtime, ma quest’errore è potrebbe passare inosservato fino al crash casuale del programma in esecuzione.
p[-10]=65;
Per questo motivo è importante comprendere come utilizzare questo potente ma delicato strumento. Analizziamo quindi questo codice al fine di mettere in evidenza un’ulteriore criticità.
#include <stdio.h>
void main()
{
int i;
char a[] = "Hello, world!";
char b[] = "Hello to you too!";
printf("%s\n%s\n", a, b);
for (i = 7; i < 19; i++) {
a[i] = '.';
}
printf("%s\n%s\n", a, b);
return;
}
Come si può notare si cicla sull’array a con un indice che va da 7 a 19, ma gli elementi dell’array sono solo 14! L’effetto ottenuto è lo “sforamento” nelle aree di memoria adiacenti usate da altre variabili, il tutto senza alcun messaggio di errore o di warning durante la compilazione o l’esecuzione. Gli array occupano aree adiacenti di memoria così come nello schema seguente:
a b
H e l l o , w o r l d !\0 H e l l o t o y o u t o o !\0
Il ciclo inizia a sovrascrivere il carattere '.' a partire dall’ottavo carattere, ovvero dalla lettera 'w' per 12 caratteri consecutivi, il risultato all’interno della memoria è questo:
a b
H e l l o , . . . . . . . . . . . . t o y o u t o o !\0
Quindi viene anche sovrascritto il carattere di terminazione della stringa a nonché altri caratteri di b, per cui quando la funzione printf stampa tutti i caratteri in a ma non trovando il cosiddetto “tappo” della stringa, stampa anche b. In questo caso non si evidenzia nessun tipo di errore, sintatticamente non si è contravvenuto nessuna regola ma questo codice è da ritenersi formalmente errato e il completamento di un errore del genere comporta dei risultati assolutamente imprevedibili perché potrebbe comportare la sovrascrittura di una qualsiasi altra variabile.