Un comando di assegnamento può modificare il contenuto della locazione associata alla variabile.
Se la variabile viene usata dentro un'espressione (ad esempio nella parte destra di un comando di assegnamento), allora il valore memorizzato dentro la locazione associata alla variabile viene impiegato nel calcolo del valore dell'espressione.
I puntatori permettono ad un programma di accedere alla memoria, manipolare indirizzi, allocare byte dinamicamente, liberare celle di memoria inutilizzate. Uno dei punti di forza del C è proprio l'enorme predisposizione alla manipolazione di puntatori, fondamentale ad esempio nella programmazione di sistema. Grazie a questa sua caratteristica, il C (o il C++) e' il linguaggio di programmazione in cui sono scritti quasi tutti i S.O. moderni.
In realtà abbiamo già usato i puntatori a partire dalla terza lezione, dove è stata introdotta la funzione di input scanf.
...
scanf("%d",&valore); // leggiamo un valore
...
Infatti l'operatore &, detto operatore d'indirizzo si applica ad una variabile per ottenere l'indirizzo della cella di memoria dove è memorizzato il valore corrispondente.
Gli indirizzi formano un insieme di valori
manipolabili mediante
variabili di tipo puntatore.
Attenzione però: non esiste un unico tipo puntatore, ma tanti tipi diversi a seconda del tipo del valore memorizzato nella locazione (in particolare è essenziale distinguere la dimensione in byte del dato puntato).
Ad esempio, la dichiarazioneint *p;
dichiara una variabile p di tipo puntatore a int, mentre la dichiarazione
char *q;
dichiara una variabile q di tipo puntatore a char.
L'intervallo dei
valori ammissibili per un puntatore
comprenderà sempre il valore speciale NULL
(una
costante che vale 0 sulla
maggior parte delle
macchine) ed un certo insieme di interi positivi interpretati come
indirizzi di memoria della macchina.
(In generale non ci interesserà osservare il valore di una locazione, ma semplicemente possederlo).
Graficamente, i valori di tipo puntatore vengono rappresentati come frecce che partono dalla cella associata alla variabile puntatore e finiscono sulla locazione puntata.
Nell'esempio vediamo che i puntatori vengono allocati in memoria come ordinarie variabili.
Il puntatore p, non essendo stato inizializzato, conterrà l'indirizzo di una cella di memoria ignota (notare la freccia pendente).
Il puntatore q conterrà l'indirizzo speciale NULL (nessuna cella puntata).
Il caso più interessante è quello del puntatore r che conterrà l'indirizzo della cella associata alla varibile intera n. Infatti l'assegnamento
r=&n;associa a r l'indirizzo della variabile n.
Cosa possiamo fare una volta che conosciamo l'indirizzo di una cella? Possiamo ovviamente osservarne e modificarne il valore. A questo scopo serve l'operatore unario di indirezione *, che applicato ad un puntatore riferisce l'oggetto puntato. Ad esempio, dopo l'assegnamento r=&n; possiamo usare indifferentemente le notazioni n e *r.
Ad esempio, provate
ad indovinare cosa stampa il programma:
int main () {
int n;
int *r;
n=0;
r=&n;
printf("%d\n",*r);
*r=1;
printf("%d\n",n);
}
Puntatori e Array
I puntatori sono strettamente correlati agli array, in quanto il nome di un array è essenzialmente il puntatore al primo elemento (quello di indice 0). Ad esempio, in
int a[50];
int *p;
p = &a[0];
p = a;
i due assegnamenti sono del tutto equivalenti.
Infatti, nella lettura di stringhe con scanf passiamo direttamente il nome dell'array, senza usare l'operatore di indirizzo;
Sussiste però una differenza fondamentale: mentre il valore di un puntatore può essere modificato (il puntatore potrà puntare celle diverse nel corso del programma), l'indirizzo base di un array e quindi il valore di a visto come puntatore è costante per tutto il programma (possiamo solo cambiare il contenuto delle celle dell'array, non l'indirizzo base associato ad a).
A livello di tipo di dato, un parametro di tipo array di interi di lunghezza non specificata (ad esempio int a[]) può essere dichiarato in maniera del tutto equivalente come avente tipo puntatore a interi (int *a).
Passaggio
di parametri per indirizzo
int n,m,aux;
...
aux = n;
n = m; // senza aux avrei perso il valore originale di n
m = aux;
...
Supponiamo di voler definire una funzione per scambiare i valori di due variabili intere e di realizzarla nel seguente modo (sbagliato!):
void scambia(int n, int m) {
int aux;
aux = n;
n = m;
m = aux;
}
Cosa succede se proviamo ad usarla nel programma sotto?
int main() {
int n=10;
int m=5;
printf("n == %d, m == %d", n, m);
scambia(n,m);
printf("n == %d, m == %d", n, m);
return EXIT_SUCCESS;
}
I valori non vengono scambiati!
Questo perché i parametri sono passati per valore, ovvero la procedura scambia riceve solo una copia dei valori e non ha modo di modificare i parametri originali (solo la loro copia locale).
Siccome vogliamo modificare il valore
dei parametri avremmo invece
dovuto passare a scambia
l'indirizzo delle celle di
memoria associate alle variabili. Dobbiamo cioe' utilizzare il
passaggio dei parametri per
indirizzo.
Ecco quindi la versione corretta della procedura scambia, assieme al programma per collaudarla:
#include <stdio.h>
#include <stdlib.h>
void scambia(int *, int *);
int main() {
int n=10;
int m=5;
printf("n == %d, m == %d\n", n, m);
scambia(&n,&m);
printf("n == %d, m == %d\n", n, m);
return EXIT_SUCCESS;
}
void scambia(int *n, int *m) {
int aux;
aux = *n;
*n = *m;
*m = aux;
}
In blu sono evidenziate le parti del codice che
sfruttano il
meccanismo dei puntatori.
Esercitazione non guidata
<>Risolvere il seguente esercizio e spedire il solo file sorgente (suffisso .c) all'indirizzo paolo.santi@iit.cnr.it come attachment di un messaggio di posta elettronica con subject: Esercitazione Lezione 8, indicando il vostro nome, cognome e gruppo di appartenenza.int main(int argc, char *argv[])
La variabile argc contiene il numero di parametri che compongono la linea di comando con la quale il programma è stato lanciato.
L'array argv è un array di puntatori a variabili di tipo char che può anche essere visto come un array di stringhe, ognuna corrispondente ad una delle parole che compongono la linea di comando con cui viene invocato il programma.
Poiché argv[0] contiene sempre il nome col quale è stato invocato il programma, argc vale almeno 1.
Scrivere un programma che
stampa la somma di tutti i valori passati al programma da linea di
comando (assumendo che siano positivi).
Ad esempio, se una volta compilato l'eseguibile si chiamasse somma, allora il comando da shell Linux:
> somma 1 10 5 42
dovrebbe stampare il valore 58.
Nota: ricordate che una stringa è un array di caratteri (terminato dal carattere speciale '\0' ) e che il suo indirizzo base è dato dalla locazione del primo carattere.