Guida Come funzionano i puntatori in C

ManHunter

Utente Jade
14 Settembre 2009
985
111
780
818
Ultima modifica da un moderatore:
Salve a tutti,

oggi ho deciso di scrivere questa guida per trattare di un argomento alquanto ostico per molti: i puntatori.
Ma bando alle ciance, iniziamo subito.

Prima di tutto, iniziamo con il fare un po' di teoria.
Come tutti saprete, alla sua dichiarazione una variabile è organizzata in modo da avere un suo riferimento e un suo valore. Il riferimento di una variabile è univoco e rappresenta la locazione di memoria in cui essa risiede, in modo da poter ritrovarla quando verrà richiamata. Il valore della variabile, invece, può variare nel tempo e rappresenta il contenuto di quella cella di memoria dove essa risiede.

In sintesi, quindi, la variabile è composta da:
- Left Value (LV): riferimento della variabile o indirizzo di memoria
- Right Value (RV): contenuto di tale cella
Tali value sono regolati da specifiche particolari. Un esempio è l'assegnazione: in un'assegnazione di variabile il significato della stessa cambia a seconda della posizione rispetto al simbolo "=".

Vi porto un esempio per permettervi di capire meglio quanto detto:
C:
.
.
.
int a; /*a rappresenta il nome di una variabile di tipo int*/
.
.
.
a = 1; /*inseriamo nella cella di locazione di memoria individuata da a il valore 1, quindi abbiamo che: LV(a) <- 1*/
a = a +3; /*inseriamo nella cella di locazione di memoria individuata da a il valore di a + 3. Questo significa: LV(a) <- RV(a) + 3*/

Questa parte è fondamentale quindi, prima di proseguire, assicuratevi di aver assimilato il concetto.
Fatta questa parentesi sull'organizzazione delle variabili in memoria, passiamo all'oggetto vero e proprio della guida: il puntatore.

Il puntatore è una variabile non molto diversa dalle altre che normalmente utilizzate. La sua particolarità è che esso contiene l'indirizzo in memoria di un'altra variabile, ovvero il suo Left Value.
Nella pratica possiamo scrivere così:
C:
int a = 1, y = 2, z[10];
int *ptr;

ptr = &a; /*LV(ptr) <- LV(a), ptr punta ora ad a*/
b = *ptr; /*LV(b) <- RV(a), b è uguale al valore destro della variabile puntata*/
*ptr = 0; /*RV(a) <- 0*/
ptr = &z[0]; /*LV(a) <- LV(z), ptr punta ora a z[0]*/

Questo semplice esempio ci fa capire molte cose.
- int a = 1: è la normale dichiarazione di una variabile di tipo intero, di identificativo a
- int *ptr: è la dichiarazione di una variabile di tipo "puntatore a" (in questo caso ad intero). La sintassi, espressa con la BNF, che definisce la dichiarazione di un puntatore è: <type> *<exp>;
- b = *ptr: l'operatore unario * è detto operatore di indirezione o di deferenzazione (indirection o deferencing). In una dichiarazione specifica che quella che lo segue sarà una variabile puntatore al tipo dichiarato. In un'espressione, applicato ad una variabile puntatore, permette di accedere alla variabile puntata dal puntatore.
- ptr = &a: è l'assegnazione al Right Value di ptr del Left Value di a. Ne deduciamo, quindi, che l'operatore unario "&" restituisce il valore sinistro della variabile cui è applicato. In questo caso, si dice che ptr punta ad a.

I puntatori sono molto utili nella programmazione, in particolar modo nelle seguenti situazioni:
- Scambio di parametri nelle funzioni
- Costruzione di Array e strutture dati più complesse (un esempio: la lista)
- Allocazione dinamica della memoria


Un altro concetto importante da chiarire è come i puntatori possono ritornare utili come argomenti di funzioni. Dal momento che nel C gli argomenti delle funzioni sono passati tutti per valore, esistono alcuni problemi difficilmente risolvibili senza l'ausilio di puntatori. Poniamo la nostra attenzione su questa funzione:

C:
void swap(int x, int y) /*SBAGLIATA*/
{
   int tmp;

   tmp = x;
   x = y;
   y = tmp;
}
Tale funzione riceve come parametri due interi e ne scambia i valori. Tutto sarebbe corretto, se non fosse che lo scambio è effettivo solo all'interno della funzione! Questo avviene a causa del passaggio per valore: i due interi passati alla funzione sono delle "copie" delle vere variabili passate come argomenti. Quindi, alla fine della funzione, esse non saranno più visibili dall'esterno.
La funzione corretta fa uso dei puntatori:
C:
void swap(int *ptrX, int *ptrY) /*CORRETTA*/
{
   int tmp;

   tmp = *ptrX;
   *ptrX = *ptrY;
   *ptrY = tmp;
}
La funzione, costruita in questo modo, viene messa in condizione di poter modificare le variabili originali, non operando più su delle copie.



Spero di essere stato abbastanza chiaro nella trattazione dell'argomento e nell'esposizione dei concetti. Per ogni informazione aggiuntiva, critiche e consigli scrivete qui di seguito utilizzando toni calmi e pacati.
La prossima guida tratterò dei puntatori, dei vettori e del modo in cui essi sono strettamente collegati.

Saluti!
 
Come mai non è possibile usare una puntatore per gestire le strutture?

Mi riferisco al fatto di passare a una funzione, una struttura per riferimento senza il "typedef"
 
Ultima modifica da un moderatore:
Guida in ogni caso ottimo aiuto, puntatori a mio avviso sono parte piu dura del C e richiedono anche anni prima di essere capiti _bene_.

Se gradite, alcune integrazioni alla guida che mi vengono in mente.

1) I puntatori non puntano solo a delle variabili, ma sono variabili che contengono un indirizzo di memoria, pertanto possono puntare anche a spazi di memoria vuoti, o se errati o non inizializzati a zone inaccessibili (seg. fault). Sono sostanzialmente variabili (variabili data type a 64 bit in x86_64) che contengono un indirizzo di memoria.

2) In cpu con mmu, la memoria e' virtuale, quindi un indirizzo contenuto in un puntatore non corrisponde a indirizzo fisico di memoria, ma un indirizzo virtuale. In cpu senza mmu, puntatore e' indirizzo fisico. Memoria puo essere immaginata come un altissimo armadio a cassetti di 64bit, (8 bytes) ad esmepio (considerando il bus 64bit di x86_64). Ogni cassetto ha un indirizzo, cioe' 0, 8, 16 etc, ma si puo accedere anche a un solo byte del cassetto.

Codice:
int *p = 8;

/* cast a uint8_t *, leggo contenuto indirizzo 9 */
uint8_t  b = *((uint8_t *)p + 1);

Ma cosa vuol dire punta
Un puntatore contiene un indirizzo di memoria. Come un fucile che punta a un bersaglio, il puntatore "punta" a un cassetto dell'armadio di cui spiego sopra.

Come mai non è possibile usare una puntatore per gestire le strutture?
Mi riferisco al fatto di passare a una funzione, una struttura per riferimento senza il "typedef"

Non si passano strutture per reference in C, quello si fa in c++, ma tramite puntatori. Non serve il typedef, ad esempio io non lo uso mai per motivi di leggibilita'.

Codice:
#include <stdio.h>

struct trullallero {
        int pappa;
};

int test(struct trullallero *t)
{
        printf("%d\n", t->pappa);
}

int main()
{
        struct trullallero t;

        t.pappa = 100;
        test(&t);
}
 
  • Mi piace
Reazioni: 0xbro
Ultima modifica:
Ottima spiegazione, mi permetto però di aggiungere un ulteriore esempio per far capire meglio il concetto.
Prendiamo ad esempio un programma in cui è presente una funzione che aumenta il valore di due variabili, una passata per valore e una seconda passata per indirizzo alla funzione che esegue l'operazione di incremento.​

C++:
#include <stdio.h>
/*dichiaro la funzione che incrementa le variabili e che verrà chiamata nel main()*/
void Aggiungi(int x, int *y){ //passaggio per valore della variabile a e passaggio per indirizzo della varibile b
    x++;
    (*y)++;
    printf("Valori delle var. a e b nella funzione chiamata: %d %d \n", x, *y); //stampo il valore a video
    return;
}
int main(){
    int a=0; // dichiaro la variabile a e la inizializzo a zero
    int b=0; // dichiaro la variabile b e la inizializzo a zero
    /* notare bene la chiamata alla funzione Aggiungi():
        a viene passata per valore al parametro x della funzione
        mentre b viene passata per referenza (tramite l'operatore & che indica l'indirizzo di memoria della variabile) al parametro *y    */
    Aggiungi(a,&b); //chiamo la funzione Aggiungi() per eseguire l'incremento delle variabili a e b
    printf("Valori di a e b nella funzione principale : %d %d \n", a, b); //stampo il valore a video
    return 0;
}
Se si va a vedere l'output generato dal programma questo sarà:
Valori delle var. a e b nella funzione chiamata: 1 1
Valori di a e b nella funzione principale : 0 1
Questo perchè l'incremento di x (parametro passato per valore) all'interno della funzione Aggiungi() non modifica il valore di a, mentre l'incremento della variabile indirizzata da y (parametro passato per referenza) modifica il valore iniziale di b.​