I puntatori

29.1 Cosa sono i puntatori

Una cosa da tenere bene a mente e' la definizione di puntatore: un puntatore e' una variabile che contiene l'indirizzo di un'altra variabile. Come gia' illustrato nel capitolo 1, si puo' paragonare la memoria di un calcolatore ad un quaderno a quadretti dove all'interno di ogni quadretto si puo' inserire un valore. Il microprocessore individua ogni cella di memoria (il quadretto) in base al suo indirizzo all'interno della RAM (il foglio) usando dei sistemi di indirizzamento particolari. Ad esempio potrebbe individuare un quadretto tramite le coordinate riga/colonna: la riga 37 e colonna 44 identificano insieme un preciso quadretto all'interno del foglio. Un qualsiasi sistema di individuazione del quadretto viene definito indirizzamento. In realta' all'interno della RAM tutto e' molto piu' complesso ed ogni cella di memoria ha un indirizzo ben preciso. Bene, una variabile di tipo puntatore puo' contenere l'indirizzo di uno di questi 'quadretti' di memoria. E' importante aver chiaro che il contenuto di un puntatore e' un indirizzo e non il valore di una variabile. Le dimensioni fisiche delle variabili dipendono dall'architettura della macchina. Ad esempio, su una macchina ipotetica qualsiasi una variabile di tipo char puo' avere una dimensione di un byte, uno short int 2 byte ed un long 4 byte. Su tale macchina, una variabile puntatore puo' contenere l'indirizzo di una cella di memoria se punta ad un char, di 2 celle di memoria adiacenti se punta ad uno short int e di 4 celle di memoria adiacenti se punta ad un long. Un puntatore e' un gruppo di 2 o 4 celle di memoria che contengono l'indirizzo di memoria di altre celle di memoria. La dimensione della variabile puntatore dipende dalla macchina e piu' precisamente dalla dimensione dei registri della macchina. Esistono vari tipi di puntatori: i puntatori a char, i puntatori ad int, i puntatori a long e cosi' via. Per ogni tipo di variabile esiste un tipo di puntatore correlato. Cio' significa che non e' possibile usare ad esempio un puntatore a char per puntare una variabile di tipo int o viceversa. La sintassi della definizione di un puntatore e' la seguente:

tipo *nomepuntatore;

dove 'tipo' e' il tipo della variabile puntata, 'nomepuntatore' e' il nome della variabile puntatore ed il carattere '*' e' l'operatore di deriferimento (o indirezione). Quando applicato ad una variabile di tipo puntatore, il carattere '*' permette di accedere al valore della variabile puntata. L'operatore unario '&', fornisce l'indirizzo di una variabile:

int main (void)
{
int var_int = 0, var_int2;   /* variabili di tipo intero  */
int *punt_int;          /* dichiarazione di puntatore a intero */
punt_int = &var_int;    /* l'indirizzo della variabile var_int viene assegnato al puntatore */
var_int2 = *punt_int;
*punt_int = 2;
}

nell'esempio precedente viene dichiarata una variabile di tipo 'int var_int'; nella seconda riga viene dichiarato un puntatore a int 'punt_int'; infine nella terza riga viene prelevato l'indirizzo della variabile var_int mediante l'operatore '&' che viene assegnato al puntatore punt_int. Ora il puntatore punt_int si dice che 'punta' a var_int, in quanto contiene l'indirizzo della variabile var_int. Ad un puntatore e' possibile assegnare unicamente indirizzi di oggetti presenti in memoria. Non e' possibile percio' assegnare il valore di una espressione o l'indirizzo di una variabile di tipo register. Nella penultima riga dell'esempio precedente viene utilizzato l'operatore '*' per reperire il contenuto della variabile puntata dal puntatore punt_int. Alla fine la variabile var_int2 conterra' lo stesso valore della variabile var_int. Nell'ultima riga, utilizzando ancora una volta l'operatore '*', viene assegnato il valore 2 alla variabile puntata da punt_int. Alla fine la variabile var_int conterra' il nuovo valore (2). Un espressione del tipo:

double *dp, atof(char *);

dichiara un puntatore a double ed una funzione che accetta un puntatore a char come argomento e ritorna un double. Un puntatore deve puntare ad un tipo ben preciso, percio' un puntatore a char puo' contenere solo indirizzi di variabili di tipo char, un puntatore ad int puo' contenere solo indirizzi di variabili di tipo int, un puntatore a double puo' contenere solo indirizzi di variabili di tipo double e cosi' via. L'unica eccezione e' il puntatore a void. Il puntatore a void non punta ad una variabile di un tipo ben preciso, quindi puo' essere utilizzato per puntare a qualsiasi tipo. Un puntatore di tipo void pero', non puo' essere dereferenziato. Un puntatore ad un tipo puo' essere utilizzato come se fosse la variabile puntata, percio':

*ip = *ip   3;

e' un'espressione perfettamente legale. Le espressioni seguenti sono equivalenti:

*ip = *ip   1;
*ip =1;
  *ip;
(*ip)  

nell'ultima riga le parentesi sono necessarie, in quanto l'operatore di incremento ( ) e' associativo da destra a sinistra. Un errore comune e' usare un puntatore che...non punta a nulla! Quando si dichiara un puntatore infatti, si dichiara una variabile non inizializzata, cioe' vuota: in effetti non e' esattamente cosi' in quanto se non inizializzata, la variabile puntatore puo' contenere al suo interno un valore imprevedibile e puo' essere anche la causa di un crash di sistema. Prima di usare un puntatore occorre inizializzarlo con un indirizzo. Ad esempio:

int *pi;
*pi = 100;    /*   sbagliato  */

int i;
int *pi;

pi = &i;
*pi = 100;   /* corretto  */

Un altro errore che puo' rivelarsi insidioso, riguarda gli spazi tra i vari operatori. In effetti non si tratta di un errore che riguarda esclusivamente i puntatori, ma piuttosto di un errore generale sull'uso dei vari operatori. Il problema riguarda il trattamento erroneo di oggetti di un carattere come oggetti di piu' caratteri. In altre parole i caratteri '/' e '*', se presi singolarmente rappresentano un operatore di divisione l'uno e un operatore di deferimento l'altro: se presi insieme d'altro canto, rappresentano l'inizio di un commento ('/*'):

a = b/*c;   /*  sbagliato  */

a = b / *c  /*  corretto   */

la prima riga non e' corretta in quanto il compilatore interpreta la stringa di caratteri '/*' come l'inizio di un commento, ignorando tutto cio' che segue fino al prossimo '*/'. L'intenzione originaria invece era quella di assegnare ad a il risultato della divisione di b per il contenuto della variabile puntata da c. Per ovviare al problema e' sufficiente separare sempre i vari operatori con almeno un carattere di spaziatura. In questo modo si eliminano errori insidiosi e contemporaneamente si rende il codice piu' leggibile. Queste insidie non riguardano solo l'operatore '*' ma possono nascere anche con altri operatori. Alcuni vecchi compilatori infatti considerano la stringa '= ' cosi' come i compilatori moderni considerano la stringa ' ='. Con i primi, potrebbero nascere delle ambiguita':

a=-1;   /*  sbagliato  */

a = -1  /* corretto  */

come viene interpretata la prima riga? Un vecchio compilatore potrebbe assegnare ad a il valore di a diminuito di una unita', mentre un nuovo compilatore farebbe cio' che ci si aspetta: assegnerebbe ad a il valore '-1'. Ancora una volta, l'uso delle spaziature (cosi' come mostrato nell'ultima riga) permette di eliminare potenziali ambiguita'. Occorre fare attenzione comunque a non commettere l'errore opposto:

a   /* strano ma vero */ = 1

       corrisponde a:

a  = 1

         mentre:

p - > a

          non corrisponde a:

p -> a
Nella prima riga i due simboli vengono letti dal compilatore come un unico simbolo, nonostante la presenza di spazi e persino di un commento. Viceversa, nella terza riga, i due simboli non vengono interpretati come un unico simbolo. Come gia' menzionato precedentemente, in questa guida si cerchera' di seguire sempre alcune regole di stile comuni, una di queste rigurda proprio la spaziatura tra i vari oggetti. Per questi ed altri errori insidiosi del linguaggio C, e' possibile consultare il libro: C Traps and Pitfalls, Andrew Koenig, Addison-Wesley, 1989, ISBN 0-201-17928-8.

29.2 Argomenti delle funzioni e puntatori

Nel C gli argomenti alle funzioni vengono passati per valore e non per riferimento. Cio' significa che il valore dell'argomento passato ad una funzione viene copiato all'interno di una variabile temporanea (automatica), che nasce quando viene chiamata la funzione e muore quando termina ala funzione. Ne consegue che una funzione non puo' alterare direttamente il contenuto degli argomenti passategli. E' possibile pero' usare i puntatori per superare quest'ostacolo. Quando ad una funzione viene passato un puntatore ad una variabile, la funzione puo' modificare il contenuto della variabile usando il puntatore. Nell'esempio seguente:

void swap(int x, int y) /* sbagliato */
{
int temp;
temp = x;
x = y;
y = temp;
}

la funzione swap scambia il contenuto della variabile x con quello della variabile y e viceversa. In realta', quando viene chiamata con l'istruzione swap(a,b), le variabili 'a' e 'b' non vengono minimamente alterate. Questo perche' in realta' alla funzione vengono copiati i valori di queste due variabili all'interno di due variabile temporanee interne alla funzione. Per consentire alla funzione di modificare il contenuto delle variabili del chiamante si possono usare i puntatori, chiamando la funzione con l'istruzione 'swap(&a,&b)'. In questp modo viene passato l'indirizzo di queste variabili alla funzione che cambia leggermente:

void swap (int *px, int *py)
{
	int temp;
	temp = *px;
	*px = *py;
	*py = temp;
}

29.4 Relazioni tra puntatori e vettori

In C esiste una stretta relazione tra puntatori e vettori: l'accesso agli elementi di un vettore puo' essere effettuato con gli indici o con i puntatori indifferentemente. Percio' un espressione con vettori e indici e' equivalente ad una espressione con puntatori e scostamenti (scostamenti o spiazzamenti o offset). Considerando ad esempio un vettore V[10]:

char vc[10];
int  vi[10];

char *punt_char;
int  *punt_int;

punt_char = &v[0];
punt_char = v;

la prima riga dichiara un vettore di char, la seconda riga un vettore di int, la terza un puntatore a char e la quarta un puntatore ad int. Le ultime due assegnano l'indirizzo del primo elemento del vettore di char al puntatore punt_char e sono equivalenti: infatti in C il nome di un vettore corrisponde all'indirizzo del suo primo elemento. Quindi vc equivale a &vc[0]. Se 'i' e' un valore da 0 a 9, per accedere all'elemento i-esimo del vettore dichiarato sopra, occorre scrivere vc[i], pero' si puo' anche scrivere punt_char[i] in quanto dopo l'assegnamento 'punt_char = &vc[0]', punt_char e vc contengono lo stesso valore. Dopo l' assegnamento, punt_char punta al primo elemento di vc percio' incrementando punt_char e' possibile accedere al successivo elemento di vc:

char vc[3];
char *punt_char;
                       --------------- 
4 celle di memoria:   |1  |2  |3  |\0 |
                       --------------- 
                      ^   ^   ^   ^
indirizzo 100 _______/   /   /   /
                        /   /   /
indirizzo 101 _________/   /   /
                          /   /
indirizzo 102 ___________/   /
                            /
indirizzo 103 _____________/

vc[0]  ----> 1
vc[1]  ----> 2
vc[2]  ----> 3
vc[3]  ----> \0

&vc[0]  ----> 100
&vc[1]  ----> 101
&vc[2]  ----> 102
&vc[3]  ----> 103

            dopo:

punt_char = &vc[0];

           allora:

punt_char      ----> 101
punt_char   2  ----> 102
punt_char   3  ----> 103

             e quindi:

*(punt_char  )    corrisponde a vc[1]
*(punt_char   2)  corrisponde a vc[2]
*(punt_char   3)  corrisponde a vc[3]

Un puntatore punta ad una o piu' celle di memoria a seconda del tipo. Supponendo ad esempio che su una macchina X il tipo char equivalga ad 1 byte ed il tipo int a 4 byte, ogni elemento di vc e' lungo 1 byte, mentre ogni elemento di vi e' lungo 4 byte. Considerando cio', dopo l'assegnamento 'punt_int = &vi[0];' punt_int punta al primo elemento del vettore vi, percio', incrementando punt_int, si accede al secondo elemento di vi e cioe' alla quinta cella di memoria consecutiva. Ogni incremento di punt_char permette di puntare alla cella di memoria successiva mentre ogni incremento di punt_int permette di puntare alle 4 celle di memoria successive. In tale macchina X utilizzando punt_char ci si sposta di una cella alla volta mentre usando punt_int di 4 celle alla volta. Piu' in generale, dato un tipo T qualsiasi, tale tipo avra' una dimensione D ben precisa, percio' ogni incremento di un puntatore di tipo T permettera' di spostarsi di D celle alla volta. Ora occorre analizzare un punto cruciale che solitamente crea non poche confusioni a chi si accosta al C per la prima volta: la relazione tra puntatori e vettori. In C infatti esistono delle relazioni strette tra puntatori e vettori:

               se:

punt_char = &vc[0];

            allora:

punt_char equivale a vc (nel senso che contengono lo stesso indirizzo)

              percio':

vc[i]            corrisponde a:   punt_char[i]

vc[i]            corrisponde a:   *(punt_char   i)

*(vc   i)        corrisponde a:  vc[i]

&vc[i]           corrisponde a:  (vc   i)

*(punt_char  i)  corrisponde a:  punt_char[i]

in altre parole, si puo' accedere ad un elemento di un vettore utilizzando indici o puntatori indifferentemente. Quando si fa riferimento ad un elemento di un vettore infatti, il C trasforma l'espressione vc[i] in *(vc i). Attenzione ancora una volta a non confondere l'espressione *(vc 1) con * vc! La prima incrementa un indirizzo di 1 e ritorna l'oggetto puntato a quell'indirizzo (il risultato della somma non viene posto all'interno di vc). La seconda non e' ammissibile in quanto corrisponde a vc = vc 1, ma vc e' il nome di un vettore e non una variabile di tipo int! Lo standard C dichiara che all'interno di una espressione che contiene un vettore, tale vettore decade sempre a puntatore tranne 3 casi: 1) il vettore e' l'operando dell'operatore 'sizeof', 2) il vettore e' operando dell'operatore '&' e 3) il vettore e' una stringa costante che inizializza un altro vettore. Attenzione alla confusione in agguato: un puntatore ad un vettore ed un vettore sono due TIPI DIVERSI di variabili, pero' in molti contesti il nome di un vettore decade a puntatore, viene cioe' trasformato in puntatore. Ad esempio se passato ad una funzione come argomento. Quindi in partenza un vettore ed un puntatore a tale vettore sono due oggetti ben distinti, pero', se il nome di un vettore viene ad esempio passato come argomento ad una funzione, viene trasformato in puntatore. Ma il nome di un vettore NON e' un puntatore. Infatti un puntatore e' una variabile che puo' contenere un indirizzo, mentre il nome di un vettore non e' una variabile che puo' contenere un indirizzo. Percio':

                 se:

int intero = 0;
int vi[4] = {1, 2, 3};
int *punt_vi;

               allora:

punt_vi = &vi[1];     /* corretto */

punt_vi = &intero;    /* corretto */

vi = &intero;         /* sbagliato! */

l'ultima riga e' sbagliata in quanto vi e' un vettore di interi e non un puntatore, percio' e' un tipo di variabile che non puo' contenere indirizzi di altre variabili. L'unico tipo di variabile che puo' contenere indirizzi e' la variabile puntatore. La confusione sorge quando si usa l'espressione 'decade a puntatore', intendendo che, in alcuni contesti, il nome di un vettore viene trasformato in puntatore. Quando viene passato il nome di un vettore come argomento ad una funzione, cio' che viene passato in realta' e' l'indirizzo del primo elemento di tale vettore (&v[0]): poiche' gli argomenti delle funzioni al loro interno sono delle variabili locali (temporanee ed automatiche) in questo caso tale variabile e' a tutti gli effetti un puntaore, ossia una variabile che contiene un indirizzo. Cio' puo' essere utile in molti contesti, ad esempio:

int main ()

   {
    int lung;
    char s[] = "una stringa qualsiasi";

    lung = strlen (s);

    return 0;
   }


/* strlen: ritorna la lunghezza della stringa s */

int strlen (char *s)
    {
	int n;
	
	for (n = 0; *s != '\0', s  )
	n  ;
	
	return n;
    }

Poiche' all'interno della funzione strlen 's' e' una variabile locale, non esistono interferenze con la stringa 's' contenuta nel main. La variabile s della funzione e' un puntatore a carattere, percio' puo' essere tranquillamente incrementato. Cio' che viene incrementato e' il puntatore, ossia la variabile 's' privata della funzione, senza coinvolgere minimamente la variabile s del main che e' una stringa o meglio, un vettore di caratteri. Alla luce di cio' le chiamate: strlen (s), strlen (puntatore_ad_s) e strlen ("una stringa qualsiasi") sono tutte legali. Occorre infine distinguere tra puntatore al primo elemento di un vettore e puntatore ad un intero vettore: non e' la stessa cosa! Infatti, dato ad esempio un vettore di 10 interi, incrementando un puntatore al primo elemento del vettore, si punta al secondo elemento del vettore ma, incrementando un puntatore ad un intero vettore di 10 interi, si punta all'area di memoria successiva al vettore di 10 interi. Percio' se in una macchina X un intero vale 4 byte, un puntatore ad un intero se incrementato, si sposta di 4 byte in 4 byte. Ma se si fa riferimento ad un puntatore ad un vettore di 10 interi, ogni incremento di tale puntatore permette di spostarsi di 40 byte in 40 byte. In C non esiste l'oggetto stringa, nel senso che non esiste il tipo primitivo stringa, al contrario, una stringa viene rappresentata internamente come un vettore di char. Quando in un programma e' presente una stringa del tipo: "salve, io sono una stringa" il compilatore accede a tale stringa tramite un puntatore al primo carattere del vettore di char che contiene la stringa:

char *punt_char;
punt_char = "salve, io sono una stringa";

poiche' 'punt_char' e' una variabile di tipo puntatore a char, nell'ultima riga viene inizializzata con l'indirizzo del primo elemento del vettore di char che contiene la stringa "salve, io sono una stringa". Esiste pero' una differenza fondamentale tra le seguenti espressioni:

char vc[] = "salve, io sono una stringa";

      e

char *punt_char = "salve, io sono una stringa";

nella prima riga infatti, viene allocato un vettore di char abbastanza ampio da contenere la stringa "salve, io sono una stringa", cioe' un vettore di 27 byte (26 caratteri piu' il carattere '\0'); questo non e' vero per l'ultima riga, dove viene dichiarato un puntatore a char ed inizializzato con l'indirizzo del vettore che contiene la stringa. Modificare un qualsiasi carattere del vettore vc e' un'operazione perfettamente legale, in quanto viene modificato un valore contenuto all'interno di un'area di memoria modificabile; modificare punt_char in modo che punti ad un altra locazione di memoria contenente un carattere o una stringa di caratteri e' anch'essa un'operazione legale. Cio' che invece non dovrebbe essere modificata e' la stringa costante non definita esplicitamente come vettore, ossia come mostrato nell'ultima riga. In quest'ultimo caso infatti, il risultato sara' indefinito! Per concludere con le stringhe ecco una versione della funzione strcpy:

/* strcpy: copia t in s; */
void strcpy(char *s, char *t)
   {
	while ((*s   = *t  ) != '\0')
	;
   }

passando due stringhe alla funzione strcpy viene copiata la prima nella seconda. Poiche' s e t sono i puntatori ai vettori che contengono le stringhe, il ciclo while copia il contenuto puntato da t nel contenuto puntato da s un carattere alla volta. Il ciclo termina quando viene incontrato il carattere null '\0' che viene anch'esso copiato. Tale funzione puo' essere scritta in un modo piu' compatto:

/* strcpy: copia t in s; */
void strcpy(char *s, char *t)
   {
	while (*s   = *t  )
	;
   }

infatti, come illustrato in un precedente capitolo, una espressione all'interno di una condizione viene valutata producendo un risultato numerico: quando tale risultato e' uguale a 0 la condizione risultera' falsa ed il ciclo terminera'. In questo caso quando viene incontrato il carattere '\0', nel valutare l'espressione, tale valore viene assegnato all'elemento del vettore puntato da s e, poiche' tale valore rende la condizione falsa, il ciclo terminera'.

29.4 Puntatori e vettori multidimensionali

In C e' possibile definire dei vettori a piu' dimensioni utilizzando l'operatore []. Ad esempio un vettore a due dimensioni composto da 3 righe e da 4 colonne e' dichiarato come segue:

int vi [3] [4];

In C un vettore bidimensionale e' in realta' ad una sola dimensione, dove ogni elemento e' a sua volta un vettore. Questa osservazione e' molto importante in quanto: quando un vettore a due dimensioni viene passato come argomento ad una funzione, il vettore decade a puntatore e quindi cio' che viene passato e' in realta' un puntatore ad un vettore che contiene altri vettori. Cio' implica che nel passare un vettore multidimensionale ad una funzione, non e' indispensabile indicare la lunghezza della prima dimensione, mentre le altre dimensioni devono essere indicate. Per passare ad esempio il vettore vi[3][4] alla funzione f, si potrebbe scrivere:

f (int vi [3] [4]) { ...istruzioni della funzione ...}

         oppure:

f (int vi [] [4]) { ...istruzioni della funzione ...}  /* non occorre specificare la prima dimensione  */

         oppure:

f (int (*punt_vi) [4]) { ...istruzioni della funzione ...}

nella prima riga viene passato un vettore di 3 righe e di 4 colonne alla funzione f; nella seconda riga viene passato lo stesso vettore ma non viene specificata la prima dimensione; nella terza riga viene passato alla funzione un puntatore ad un vettore di 4 interi. Nell'ultima riga le parentesi sono obbligatorie poiche' le parentesi quadre hanno precedenza maggiore rispetto all'operatore '*'. Quindi il vettore vi, se passato come argomento ad una funzione, decade a puntatore. Se si omettessero le parentesi nella terza riga, si passerebbe alla funzione un qualcosa di totalmente diverso: int *punt_vi [4] e cioe' un vettore di 4 puntatori ad int, che non rappresenta affatto un vettore a due dimensioni. A questo punto, a scanso di equivoci, occorre fare una distinzione netta tra vettori a due dimensioni e vettori di puntatori:

int vi [5] [3];

int *vpi [5];

Le espressioni vi[1][2] e vpi[1][2] sono perfettamente legali entrambe, pero' la differenza e' sostanziale. la prima riga dichiara un vettore vi di 5 righe e 3 colonne per cui viene allocato uno spazio di memoria pari a 3 x 5 = 15 locazioni di memoria ciascuna di ampiezza pari ad un int. La seconda riga al contrario dichiara un vettore di 5 puntatori a int, per cui vengono allocati solamente 5 puntatori non inizializzati. Supponendo di inizializzare tale struttura in modo che ogni elemento contenga un puntatore che punti ad un vettore di 3 elementi, verrebbero allocate 15 locazioni di memoria pari ad un int piu' le 5 locazioni di memoria necessarie per i 5 puntatori. La cosa interessante e' che ogni puntatore contenuto nel vettore puo' puntare a vettori di lunghezze diverse. Si potrebbe ad esempio dichiarare un vettore di puntatori a stringhe, dove ogni stringa rappresenta un mese dell'anno:

char *vp_mesi [] = {"gennaio", "febbraio", "marzo", "aprile"...}

chiaramente si potrebbe fare la stessa cosa utilizzando un vettore a due dimensioni:

char v_mesi [][9] = {"gennaio", "febbraio", "marzo", "aprile"...}

la differenza tra le due dichiarazioni e' che nella seconda vengono allocati 12 vettori lunghi 9 byte ciascuno, ossia 12 x 9 = 108 byte. Infatti poiche' non e' possibile dichiarare un vettore a due dimensioni dove la seconda dimensione ha una lunghezza variabile, occorre utilizzare la lunghezza massima della seconda dimensione del vettore v_mesi, tale da poter contenere la stringa di lunghezza massima (cioe' settembre, 9 byte). Cio' causa uno spreco di memoria:

                utilizzando:

char *vp_mesi [] = {"gennaio", "febbraio", "marzo", "aprile"...}

                    si ha:

vp_mesi:
  ---                -------  
 |100|------------> |gennaio|
 |   |               ------- 
 |   |     Lung.:    1234567
 |   |
 |   |               -------- 
 |101|------------> |febbraio|
 |   |               -------- 
 |   |     Lung.:    12345678
 |   |
 |   |               ----- 
 |102|------------> |marzo|
 |   |               ----- 
 |   |     Lung.:    12345
 .   .
 .   .              .......          
 .   .              .......

        mentre utilizzando:

char v_mesi [][9] = {"gennaio", "febbraio", "marzo", "aprile"...}

              si ha:

v_mesi:
 --------- --------- --------- ---------  . . .
|gennaio  |febbraio |marzo    |aprile   |
 --------- --------- --------- ---------  . . .
 123456789 123456789 123456789 123456789
 (lung. 1) (lung. 2) (lung. 3)

Usando i vettori, tutti gli elementi hanno lunghezza uguale e ogni elemento ha la dimensione massima necessaria per contenere la stringa di lunghezza maggiore (in questo esempio 'settembre', lunga 9 caratteri), percio', per i mesi piu' corti (come 'marzo' che e' una stringa lunga 5), c'e' uno spreco di spazio. Con i puntatori le stringhe puntate possono avere lunghezze differenti e quindi lo spazio e' ottimizzato. In realta' occorre sempre ricordare che una stringa termina con il carattere nullo '\0', percio' il vettore che contiene il mese settembre non e' lungo 9 ma bensi' 10 byte ("settembre\0"). Nell'esempio visto sopra 100, 101 e 102 sono gli indirizzi (ogni elemento del vettore e' un puntatore) alle stringhe: gennaio, febbraio, marzo...etc.

29.5 Argomenti passati al programma nella linea comando

Quando viene eseguito un programma scritto in C, cio' che viene eseguita e' la funzione main() contenuta al suo interno, alla quale vengono sempre passati due argomenti, che per convenzione vengono chiamati 'argc' e 'argv'. L'argomento argc sta per 'argument count' e rappresenta il numero degli argomenti passati al programma. L'argomento argv invece, sta per 'argument vector' ed e' un puntatore al primo elemento di un vettore di puntatori a char (ogni puntatore punta ad una stringa) dove ogni stringa rappresenta uno degli argomenti passati al programma. Oltre agli argomenti passati esplicitamente, quando viene eseguito un programma viene sempre passato implicitamente almeno un argomento: il nome del programma stesso. Se ad esempio ad un progamma vengono passati 2 argomenti, argc vale 3 poiche' il primo argomento e' sempre il nome del programma. Cio' significa che in tale caso, argv[0] conterra' il nome del programma (piu' esattamente il puntatore alla stringa che rappresenta il nome del programma), argv[1] il primo argomento e argv[2] il secondo. Se ad esempio eseguiamo il comando echo passandogli 2 argomenti:

echo maurizio silvestri

all'interno del programma echo, argc sara' 3, argv[0] conterra' il puntatore alla stringa "echo" (il nome del programma), argv[1] conterra' il puntatore alla stringa "maurizio" (il primo argomento) e argv[2] conterra' il puntatore alla stringa "silvestri" (il secondo argomento). Il primo argomento e' argv[1] mentre l'ultimo e' argv[argc-1]. Lo standard richiede che argv[argc] sia sempre un puntatore nullo. Poiche' come gia' visto e' possibile accedere ad un oggetto in memoria sia mediante indici che mediante puntatori, le seguenti due versioni del programma echo sono equivalenti:

                 versione 1:

#include <stdio.h>
/* echo argomenti nella linea comandi; 1a versione */
main(int argc, char *argv[])
   {
	
	int i;
	
	for (i = 1; i < argc; i  )
	    printf("%s%s", argv[i], (i < argc-1) ? " " : "");
	
	printf("\n");
	return 0;

   }

                 versione 2:

#include <stdio.h>
/* echo argomenti nella linea comandi; 2a versione */
main(int argc, char *argv[])
   {

	while (--argc > 0)
	    printf("%s%s", *  argv, (argc > 1) ? " " : "");
        
        printf("\n");
 	return 0;
   }

nella seconda versione, poiche' argv e' un puntatore al primo elemento del vettore di stringhe, incrementandolo si puntera' all'elemento successivo. Analizzando lo schema di esempio seguente:

                 --(vettore di puntatori. Argv punta inzialmente all'indirizzo ipotetico 1000, indirizzo che, a sua volta,
                |   contiene un puntatore. Tale puntatore contiene l'indirizzo 6000) 
                | 
                v
 ----            ----                ----------  
|1000|--------> |6000|------------> |e|c|h|o|\0|
 ----           |    |               ---------- 
(argv)          |    |              ^ ^ ^ 
                |    |               \ \ \________________________________________________ (indirizzo: 6002)
                |    |                \ \
                |    |                 \ \____________________________ (indirizzo: 6001)
                |    |                  \ 
                |    |                   \_______ (indirizzo: 6000)
                 ---- 
 ----           |    |               ----------------- 
|1100|--------> |6100|------------> |m|a|u|r|i|z|i|o\0|
 ----           |    |               ----------------- 
(  argv)        |    |              ^
                |    |               \________________________ (indirizzo: 6100)
                |    |
                 ---- 
 ----           |    |               ------------------- 
|1200|--------> |6200|------------> |s|i|l|v|e|s|t|r|i\0|
 ----           |    |               ------------------- 
(  argv)        |    |              ^
                |    |               \________________________ (indirizzo: 6200)
                |    |
                 ---- 
                .    .
                .    .

si puo' osservare che il puntatore argv contiene l'indirizzo del vettore di puntatori a char (1000). Quindi, all'indirizzo 1000 e' presente il primo elemento del vettore che, a sua volta, contiene un altro puntatore. Questo secondo puntatore contiene l'indirizzo 6000. All'indirizzo 6000 e' memorizzata la stringa "echo" (cioe' il nome del programma). Incrementando argv, si punta all'elemento successivo del vettore ( argv = 1100). All'indirizzo 1100 e' presente il secondo elemento del vettore che contiene un altro puntatore. Quest'ultimo puntatore contiene l'indirizzo 6100, cioe' l'indirizzo dove e' memorizzato il secondo argomento (la stringa "maurizio\0"). E cosi' via. Occorre pero' distinguere le tre espressioni:

1) *  argv;

2) *  argv [i];           /*  che corrisponde a *  (argv[i]);  */

3  (*  argv) [i];
infatti mentre la prima e la terza espressione scorrono gli argomenti passati al programma, la seconda scorre i caratteri dell'argomento puntato dall'indice i. E' come se all'interno di un vettore a due dimensioni la prima e la terza espressione scorrano le righe mentre la seconda scorra le colonne. L'espressione * argv[i] corrisponde all'espressione * (argv[i]), in quanto l'operatore [] ha la precedenza sugli operatori * e :
            se argv = 1000, allora:

argv   ---> 1000    quindi     argv[0] ---> 6000   e quindi      argv[0] ---> 6001  ...etc

  argv ---> 1100    quindi     argv[0] ---> 6101   e quindi      argv[0] ---> 6102  ...etc

  argv ---> 1200    quindi     argv[0] ---> 6201   e quindi      argv[0] ---> 6202  ...etc


e via dicendo, ma se argv = 1000, allora e' vero anche:

argv   ---> 1000    quindi     argv[0]   ---> 6000 e quindi   (  argv)[0] ---> 6101
                                                   --------------------------------    

                          ossia:

(  argv)[0] ---> 6101  quindi     argv[0] ---> 6102  quindi     argv[0] ---> 6103  quindi     argv[0] ---> 6104 ...etc

(  argv)[0] ---> 6201  quindi     argv[0] ---> 6202  quindi     argv[0] ---> 6203  quindi     argv[0] ---> 6204 ...etc

(  argv)[0] ---> 6301  quindi     argv[0] ---> 6302  quindi     argv[0] ---> 6303  quindi     argv[0] ---> 6304 ...etc

Quindi, con * argv[0], viene preso il primo elemento (argv[0]) ed incrementato, restituendo il contenuto dell'oggetto puntato dall'indirizzo ottenuto. In altre parole se argv[0] contiene l'indirizzo 6000, l'espressione argv[0] produce l'indirizzo 6001 e l'operatore * ritorna il carattere puntato dall'indirizzo 6001, cioe' il carattere 'c' della stringa "echo\0" (il nome del programma). Incrementando argv si puntera' invece all'elemento successivo del vettore di puntatori, cioe' se argv contiene inizialmente l'indirizzo 1000, l'operazione argv produrra' come risultato 1100, cioe' l'indirizzo del secondo puntatore del vettore di puntatori. Quindi l'espressione * argv, produce come risultato 6100, che e' il valore del secondo puntatore (cioe' l'indirizzo della stringa "maurizio\0"). Se si volesse pero' accedere al secondo carattere di tale stringa, occorrerebbe far riferimento al secondo elemento del vettore che contiene la stringa (una stringa e' un vettore di caratteri) utilizzando l'espressione (* argv)[1]. Infatti argv incrementa il puntatore argv, portandolo da 1000 a 1100; all'indirizzo 1100 e' presente un puntatore, percio' * argv accede al contenuto di tale puntatore e cioe' 6100; Infine l'indirizzo 6100 e' l'indirizzo iniziale del vettore che contiene la stringa "maurizio\0", percio', per accedere al secondo elemento di tale vettore occorre a questo punto usare l'espressione argv[1], che ritornera' il carattere 'a'. Un'espressione equivalente potrebbe essere: ** argv che ritorna l'oggetto puntato da un puntatore a puntatore. Un esempio concreto di tutto cio' e' illustrato nel seguente programma 'find':

#include <stdio.h>
#include <string.h>
#define MAXLINE 1000

int getline(char *line, int max);

/* find: stampa le righe che corrispondono allo schema */
main(int argc, char *argv[])
{
	char line[MAXLINE];
	long lineno = 0;
	int c, except = 0, number = 0, found = 0;
	while (--argc > 0 && (*  argv)[0] == '-')
		while (c = *  argv[0])
			switch (c) {
			case 'x':
				except = 1;
				break;
			case 'n':
				number = 1;
				break;
			default:
				printf("find: opzione illegale %c\n", c);
				argc = 0;
				found = -1;
				break;
			}

	if (argc != 1)
		printf("Sintassi: find -x -n schema\n");
	else
		while (getline(line, MAXLINE) > 0)
		 {
			lineno  ;
			if ((strstr(line, *argv) != NULL) != except)
			 {
				if (number)
					printf("%ld:", lineno);
				printf("%s", line);
				found  ;
			 }
		 }
	return found;
}

/* getline: legge una riga e la mette all'interno di s; ritorna length */
int getline(char s[],int lim)
{
	int c, i;
	
	for (i=0; i < lim-1 && (c=getchar())!=EOF && c!='\n';   i)
		s[i] = c;
	
	if (c == '\n')
	 {
		s[i] = c;
		  i;
	 }

	s[i] = '\0';
	return i;
}

Il programma find ricerca una stringa all'interno delle righe passate in input ed accetta i parametri -x e -n. Il parametro -n indica il numero della riga ed il parametro -x (da 'eXcept', cioe' 'ad eccezione di') effettua una ricerca inversa, segnalando tutte le righe che non contengono la parola da ricercare. L'uso di argomenti opzionali (flag) che iniziano con il carattere '-' e' una convenzione nel mondo Unix/Linux.

29.6 Puntatori a funzioni

Le funzioni, pur non essendo delle variabili, hanno un indirizzo in memoria e sono perfettamente indirizzabili mediante i puntatori. Una volta associata una funzione ad un puntatore, tale puntatore puo' essere riassegnato, inserito in un vettore, passato ad un'altra funzione etc. La dichiarazione di un puntatore a funzione ha la seguente sintassi:

tipo (*puntatore) (tipo argomento1, tipo argomento2...)

                esempio:

char (*punt) (int, int)  /* puntatore a funzione che accetta 2 interi in input e ritorna un char */

dove 'tipo' e' il tipo ritornato dalla funzione, '(*puntatore)' e' il puntatore a funzione e 'argomento1', 'argomento2' e cosi' via, sono gli eventuali argomenti della funzione con i rispettivi tipi. Rispetto agli altri tipi di puntatore, i puntatori a funzione hanno alcune differenze: 1) non esistono puntatori di puntatori a funzione 2) la concordanza tra i tipi nella dichiarazione del puntatore e la funzione e' obbligatoria 3) non e' utilizzabile l'aritmetica dei puntatori. Le parentesi tonde che racchiudono il nome del puntatore a funzione sono obbligatorie, altrimenti si dichiarerebbe una funzione che ritorna un puntatore e non un puntatore a funzione. Infatti le parentesi tonde hanno priorita' rispetto al simbolo '*' per cui:

char *punt ()

dichiara una funzione che ritorna un puntatore a char, mentre:

char (*punt) ()

dichiara un puntatore ad una funzione che ritorna un char! Ecco un esempio di utilizzo di puntatori a funzione:

#include <stdio.h>
int sottrazione(int, int);
int somma (int, int);
int prodotto(int, int);
int divisione(int, int);

int main()
{
	int a = 48;
	int b = 2;
	int risultato,scelta;
	int (*puntatore)();   /* puntatore a funzione che ritorna un int */

	for (;;)
	{
		printf("digita 1 per la somma\n");
		printf("digita 2 per la sottrazione\n");
		printf("digita 3 per il prodotto\n");
		printf("digita 4 per la divisione\n");
		printf("digita 0 per uscire\n");

		scanf("%d", &scelta);
		switch(scelta)
		{
			case 1:
				puntatore = somma;       /* assegna l'indirizzo della funzione al puntatore */
				break;
			case 2:
				puntatore = sottrazione; /* assegna l'indirizzo della funzione al puntatore */
				break;
			case 3:
				puntatore = prodotto;    /* assegna l'indirizzo della funzione al puntatore */
				break;
			case 4:
				puntatore = divisione;   /* assegna l'indirizzo della funzione al puntatore */
				break;
			case 0:
				exit(0);
		}

		risultato = puntatore(a, b);
		printf("Il risultato vale %d", risultato);
		break;
	}
}

int somma(int a, int b)
	{
	return a   b;
	}
int sottrazione(int a, int b)
	{
	return a - b;
	}
int prodotto(int a, int b)
	{
	return a * b;
	}
int divisione(int a, int b)
	{
	return a / b;
	}

in questo esempio l'espressione: 'int (*puntatore)();' dichiara un puntatore a funzione che ritorna un int. Successivamente, una espressione del tipo 'puntatore = somma;' assegna l'indirizzo della funzione al puntatore. Infine, l'espressione 'risultato = puntatore(a, b);' assegna alla variabile 'risultato' quanto ritornato dalla funzione chiamata dal puntatore 'puntatore'. In altre parole l'espressione puntatore(a, b) equivale alla chiamata somma (a, b), sottrazione(a, b), prodotto (a, b) o divisione (a, b) a seconda di quale indirizzo e' contenuto nel puntatore. Inizialmente vengono dichiarati due interi inizializzati con i valori 2 e 48; successivamente nel ciclo infinito di for viene richiesta l'operazione che si intende eseguire. Digitando 2 ad esempio, viene eseguita l'istruzione 'puntatore = sottrazione;' che assegna al puntatore l'indirizzo della funzione 'sottrazione'. Digitando infine 0 si esce dal ciclo e viene eseguita la funzione puntata dal puntatore (in questo caso la sottrazione). Cosi' come per i vettori, anche per le funzioni vale la regola che il nome corrisponde all'indirizzo, per cui, per associare l'indirizzo di una funzione ad un puntatore a funzione e' possibile usare l'operatore & oppure il nome della funzione stessa. Le espressioni:

puntatore = &funzione

          e 

puntatore = &funzione

sono pertanto equivalenti. In C si possono trovare delle espressioni piuttosto complesse: per decifrarle, occorre prestare attenzione alle parentesi. Ecco alcuni esempi:

char ** argv;
	/* puntatore a puntatori a char  */

int (*puntvett) [10]
	/* puntatore a vettore di 10 int */

int *vettpunt [10]
	/* vettore di 10 puntatori ad int */

void * funz()
	/* funzione che ritorna un puntatore a void */

void (* puntfunz) ()
	/* puntatore a funzione che ritorna un void */

char (*(*funz()) []) ()	
	/* funzione che ritorna un puntatore ad un vettore di puntatori a funzioni che ritornano un char */

char  (*(*funz[3]) ()) [5] /* vettore di 3 puntatori a funzioni che ritornano un puntatore ad un vettore di 5 char */

Osservando ad esempio le parentesi dell'espressione: char (*(*funz()) []) () si puo' effettuare una scomposizione, iniziando ad analizzare le parentesi piu' interne:

char (*(*funz()) []) ()

1) eliminando cio' che e' fuori dalle parentesi tonde (char e ()) si ha:

*(*funz()) []

2) eliminando cio' che e' fuori dalle parentesi tonde (* e []) si ha:

*funz()   ossia una funzione che ritorna un puntatore. Ora questo e' il punto di partenza!

Partendo da questa funzione che ritorna un puntatore vediamo che nel passo precedente (2) abbiamo tolto * e []
(ossia vettore di puntatori), percio' abbiamo:

una funzione che ritorna un puntatore a: un vettore di puntatori

infine nel passo precedente ancora (1) abbiamo tolto char e () (ossia funzione che ritorna un char), per cui abbiamo:

una funzione che ritorna un puntatore ad un vettore di puntatori a: funzioni che ritornano un char! ;o)

ad ogni modo e' possibile semplificare le dichiarazioni cosi' complesse utilizzando l'istruzione typedef. L'istruzione typedef permette di creare dei tipi nuovi. Ad esempio l'espressione:

typedef int intero;

definisce 'intero' come sinonimo di int. Oppure:

typedef char * Stringa;

definisce 'Stringa' come sinonimo di puntatore a char. Una volta scritta questa espressione e' possibile usare 'Stringa' al posto di 'char *' in qualsiasi contesto. Percio' un'espressione del tipo 'char * funz()' puo' essere scritta anche come Stringa funz().

Inizio della guida  Il preprocessore C  Indice  Le strutture

Copyright (c) 2002-2003 Maurizio Silvestri