Linguaggi | Manuali | Compilatori | Programmi | Script | Software | Linux | Windows | Html
Linguaggi

C
C++
JAVA
PERL
COBOL
PASCAL
MATLAB
FORTRAN77
FORTRAN90

JAVASCRIPT

VISUALBASIC

Sistemi operativi

LINUX
WINDOWS
UNIX
MAC

Software

AUTOCAD
GNUPLOT
OCTAVE
SCILAB

I puntatori
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.

Partner

Guida Fortran
Guida Matlab

English Version
Tutorials
Programming
Lavoro
Lavoro in rete
Telelavoro
Webmaster
Webmaster
Xml

Gratis
Autore
G. Ciaburro
Curriculum
Tesi