Esercizio - Puntatori, array e passaggio dei parametri (laboratorio 3)
In questa pagina 4
Prima parte del terzo laboratorio del corso di Laboratorio di Programmazione, Ing. Informatica UniPD: domande di discussione e tre esercizi su puntatori, array in stile C e passaggio dei parametri. La seconda parte (la classe MyVector) è in Esercizio - MyVector con memoria dinamica (laboratorio 3). 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 →, 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 →.
Domande di discussione
Testo (dal libro di testo, cap. 17, domande di ripasso). 1. What is a dereference operator and why do we need one? 2. What is an address? How are memory addresses manipulated in C++? 3. What information about a pointed-to object does a pointer have? What useful information does it lack? 4. What can a pointer point to? 5. When do we need a pointer (instead of a reference or a named object)?
1. L'operatore di dereference (*). Dato un puntatore, *p dà l'oggetto puntato: serve perché un puntatore contiene un indirizzo, non il dato; senza dereference non si potrebbe leggere né scrivere ciò a cui punta. *p è un lvalue: *p = 7; scrive nell'oggetto puntato.
2. Indirizzo. Un numero che identifica un byte della memoria (la memoria è indirizzata a byte). In C++ si ottiene con & (&x), si conserva in una variabile puntatore, si sposta con l'aritmetica dei puntatori (p + 1), si confronta (==, <), si converte con i cast (void*, reinterpret_cast). Non si fanno somme fra due indirizzi.
3. Che informazioni ha un puntatore? L'indirizzo e il tipo dell'oggetto puntato (quindi quanti byte leggere e come interpretarli). Non ha: se l'oggetto esiste ancora (può essere pendente), se l'indirizzo è valido o inizializzato, quanti elementi ci sono (è il primo di un array? di quanti?), a chi appartiene la memoria (chi deve liberarla).
4. A che cosa può puntare? A una variabile (&x), a un elemento di un array, a un oggetto sul free store (new), a un altro puntatore, a una funzione, a nulla (nullptr); con void* a memoria grezza di tipo non specificato.
5. Quando serve un puntatore? Quando l'oggetto può non esserci (nullptr), quando deve cambiare oggetto durante la vita del puntatore (una reference non si sposta), quando si usa memoria dinamica (new restituisce un puntatore), per scorrere array e buffer, per interfacciarsi con codice C e per il polimorfismo con puntatori a classi base. In tutti gli altri casi: un oggetto con nome o una reference, più sicuri.
Esercizio 1: array, puntatore a un elemento e scritture fuori dai limiti
Testo. Creare un programma che: a. definisce una funzione
f(), chiamata dalmain, che definisce un array stile C di 10intcome variabile locale automatica; b. inf(), crea un puntatore a uno degli elementi dell'array a scelta (non il primo) e scrive su tutto l'array usando l'operatore[]applicato al puntatore; c. definisce una funzionef_illegal()che definisce un array analogo ma scrive fuori dai limiti; verificare se questo causa un errore in esecuzione o meno in funzione di quanto dista la memoria a cui si accede illegalmente dall'array.
Il puntatore a un elemento centrale, p = &a[4], raggiunge l'intero array con indici da a : p[i] equivale a *(p + i), quindi p[-4] è a[0] e p[5] è a[9]. I limiti restano quelli dell'array: i puntatori non li conoscono.
Per f_illegal la distanza dall'ultimo elemento viene letta da cin, così si può provare con valori diversi senza ricompilare.
#include <iostream>
using namespace std;
constexpr int N = 10;
void f()
{
int a[N]; // array stile C, variabile locale automatica (nello stack)
int* p = &a[4]; // puntatore a un elemento in mezzo (non il primo)
// con l'operatore [] applicato al puntatore si raggiunge TUTTO l'array: indici da -4 a +5
for (int i = -4; i <= 5; ++i)
p[i] = (i + 4) * 10; // p[-4] e' a[0], p[5] e' a[9]
for (int i = 0; i < N; ++i)
cout << a[i] << ' ';
cout << '\n';
}
// Scrive FUORI dai limiti: a[N + distanza]. L'esito dipende da che cosa c'e' in quella zona di memoria.
void f_illegal(int distanza)
{
int a[N];
for (int i = 0; i < N; ++i) a[i] = i;
a[N - 1 + distanza] = 12345; // distanza = 0: ultimo elemento (lecito); 1: subito dopo la fine...
cout << "scritto a[" << N - 1 + distanza << "] senza errori\n";
}
int main()
{
f();
int distanza;
cin >> distanza;
f_illegal(distanza);
cout << "il programma e' arrivato alla fine\n";
return 0;
}Con l'ingresso 7:
0 10 20 30 40 50 60 70 80 90
scritto a[16] senza errori
il programma e' arrivato alla fineEsecuzioni al variare della distanza (a[9 + distanza]; il programma è compilato senza controlli sui limiti, come con g++):
| distanza | indice scritto | esito |
|---|---|---|
| 0 | a[9] |
lecito: ultimo elemento |
| 1 | a[10] |
nessun errore visibile: si sovrascrive la memoria subito dopo l'array |
| 5, 7 | a[14], a[16] |
nessun errore visibile |
| 100 | a[109] |
nessun errore visibile |
a[1000009] |
errore a run-time (accesso fuori dalla memoria del programma: su Linux segmentation fault, qui memory access out of bounds) |
|
a[100000009] |
errore a run-time |
Conclusioni:
- Scrivere appena oltre la fine dell'array non dà errore: i byte sono dentro la memoria del programma (altre variabili locali, il record di attivazione del chiamante, altri dati). Il programma sembra funzionare ma ha corrotto altri dati; il sintomo può comparire molto dopo e altrove. Con altri compilatori o altre ottimizzazioni la stessa riga si comporta in modo diverso: è comportamento indefinito.
- Scrivere molto lontano esce dalla memoria assegnata al processo: errore immediato (segmentation fault).
- Il compilatore, se vede un indice costante, può avvertire (
array index 10 is past the end of the array), ma con un indice letto dacinnon può: il controllo non esiste. - È il motivo per cui
std::vectorconat()controlla i limiti e per cuiMyVectorhasafe_get/safe_set(Esercizio - MyVector con memoria dinamica (laboratorio 3)).
Esercizio 2: sizeof e decadimento dell'array
Testo. Creare un programma che: a. definisce nel
mainun array didoublegrande a piacere (la dimensione definita conconstexpro variabile costante); b. stampa la dimensione dell'array consizeof; c. passa l'array come argomento a una funzioneprint_array_length()che stampa la dimensione dell'array consizeof. La funzione conosce la dimensione dell'array? Riesce ad accedere ai dati? L'accesso è lecito? Commentare i risultati.
#include <iostream>
using namespace std;
// "double a[]" e "double* a" sono la stessa cosa: l'array DECADE a puntatore al primo elemento
void print_array_length(double a[])
{
cout << "dentro la funzione: sizeof(a) = " << sizeof(a) << " byte (un puntatore)\n";
cout << "ma a[0] = " << a[0] << " e a[3] = " << a[3] << " sono raggiungibili\n";
}
// la soluzione corretta: passare ANCHE la lunghezza
double somma(const double* a, int n)
{
double s = 0;
for (int i = 0; i < n; ++i) s += a[i];
return s;
}
int main()
{
constexpr int n = 8;
double v[n] = {1, 2, 3, 4, 5, 6, 7, 8};
cout << "nel main: sizeof(v) = " << sizeof(v) << " byte, elementi = " << sizeof(v) / sizeof(v[0]) << '\n';
print_array_length(v);
cout << "somma = " << somma(v, n) << '\n';
return 0;
}nel main: sizeof(v) = 64 byte, elementi = 8
dentro la funzione: sizeof(a) = 4 byte (un puntatore)
ma a[0] = 1 e a[3] = 4 sono raggiungibili
somma = 36- Nel
main,sizeof(v)byte: l'array è un oggetto di tipodouble[8]. Il numero di elementi si ottiene consizeof(v) / sizeof(v[0]). - Nella funzione,
sizeof(a)vale 4 (la dimensione di un puntatore; 8 sui PC a 64 bit): il parametrodouble a[]è in realtàdouble* a. Il compilatore avverte (sizeof on array function parameter will return size of 'double *' instead of 'double[]'). - La funzione non conosce la dimensione: passando l'array se ne passa l'indirizzo del primo elemento (decadimento) e si perde il numero di elementi.
- Riesce ad accedere ai dati e l'accesso è lecito:
a[0],a[3]sono raggiungibili tramite l'indirizzo. È lecito se l'indice sta nei limiti, che la funzione però non può verificare da sola. La soluzione è passare anche la lunghezza (somma(const double* a, int n)), oppure usarevector/array, che la portano con sé.
Esercizio 3: stesso dato, per reference e per puntatore
Testo. Creare un programma che: a. definisce una variabile
inte unadoublenelmain; b. le stampa da una funzioneprint_reference()a cui sono passate per const reference; c. le stampa da una funzioneprint_pointer()a cui sono passate per puntatore.
#include <iostream>
using namespace std;
// per const reference: nessuna copia, sola lettura, nessun valore "nullo" possibile
void print_reference(const int& i, const double& d)
{
cout << "reference: i = " << i << ", d = " << d << '\n';
}
// per puntatore: il chiamante deve passare &, e la funzione deve gestire nullptr
void print_pointer(const int* i, const double* d)
{
if (i == nullptr || d == nullptr) {
cout << "pointer: argomento nullo\n";
return;
}
cout << "pointer: i = " << *i << ", d = " << *d << '\n';
}
int main()
{
int i = 42;
double d = 2.5;
print_reference(i, d); // si passano gli oggetti
print_pointer(&i, &d); // si passano gli indirizzi
print_pointer(nullptr, &d); // solo il puntatore ammette "nessun oggetto"
print_reference(7, 1.5); // la const reference accetta anche i literal
return 0;
}reference: i = 42, d = 2.5
pointer: i = 42, d = 2.5
pointer: argomento nullo
reference: i = 7, d = 1.5Le differenze nell'uso:
print_reference(const int&, const double&) |
print_pointer(const int*, const double*) |
|
|---|---|---|
| chiamata | print_reference(i, d) |
print_pointer(&i, &d) |
| accesso | direttamente i |
*i |
| argomenti nulli | impossibili | nullptr possibile: la funzione deve controllare |
| literal | print_reference(7, 1.5) ok (oggetto temporaneo) |
no: &7 non esiste |
| lettura | const impedisce di modificare |
const int* (puntatore a costante) impedisce di modificare il dato |
Criterio (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 →): se nessun oggetto è un valore plausibile, puntatore; altrimenti reference o const reference. Qui l'input è sempre presente: print_reference è la scelta naturale.
Versione ripasso
- Domande: (1)
*p= oggetto puntato (lvalue), serve perché il puntatore contiene un indirizzo; (2) indirizzo = numero di un byte;&x, variabili puntatore, aritmetica, confronti, cast; (3) il puntatore ha indirizzo e tipo, non sa se l'oggetto esiste/è valido, quanti elementi ci sono, chi lo possiede; (4) può puntare a variabile, elemento di array, oggetto sul free store, puntatore, funzione,nullptr,void*per memoria grezza; (5) serve quando l'oggetto può mancare, deve cambiare, memoria dinamica, array/buffer, codice C, polimorfismo. - Esercizio 1:
int a[10]; int* p = &a[4];ep[i]per da a scrive tutto l'array (p[-4]èa[0],p[5]èa[9]).
void f() { int a[10]; int* p = &a[4];
for (int i = -4; i <= 5; ++i) p[i] = (i + 4) * 10; } // a = 0 10 20 ... 90
void f_illegal(int distanza) { int a[10]; a[9 + distanza] = 12345; } // fuori dai limiti- Fuori dai limiti: distanza 1, 5, 100: nessun errore visibile (si corrompono altri dati, comportamento indefinito); distanza o : errore a run-time (segmentation fault). Il compilatore avverte solo se l'indice è costante.
- Esercizio 2: nel
mainsizeof(v) = 64(8double); nella funzionesizeof(a) = 4(un puntatore):double a[]èdouble* a. La funzione non conosce la dimensione, accede ai dati (lecito se l'indice è nei limiti); si passa anche la lunghezza,somma(const double* a, int n), o si usavector. - Esercizio 3:
print_reference(const int&, const double&)chiamata con gli oggetti, nessun nullo, accetta literal;print_pointer(const int*, const double*)chiamata con&, accesso con*, controllanullptr. Stampanoi = 42, d = 2.5;print_pointer(nullptr, &d)"argomento nullo". - Programmi, codice:
void print_reference(const int& i, const double& d) { cout << "reference: i = " << i << ", d = " << d << '\n'; }
void print_pointer(const int* i, const double* d)
{ if (i == nullptr || d == nullptr) { cout << "pointer: argomento nullo\n"; return; }
cout << "pointer: i = " << *i << ", d = " << *d << '\n'; }
double somma(const double* a, int n) { double s = 0; for (int i = 0; i < n; ++i) s += a[i]; return s; }
// main: print_reference(i, d); print_pointer(&i, &d); print_pointer(nullptr, &d); print_reference(7, 1.5);- Esercizio 2, uscita:
nel main: sizeof(v) = 64 byte, elementi = 8;dentro la funzione: sizeof(a) = 4 byte (un puntatore);somma = 36. - Errori tipici: credere che
sizeofdi un parametro array dia la dimensione dell'array; scrivere oltre la fine dell'array pensando che "non succeda niente"; dereferenziarenullptrnella versione a puntatore senza controllo.