[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]


154. C: puntatori, array e stringhe

Nel capitolo precedente sono stati mostrati solo i tipi di dati primitivi, cioè quelli a cui si fa riferimento attraverso un nome. Per poter utilizzare strutture di dati più complesse, come un array o altri tipi più articolati, si gestiscono dei puntatori alle zone di memoria contenenti tali strutture. L'idea può sembrare spaventosa a prima vista, ma tutto questo può essere gestito in modo molto semplice se si comprende bene il problema dall'inizio.

Quando si ha a che fare con i puntatori, è importante considerare che il modello di memoria che si ha di fronte è un'astrazione, nel senso che una struttura di dati appare idealmente continua, mentre nella realtà il compilatore potrebbe anche provvedere a scomporla in blocchi separati.

154.1 Puntatori

Una variabile, di qualunque tipo sia, rappresenta un valore posto da qualche parte nella memoria del sistema. Quando si usano i tipi di dati normali, è il compilatore a prendersi cura di tradurre i riferimenti agli spazi di memoria rappresentati simbolicamente attraverso dei nomi.

Attraverso l'operatore di indirizzamento e-commerciale (`&'), è possibile ottenere il puntatore (riferito alla rappresentazione ideale di memoria del linguaggio C) a una variabile normale. Tale valore, che comunque appartiene ai tipi di dati primitivi (anche se questo fatto non era stato definito in precedenza), può essere inserito in una variabile particolare, adatta a contenerlo: una variabile puntatore.

Per esempio, se `p' è una variabile puntatore adatta a contenere l'indirizzo di un intero, l'esempio mostra in che modo assegnare a tale variabile il puntatore alla variabile `i'.

int i = 10;
...
p = &i;

154.1.1 Dichiarazione e utilizzo delle variabili puntatore

La dichiarazione di una variabile puntatore avviene in modo simile a quello delle variabili normali, con l'aggiunta di un asterisco davanti al nome. Per esempio,

int *p;

dichiara la variabile `p' come puntatore a un tipo `int'. È assolutamente necessario indicare il tipo di dati a cui si punta.

Non deve essere interesse del programmatore il modo esatto in cui si rappresentano i puntatori dei vari tipi di dati, diversamente non ci sarebbe l'utilità di usare un linguaggio come il C invece di un semplice assemblatore di linguaggio macchina.

Una volta dichiarata la variabile puntatore, questa viene utilizzata normalmente, senza asterisco, finché si intende fare riferimento al puntatore stesso. *1*

Attraverso l'operatore di «dereferenziazione», l'asterisco (`*'), è possibile accedere alla zona di memoria a cui la variabile punta. *2*

Attenzione a non fare confusione con gli asterischi: una cosa è quello usato nella dichiarazione, e un'altra è l'operatore.

L'esempio accennato nella sezione introduttiva potrebbe essere chiarito nel modo seguente, in modo da mostrare anche la dichiarazione della variabile puntatore.

int i = 10;
int *p;
...
p = &i;

A questo punto, dopo aver assegnato a `p' il puntatore alla variabile `i', è possibile accedere alla stessa area di memoria in due modi diversi: attraverso la stessa variabile `i', oppure attraverso la traduzione `*p'.

int i = 10;
int *p;
...
p = &i;
...
*p = 20

Nell'esempio, l'istruzione `*p=20' è tecnicamente equivalente a `i=20'. Per chiarire un po' meglio il ruolo delle variabili puntatore, si può complicare l'esempio nel modo seguente:

int i = 10;
int *p;
int *p2;
...
p = &i;
...
p2 = p;
...
*p2 = 20

In particolare è stata aggiunta una seconda variabile puntatore, `p2', solo per fare vedere che è possibile passare un puntatore anche ad altre variabili, e in tal caso non si deve usare l'asterisco. Comunque, in questo caso, `*p2=20' è tecnicamente equivalente sia a `*p=20' che a `i=20'.

154.1.2 Passaggio di parametri per riferimento

Il linguaggio C utilizza il passaggio dei parametri alle funzioni per valore, e per ottenere il passaggio per riferimento occorre utilizzare i puntatori. Si immagini di volere realizzare una funzione (stupida) che modifica la variabile utilizzata nella chiamata, sommandovi una quantità fissa. Invece di passare il valore della variabile da modificare, si può passare il suo puntatore, e la funzione, fatta appositamente per questo, agirà nell'area di memoria a cui punta questo puntatore.

...
void funzione_stupida( int *x ) {
    (*x)++;
}
...
main() {
    int y = 10;
    ...
    funzione_stupida( &y );
    ...
}    

L'esempio mostra la dichiarazione e descrizione di una funzione che non restituisce alcun valore, e riceve un parametro puntatore a un intero. Il lavoro della funzione è solo quello di incrementare il valore contenuto nell'area di memoria a cui si riferisce tale puntatore.

Poco dopo, nella funzione `main()' inizia il programma vero e proprio; viene dichiarata la variabile `y', un intero normale inizializzato a 10, e a un certo punto viene chiamata la funzione vista prima, passando il puntatore a `y'.

Il risultato è che dopo la chiamata, la variabile `y' contiene il valore precedente incrementato di un'unità.

154.2 Array

Nel linguaggio C, l'array è una sequenza ordinata di elementi dello stesso tipo nella rappresentazione ideale di memoria che si ha di fronte. In questo senso, quando si dichiara un array, quello che il programmatore ottiene in pratica, è solo il riferimento alla posizione iniziale di questo; gli elementi successivi verranno raggiunti tenendo conto della lunghezza di ogni elemento.

Visto in questi termini, si può intendere che l'array in C è sempre a una sola dimensione, tutti gli elementi devono essere dello stesso tipo in modo da avere la stessa dimensione, e la quantità degli elementi è fissa.

Inoltre, dal momento che quando si dichiara l'array si ottiene solo il riferimento al suo inizio, è compito del programmatore ricordare la dimensione massima di questo, perché non c'è alcun modo per determinarlo durante l'esecuzione del programma.

Infatti, quando un programma tenta di accedere a una posizione oltre il limite degli elementi esistenti, c'è il rischio che non si verifichi alcun errore, arrivando però a dei risultati imprevedibili.

154.2.1 Dichiarazione e utilizzo degli array

La dichiarazione di un array avviene in modo intuitivo, definendo il tipo degli elementi e la loro quantità. L'esempio seguente mostra la dichiarazione dell'array `a' di sette elementi di tipo `int'.

int a[7];

Per accedere agli elementi dell'array si utilizza un indice, il cui valore iniziale è sempre zero, e di conseguenza, l'ultimo ha indice n-1, dove n corrisponde alla quantità di elementi esistenti.

...
a[1] = 123;

L'esempio mostra l'assegnamento del valore 123 al secondo elemento.

154.2.1.1 Inizializzazione

In presenza di array di piccole dimensioni, e soprattutto monodimensionali, può essere sensato attribuire un valore iniziale agli elementi di questo, all'atto della dichiarazione. *3*

int a[] = { 123, 453, 2, 67 };

L'esempio dovrebbe chiarire il modo: non occorre specificare il numero di elementi, perché questi sono esattamente quelli elencati nel raggruppamento tra le parentesi graffe.

Questo fatto potrebbe fare supporre erroneamente che si possano rappresentare degli array costanti attraverso un elenco tra parentesi graffe: non è così, questa semplificazione vale solo nel momento della dichiarazione.

154.2.2 Scansione di un array

La scansione di un array avviene generalmente attraverso un'iterazione enumerativa, in pratica con un ciclo `for' che si presta particolarmente per questo scopo. Si osservi l'esempio seguente:

int a[7];
int i;
...
for (i = 0; i < 7, i++) {
    ...
    a[i] = ...;
    ...
}

L'indice `i' viene inizializzato a zero, in modo da cominciare dal primo elemento dell'array; il ciclo può continuare fino a che `i' continua a essere inferiore a sette, infatti l'ultimo elemento dell'array ha indice sei; alla fine di ogni ciclo, prima che riprenda il successivo, viene incrementato l'indice di un'unità.

Per scandire un array in senso opposto, si può agire in modo analogo, come nell'esempio seguente:

int a[7];
int i;
...
for (i = 6; i >= 0, i--) {
    ...
    a[i] = ...;
    ...
}

Questa volta l'indice viene inizializzato in modo da puntare alla posizione finale; il ciclo viene ripetuto fino a che l'indice è maggiore o uguale a zero; alla fine di ogni ciclo, l'indice viene decrementato di un'unità.

154.2.3 Array multidimensionali

Si è detto che gli array in C sono monodimensionali, però nulla vieta di poter creare un array i cui elementi siano array tutti uguali. Per esempio, nel modo seguente,

int a[5][7];

si dichiara un array di sette elementi che a loro volta sono array di cinque elementi di tipo `int'. Nello stesso modo si possono definire array con più di due dimensioni.

Quando si creano array multidimensionali, si tende a considerare il contrario rispetto a quanto affermato, cioè, per fare riferimento all'esempio precedente, che si tratti di un array di cinque elementi che a loro volta sono array di sette interi. Non è molto importante stabilire quale sia la realtà dei fatti, quello che conta è che il programmatore abbia chiaro in mente come intende gestire la cosa. L'esempio seguente mostra il modo normale di scandire un array a due dimensioni.

int a[5][7];
int i1;
int i2;
...
for (i1 = 0; i1 < 5, i1++) {
    ...
    for  (i2 = 0; i2 < 7, i2++) {
	...
	a[i1][i2] = ...;
	...
    }
    ...
}

154.2.4 Natura dell'array

Inizialmente si è accennato al fatto che quando si crea un array, quello che viene restituito in pratica è un puntatore alla sua posizione iniziale, ovvero all'indirizzo del primo elemento di questo.

Si può intuire che non sia possibile assegnare a un array un altro array, anche se ciò potrebbe avere significato. Al massimo si può assegnare elemento per elemento.

Per evitare errori del programmatore, la variabile che contiene l'indirizzo iniziale dell'array, e che in pratica rappresenta l'array stesso, è in sola lettura. Quindi, nel caso dell'array già visto,

int a[7];

la variabile `a' non può essere modificata, mentre i singoli elementi `a[i]' sì. Data la filosofia del linguaggio C, se fosse possibile assegnare un valore alla variabile `a', si modificherebbe il puntatore, facendo in modo che questo punti a un array differente. Ma per raggiungere questo risultato è meglio usare i puntatori in modo esplicito. Si osservi l'esempio seguente:

#include <stdio.h>

main() {
    int ai[3];
    int *pi;

    pi = ai; /* pi diventa un alias dell'array ai */

    pi[0] = 10;
    pi[1] = 100;
    pi[2] = 1000;

    printf ( "%d %d %d \n",  ai[0], ai[1], ai[2] );
}

Viene creato un array, `ai', di tre elementi di tipo `int', e subito dopo una variabile puntatore, `pi', al tipo `int'. Si assegna quindi alla variabile `pi' il puntatore rappresentato da `ai'; da quel momento si può fare riferimento all'array indifferentemente con il nome `ai' o `pi'.

In modo analogo, si può estrapolare l'indice che rappresenta l'array dal primo elemento. Si veda la trasformazione dell'esempio appena visto, nel modo seguente:

#include <stdio.h>

main() {
    int ai[3];
    int *pi;

    pi = &ai[0]; /* pi diventa un alias dell'array ai */

    pi[0] = 10;
    pi[1] = 100;
    pi[2] = 1000;

    printf ( "%d %d %d \n",  ai[0], ai[1], ai[2] );
}

154.2.5 Array e funzioni

Si è visto che le funzioni possono accettare solo parametri composti da tipi di dati elementari, compresi i puntatori. In questa situazione, l'unico modo per trasmettere a una funzione un array attraverso i parametri, è quello di inviarne il puntatore. Di conseguenza, le modifiche che verranno apportate da parte della funzione si rifletteranno nell'array di origine. Si osservi l'esempio seguente:

#include <stdio.h>

void elabora( int *pi ) {

    pi[0] = 10;
    pi[1] = 100;
    pi[2] = 1000;
}

main() {
    int ai[3];

    elabora( ai );
    printf ( "%d %d %d \n",  ai[0], ai[1], ai[2] );
}

La funzione `elabora()' utilizza un solo parametro, rappresentato da un puntatore a un tipo `int'. La funzione presume che il puntatore si riferisca all'inizio di un array di interi e così assegna alcuni valori ai primi tre elementi (anche il numero degli elementi non può essere determinato dalla funzione).

All'interno della funzione `main()' viene dichiarato l'array `ai' di tre elementi interi, e subito dopo viene passato come parametro alla funzione `elabora()'. Così facendo, si passa il puntatore al primo elemento dell'array.

Infine, la funzione altera gli elementi come è già stato descritto, e gli effetti si possono osservare.

10 100 1000

L'esempio potrebbe essere modificato per presentare la gestione dell'array in modo più elegante. Per la precisione si tratta di ritoccare la funzione `elabora'.

void elabora( int ai[] ) {

    ai[0] = 10;
    ai[1] = 100;
    ai[2] = 1000;
}

Si tratta della stessa identica cosa, solo che si pone l'accento sul fatto che l'argomento è un array di interi. Infatti, essendo un array di interi un puntatore a un intero, questa notazione fa sì che la lettura del sorgente diventi più facile.

154.3 Stringhe

Le stringhe, nel linguaggio C, non sono un tipo di dati a sé stante; si tratta solo di un array di caratteri con una particolarità: l'ultimo carattere è sempre `\0' (pari a una sequenza di bit a zero). In questo modo, si evita di dover accompagnare le stringhe con l'informazione della loro lunghezza, dal momento che il C non offre un metodo per determinare la dimensione degli array.

Con questa premessa, si può intendere che il trattamento delle stringhe in C non sia una cosa tanto agevole; in particolare non si possono usare operatori di concatenamento. Per tutti i tipi di elaborazione occorre intervenire a livello di array di caratteri.

154.3.1 Array di caratteri e array stringa

Una stringa è un array di caratteri, ma un array di caratteri non è necessariamente una stringa: per esserlo occorre che l'ultimo elemento sia il carattere `\0'.

char ac[20];

L'esempio mostra la dichiarazione di un array di caratteri, senza specificare il suo contenuto. Per il momento non si può parlare di stringa, soprattutto perché per essere tale, la stringa deve contenere dei caratteri.

char ac[] = { 'c', 'i', 'a', 'o' };

Questo esempio mostra la dichiarazione di un array di quattro caratteri. All'interno delle parentesi quadre non è stata specificata la dimensione perché questa si determina dall'inizializzazione. Anche in questo caso non si può ancora parlare di stringa, perché manca la terminazione.

char acz[] = { 'c', 'i', 'a', 'o', '\0' };

Questo esempio mostra la dichiarazione di un array di cinque caratteri corrispondente a una stringa vera e propria. L'esempio seguente è tecnicamente equivalente, solo che utilizza una rappresentazione più semplice.

char acz[] = "ciao";

Pertanto, la stringa `"ciao"' è un array di cinque caratteri perché rappresenta implicitamente anche la terminazione.

In un sorgente C ci sono varie occasioni di utilizzare delle stringhe letterali (delimitate attraverso gli apici doppi), senza la necessità di dichiarare l'array corrispondente. Però è importante tenere presente la natura delle stringhe per sapere come comportarsi con loro. Per prima cosa, bisogna rammentare che la stringa, anche se espressa in forma letterale, è un array di caratteri, e come tale restituisce semplicemente il puntatore del primo di questi caratteri.

char *pc;
...
pc = "ciao";
...

L'esempio appena mostrato, mostra il senso di quanto detto: non esistendo un tipo di dati «stringa», si può assegnare una stringa solo a un puntatore al tipo `char'. L'esempio seguente non è valido, perché non si può assegnare un valore alla variabile che rappresenta un array.

char ac[];
...
ac = "ciao"; /* non si può */
...

154.3.2 Stringhe come parametri di una funzione

Quando si utilizza una stringa tra i parametri della chiamata di una funzione, questa riceverà il puntatore all'inizio della stringa. In pratica, si ripete la stessa situazione già vista per gli array in generale.

#include <stdio.h>

void elabora( char *acz ) {

    printf ( acz );
}

main() {

    elabora( "ciao\n" );
}

L'esempio mostra una funzione banale che si occupa semplicemente di emettere la stringa ricevuta come parametro, utilizzando `printf()'. La variabile utilizzata per ricevere la stringa è stata dichiarata come puntatore al tipo `char', poi tale puntatore è stato utilizzato come parametro per la funzione `printf()'. Volendo scrivere il codice in modo più elegante si poteva dichiarare apertamente la variabile ricevente come array di caratteri di dimensione indefinita. Il risultato è lo stesso.

#include <stdio.h>

void elabora( char acz[] ) {

    printf ( acz );
}

main() {

    elabora( "ciao\n" );
}

154.3.3 Caratteri speciali e sequenze di escape

Nel capitolo precedente, in occasione della descrizione delle costanti letterali per i tipi di dati primitivi, era già stato descritto il modo con cui si possono rappresentare alcuni caratteri speciali attraverso delle sequenze di escape. La tabella 153.3 riporta l'elenco delle corrispondenze più comuni.

154.3.4 Parametri della funzione main()

La funzione `main()', se viene dichiarata con i suoi parametri tradizionali, permette di acquisire la riga di comando utilizzata per avviare il programma. La dichiarazione completa è la seguente:

main( int argc, char *argv[] ) {
   ...
}

Gli argomenti della riga di comando vengono convertiti in un array di stringhe (cioè di puntatori a `char'), in cui il primo elemento è il nome utilizzato per avviare il programma, e gli elementi successivi sono gli altri argomenti. Il primo parametro, `argc', serve a contenere la dimensione di questo array, e permette di conoscere il limite per la scansione del secondo parametro, `argv', che come si vede è un array di puntatori al tipo `char'.

È il caso di annotare che questo array avrà sempre almeno un elemento: il nome utilizzato per avviare il programma, e di conseguenza, `argc' sarà sempre maggiore o uguale a uno.

L'esempio seguente mostra in che modo gestire tale array, attraverso la semplice riemissione degli argomenti attraverso lo standard output.

#include <stdio.h>

main( int argc, char *argv[] ) {

    int i;

    printf( "Il programma si chiama %s\n", argv[0] );

    for ( i = 1; i <= argc; i++ ) {
	printf( "argomento n. %d: %s\n", i, argv[i] );
    }
}

154.4 Puntatori e funzioni

Nello standard C ANSI, la dichiarazione di una funzione è in pratica la definizione di un puntatore alla funzione stessa, un po' come accade con gli array. In generale, è possibile dichiarare dei puntatori a un tipo di funzione definito in base al valore restituito e ai tipi di parametri richiesti.

<tipo> (*<nome-puntatore>)(<tipo-parametro>[,...]);

L'esempio seguente mostra la dichiarazione di un puntatore a una funzione che restituisce un valore di tipo `int' e utilizza due parametri di tipo `int'.

int (*f)(int, int);

L'assegnamento del puntatore avviene nel modo più semplice possibile, trattando il nome della funzione nello stesso modo in cui si fa con gli array: come un puntatore.

int (*f)( int, int );
int prodotto( int, int ); /* prototipo di funzione descritta più avanti */
...
f = prodotto; /* il puntatore f contiene il riferimento alla funzione */

Una volta assegnato il puntatore, si può eseguire una chiamata di funzione semplicemente utilizzando il puntatore, per cui,

i = f( 2, 3 );

risulta equivalente a quanto segue:

i = prodotto( 2, 3 );

Nel linguaggio C precedente allo standard ANSI, perché il puntatore potesse essere utilizzato in una chiamata di funzione, occorreva indicare l'asterisco, in modo da dereferenziarlo.

i = (*f)( 2, 3 );

---------------------------

Appunti Linux 2000.04.12 --- Copyright © 1997-2000 Daniele Giacomini --  daniele @ pluto.linux.it


1.) L'asterisco usato nella dichiarazione serve a definire il tipo di dati, quindi, la dichiarazione `int *p', rappresenta la dichiarazione di una variabile di tipo `int *'.

2.) Dereferenziare significa togliere il riferimento, e raggiungere i dati a cui un puntatore si riferisce.

3.) Alcuni compilatori consentono l'inizializzazione degli array solo quando questi sono dichiarati all'esterno delle funzioni, e quindi hanno un campo di azione globale, e quando, all'interno delle funzioni, sono dichiarati come statici.


[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]