Programmare in C – I Puntatori: gli indirizzi di memoria

La lezione precedente sarà più semplice da capire se faremo chiarezza sugli indirizzi di memoria e su come funzionano.

Tutti i computer hanno una memoria, conosciuta anche come RAM (random access memory). Per esempio, il vostro computer potrebbe avere una RAM di 16, 32 o 64 megabytes. La RAM contiene i programmi che il vostro computer sta eseguendo, assieme ai dati su cui essi lavorano, le variabili e le strutture dati tanto per intenderci. La memoria può essere pensata come un semplice arry di bytes, dove ogni locazione ha il suo indirizzo – l'indirizzo del primo byte è 0, l'indirizzo del secondo byte è 1 e così via. Gli indirizzi di memoria sono pertanto indici di un array. Il computer può accedere ad ogni indirizzo nella memoria in qualsiasi momento (da cui il nome, random access memory). Può inoltre raggruppare bytes assieme per formare variabili più grandi, altri array e strutture dati. Per sempio, una variabile floating point occupa 4 bytes contigui di memoria. In un programma potrebbe esserci la seguente dichiarazione:

float f;

Questa istruzione dice: “dichiara una locazione di memoria chiamata f che può contenere valori a virgola mobile”. Quando il programma è in esecuzione, il computer riserva lo spazio per la variabile f da qualche parte nella memoria. Questa locazione ha un indirizzo fissato nello spazio di memoria, così come illustrato in figura:

La variabile f occupa 4 bytes di memoria RAM e la locazione ha un indirizzo specifico che in questo caso è 248,440.

Quando si pensa alla variabile f, il computer pensa a sua volta ad uno specifico indirizzo di memoria (in questo caso 248,440). Di conseguenza, quando si scrive un'istruzione come questa:

f = 3.14;

Il compilatore la traduce come “Memorizza il valore 3.14 nella locazione di memoria 248,440”. Il computer pensa e penserà sempre in termini di indirizzi di memoria e di contenuto degli indirizzi.

Ci sono pertanto alcuni effetti indiretti per via del modo con cui il computer lavora sulla memoria. Per esempio, supponiamo che in un programma in esecuzione sia inclusa questa porzione di codice:

int i, s[4], t[4], u=0;

for (i=0; i<=4; i++)
{
        s[i] = i;
        t[i] =i;
}
printf("s:t\n");
for (i=0; i<=4; i++)
        printf("%d:%d\n", s[i], t[i]);
printf("u = %d\n", u);

L'output che otterremo sarà probabilmente:

s:t
1:5
2:2
3:3
4:4
5:5
u = 5

Perchè t[0] e u sono sbagliati? Se si osserva attentamente il codice, si nota che nel ciclo for scrive un elemento dopo la fine di ogni array. Nella memoria, gli array sono posizionati l'uno dopo l'altro, così come in figura:

Di conseguenza, quando si cerca di scrivere s[4], che non esiste, il sistema scrive su t[0] perchè t[0] si trova dove dovrebbe essere s[4]. Quando si scrive t[4] si sta praticamente scrivendo erroneamente su u. Per quanto riguarda il computer, s[4] è un semplice indirizzo in cui si può scrivere, ma la logica del programma è “errata”. Pertanto il programma in esecuzione “rovina” l'array t. Se poi si va ad eseguire l'istruzione s[1000000] = 5 si potrebbero verificare delle conseguenze ancora più gravi.

Ad esempio la locazione s[1000000] potrebbe essere al di fuori dello spazio di memoria del programma. In altre parole, si sta cercando di scrivere in un'area di memoria che il programma non possiede. In un sistema con spazi di memoria protetti (UNIX, Windows 98/NT), questa istruzione determina il crash del programma. In altri sistemi (Windows 3.1, Mac), comunque, il sistema non si rende conto di ciò che state facendo e quindi potreste danneggiare codice o variabili di altre applicazioni in esecuzione. Gli effetti di tali violazioni possono quindi variare da “non è successo nulla” al completo crash del sistema. Nella memoria, i, s, t e u sono tutte posizionate l'una dopo l'altra da uno specifico indirizzo in poi. Se si scrive al di fuori dei confini di una variabile, il computer eseguirà quello che gli è stato chiesto ma poi succederà inevitabilmente che un'altra locazione di memoria sarà danneggiata.

Poiché il C e il C++ non eseguono alcun tipo di controllo sul range degli indirizzi in memoria, quando si accede ad un elemento di un array è essenziale per il programmatore porre particolre attenzione al range dell'array, mantenendo gli indici con cui si accede ai dati all'interno dei limiti della struttura. Leggere o scrivere al di fuori dei limiti di memoria dell'array può provocare strani comportamenti nel programma.

Provare quindi il seguente codice:

#include <stdio.h>

int main()
{
    int i,j;
    int *p;   /* un puntatore ad un intero */
    printf("%d %d\n", p, &i);
    p = &i;
    printf("%d %d\n", p, &i);
    return 0;
}

Questo codice dice al compilatore di stampare l'indirizzo contenuto in p, assieme all'indirizzo di i. La variabile p mostrerà un valore strano oppure 0. L'indirizzo di i è generalmente un valore grande. Per esempio, quando abbiamo eseguito questo codice sulla nostra macchina, abbiamo ottenuto questo output:

0 2147478276
2147478276 2147478276
Che significa: l'indirizzo di i è 2147478276. Una volta che l'istruzione p=&i è stata eseguita, p contiene l'indirizzo di i.
Provate anche voi quest'altro codice:

#include <stdio.h>

void main()
{
    int *p;   /* a pointer to an integer */

    printf("%d\n",*p);
}

Questo codice dice al compilatore di stampare il valore a cui p punta. P non è però stato ancora inizializzato, può contenere il valore 0 oppure qualche indirizzo di memoria casuale. Nella maggior parte dei casi, un errore di run-time (tipicamente segmentation fault) viene rilevato: significa che avete usato un puntatore che punta ad un'area di memoria non valida.
Ricordate che un puntatore non inizializzato è il principale motivo per cui si verifica segmentation fault.

Avendo detto tutto ciò, possiamo vedere i puntatori sotto una nuova luce. Prendete ad esempio questo programma:

#include <stdio.h>

int main()
{
    int i;
    int *p;   /* a pointer to an integer */
    p = &i;
    *p=5;
    printf("%d %d\n", i, *p);
    return 0;
}

Questo è quello che succede:

La variabile i occupa 4 bytes di memoria. Il puntatore p occupa altri 4 bytes (nella maggior parte dei sistemi in commercio, un puntatore occupa tipicamente 4 bytes di memoria. Gli indirizzi di memoria sono a 32 bits sulla maggior parte delle CPU, e il numero di sistemi a 64-bit sta rapidamente aumentando). La locazione di i ha uno specifico indirizzo, in questo caso 248,440. Il puntatore p contiene questo indirizzo una volta che si è eseguita l'istruzione p=&i;. La variabile *p e i sono ora equivalenti.

Il puntatore p contiene l'indirizzo di i. Quando si scrive qualcosa di questo tipo in un programma:

printf(“%d”, p); ciò che viene stampato è il valore corrente della variabile i.

Scarica subito una copia gratis

Una risposta

  1. Avatar photo Ionela 27 Novembre 2009

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend