Salta al contenuto
Note per Studenti Esercizio - Aritmetica dei puntatori (primo appello 2021-22, teoria)

Esercizio - Aritmetica dei puntatori (primo appello 2021-22, teoria)

Esame
In questa pagina 3

Domanda aperta 3 del primo appello 2021-22 (25 gennaio 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. La stessa domanda è ricomparsa nel quarto appello 2022-23: Esercizio - Aritmetica dei puntatori (quarto appello 2022-23, teoria), con altri esempi. Teoria: C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast →.

Testo. Definire l'aritmetica dei puntatori illustrando: 1) il concetto alla base dell'aritmetica; 2) con quali operatori e operandi funziona; 3) due esempi di funzionamento, ciascuno corredato da codice che deve essere commentato mediante descrizione e/o grafici.

1. Il concetto

Un puntatore T* contiene un indirizzo e conosce il tipo T dell'oggetto a cui punta, quindi la sua dimensione sizeof(T). L'aritmetica dei puntatori sfrutta questa informazione: sommare o sottrarre un intero nn a un puntatore lo sposta di nn slot, cioè di nn elementi di tipo T e non di nn byte. In byte lo spostamento è

n×sizeof(T)n \times \mathtt{sizeof}(T)

È utile perché un array è una sequenza di oggetti contigui: partendo dall'indirizzo del primo, con questo meccanismo si raggiunge qualsiasi elemento, e p[i] è per definizione *(p + i). L'aritmetica è sensibile al contesto: lo stesso + 2 sposta di 8 byte su un int* (con int da 4 byte) e di 16 su un double*.

2. Operatori e operandi

operazione operandi risultato
p + n, n + p puntatore, intero puntatore, nn elementi più avanti
p - n puntatore, intero puntatore, nn elementi indietro
p += n, p -= n puntatore, intero sposta p
++p, p++, --p, p-- puntatore sposta di un elemento
q - p due puntatori dello stesso tipo, nello stesso array intero con segno: numero di elementi fra i due (non di byte)
p == q, p != q, p < q, p > q due puntatori dello stesso array confronto degli indirizzi
p[i], *(p + i) puntatore, intero l'ii-esimo elemento (anche i<0i < 0)

Non esistono: la somma di due puntatori (p + q), il prodotto o la divisione. Le operazioni sono lecite solo restando dentro l'array puntato, più la posizione "uno dopo l'ultimo" (che si può formare e confrontare ma non dereferenziare); fuori è comportamento indefinito.

3. Due esempi

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

int main()
{
    // ESEMPIO 1: scorrere un array spostando il puntatore
    int a[5] = {10, 20, 30, 40, 50};
    int* p = &a[0];                                  // oppure p = a (decadimento)
    for (int* q = p; q != p + 5; ++q)                // p + 5 e' "uno dopo l'ultimo": si puo' confrontare, non dereferenziare
        cout << *q << ' ';
    cout << '\n';

    // ESEMPIO 2: p + n e differenza fra puntatori
    int* r = p + 3;                                  // punta a a[3] = 40: avanza di 3 ELEMENTI (3 * sizeof(int) byte)
    cout << "*r = " << *r << ", r[-2] = " << r[-2] << '\n';       // r[-2] e' a[1]
    cout << "r - p = " << r - p << " elementi, in byte = "
         << reinterpret_cast<char*>(r) - reinterpret_cast<char*>(p) << '\n';
    cout << "r > p: " << (r > p) << '\n';
    // p + r;                                        // errore: non si sommano due puntatori

    // stessa aritmetica con tipi di dimensione diversa
    double d[3] = {1.5, 2.5, 3.5};
    double* pd = d;
    pd += 2;                                         // avanza di 2 double = 16 byte
    cout << "*pd = " << *pd << ", sizeof(double) = " << sizeof(double) << '\n';
    return 0;
}
10 20 30 40 50 
*r = 40, r[-2] = 20
r - p = 3 elementi, in byte = 12
r > p: 1
*pd = 3.5, sizeof(double) = 8

Esempio 1: scorrere un array con un puntatore. int a[5] = {10, 20, 30, 40, 50}; int* p = &a[0];. Il ciclo for (int* q = p; q != p + 5; ++q) sposta q di un elemento a ogni giro. Il punto p + 5 è "uno dopo l'ultimo": serve da condizione di fine (si confronta, non si dereferenzia). Stampa 10 20 30 40 50.

indice:      a[0]  a[1]  a[2]  a[3]  a[4]  (a[5]: oltre la fine)
valore:       10    20    30    40    50
indirizzo:    A    A+4   A+8  A+12  A+16   A+20
              ^                           ^
              p                         p + 5

Esempio 2: spostamenti e differenza fra puntatori. int* r = p + 3; punta a a[3] =40= 40: l'indirizzo è A+3⋅4=A+12A + 3 \cdot 4 = A + 12. Quindi *r = 40, r[-2] è a[1] =20= 20 (indici negativi: si torna indietro dall'elemento puntato). La differenza r - p vale 3 (elementi), mentre in byte la distanza è 3×4=123 \times 4 = 12 (convertendo i puntatori a char* si leggono 12 byte). Con tipi più grandi lo slot cambia: double* pd = d; pd += 2; sposta di 16 byte e punta al terzo elemento, 3.5.

p -> a[0]=10   a[1]=20   a[2]=30   r -> a[3]=40   a[4]=50
     <---------- r - p = 3 elementi = 12 byte ---------->

Casi limite da conoscere: p + r non compila; *(p + 5) legge fuori dall'array (indefinito); r > p è vero (indirizzo maggiore).

Versione ripasso

  • Concetto: T* ha indirizzo + tipo ⇒\Rightarrow p + n sposta di nn elementi, cioè n×sizeof(T)n \times \mathtt{sizeof}(T) byte. Serve per scorrere array contigui: p[i] ≡\equiv *(p + i). Dipende dal tipo: + 2 = 8 byte su int*, 16 su double*.
  • Operatori e operandi:
operazione operandi risultato
p ± n, p += n, ++p puntatore, intero puntatore spostato
q - p due puntatori stesso array numero di elementi (con segno)
== != < > due puntatori stesso array confronto
p[i] puntatore, intero elemento ii (anche negativo)

Non esiste p + q. Lecito solo dentro l'array e fino a "uno dopo l'ultimo" (non dereferenziabile).

  • Esempio 1: int a[5] = {10,20,30,40,50}; int* p = &a[0]; for (int* q = p; q != p + 5; ++q) cout << *q << ' '; →\to 10 20 30 40 50; p + 5 = fine.
  • Esempio 2: int* r = p + 3; →\to *r = 40, r[-2] = 20, r - p = 3 elementi = 12 byte; double* pd = d; pd += 2; →\to 16 byte, *pd = 3.5; r > p vero.
p -> 10  20  30  r -> 40  50  (fine = p + 5)        r - p = 3 elementi = 3 * sizeof(int) = 12 byte
  • Errori tipici: credere che p + 1 sposti di un byte; dire che q - p vale n×sizeofn \times \mathtt{sizeof} (è nn); sommare due puntatori; dereferenziare p + 5; usare l'aritmetica fuori dall'array.

Esercizi su questo argomento

Teoria collegata