sabato, novembre 04, 2006

Puntatori


#include <stdio.h>
#include <stdlib.h>

int main() {

/*
Una variabile corrisponde a una locazione di memoria capace di ospitare
valori corrispondenti al tipo di variabile dichiarata.
Una variabile intera, ad esempio, corrisponde ad una locazione di 4 byte
che vengono interpretati in base alla codifica dei numeri interi.
Oppure, una variabile double corrisponde ad una locazione di 8 byte
interpretati in base alla codifica dei numeri floating-point.

Un puntatore, da questo punto di vista, e' una variabile come le altre:
corrisponde ad una locazione di memoria capace di ospitare dei valori.
La differenza (essenziale) e' che il tipo di valori ospitati in una
variabile di tipo puntatore e' proprio l'INDIRIZZO DI MEMORIA (di
una qualche variabile).
Cosi' una variabile di tipo puntatore-a-interi, potra' ospitare (come
valore!) l'indirizzo di memoria corrispondente a una variabile intera.
Un puntatore-a-double ospitera' indirizzi di memoria corrispondenti a
variabili double.
E' importante sottolineare che i puntatori sono sempre
"puntatori-a-qualcosa": non esistono (in realta' esistono, ma hanno
una sintassi propria, esplicita...) puntatori a locazioni di memoria
"generici", senza specificazione del tipo di locazione di cui il
puntatore gestisce gli indirizzi.

*/

//dichiarazione di una variabile:
float variabile_float;
int variabile_intera;

//dichiarazione di un puntatore:
float *puntatore_a_float;
int *puntatore_a_int;
//antepongo un asterisco davanti al nome della variabile

//una variabile float puo' contenere un valore numerico in virgola mobile:
variabile_float=5.5;
//e similmente:
variabile_intera=10;

//un puntatore a float, puo' contenere un indirizzo di una variabile float:
puntatore_a_float = &variabile_float;
//l'operatore & davanti al nome di una variabile (qualsiasi) ne restituisce
//l'indirizzo di memoria

//Similmente:
puntatore_a_int = &amp;variabile_intera;

//mentre, ovviamente, sarebbe sbagliata un'assegnazione del tipo:
//puntatore_a_int = &variabile_float;
//perche', come si diceva, un puntatore e' sempre legato a un certo "tipo" di
//variabili cui puo' immagazzinare l'indirizzo

/*
Per avere un'idea piu' concreta, proviamo a stampare a video il contenuto
di alcune variabili normali e di variabili di tipo puntatore:
*/

printf("\n\n");
printf(
"variabile_intera contiene il valore: %d\n", variabile_intera);
printf(
"puntatore_a_int contiene il valore: %p\n", puntatore_a_int);
//il codice %p e' usato come segnaposto per i puntatori nel comando printf
//il formato di printf per un puntatore e' un numero in base 16 con
//un prefisso di due caratter: 0x (zero, ics)
printf("e in effetti l'indirizzo di variabile_intera e': %p\n", &variabile_intera);
printf(
"l'indirizzo di puntatore_a_int, invece, e': %p\n",&puntatore_a_int);
printf(
"che non ha nulla a che fare col suo contentuo...\n");

//Se volessimo "giocare" con l'indirizzo di memoria di puntatore_a_int,
//dovremmo usare una variabile capace di gestire questo tipo di indirizzi:
int **puntatore_a_puntatore_a_int;
//notare il doppio asterisco (puntatore-a-puntatore-a-...)
//(e ovviamente la generalizzazione con un numero fissato ma arbitrario di
//asterischi e' del tutto lecita in C++)

puntatore_a_puntatore_a_int = &puntatore_a_int;
printf(
"il contenuto di puntatore_a_puntatore_a_int, invece, e': %p\n",puntatore_a_puntatore_a_int);
printf(
"ovvero, proprio l'indirizzo di puntatore_a_int!!\n");

printf(
"\n\n");


/*
Oltre all'operatore &, ovvero l'operatore "indirizzo di", riferito
a una variabile, esiste l'operatore contrario: "contenuto di", riferito a un
indirizzo, ossia a un puntatore: datemi l'indirizzo di una variabile, e io vi
restituiro' il contenuto di quella variabile di cui mi avete dato l'indirizzo.
Il contetto e' molto semplice. La cosa molto confusionaria e' che questo
operatore e' realizzato in C++ con lo stesso simbolo che si usa per DICHIARARE
un variabile-puntatore: l'asterisco.
*/
printf("Il contenuto della variabile all'indirizzo %p, e' %d\n\n",
puntatore_a_int,
*puntatore_a_int);
/*
Chiariamo, dunque:

- in DICHIARAZIONE, l'asterisco anteposto a al nome di una variabile,
indica che la variabile che stiamo dichiarando e' un puntatore;

- in un'ESPRESSIONE, l'asterisco davanti al nome di una variabile-puntatore
indica che stiamo considerando il contenuto della locazione di memoria
all'indirizzo memorizzato dalla quella variabile-puntatore
(non e' possibile anteporre un asterisco ad una variabile comune)

*/

//Pertanto, espressioni come le seguenti sono del tutto legittime:

int i = 10; //dichiarazione (variabile normale)
int j = 20;
int *pi, *pj; //dichiarazione (asterisco: e' un puntatore!)

//espressione: l'asterisco rappresenta "il contenuto all'indirizzo:"
pi = &i; //uso di "indirizzo di"
j = *pi; //uso di "contenuto all'indirizzo in"
//e' come se avessimo scritto: j = i; dal momento che pi contiene proprio
//l'indirizzo di i, per l'assegnazione immediatamente precedente!

/*
Una volta chiarito il senso degli operatori unari, il loro "risultato" (ovvero
"l'indirizzo di questa variabile" o "il contenuto all'indirizzo di questo
puntatore", si comportano del tutto normalmente, e possono essere usati anche
in contesti complessi:
*/

int z;

z = *(&i) + *(&j);
//modo complicato per scrivere z = i + j;
z = (*pi + 5); //sommo 5 al contenuto (intero) all'indirizzo memorizzato in pi

//attenzione a questa:
*pi = 5;
/*
Assegno il valore 5 alla cella di memoria il cui indirizzo e' memorizzato in pi.
Se quell'indirizzo era l'indirizzo di una variabile gia' dichiarata (nel nostro
caso la variabile i, secondo l'assegnazione a riga 219), significa che stiamo
effettivamente modificando il contenuto di quella variabile: e' come aver
scritto i=5;
*/

float x=355.0/113.0;
float *px;
px = &x;
//[*]
printf("x vale: %g \n",x);
*px =
2 * x; //e' come aver scritto: x = 2*x; dal moemnto che [*]
printf("x ora vale: %g \n",x);

printf(
"\n\n");



return 0;
}


4 commenti:

Unknown ha detto...

Una precisazione.
Volevo sottilineare una differenza importante che ho incontrato nel passare dal linguaggio C al C++.
Con il C++, infatti, anche per allocare la memoria corrispondente ad un singolo puntatore è necessario utilizzare il comando new. Solo da quel momento in avanti si potrà utilizzare liberamente il puntatore.
La differenza consiste nel fatto che in linguaggio C per utilizzare il puntatore era sufficiente farne la dichiarazione (ad esempio):
int *p1
il comando allocava autamaticamente la memoria necessaria ad ospitare un puntatore ad interi e si poteva utilizzare subito il puntatore.
Nel C++ invece è necessario:
int *p2
p2 = new int
Da questo momento si può utilizzare il puntatore.
Il pericolo altrimenti è quello di tentare di utilizzare una zona della memoria non accessibile e incappare nell'errore:
Segmentation Fault
Da quanto mi è parso di capire con la sola dichiarazione nel C++ viene allocato il puntatore ma poi finchè non si utilizza new la memoria è inaccessibile all'utente.
Per questo motivo il puntatore non da problemi se facciamo:
int i;
int *p3;
p3= &i;
Da quel momento il puntatore accederà alla zona di memoria prima contrassegnata dal nome i che è disponibile all'utente.
Questo post è nato dopo che ho perso un'oretta davanti al Pc perchè non capivo da dove saltasse fuori il Segmentation Fault..
Per abitutidine avevo dichiarato due puntatori dimenticandomi poi di usare new.

hronir ha detto...

Non capisco bene il tuo commento: sia in C che in C++ l'istruzione:
int *p1;
alloca spazio in memoria per allocare un puntatore a interi, ma NON lo spazio per allocare un intero! Inoltre, con la dichiarazione, non si può fare alcuna assunzione sul contenuto della variabile p1, ovvero non si può sapere che indirizzo essa contenga (e quindi in generale se si prova ad accedere al contenuto puntato da p1 prima di inizializzarlo, si incapperà in un segmentation-faul...).
E questo, ripeto, vale sia in C che in C++ (che io sappia...).
Il puntatore, una volta dichiarato si può cominciare ad utilizzare subito, è il SUO CONTENUTO che va prima inizializzato (con l'indirizzo di nuova memoria, con new, o con l'indirizzo di una variabile già disponibile, con l'ampersand...).
E' chiaro il punto?

Unknown ha detto...

Innanzitutto grazie per la risposta.
Ok, provo a spiegarmi meglio.
L'anno scorso (con i programmi in C) di solito dichiaravo un puntatore
int *p
dopodichè lo utilizzavo subito, ad esempio:
*p = 5
oppure lo passavo come argomento nelle funzioni
Func(p,...)
e all'interno del corpo della funzione lo utilizzavo come meglio credevo.
Tutto questto senza mai scrivere una cosa del tipo
p = malloc(sizeof(int))
Infatti malloc lo utilizzavo solo nel caso di allocazione di vettori.
L'altro giorno mi sono comportato allo stesso modo con un programma C++ cioè ho fatto:
double *p1, *p2;
...
Func(p1, p2)
e nella funzione
*p1=...
*p2=...

però mi dava Segmentation Fault.
Allora (riguardando anche gli appunti su ll'allocazione dinamica della memoria)) mi sono accorto che invece in C++ bisogna fare anche per i singoli puntatori l'allocazione utilizzando il comando new.
Per spiegare la differenza posto il seguente programma

#include stdio.h (non metto le parentesi perchè mi fa problemi con i tag HTML)

int main()
{
double *y,z;

*y=15.423;
z=0;

printf("la variabile y = %10.5f\n",*y);
z= *y;
printf("la variabile z = %10.5f\n",z);
return(0);
}

(Era un programma in C scritto dal prof. l'anno scorso per spiegarci i puntatori).
Compilandolo con gcc ed eseguendolo non si ha nessun problema:
la variabile y = 15.42300
la variabile z = 15.42300
.
Compilando lo stesso programma con g++ si ha
Segmentation fault
il programma funziona aggiungendo la riga:
y=new double;
prima di
*y=15.423.
Ecco la differenza di cui parlavo! In C non ho avuto bisogno di allocare la memoria per un intero, in C++ ho dovuto utilizzare new.
In effetti, ad essere rigorosi, C++ è più coerente: un conto è allocare un puntatore ad interi, un altro è allocare un intero.
Il linguaggio C non segnalava alcun problema (nemmeno un warning).
Spero di essere stato più chiaro.

hronir ha detto...

Allora: in realtà eri stato chiaro, avevo capito di quale differente comportamento parlavi, ma il fatto è che, a parte l'uso di new/delete al posto di malloc/free, non c'è alcuna differenza, fra e C e C++, sull'uso dei puntatori.
Non sto mettendo in dubbio la casistica che hai riportato, ma il fatto che in un caso tu non abbia avuto problemi (e nell'altro invece sì) rientra nelle eventualità che possono effettivamente verificarsi, per come viene gestita la memoria.
Il mio precedente commento resta valido, ed è quindi da considerare sbagliato l'uso dell'indirizzo contenuto in un puntatore non ancora inizializzato. Ma non perchè sintatticamente scorretto, bensì perchè non ha un comportamento univocamente determinato! Il compilatore, infatti (sia di C che di C++) NON alloca altro che lo spazio per il puntatore (dove mettere un indirizzo). Non alloca quindi dell'ulteriore spazio di memoria, nè tantomeno può darne l'indirizzo (di questo spazio di memoria aggiuntiva dove mettere valori, non indirizzi) alla variabile puntatore appena creata.
Il fatto che in un particolare caso il programma non sia crashato è del tutto fortuito, e non è legato all'uso del compilatore C in vece di quello del C++. Il fatto è che, evidentemente, la nuova variabile puntatore appena creata era riempita con qualcosa che, interpretato come indirizzo di memoria, puntava da qualche parte di "innocuo" (ovvero dove il sistema operativo non aveva già messo il dito...). In questa circostanza, l'istruzione successiva si è limitata a riempire quella zona di memoria che tuttavia non è mai stata riservata dal programma, e a cui ha avuto accesso solo perchè un valore fortuito riempiva la zona di memoria (resa invece disponibile per) il puntatore. Da questo punto di vista, non c'è differenza fra "valori singoli" e "vettori": se sei abbastanza fortunato (o sfortunato, a seconda dei punti di vista), puoi anche riempire tutta una serie di celle di memoria successive ad una cella casualmente puntata da un puntatore non inizializzato, come se avessi a disposizione non solo quella cella di memoria, ma anche tutte le seguenti.
Il C è altrettanto "coerente" (come dici tu) del C++.

PS
Siccome uno può sempre sbagliare, prima di pubblicare effettivamente questo mio commento ho fatto copia & incolla del tuo programmino e ho provato a compilarlo sia col C che col C++, ed in entrambi i casi il programma è crashato. Il mio computer era acceso da svariate ore e fino a quel momento aveva caricato diversi e onerosi programmi, per cui la memoria doveva essere particolarmente frammentata e la probabilità che una qualche variabile puntasse a una zona "innocua" era evidentemnte bassa. Può darsi che se si fa girare il programma "impudente" su un computer appena acceso e su cui non sono state caricate molte applicazioni, riesca anche l'esperimento di "sporcare" senza errori diverse celle di memoria contigue a quella puntata da un puntatore non inizializzato... Ma siamo nel campo delle pure ipotesi perchè, ad esempio, la memoria a computer appena acceso potrebbe essere "tutta zeri", e allora un qualsiasi puntatore non inizializzato punterebbe certamente a zone di memoria "hot", allocate dai processi di booting del sistema operativo stesso!!!