Salta al contenuto
Note per Studenti C++ - puntatori, array e cast

C++ - puntatori, array e cast

In questa pagina 6

La memoria vista da vicino: indirizzi, puntatori, array in stile C. È il fondamento di ciò che segue (C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete → e le classi che gestiscono memoria). In C gli stessi concetti sono in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C → e Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →; qui si aggiungono i tipi dei puntatori, le reference e i cast C++. Il motivo per studiarli, in una frase di Stroustrup: chi non capisce come un programma si mappa sulla memoria e sulle operazioni della macchina fatica a capire strutture dati, algoritmi e sistemi operativi. Capire la memoria serve anche a "non credere nella magia" di vector e dei contenitori.

Puntatori e indirizzi

La memoria è indirizzata a byte: ogni byte ha un indirizzo numerico. Anche int var = 17; ha un indirizzo e occupa 4 byte.

Definizione. Un puntatore è una variabile che contiene un indirizzo di memoria, come un int contiene un intero. Ha un tipo: T* è un "puntatore a T", quindi sappiamo che tipo di dato si trova all'indirizzo che contiene.

cpp
double d;      // variabile double
double* p;     // variabile puntatore a double

La * può stare attaccata al tipo o al nome (double* p1;, double *p2;, double * p3; sono equivalenti). Per dichiarare più puntatori in una riga si ripete * per ciascuno, e la spaziatura non cambia la semantica anche se inganna:

dichiarazione significato
double *p1, *p2; due puntatori a double
double *p3, d1; un puntatore e un double
double* p4, d2; un puntatore e un double (la * appartiene al nome)

Indirizzo, dereference

Se var si trova all'indirizzo 212212, int* ptr = &var; fa contenere 212212 a ptr: & è l'operatore indirizzo. L'operatore * (dereference) accede al dato puntato; posto a sinistra di un assegnamento fornisce un lvalue, cioè si può scrivere.

indirizzo:   ...  212  ...
var   (int):       17        <- ptr contiene 212
ptr   (int*): 212
ptr_base.cppcpp
#include <iostream>
using namespace std;

int main()
{
    int var = 17;
    int* ptr = &var;                 // & = "indirizzo di": ptr contiene l'indirizzo di var
    cout << "var = " << var << ", *ptr = " << *ptr << '\n';
    cout << "ptr == &var ? " << (ptr == &var) << '\n';

    int x = 17;
    int* pi = &x;
    double e = 2.71828;
    double* pd = &e;
    cout << "*pi = " << *pi << ", *pd = " << *pd << '\n';

    *pi = 27;                        // *pi e' un lvalue: scrive su x
    *pd = 3.14159;
    *pd = *pi;                       // copia il VALORE puntato: 27 -> double 27.0 in e
    cout << "x = " << x << ", e = " << e << '\n';
    // int i = pi;                   // errore: int* non e' int
    // pi = 7;                       // errore: int non e' int*
    // char* pc = pi;                // errore: int* non e' char*

    double* p0 = nullptr;            // puntatore "a niente"
    if (p0 == nullptr) cout << "p0 non punta a niente\n";
    if (!p0) cout << "forma equivalente\n";

    cout << "sizeof(int) = " << sizeof(int) << ", sizeof(int*) = " << sizeof(int*)
         << ", sizeof(double*) = " << sizeof(double*) << '\n';

    double *a, *b;                   // DUE puntatori
    double *c, d;                    // un puntatore (c) e un double (d)
    double* g, h;                    // idem: la * si lega al nome, non al tipo
    a = b = c = g = nullptr; d = h = 0;
    cout << "a,b: " << sizeof(a) << ' ' << sizeof(b) << "; c,d: " << sizeof(c) << ' ' << sizeof(d)
         << "; g,h: " << sizeof(g) << ' ' << sizeof(h) << '\n';
    return 0;
}
var = 17, *ptr = 17
ptr == &var ? 1
*pi = 17, *pd = 2.71828
x = 27, e = 27
p0 non punta a niente
forma equivalente
sizeof(int) = 4, sizeof(int*) = 4, sizeof(double*) = 4
a,b: 4 4; c,d: 4 8; g,h: 4 8
  • *pd = *pi; copia il valore puntato (27, convertito in double), non l'indirizzo.
  • int i = pi; e pi = 7; non compilano: int* e int sono tipi diversi.
  • Un puntatore è una variabile con la sua dimensione (sizeof(int*) = 4 qui, perché WebAssembly ha indirizzi a 32 bit; su un PC a 64 bit sono 8). La dimensione dipende dall'implementazione, ma non dal tipo puntato.
  • L'ultima riga mostra l'insidia delle dichiarazioni multiple: double* g, h; crea un puntatore (g, 4 byte) e un double (h, 8 byte).

Ogni tipo ha il suo puntatore

int*, double* e char* sono tipi diversi: ciò che si può fare con il dato puntato è diverso, e il compilatore lo tiene separato. char* pc = pi; non compila (e nemmeno pi = pc). Se invece si potesse, int* pi = &ch3; con quattro char consecutivi in memoria farebbe scrivere quattro byte con *pi = 12345;, sovrascrivendo ch3 e i vicini (anche pi stesso, se fosse lì accanto).

Il puntatore nullo

È comodo avere un valore non valido per segnalare un puntatore che non punta a niente: nullptr (C++11; prima NULL o 0). Il controllo si scrive if (p0 != nullptr) oppure if (p0). Dereferenziare nullptr è un errore.

Con i puntatori si è molto vicini all'hardware: il supporto è poco, ma il controllo dei tipi c'è. Non tutto il codice può essere ad alto livello (sistemi embedded), e questo rende std::vector molto più apprezzabile.

void* e cast

void* è un puntatore a memoria grezza (raw): rappresenta il concetto puro di "indirizzo" senza indicare come si usi quella memoria. Si può assegnare qualsiasi puntatore a un void*, ma non si può dereferenziare né usare con []: non si sa che oggetto ci sia.

Attenzione a non confondere void (nessun valore di ritorno) e void*.

Per usare un void* serve una conversione esplicita: i cast. Il prezzo è la perdita del controllo dei tipi.

I cast servono soprattutto a interfacciarsi con hardware (Register* in = reinterpret_cast<Register*>(0xff);) o con altro codice non modificabile (una funzione che chiede Buffer* quando si ha const Buffer*).

cast.cppcpp
#include <iostream>
using namespace std;

void f(void* pv)
{
    void* pv2 = pv;                       // ok: void* si copia
    (void)pv2;
    // double* pd = pv;                   // errore: nessuna conversione implicita da void*
    // *pv = 7;                           // errore: cosa dereferenzio? non si sa il tipo
    // pv[2] = 7;                         // errore: stessa ragione
    int* pi = static_cast<int*>(pv);      // ok: IO dico che ci sta un int (e perdo il controllo dei tipi)
    cout << "*pi = " << *pi << '\n';
}

void legge(const int* p)
{
    int* scrivibile = const_cast<int*>(p);   // toglie il const: pericoloso se l'oggetto e' davvero const
    ++*scrivibile;
}

int main()
{
    int i = 41;
    void* pv1 = &i;                       // qualsiasi puntatore si assegna a void*
    f(pv1);

    legge(&i);                            // i NON e' const: la modifica e' lecita
    cout << "i = " << i << '\n';

    // reinterpret_cast: guarda gli stessi byte con un altro tipo
    unsigned int n = 0x01020304;
    unsigned char* bytes = reinterpret_cast<unsigned char*>(&n);
    cout << "primo byte = " << int(bytes[0]) << " (little endian)\n";
    return 0;
}
*pi = 41
i = 42
primo byte = 4 (little endian)

const_cast è lecito solo se l'oggetto originale non è davvero const (qui i non lo è); se lo è, scrivere dà comportamento indefinito. L'ultima riga mostra che reinterpret_cast permette di guardare i byte di un unsigned int: il primo byte è 04, ordine little endian.

Puntatori e reference

Una reference è come un puntatore immutabile e dereferenziato automaticamente, oppure un nome alternativo per un oggetto (C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →). Le differenze:

puntatore reference
assegnamento cambia dove punta; per cambiare l'oggetto serve *p = 7 (p = 7 è pericoloso) cambia l'oggetto (r = 7)
origine si ottiene con & dichiarata int& r = y;
spostamento si può spostare (p1 = &x2) non si sposta dopo l'inizializzazione
accesso al dato *p oppure [] nessun operatore: il nome basta
copia assegnare due puntatori fa puntare allo stesso oggetto (shallow copy) assegnare due reference copia l'oggetto (deep copy)
valore nullo esiste nullptr non esiste una reference non valida
ptr_vs_ref.cppcpp
#include <iostream>
using namespace std;

int main()
{
    // puntatore: assegnare cambia DOVE punta
    int x1 = 10, x2 = 20;
    int* p1 = &x1;
    p1 = &x2;                        // il puntatore si e' spostato su x2
    *p1 = 7;                         // scrivo sull'oggetto puntato (x2)
    cout << "x1 = " << x1 << ", x2 = " << x2 << '\n';

    // reference: assegnare cambia l'OGGETTO, la reference non si sposta mai
    int y1 = 10, y2 = 20;
    int& r1 = y1;
    r1 = y2;                         // y1 = y2: copia del valore, r1 e' ancora alias di y1
    r1 = 7;
    cout << "y1 = " << y1 << ", y2 = " << y2 << '\n';

    // assegnare puntatori: due puntatori allo stesso oggetto (copia "shallow")
    int i = 1, j = 2;
    int *pi = &i, *pj = &j;
    pj = pi;                         // ora pi e pj puntano entrambi a i
    *pj = 99;
    cout << "i = " << i << ", j = " << j << ", *pi = " << *pi << '\n';

    // assegnare reference: copia del contenuto
    int a = 1, b = 2;
    int &ra = a, &rb = b;
    rb = ra;                         // b = a
    rb = 55;                         // modifica solo b
    cout << "a = " << a << ", b = " << b << '\n';
    return 0;
}
x1 = 10, x2 = 7
y1 = 7, y2 = 20
i = 99, j = 2, *pi = 99
a = 1, b = 55

La traccia: p1 = &x2; *p1 = 7; scrive su x2 e x1 resta 10. r1 = y2; invece copia 20 in y1 (la reference non si sposta), poi r1 = 7 scrive di nuovo y1. Dopo pj = pi; *pj = 99; sia i che *pi valgono 99 e j resta 2. rb = ra; copia il contenuto: a e b restano oggetti distinti, rb = 55 cambia solo b.

Quale scegliere per i parametri

Per cambiare una variabile ci sono tre modi (incr_v restituisce, incr_p prende int*, incr_r prende int&). Criteri:

  1. Restituire il valore è più chiaro e meno soggetto a errori; va bene per oggetti piccoli e, con il costruttore di spostamento, anche per grandi.
  2. Le reference sono più comode, ma la chiamata incr_r(x) "sembra innocente"; con incr_p(&x) il & rende esplicito che x può cambiare.
  3. Un puntatore può esprimere "input non valido" con nullptr (e la funzione deve controllarlo); una reference no. Criterio: se no-object è un valore plausibile, puntatore; altrimenti reference o const reference.

Array

Gli std::vector sono vettori dinamici e di alto livello; gli array in stile C sono strutture più semplici e più antiche, necessarie in alcune circostanze (più avanti nel corso).

Definizione. Un array è una sequenza omogenea di oggetti allocati in locazioni di memoria contigue: stesso tipo, nessuno spazio vuoto fra gli elementi. È indicizzato con [], con accesso casuale, e non c'è controllo sulla lettura e scrittura fuori dai limiti: gli errori sono facili e difficili da trovare.

  • La dimensione deve essere una costante nota a tempo di compilazione (literal, const o constexpr). I VLA (variable length array) sono dello standard C99, non del C++, anche se GCC li accetta.
  • Gli array si possono avere come variabili globali, locali (attenzione alla memoria nello stack!), come argomenti di funzioni (trasformati in puntatori) e come membri di classe.
cpp
const int max = 100;
int gai[max];                 // array globale, sempre disponibile
void f(int n)
{
    char lac[20];             // array locale: vive fino all'uscita dallo scope
    int lai[60];
    double lad[n];            // errore: dimensione non costante (se n non è nota in compilazione)
}

Puntatori agli elementi e aritmetica dei puntatori

Si può definire un puntatore a un elemento: double ad[10]; double* p = &ad[5];. Su p valgono * e []: *p = 7; scrive ad[5], p[2] = 6; scrive ad[7], p[-3] = 9; scrive ad[2] (indici negativi leciti se restano nell'array).

Aritmetica dei puntatori. I puntatori supportano la somma e la sottrazione di un intero (operatori +, -, +=, -=) e la differenza fra due puntatori dello stesso tipo. Sommare nn a un puntatore lo sposta di nn slot (elementi) a destra, sottrarre lo sposta a sinistra. Lo slot dipende dal tipo puntato: in byte lo spostamento è n×sizeof(T)n \times \mathtt{sizeof}(T).

Quindi se p è un double* all'indirizzo aa, p + 2 è all'indirizzo a+2⋅8a + 2\cdot 8. La differenza q - p tra due puntatori allo stesso array è invece il numero di elementi compresi fra i due (non i byte). Un'ulteriore dimostrazione che ciò che fa un operatore dipende dai suoi operandi.

array_ptr.cppcpp
#include <iostream>
using namespace std;

int lunghezza(const char a[])        // "const char a[]" equivale a "const char* a": il vettore decade a puntatore
{
    int count = 0;
    while (a[count]) ++count;        // finche' non trova lo '\0' finale
    return count;
}

void dimensione_in_funzione(double a[])
{
    cout << "dentro la funzione sizeof(a) = " << sizeof(a) << " (la dimensione di un puntatore)\n";
}

int main()
{
    constexpr int n = 10;
    double ad[n];                         // array: 10 double contigui, la dimensione e' una costante
    for (int i = 0; i < n; ++i) ad[i] = i * 1.5;
    cout << "sizeof(ad) = " << sizeof(ad) << ", elementi = " << sizeof(ad) / sizeof(ad[0]) << '\n';
    dimensione_in_funzione(ad);

    double* p = &ad[5];                   // puntatore a un elemento
    *p = 7;                               // ad[5]
    p[2] = 6;                             // ad[7]
    p[-3] = 9;                            // ad[2]
    cout << ad[5] << ' ' << ad[7] << ' ' << ad[2] << '\n';

    double* q = p + 2;                    // aritmetica: avanza di 2 ELEMENTI (2*sizeof(double) byte)
    p += 2;                               // adesso p e q puntano allo stesso elemento
    cout << (p == q) << ' ' << (q - &ad[0]) << '\n';   // la differenza e' in elementi: 7

    int v[4] = {0, 1, 2, 3};
    int* pv = &v[2];
    int* qv = pv + 2;                     // &v[4]: un elemento oltre la fine, si puo' formare ma non dereferenziare
    cout << "qv - pv = " << qv - pv << " elementi = "
         << reinterpret_cast<char*>(qv) - reinterpret_cast<char*>(pv) << " byte\n";

    // inizializzazione di array
    int ai[] = {1, 2, 3, 4, 5, 6};        // dimensione dedotta: 6
    int ai2[8] = {1, 2, 3};               // gli altri a 0
    double zeri[4] = {};                  // tutti a 0.0
    char chars[] = {'a', 'b', 'c'};       // NESSUN terminatore
    char ac[] = "Beorn";                  // 6 char: 'B','e','o','r','n','\0'
    cout << sizeof(ai) / sizeof(int) << ' ' << ai2[5] << ' ' << zeri[3] << ' '
         << sizeof(chars) << ' ' << sizeof(ac) << ' ' << lunghezza(ac) << '\n';
    return 0;
}
sizeof(ad) = 80, elementi = 10
dentro la funzione sizeof(a) = 4 (la dimensione di un puntatore)
7 6 9
1 7
qv - pv = 2 elementi = 8 byte
6 0 0 3 6 5

Si noti, riga per riga:

  • sizeof(ad) = 80 (10 double da 8 byte); dentro la funzione, sizeof(a) = 4: il compilatore avverte (sizeof on array function parameter will return size of 'double *'). L'informazione sulla dimensione è andata perduta.
  • p = &ad[5], p[2], p[-3]: scrivono ad[7], ad[2]; q = p + 2 e p += 2 finiscono nello stesso punto; q - &ad[0] vale 7 (elementi).
  • Con int v[4], pv = &v[2], qv = pv + 2 punta a &v[4], un elemento oltre la fine: è lecito formare quel puntatore (si usa come "fine" nei cicli) ma non dereferenziarlo. qv - pv vale 2 elementi, cioè 8 byte: l'indirizzo in qv è pv + 2*sizeof(int), mentre la differenza in C++ è 2, non 2*sizeof(int).
  • Inizializzazione: int ai[] = {1,2,...} deduce la dimensione (6); int ai2[8] = {1,2,3} mette 0 negli altri; double zeri[4] = {} tutti a 0.0; char chars[] = {'a','b','c'} ha 3 byte, nessun terminatore; char ac[] = "Beorn" ne ha 6, con lo 0 finale ('\0', non il carattere '0').

Decadimento di un array

Il nome di un array è un puntatore costante al suo primo elemento, un rvalue (un valore, non una variabile). Quindi non si può usare per la copia: x = y; con due array è un errore, e neanche int z[100] = y;. Per copiare serve un ciclo, std::copy o std::vector.

Passare un array a una funzione significa passare un puntatore: decadimento (array decay), con perdita dell'informazione sulla dimensione. È utile (evita di copiare 100 000 char in strlen(lots), che equivale a strlen(&lots[0])) ma un po' antiquato: deriva dal C e C++ lo mantiene per retrocompatibilità.

Stringhe in stile C

Le stringhe C sono array di char terminati da '\0', manipolabili con funzioni dedicate che assumono la presenza del terminatore: hanno le stesse limitazioni degli array. Sono un retaggio del C; il C++ offre il tipo std::string, di alto livello. La funzione lunghezza del programma conta i caratteri fino allo 0.

Problemi classici di puntatori e array

errore codice cosa succede
accesso con nullptr int* p = nullptr; *p = 7; errore a run-time
puntatore non inizializzato int* p; *p = 9; scrive in un posto casuale; tipico nei puntatori membro dimenticati
fuori dai limiti int a[10]; a[10] = 11; int* p = &a[10]; *p = 11; primo e ultimo elemento sono i punti critici; -Warray-bounds lo segnala
oggetto uscito dallo scope int* f() { int x = 7; return &x; } ... *f() = 15; x non esiste più (address of stack memory associated with local variable 'x' returned)
stesso, con una reference vector& ff() { vector x(7); return x; } x distrutto all'uscita: la reference punta a un oggetto morto

L'ultimo caso è logicamente analogo al precedente: ritornare una reference o un puntatore a una variabile locale è sempre un errore, perché il record di attivazione sparisce (C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →).

Domande d'esame

1. Definire l'aritmetica dei puntatori: concetto, operatori e operandi, due esempi. Traccia. Concetto: un puntatore tiene un indirizzo e il tipo dell'oggetto puntato, quindi la somma di un intero nn sposta di nn slot, cioè di n⋅sizeof(T)n \cdot \mathtt{sizeof}(T) byte. Operatori: + e - fra puntatore e intero, +=/-=, ++/--, - fra due puntatori (risultato in elementi), confronti; non si sommano due puntatori. Esempi: double* q = p + 2; e int* qv = pv + 2 con v[4] (un oltre la fine); p[2] equivale a *(p+2). Soluzioni complete: Esercizio - Aritmetica dei puntatori (primo appello 2021-22, teoria).

2. int v[4] = {0,1,2,3}; int *p = &v[2]; int *q = p + 2; — errore a run-time? a compile-time? Quanto vale q - p? Traccia. Nessun errore: q punta a v[4], un elemento oltre la fine, lecito finché non lo si dereferenzia. L'indirizzo in q è p + 2*sizeof(int); q - p vale 2 (elementi), non 2*sizeof(int).

3. Differenze fra puntatore e reference. Traccia. Tabella sopra: spostabile, nullo, dereference esplicito, shallow contro deep copy.

4. Cosa succede passando un array a una funzione? Cosa stampa sizeof dentro la funzione? Traccia. Decadimento a puntatore: la funzione riceve l'indirizzo del primo elemento e non conosce la dimensione; sizeof restituisce la dimensione del puntatore, non dell'array. L'accesso ai dati è lecito.

Versione ripasso

cpp
int var = 17;  int* ptr = &var;         // *ptr == 17
*pd = *pi;                              // copia il VALORE puntato
int i = pi;  pi = 7;  char* pc = pi;    // ERRORI: tipi diversi
cast quando rischio
static_cast<T>(x) conversioni "ragionevoli": double →\to int, void* →\to int* si perde il controllo del compilatore
reinterpret_cast<T>(x) indirizzi fissi dell'hardware (0xff), guardare i byte totalmente non portabile
const_cast<T>(x) codice di terzi che chiede un puntatore non const indefinito se l'oggetto è davvero const
dynamic_cast<T>(x) downcasting nelle gerarchie con funzioni virtuali solo con tipi polimorfici; nullptr o bad_cast
  • Parametri: tre modi di cambiare una variabile: x = incr_v(x) (restituisce), incr_p(&x) (puntatore), incr_r(x) (reference). incr_p deve gestire nullptr.
  • Funzione sugli array, codice: int lunghezza(const char a[]) { int count = 0; while (a[count]) ++count; return count; } (stringa C: si ferma al '\0'); strlen(lots) con char lots[100000] non copia 100 000 char perché il nome decade a &lots[0].
  • Errori tipici: credere che p + 1 sposti di 1 byte; dire che q - p vale 2*sizeof(int); dereferenziare &a[10]; confondere void e void*; sommare due puntatori; dire che double* a, b; dichiara due puntatori; usare reinterpret_cast dove basta static_cast.

Esercizi su questo argomento

Teoria collegata