Esercizio - Chiamata a funzione e layout della memoria (secondo appello 2021-22, teoria)
In questa pagina 3
Domanda aperta 1 del secondo appello 2021-22 (11 febbraio 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: 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++ - 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 →; per il caso con puntatori vedi Esercizio - Passaggio per copia e per riferimento con schema dello stack (secondo appello 2022-23, teoria).
Testo. Descrivere approfonditamente il meccanismo di chiamata a funzione in C++, esplicitando le strutture coinvolte e la relativa gestione della memoria. Disegnare il layout della memoria a seguito della chiamata alla funzione
f()riportata di seguito:float g(int b); int f(int a) { double in_a = a; int out_a = g(in_a); return out_a; } float g(int b) { int in_b = b; float out_bb; return out_bb; }
Il meccanismo di chiamata
- Record di attivazione (RA). Una chiamata a funzione crea una struttura dati, il function activation record, che contiene: una copia dei parametri (passaggio per valore), le variabili locali e le informazioni per tornare al chiamante (indirizzo di ritorno) e consegnare il risultato.
- Stack. I RA sono impilati in una zona di memoria detta stack, struttura LIFO. La chiamata inserisce un RA in cima; l'uscita lo toglie. Il RA ha dimensione che dipende dal numero e dal tipo delle variabili locali, ma l'inserimento richiede tempo costante.
- Ogni chiamata ha il suo RA, anche per la stessa funzione (ricorsione): non ci sono mai variabili condivise fra due chiamate, a meno che siano
statico globali (che vivono nel data/BSS, non nello stack). - Passi di una chiamata
y = g(x): si valutaxe lo si converte nel tipo del parametro; si crea il RA dig; il parametrobè inizializzato con la copia; si esegue il corpo;returncopia il valore di ritorno al chiamante e toglie il RA dig. - Altre regioni della memoria, non coinvolte nella chiamata ma presenti: text (il codice), data e BSS (globali e static), heap (
new/delete).
Cosa succede in f() e g()
Conversioni lungo la strada (da notare per l'esame):
| istruzione | conversione |
|---|---|
double in_a = a; |
int double, sicura |
g(in_a) |
argomento double parametro int b: troncamento (non sicura, accettata senza errore) |
int out_a = g(...) |
valore di ritorno float int: troncamento |
float out_bb; e return out_bb; |
variabile non inizializzata letta e restituita: valore indeterminato (comportamento indefinito); il testo la lascia così per concentrarsi sulla memoria |
Layout della memoria durante l'esecuzione di g()
Supponendo la chiamata f(7) fatta da main, nel momento in cui g sta eseguendo return lo stack contiene (dall'alto in basso, a indirizzi decrescenti):
indirizzi alti
+--------------------------------+
| RA di main |
| ... (variabile r, ecc.) |
+--------------------------------+
| RA di f |
| a int 4 B 7 | <- parametro: copia dell'argomento
| in_a double 8 B 7.0 | <- variabile locale
| out_a int 4 B (da assegnare)
| info di ritorno a main |
+--------------------------------+
| RA di g | <- cima dello stack
| b int 4 B 7 | <- copia di in_a troncata
| in_b int 4 B 7 |
| out_bb float 4 B ? | <- non inizializzata nel testo
| info di ritorno a f |
+--------------------------------+
indirizzi bassi (lo stack cresce verso il basso)
text : istruzioni di main, f, g
data/bss: nessuna variabile globale in questo codice
heap : vuoto (nessuna new)Quando g ritorna, il suo RA scompare e out_a riceve il valore restituito (convertito a int); poi f ritorna e scompare anche il suo RA. Nessuna variabile locale sopravvive: le variabili automatiche muoiono con il RA.
Un programma che mostra l'ordine reale dei RA (distanze in byte da una variabile di main, dove i numeri negativi vanno verso gli indirizzi bassi; con out_bb = 2.75 inizializzato):
#include <iostream>
using namespace std;
char* base; // un punto di riferimento nel RA di main
long dist(const void* p) { return static_cast<long>(static_cast<const char*>(p) - base); }
float g(int b);
int f(int a)
{
double in_a = a; // int -> double: conversione sicura
int out_a = g(in_a); // double -> int in ingresso a g (troncamento), float -> int in uscita
cout << "RA di f: &a = " << dist(&a) << ", &in_a = " << dist(&in_a) << ", &out_a = " << dist(&out_a) << '\n';
return out_a;
}
float g(int b)
{
int in_b = b;
float out_bb = 2.75f; // nel testo e' dichiarata e restituita senza valore: qui la inizializziamo
cout << "RA di g: &b = " << dist(&b) << ", &in_b = " << dist(&in_b) << ", &out_bb = " << dist(&out_bb) << '\n';
return out_bb;
}
int main()
{
char riferimento;
base = &riferimento;
int r = f(7);
cout << "f(7) = " << r << '\n'; // out_bb = 2.75 -> int = 2
return 0;
}RA di g: &b = -51, &in_b = -55, &out_bb = -59
RA di f: &a = -19, &in_a = -27, &out_a = -31
f(7) = 2L'RA di g ha indirizzi più bassi di quello di f (che sono più bassi di main): lo stack cresce verso il basso. In ciascun RA i dati sono a distanza di pochi byte. Con f(7): in_a = 7.0, g(7.0) riceve b = 7, out_bb = 2.75 torna come 2.75f e out_a diventa 2 (troncamento).
Versione ripasso
- Meccanismo: ogni chiamata crea un RA (copia dei parametri, variabili locali, info di ritorno), impilato nello stack (LIFO); inserimento in tempo costante; dimensione variabili locali; ogni chiamata ha il suo RA; all'uscita il RA sparisce (le automatiche muoiono). Altre regioni: text, data, BSS, heap. 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 →.
- Codice dato:
f(int a):in_a(double),out_a(int) eg(in_a);g(int b):in_b(int),out_bb(float) restituita senza valore. - Conversioni:
intdoublesicura;g(in_a):doubleint(tronca); ritornofloatint(tronca);out_bbnon inizializzata = valore indeterminato (UB). - Layout con
gin esecuzione (dall'alto verso il basso):main| RA dif:a(int 4 B),in_a(double 8 B),out_a(int 4 B), ritorno | RA dig:b(4 B),in_b(4 B),out_bb(float 4 B), ritorno cima. Stack verso indirizzi bassi; data/BSS e heap senza variabili.
RA di main -> RA di f (a, in_a, out_a) -> RA di g (b, in_b, out_bb) <- cima- Verifica con il programma: distanze da
main:fa ;ga :gpiù in basso dif.f(7)restituisce 2 (da2.75f). - Errori tipici: mettere il RA nell'heap o nel free store; dire che il RA ha dimensione fissa; dimenticare che le conversioni double int tagliano; disegnare le locali in ordine di indirizzo crescente senza dire in che direzione cresce lo stack.