Variabili, Scope e Durata di Vita
Impara le quattro durate di archiviazione, come scope e durata di vita sono concetti distinti, le insidie dell'inizializzazione e il problema dell'ordine di inizializzazione statica.
Scope e Durata di Vita Non Sono la Stessa Cosa
Uno dei fraintendimenti più comuni in C++ è confondere lo scope con la durata di vita. Sono concetti correlati ma distinti:
Lo scope è una proprietà a tempo di compilazione — determina dove un nome è visibile nel codice sorgente. Una variabile dichiarata all'interno di un blocco {} è visibile solo all'interno di quel blocco e di tutti i blocchi annidati.
La durata di vita è una proprietà a runtime — determina quando un oggetto esiste in memoria. La durata di vita di un oggetto inizia quando il suo costruttore termina e finisce quando il suo distruttore inizia.
In molti casi, scope e durata di vita coincidono: una variabile locale è visibile all'interno del suo blocco e vive fino alla fine del blocco. Ma possono divergere. Una variabile locale static è visibile solo all'interno della sua funzione (scope limitato) ma vive per l'intera durata del programma (durata di vita illimitata). Un oggetto allocato dinamicamente ha una durata di vita che si estende oltre lo scope del puntatore che l'ha creato.
Le Quattro Durate di Archiviazione
Il C++ definisce quattro durate di archiviazione che controllano quando gli oggetti vengono creati e distrutti:
#include <iostream>
#include <string>
// Static storage duration — lives for the entire program
int global_count = 0; // Initialized before main()
static int file_local = 42; // Same, but only visible in this file
void demonstrate_storage() {
// Automatic storage duration — created on entry, destroyed on exit
int local = 10; // Born here, dies at closing brace
// Static storage duration (local scope)
static int call_count = 0; // Initialized ONCE, persists across calls
++call_count;
std::cout << "Call #" << call_count << '\n';
// Thread-local storage duration (C++11)
// thread_local int per_thread = 0; // One copy per thread, lives until thread ends
}
int main() {
demonstrate_storage(); // Call #1
demonstrate_storage(); // Call #2 — call_count is still alive!
demonstrate_storage(); // Call #3
// Dynamic storage duration — you control the lifetime
int* heap_int = new int{99}; // Born here
std::cout << *heap_int << '\n';
delete heap_int; // Dies here — you must not forget!
heap_int = nullptr; // Good practice: nullify after delete
// In modern C++, prefer smart pointers over raw new/delete
// auto ptr = std::make_unique<int>(99); // Covered in later modules
}Premi Esegui per compilare ed eseguire questo esempio.
Forme di Inizializzazione e Zero-Initialization
Il C++ ha tre concetti fondamentali di inizializzazione:
Inizializzazione predefinita — la variabile viene costruita senza un inizializzatore. Per i tipi integrati (int, double, ecc.), questo significa che il valore è indeterminato (leggerlo è un comportamento indefinito!). Per i tipi classe, viene chiamato il costruttore predefinito.
Inizializzazione per valore — attivata da parentesi vuote T() o parentesi graffe vuote T{}. Per i tipi integrati, questo inizializza a zero: gli interi diventano 0, i puntatori diventano nullptr, i booleani diventano false. Per i tipi classe, viene chiamato il costruttore predefinito (e i membri vengono inizializzati per valore se nessun costruttore li inizializza).
Zero initialization — una fase specifica che si verifica per le variabili static/thread-local prima di qualsiasi altra inizializzazione. Tutte le variabili statiche vengono inizializzate a zero prima dell'avvio del programma.
Ecco perché static int x; è garantito essere 0, ma un int x; locale non lo è — la variabile locale è inizializzata in modo predefinito (indeterminata), mentre la variabile statica passa prima attraverso la zero initialization.
Inizializzazione in Pratica
Capire quando una variabile è zero rispetto a indeterminata è fondamentale per evitare bug:
#include <iostream>
struct Point {
int x;
int y;
};
static int global_int; // Zero-initialized → 0
static Point global_point; // Zero-initialized → {0, 0}
int main() {
int a; // Default-initialized → INDETERMINATE (UB to read!)
int b{}; // Value-initialized → 0
int c = int(); // Value-initialized → 0
int d = {}; // Value-initialized → 0
Point p1; // Default-initialized → members are INDETERMINATE
Point p2{}; // Value-initialized → {0, 0}
Point p3{1, 2}; // Aggregate-initialized → {1, 2}
// Array initialization
int arr1[5]; // Default-initialized → INDETERMINATE values!
int arr2[5]{}; // Value-initialized → {0, 0, 0, 0, 0}
int arr3[5]{1, 2}; // Partially initialized → {1, 2, 0, 0, 0}
std::cout << "global_int: " << global_int << '\n'; // Safe: 0
std::cout << "b: " << b << '\n'; // Safe: 0
// std::cout << a; // DANGER: UB — reading uninitialized variable!
std::cout << "p2: (" << p2.x << ", " << p2.y << ")\n"; // Safe: (0, 0)
std::cout << "p3: (" << p3.x << ", " << p3.y << ")\n"; // Safe: (1, 2)
}Premi Esegui per compilare ed eseguire questo esempio.
Ordine di Distruzione: LIFO
Gli oggetti con durata di archiviazione automatica (variabili locali) vengono distrutti nell'ordine inverso alla loro costruzione — Last In, First Out (LIFO). Questa non è solo una convenzione; lo standard lo garantisce.
Questo è enormemente importante quando gli oggetti dipendono l'uno dall'altro. Se l'oggetto b referenzia l'oggetto a, devi dichiarare a prima di b. Poi b viene distrutto per primo (mentre a è ancora vivo), e a viene distrutto per secondo. Se l'ordine fosse invertito, il distruttore di b potrebbe provare a usare un a morto — comportamento indefinito.
Per le variabili statiche, la distruzione avviene nell'ordine inverso alla costruzione dopo il ritorno di main(). Le variabili globali definite nella stessa unità di traduzione sono costruite dall'alto verso il basso e distrutte dal basso verso l'alto. Ma tra unità di traduzione diverse, l'ordine di costruzione dei globali è non specificato — questo è il problema dell'ordine di inizializzazione statica.
Il problema dell'ordine di inizializzazione statica è una delle insidie più note del C++. Quando hai variabili globali o statiche in unità di traduzione diverse (file .cpp diversi), lo standard C++ non specifica l'ordine in cui vengono inizializzate. Se un globale dipende da un altro di un'unità di traduzione diversa, potrebbe essere usato prima di essere costruito.
La soluzione classica è l'idioma Construct on First Use: sostituisci la variabile globale con una funzione che restituisce un riferimento a una variabile locale statica. Le variabili locali statiche sono garantite essere inizializzate la prima volta che la funzione viene chiamata (e questo è thread-safe dal C++11).
- L'ordine di inizializzazione dei globali tra unità di traduzione diverse non è specificato
- Se il globale A (in file1.cpp) dipende dal globale B (in file2.cpp), B potrebbe non esistere ancora quando A viene costruito
- Soluzione: usa l'idioma Construct on First Use — avvolgi i globali in funzioni con variabili locali statiche
- L'inizializzazione di variabili locali statiche è thread-safe dal C++11 (noti come 'magic statics')
L'Idioma Construct on First Use
Questo pattern risolve il problema dell'ordine di inizializzazione statica posticipando la costruzione al primo utilizzo:
#include <string>
#include <iostream>
// BAD: global depends on another global — order is unspecified across files
// std::string bad_prefix = "[LOG] "; // What if this is used before construction?
// GOOD: Construct on First Use idiom
std::string& log_prefix() {
static std::string prefix = "[LOG] "; // Constructed on first call, thread-safe
return prefix;
}
std::string& app_name() {
static std::string name = "MyApp";
return name;
}
void log(const std::string& message) {
// Safe: log_prefix() and app_name() are guaranteed to be initialized
std::cout << log_prefix() << app_name() << ": " << message << '\n';
}
int main() {
log("Starting up");
log("Ready");
}Premi Esegui per compilare ed eseguire questo esempio.
- Lo scope (dove un nome è visibile) e la durata di vita (quando un oggetto esiste) sono concetti distinti che possono divergere
- Il C++ ha quattro durate di archiviazione: automatica (stack), statica (durata del programma), thread-local e dinamica (heap)
- I tipi integrati inizializzati in modo predefinito hanno valori indeterminati — leggerli è un comportamento indefinito
- L'inizializzazione per valore con {} garantisce la zero initialization per i tipi integrati
- Le variabili locali vengono distrutte in ordine LIFO — dichiara le dipendenze prima dei loro dipendenti
- Usa l'idioma Construct on First Use per evitare il problema dell'ordine di inizializzazione statica tra unità di traduzione
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Qual è il valore di un `int x;` locale (senza inizializzatore) in C++?
2. Cos'è il problema dell'ordine di inizializzazione statica?
3. Come differisce `static int count = 0;` dentro una funzione da `int count = 0;`?