Tutorial
8
Scritto da Roberto Navigli (roberto.navigli@iol.it)
Game Programming Italia: www.gpi.eden.it
I
puntatori
Il
linguaggio C deve la sua enorme diffusione a molte
ragioni, ma tra queste ce n'è una che lo distingue
dalla maggior parte degli altri linguaggi. Si tratta
dei famigerati puntatori. Non che siano disponibili
solo in C, ma certamente questo è il linguaggio
che lascia maggiore libertà al programmatore
in fatto di gestione della memoria. Maggiore libertà
è sinonimo non solo di potere sulla macchina,
ma anche di lavoro a carico del programmatore, preoccupazioni
e mal di testa
ma spesso anche un'efficienza
maggiore. In questo il C rassomiglia all'Assembly
che mette la macchina nelle mani del programmatore.
Sta al programmatore districarsi ed evitare l'infittirsi
dei bug nei propri programmi.
Ma che cos'è un puntatore? Un puntatore è
una variabile che contiene l'indirizzo di una locazione
di memoria. In genere siamo abituati a pensare che
le variabili contengano valori di tipo intero, virgola
mobile, stringa ecc. I puntatori sono variabili che
hanno un tipo molto particolare, il tipo "indirizzo".
Sostanzialmente possiamo vedere il puntatore come
un intero, con la differenza che questo intero non
rappresenta un semplice "numero", ma un
indirizzo di memoria. Definiamo ad esempio un intero:
int
i;
Questo
intero sarà effettivamente memorizzato in una
locazione di memoria. Questa locazione di memoria
è identificata da un numero che ne indica la
posizione precisa all'interno della memoria a disposizione.
Per conoscere l'indirizzo di una variabile, è
sufficiente anteporre alla stessa l'operatore unario
&. Ad esempio per avere l'indirizzo della variabile
i è sufficiente scrivere &i. Ma dove memorizzare
questo indirizzo? Scrivere:
int
a = &i; // errore in C++, in C si ottiene solo
un warning
non
funziona in C++ perché l'indirizzo non è
di tipo intero. Per memorizzare questo indirizzo abbiamo
bisogno di un puntatore, cioè di una variabile
che possa contenere un indirizzo di memoria. Anche
i puntatori però differiscono tra loro: puntare
a una locazione di memoria contenente un intero è
diverso da puntare a una zona di memoria contenente
un floating point. Per questo è possibile specificare
il tipo di puntatore, ad esempio:
int
*a; // puntatore a intero
float *f; // puntatore a floating point
Per
tornare all'esempio di prima, è sufficiente
scrivere:
int
i; // un intero
int *a = &i; // a "punta" all'intero
i, cioè ne contiene l'indirizzo
Una
volta noto l'indirizzo di una determinata variabile,
è possibile accedere al contenuto della stessa.
Questo è possibile grazie all'operatore unario
*, da anteporre al nome della variabile. Ad esempio:
int
i = 5; // i è un intero
int *a = &i; // a è puntatore all'intero
i
int b = *a; // a b è assegnato il valore della
variabile i (=5)
*a = 6; // ora la variabile i contiene 6
Ma
allora a cosa servono i puntatori? Non sarebbe più
semplice modificare direttamente l'intero invece che
utilizzare un puntatore per modificarne il contenuto?
Certamente, ma i puntatori permettono di fare molte
cose che altrimenti non sarebbero possibili. Consideriamo
il seguente (classico) esempio:
void
scambia(int *a, int *b)
{
int c = *a;
*a = *b;
*b = c;
}
void
main()
{
int i = 1, j = 2;
scambia(&i, &j);
// ora i contiene 2 e j contiene 1
}
Questo
esempio è particolarmente semplice, ma significativo.
In C il passaggio di parametri avviene per valore:
questo significa che le variabili passate come parametri
vengono copiate in altre variabili, locali alla funzione.
Modificare queste ultime non apporterebbe nessun cambiamento
alle variabili passate come parametro:
void
scambia(int a, int b)
{
int c = a;
a = b;
b = c;
// ora b contiene a e a contiene b, ma entrambe
// sono delle semplici copie di i e j e quindi
// saranno definitivamente perdute
// all'uscita della funzione
}
void
main()
{
int i = 1, j = 2;
scambia(i, j);
// i contiene ancora 1 e j contiene ancora 2
}
Un
altro motivo che rende i puntatori il pane quotidiano
dei programmatori C sono le stringhe. In C una stringa
è rappresentata come una sequenza di caratteri
terminata dal carattere ASCII 0. Per riferirsi a una
stringa, si utilizza un puntatore al primo carattere.
Ad esempio:
char
*stringa = "ciao";
definisce
un puntatore al primo carattere della stringa, la
'c',
*stringa
= 'm';
modifica
la 'c' in 'm': la stringa ora è diventata "miao".
Lo stesso risultato si sarebbe ottenuto con:
stringa[0]
= 'm';
Infatti
un array è indicato con il puntatore (costante)
a una zona di memoria (gli indici di un array iniziano
da 0). Ad esempio:
int
array[5] = { 1, 2, 3, 4, 5 }, i;
//
questa istruzione è equivalente alla successiva
array[1] = 0;
*(array+1)
= 0; // come sopra
array = &i; // errore: array è un puntatore
costante
E
se volessimo memorizzare un array di stringhe? Niente
di più semplice!
char
*stringhe[5] =
{ "primo", "secondo", "terzo",
"quarto", "quinto" };
In
realtà, stringhe è un array di 5 puntatori
a carattere. Ciascun puntatore viene inizializzato
con l'indirizzo di memoria dei caratteri 'p', 's',
't', 'q' e 'q' delle rispettive stringhe. Scrivere
stringhe[0][10] permette di accedere all'undicesimo
carattere a partire dalla 'p' di primo. Questo è
illegale, perché la stringa è lunga
solo cinque caratteri. Accedere a dati di cui non
si conosce nulla è in ogni caso pericoloso.
In realtà le stringhe con cui viene inizializzato
l'array sono lunghe un carattere in più, perché
il compilatore si occupa automaticamente di aggiungere
il carattere terminatore alla fine di ciascuna stringa.
E' come se avessimo scritto:
char
*stringhe[] = { "primo\0", "secondo\0",
"terzo\0", "quarto\0", "quinto\0",
};
Il
carattere terminatore è molto importante per
determinare la fine della stringa. Spesso è
anche la causa di molti bug nei programmi se non si
presta la dovuta attenzione. Un'ultima cosa riguardo
questa linea di codice: alla fine dell'ultima stringa
è stata inserita una virgola. E' una piccola
comodità per poter inserire rapidamente altre
stringhe all'occorrenza. Inoltre non è necessario
specificare il numero di stringhe con cui viene inizializzato
l'array, perché il compilatore calcola automaticamente
il numero di quelle specificate tra parentesi graffe.
Consideriamo
adesso il classico esempio di funzione che preleva
gli argomenti da console (in versione C++):
#include
<iostream>
using
namespace std;
void
main(int argc, char *argv[])
{
cout << "numero degli argomenti: "
<< argc << endl;
int k = 0;
while(k < argc)
{
cout << "argomento " << k <<
": " << argv[k] << endl;
k++;
}
}
Per
quanto riguarda la sintassi di cout, rimando agli
articoli sul C++. Per ora è sufficiente sapere
che per stampare sulla console basta utilizzare l'operatore
<< seguito da una variabile di un tipo predefinito
(interi, stringhe, ecc.). Questo operatore può
essere applicato quante volte si vuole. endl è
invece un manipolatore, che rappresenta il carattere
di "a capo". Lo scopo del programma è
stampare il numero di argomenti inseriti dall'utente
alla chiamata del programma e visualizzare quindi
la lista degli argomenti. Ad esempio, se il programma
viene compilato con il nome prova.exe, allora digitando
prova.exe
paperone paperino
si
ottiene in output:
numero
degli argomenti: 3
argomento 0: prova.exe
argomento 1: paperone
argomento 2: paperino
Passiamo
ora ad analizzare il programma vero e proprio. Gli
argomenti della funzione main sono standard: un intero
(che rappresenta il numero di parametri) e un array
di puntatori a char (diciamo, un array di stringhe).
Poiché il C non svolge controlli sull'indicizzazione
degli array, è possibile accedere anche a una
zona di memoria non valida. Per questo motivo, bisogna
prestare molta attenzione quando si scrive codice
C per l'indicizzazione. Il C++ fortunatamente mette
a disposizione del programmatore la libreria standard
contenente un'implementazione "sicura" degli
array: la classe vector. Parleremo di questa classe
nella sezione dedicata alle STL (Standard Template
Library).
Problemi
comuni con i puntatori
I puntatori possono facilmente portare a errori di
programmazione. Bug dovuti ai puntatori sono difficili
da scovare e possono anche apparire solo in alcuni
casi specifici. Chiunque abbia a che fare con i puntatori
si trova prima o poi a combattere con qualche errore
di distrazione relativo ai puntatori. Gli errori sui
puntatori sono dovuti soprattutto alla mancata inizializzazione
degli stessi, all'accesso non previsto a certe zone
di memoria e al confronto di puntatori a oggetti diversi.
Ad
esempio, il seguente frammento:
char
*c;
cout
<< c;
cerca
di stampare la stringa c, anche se in effetti essa
non è stata inizializzata. Quello che si ottiene,
se tutto va bene, è la stampa a caratteri di
una zona di memoria di cui non è noto l'indirizzo
a priori.
L'accesso
non previsto a zone di memoria al di fuori del dominio
dell'applicazione è molto comune quando si
utilizza l'indicizzazione. Ad esempio:
char
str[5] = "ciao";
//
si tenta di accedere al sesto elemento della stringa!
cout << str[5];
In
questo frammento si fa un errore molto comune (spesso
dovuto a una semplice distrazione): si cerca di accedere
al sesto elemento (dimenticando che l'indicizzazione
in C parte da 0) della stringa. Non è detto
che questo frammento causi un errore in fase di esecuzione,
ma comunque non è chiaro quale carattere verrà
stampato poiché non sappiamo che cosa si trovi
dopo la stringa "ciao"!
Spesso
si cade nel tranello di confrontare stringhe o altri
oggetti puntati mediante il confronto di puntatori.
Questo in generale non è possibile. Ad esempio:
char
*a = "abc", *b = "abc";
if
(a == b) cout << "a è uguale a b";
else cout << "a è diverso da b";
Al
contrario delle aspettative, questo frammento restituisce
in output:
a
è diverso da b
La
ragione è semplice: il confronto non avviene
sulle stringhe, ma sui puntatori (ovvero si controlla
che i due puntatori si riferiscano alla stessa zona
di memoria, il che non avviene perché le due
stringhe, anche se uguali, sono allocate in zone di
memoria differenti).
Il
codice corretto è il seguente:
#include
<iostream>
#include "string.h"
using namespace std;
void
main()
{
char *a = "abc", *b = "abc";
if (!strcmp(a, b)) cout << "a e b sono
uguali";
else cout << "a e b sono diverse!";
}
Nonostante
questi problemi (tipici per chi ha a che fare per
la prima volta con i puntatori), i puntatori permettono
di produrre codice molto efficiente incrementando
di molto le prestazioni di un programma time critical.
|