Sari la conținut

Variabile, domeniul de aplicare și durata de viață

Learn the four storage durations, how scope and lifetime are distinct concepts, initialization pitfalls, and the static initialization order fiasco.

Scope and Lifetime Are Not the Same Thing

One of the most common misconceptions in C++ is conflating scope with lifetime. They are related but distinct concepts:

Scope is a compile-time property — it determines where a name is visible in the source code. A variable declared inside a block{}is visible only within that block and any nested blocks.

Lifetime is a runtime property — it determines when an object exists in memory. An object's lifetime begins when its constructor completes and ends when its destructor starts.

In many cases, scope and lifetime coincide: a local variable is visible within its block and lives until the block ends. But they can diverge. Astaticlocal variable is only visible inside its function (limited scope) but lives for the entire program (unlimited lifetime). A dynamically allocated object has a lifetime that extends beyond the scope of the pointer that created it.

Cele patru durate de stocare

C++ definește patru durate de stocare care controlează momentul în care obiectele sunt create și distruse:

storage.cppc++23
#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
}
Rezultat

Apasă Rulează pentru a compila și rula acest exemplu.

Forme de inițializare și zero-inițializare

C++ are trei concepte fundamentale de inițializare:

Inițializare implicită — variabila este construită fără un inițializator. Pentru tipurile încorporate (int, double, etc.), aceasta înseamnă că valoarea este nedeterminată (a citi este un comportament nedefinit!). Pentru tipurile de clasă, constructorul implicit este apelat.

Inițializarea valorii — declanșată de paranteze goaleT()sau acolade goaleT{}. Pentru tipurile încorporate, acest zero-inițializează: numerele întregi devin 0, pointerii devin nullptr, booleanele devin false. Pentru tipurile de clasă, constructorul implicit este apelat (și membrii sunt inițializați cu valoare dacă niciun constructor nu îi inițializează).

Inițializare zero — o fază specifică care are loc pentru variabilele statice/locale de fir înainte de orice altă inițializare. Toate variabilele statice sunt inițializate la zero înainte de începerea programului.

Acesta este motivul pentru carestatic int x;este garantat a fi 0, dar unint x;local este nu — variabila locală este inițializată implicit (nedeterminată), în timp ce variabila statică trece mai întâi prin inițializare zero.

Inițializarea în practică

Înțelegerea când o variabilă este zero față de nedeterminată este esențială pentru evitarea erorilor:

init_forms.cppc++23
#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)
}
Rezultat

Apasă Rulează pentru a compila și rula acest exemplu.

Ordin de distrugere: LIFO

Obiectele cu durata de stocare automată (variabile locale) sunt distruse în ordinea inversă construcției lor — Last In, First Out (LIFO). Aceasta nu este doar o convenție; standardul o garantează.

Acest lucru contează enorm atunci când obiectele depind unele de altele. Dacă obiectulbface referire la obiectula, trebuie să declarațiaînainte deb. Apoibeste distrus primul (în timp ceaeste încă în viață), iaraeste distrus în al doilea rând. Dacă ordinea ar fi inversată, destructorul luibar putea încerca să folosească unamort - comportament nedefinit.

Pentru variabilele statice, distrugerea are loc în ordinea inversă a construcției după revenireamain(). Variabilele globale definite în aceeași unitate de traducere sunt construite de sus în jos și distruse de jos în sus. Dar între unitățile de traducere, ordinea de construcție a globalelor este nespecificată - acesta este fiasco-ul ordinii de inițializare statică.

Capcană

Fiasco-ul comenzii de inițializare statică este una dintre cele mai notorii capcane din C++. Când aveți variabile globale sau statice în unități de traducere diferite (fișiere .cpp diferite), standardul C++ nu specifică ordinea în care sunt inițializate. Dacă o globală depinde de alta dintr-o unitate de traducere diferită, aceasta poate fi utilizată înainte de a fi construită.

Remedierea clasică este expresia Construct on First Use: înlocuiți variabila globală cu o funcție care returnează o referință la o variabilă statică locală. Se garantează că statica locală va fi inițializată prima dată când funcția este apelată (și aceasta este sigură pentru fire de execuție începând cu C++11).

  • Ordinea de inițializare a globalelor între unitățile de traducere este nespecificată
  • Dacă A global (în fișierul 1.cpp) depinde de B global (în fișierul 2.cpp), este posibil ca B să nu existe încă atunci când A este construit
  • Remediere: utilizați expresia Construct on First Use — includeți globalurile în funcții cu statice locale
  • Inițializarea statică locală este sigură pentru fire de execuție începând cu C++11 (cunoscut sub numele de „statică magică”)

Idiomul de construcție pentru prima utilizare

Acest model rezolvă fiasco-ul comenzii de inițializare statică prin amânarea construcției până la prima utilizare:

construct_on_first_use.cppc++23
#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");
}
Rezultat

Apasă Rulează pentru a compila și rula acest exemplu.

Idei cheie
  • Domeniul de aplicare (unde un nume este vizibil) și durata de viață (când există un obiect) sunt concepte distincte care pot diverge
  • C++ are patru durate de stocare: automată (stiva), static (durata de viață a programului), thread-local și dinamic (heap)
  • Tipurile încorporate inițializate implicit au valori nedeterminate - citirea lor este un comportament nedefinit
  • Value initialization with {} guarantees zero-initialization for built-in types
  • Variabilele locale sunt distruse în ordinea LIFO - se declară dependențe înaintea dependențelor lor
  • Use the Construct on First Use idiom to avoid the static initialization order fiasco across translation units

Test — Verifică-ți cunoștințele

(15 XP)

1. What is the value of a local`int x;`(without an initializer) in C++?

2. Care este fiasco-ul comenzii de inițializare statică?

3. Cum diferă`static int count = 0;`într-o funcție de`int count = 0;`?