Salta al contenuto
Note per Studenti Esercizio - Passaggio per copia e per riferimento con schema dello stack (secondo appello 2022-23, teoria)

Esercizio - Passaggio per copia e per riferimento con schema dello stack (secondo appello 2022-23, teoria)

Esame
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 di f().

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

passaggio_stack.cppcpp
#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; fuori a vale ancora 10.
  • per_riferimento(b): r è un alias di b; r = r + 1 modifica b: 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 (55): 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 ∝\propto dimensione. Per riferimento (void f(int& r)): alias dell'oggetto del chiamante; nessuna copia; le modifiche si vedono (con const& 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.
cpp
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)
  • p e q sono due puntatori diversi che puntano allo stesso int; *q legge 5 ("The value is 5"); q = nullptr; non cambia p, *q = 7; cambia i. Verifica: &i = -11, &p = -15, q = -11, &q = -35 (distanze da main).
  • Errori tipici: mettere q nel RA di main; disegnare q come alias di p (è una copia); dire che il passaggio per puntatore sia "per riferimento"; dimenticare che lo stack cresce verso il basso.

Esercizi su questo argomento

Teoria collegata