I puntatori

Ogni variabile di un programma viene memorizzata in uno o più byte (a seconda del tipo) allocati in sequenza a partire da una certa locazione (indirizzo di memoria).
puntatori 1

Un comando di assegnamento può modificare il contenuto della locazione associata alla variabile.

puntatori 2

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.


puntatori 3

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 dichiarazione
    int *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.

puntatori 4

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

In programmazione capita frequentemente di dover scambiare il valore di due variabili (es. n e m). Per eseguire questa operazione è indispensabile una variabile di appoggio (es. aux):
  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.


Esercizio:

La funzione main può ricevere due parametri, chiamati convenzionalmente argc e argv per comunicare col sistema operativo:
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.



Torna alla HomePage del corso