Esercizio - Passaggio per copia e per riferimento con schema dello stack (secondo appello 2022-23, teoria)
In questa pagina 3
Domanda aperta 3 del secondo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: 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 →, 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 →, 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 →. Un esercizio con lo schema dei record di attivazione di due funzioni: Esercizio - Chiamata a funzione e layout della memoria (secondo appello 2021-22, teoria).
Testo. Descrivere la differenza tra passaggio di argomenti per copia e per riferimento in C++. Riportare un esempio di codice di una chiamata a funzione con un parametro passato per copia, e un altro esempio di una chiamata a funzione con un parametro passato per riferimento. Dato il seguente codice:
#include <iostream> void f(int *q) { std::cout << "The value is " << *q << std::endl; } int main() { int i = 5; int *p = &i; f(p); return 0; }Riportare lo schema dello stack (inclusi i dettagli sulle variabili
i,p,q) durante l'esecuzione dif().
Copia contro riferimento
| per copia (per valore) | per riferimento | |
|---|---|---|
| sintassi | void f(int x) |
void f(int& r) (o const int& r) |
| che cosa riceve la funzione | una variabile locale nuova, inizializzata con una copia del valore dell'argomento | un alias (un secondo nome) dell'oggetto del chiamante |
| modifiche | cambiano solo la copia; il chiamante non le vede | cambiano l'originale (con const&: nessuna modifica permessa) |
| costo | copia dell'oggetto: banale per int, pesante per vector, immagini, testi |
nessuna copia: si passa l'indirizzo |
| argomento | qualunque espressione | int& richiede un lvalue; const int& accetta anche literal (temporaneo) |
| quando | oggetti piccoli, dati da non modificare | oggetti grandi (con const), funzioni che devono modificare o produrre più di un output |
Il passaggio per puntatore (come in f(int* q)) è un caso particolare del passaggio per copia: si copia l'indirizzo, quindi la funzione può modificare l'oggetto puntato (*q) ma non cambiare il puntatore del chiamante.
Un esempio per ciascuno
#include <iostream>
using namespace std;
char* base; // riferimento nel RA di main
long dist(const void* p) { return static_cast<long>(static_cast<const char*>(p) - base); }
// PER COPIA: x e' una variabile locale, copia del valore di a
void per_copia(int x)
{
x = x + 1; // modifica solo la copia
cout << " per_copia: x = " << x << '\n';
}
// PER RIFERIMENTO: r e' un alias di b: nessuna copia
void per_riferimento(int& r)
{
r = r + 1; // modifica l'originale
cout << " per_riferimento: r = " << r << '\n';
}
// il codice dell'esercizio: q e' una COPIA del puntatore p
void f(int* q)
{
std::cout << "The value is " << *q << std::endl;
cout << " dentro f: &q = " << dist(&q) << ", q = " << dist(q) << " (indirizzo di i, relativo)\n";
}
int main()
{
char riferimento;
base = &riferimento;
int a = 10, b = 10;
per_copia(a);
per_riferimento(b);
cout << "dopo: a = " << a << " (invariata), b = " << b << " (modificata)\n";
int i = 5;
int* p = &i;
cout << "in main: &i = " << dist(&i) << ", &p = " << dist(&p) << ", p = " << dist(p) << " (indirizzo di i)\n";
f(p);
return 0;
} per_copia: x = 11
per_riferimento: r = 11
dopo: a = 10 (invariata), b = 11 (modificata)
in main: &i = -11, &p = -15, p = -11 (indirizzo di i)
The value is 5
dentro f: &q = -35, q = -11 (indirizzo di i, relativo)per_copia(a):xè una copia (10), diventa 11 dentro la funzione; fuoriavale ancora 10.per_riferimento(b):rè un alias dib;r = r + 1modificab: fuori vale 11.
Lo schema dello stack durante f()
Variabili coinvolte: i e p sono locali di main; q è il parametro di f, quindi appartiene al record di attivazione di f. Il passaggio di p a f(int* q) è per copia del puntatore: q è una variabile nuova, distinta da p, che contiene lo stesso indirizzo.
indirizzi alti
+------------------------------------------+
| RA di main |
| i : int valore 5 | <- indirizzo, per esempio, 0x1000
| p : int* valore 0x1000 (= &i) | <- indirizzo, per esempio, 0x0ff8; punta a i
+------------------------------------------+
| RA di f | <- cima dello stack
| q : int* valore 0x1000 (= &i) | <- indirizzo, per esempio, 0x0fe8; COPIA di p
| informazioni di ritorno a main |
+------------------------------------------+
indirizzi bassi (lo stack cresce verso il basso)
p ---------+
v
i = 5 <---------- q (p e q sono due puntatori diversi che puntano allo stesso int)| variabile | dove vive | tipo | contenuto durante f() |
|---|---|---|---|
i |
RA di main |
int |
5 |
p |
RA di main |
int* |
l'indirizzo di i |
q |
RA di f |
int* |
l'indirizzo di i (copia del valore di p) |
*q legge i (): per questo f stampa The value is 5. Nel programma sopra (distanze in byte da una variabile di main) si vede lo stesso schema: &i = -11, &p = -15 e, dentro f, q = -11 (lo stesso indirizzo di i) mentre &q = -35: q è una variabile a un indirizzo più basso, nel RA di f, diverso da quello di p. Se f facesse q = nullptr; non cambierebbe p; se facesse *q = 7; cambierebbe i.
Versione ripasso
- Per copia (
void f(int x)): la funzione riceve una variabile locale nuova con una copia del valore; le modifiche non escono; costo dimensione. Per riferimento (void f(int& r)): alias dell'oggetto del chiamante; nessuna copia; le modifiche si vedono (conconst&non si può modificare);int&vuole un lvalue,const int&accetta i literal. Per puntatore = copia dell'indirizzo: si modifica*q, non il puntatore del chiamante.
void per_copia(int x) { x = x + 1; } // a = 10 resta 10
void per_riferimento(int& r) { r = r + 1; } // b = 10 diventa 11- Schema dello stack durante
f()(dall'alto, indirizzi decrescenti):
RA di main: i = 5 ; p = &i (int*, punta a i)
RA di f: q = &i (int*, COPIA di p) ; informazioni di ritorno <- cima| variabile | RA | tipo | contenuto |
|---|---|---|---|
i |
main |
int |
5 |
p |
main |
int* |
indirizzo di i |
q |
f |
int* |
indirizzo di i (copia di p) |
peqsono due puntatori diversi che puntano allo stessoint;*qlegge 5 ("The value is 5");q = nullptr;non cambiap,*q = 7;cambiai. Verifica:&i = -11,&p = -15,q = -11,&q = -35(distanze damain).- Errori tipici: mettere
qnel RA dimain; disegnareqcome alias dip(è una copia); dire che il passaggio per puntatore sia "per riferimento"; dimenticare che lo stack cresce verso il basso.