Přeskočit na obsah

Proměnné, rozsah a životnost

Naučte se čtyři doby trvání úložiště, jak jsou rozsah a životnost odlišné pojmy, úskalí inicializace a fiasko statického pořadí inicializace.

Rozsah a životnost nejsou totéž

Jednou z nejčastějších mylných představ v C++ je spojování rozsahu s životnost. Jsou to příbuzné, ale odlišné pojmy:

Scope je vlastnost v době kompilace – určuje, kde je jméno viditelné ve zdrojovém kódu. Proměnná deklarovaná uvnitř bloku{}je viditelná pouze v rámci tohoto bloku a všech vnořených bloků.

Lifetime je vlastnost runtime — určuje, kdy objekt existuje v paměti. Životnost objektu začíná, když je dokončen jeho konstruktor, a končí, když začíná jeho destruktor.

V mnoha případech se rozsah a životnost shodují: lokální proměnná je viditelná v jejím bloku a žije, dokud blok neskončí. Ale mohou se rozcházet. Lokální proměnnástaticje viditelná pouze uvnitř její funkce (omezený rozsah), ale žije pro celý program (neomezená životnost). Dynamicky alokovaný objekt má životnost, která přesahuje rozsah ukazatele, který jej vytvořil.

Čtyři doby skladování

C++ definuje čtyři doby ukládání, které řídí, kdy jsou objekty vytvářeny a ničeny:

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
}
Výstup

Stisknutím tlačítka Spustit zkompilujete a spustíte tento příklad.

Inicializační formuláře a nulová inicializace

C++ má tři základní koncepty inicializace:

Výchozí inicializace — proměnná je vytvořena bez inicializátoru. U vestavěných typů (int, double, atd.) to znamená, že hodnota je neurčitá (její čtení je nedefinované chování!). Pro typy tříd se volá výchozí konstruktor.

Inicializace hodnoty — spouští se prázdnými závorkamiT()nebo prázdnými složenými závorkamiT{}. U vestavěných typů to nula inicializuje: celá čísla se stanou 0, ukazatele se stanou nullptr, booleovské hodnoty se stanou false. U typů tříd se volá výchozí konstruktor (a členové jsou hodnotově inicializováni, pokud je neinicializuje žádný konstruktor).

Nulová inicializace — specifická fáze, která nastává pro statické/lokální proměnné pod vláknem před jakoukoli jinou inicializací. Všechny statické proměnné jsou před spuštěním programu inicializovány nulou.

To je důvod, proč je zaručeno, žestatic int x;bude 0, ale místníint x;není — lokální proměnná je defaultně inicializována (neurčitá), zatímco statická proměnná prochází nejprve nulovou inicializací.

Inicializace v praxi

Pochopení toho, kdy je proměnná nulová nebo neurčitá, je rozhodující pro zamezení chyb:

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)
}
Výstup

Stisknutím tlačítka Spustit zkompilujete a spustíte tento příklad.

Příkaz zničení: LIFO

Objekty s automatickou dobou uložení (lokální proměnné) jsou zničeny v opačném pořadí jejich konstrukce — Last In, First Out (LIFO). To není jen konvence; norma to zaručuje.

To je nesmírně důležité, když jsou objekty na sobě závislé. Pokud objektbodkazuje na objekta, musíte deklarovatapředb. Poté jebzničen jako první (zatímcoaje stále naživu) aaje zničen jako druhý. Pokud by bylo pořadí obráceno, destruktorbby se mohl pokusit použít mrtvýa– nedefinované chování.

U statických proměnných se zničení děje v opačném pořadí konstrukce po návratumain(). Globální proměnné definované ve stejné translační jednotce jsou konstruovány shora dolů a zničeny zdola nahoru. Ale napříč překladovými jednotkami je pořadí konstrukce globals nespecifikováno — toto je fiasko statického inicializačního pořadí.

Úskalí

Statické fiasko inicializačního příkazu je jedním z nejznámějších úskalí v C++. Pokud máte globální nebo statické proměnné v různých překladových jednotkách (různé soubory .cpp), standard C++ neurčuje pořadí, ve kterém jsou inicializovány. Pokud jeden globální závisí na druhém z jiné překladové jednotky, může být použit před tím, než je zkonstruován.

Klasickou opravou je idiom Construct on First Use: nahraďte globální proměnnou funkcí, která vrací odkaz na lokální statickou proměnnou. Je zaručeno, že místní statika bude inicializována při prvním volání funkce (a to je od C++11 bezpečné pro vlákna).

  • Pořadí inicializace globalů napříč překladovými jednotkami není specifikováno
  • If global A (in file1.cpp) depends on global B (in file2.cpp), B might not exist yet when A is constructed
  • Fix: use the Construct on First Use idiom — wrap globals in functions with local statics
  • Místní statická inicializace je bezpečná pro vlákna od C++11 (známá jako „magická statika“)

Konstrukce idiomu prvního použití

Tento vzor řeší fiasko statického inicializačního příkazu odložením konstrukce do prvního použití:

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");
}
Výstup

Stisknutím tlačítka Spustit zkompilujete a spustíte tento příklad.

Klíčové poznatky
  • Rozsah (kde je viditelný název) a životnost (když objekt existuje) jsou odlišné pojmy, které se mohou lišit
  • C++ má čtyři doby ukládání: automatické (zásobník), statické (životnost programu), místní vlákno a dynamické (hromada)
  • Výchozí inicializované vestavěné typy mají neurčité hodnoty — jejich čtení je nedefinované chování
  • Inicializace hodnoty pomocí {} zaručuje nulovou inicializaci pro vestavěné typy
  • Lokální proměnné jsou zničeny v pořadí LIFO – deklarujte závislosti před jejich závislými
  • Použijte idiom Construct on First Use, abyste se vyhnuli fiasku statického inicializačního pořadí napříč překladovými jednotkami

Kvíz — Otestujte své znalosti

(15 XP)

1. Jakou hodnotu má místní`int x;`(bez inicializátoru) v C++?

2. Jaké je fiasko statického inicializačního příkazu?

3. Jak se`static int count = 0;`uvnitř funkce liší od`int count = 0;`?