Переменные, масштаб и продолжительность жизни
Изучите четыре продолжительности хранения, как объем и срок службы являются различными понятиями, ловушки инициализации и фиаско статического порядка инициализации.
Сфера и продолжительность жизни не одно и то же
Одним из наиболее распространенных заблуждений в C++ является объединение scope с lifetime. Это взаимосвязанные, но разные понятия:
Scope — это свойство времени компиляции — оно определяет , где имя видно в исходном коде. Переменная, объявленная внутри блока {}, видна только внутри этого блока и любых вложенных блоков.
Lifetime — это свойство времени выполнения — оно определяет , когда объект существует в памяти. Жизнь объекта начинается, когда его конструктор завершается и заканчивается, когда начинается его разрушитель.
Во многих случаях масштаб и срок службы совпадают: локальная переменная видна внутри блока и живет до конца блока. Но они могут расходиться. Местная переменная static видна только внутри своей функции (ограниченный объем), но живет для всей программы (неограниченный срок службы). Динамически выделенный объект имеет срок службы, который выходит за рамки указателя, который его создал.
Четыре срока хранения
C++ определяет четыре периода хранения, которые контролируют создание и уничтожение объектов:
#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
}Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.
Форма инициализации и нулевая инициализация
C++ имеет три основные концепции инициализации:
По умолчанию инициализация — переменная построена без инициализатора. Для встроенных типов (int, double и т.д.) это означает, что значение является неопределенным (чтение неопределенного поведения!). Для типов классов называется конструктор по умолчанию.
Инициализация ценности — запускается пустыми скобками T() или пустыми брекетами T{}. Для встроенных типов это ноль-инициализирует : целые числа становятся 0, указатели становятся nullptr, булевы становятся ложными. Для типов классов называется конструктор по умолчанию (и члены инициализируются, если ни один конструктор их не инициализирует).
Нулевая инициализация — особая фаза, которая происходит для статических/поточно-локальных переменных перед любой другой инициализацией. Все статические переменные нулевой инициализации перед началом программы.
Вот почему static int x; гарантированно равен 0, но локальная int x; не — локальная переменная инициализируется по умолчанию (неопределенная), в то время как статическая переменная сначала проходит нулевую инициализацию.
Инициализация на практике
Понимание того, когда переменная равна нулю и неопределенна, имеет решающее значение для предотвращения ошибок:
#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)
}Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.
Орден уничтожения: ЛИФО
Объекты с автоматической длительностью хранения (местные переменные) уничтожаются в обратном порядке их построения** — Last In, First Out (LIFO). Это не просто конвенция, это гарантирует стандарт.
Это очень важно, когда объекты зависят друг от друга. Если объект b объект ссылки aВы должны заявить a перед bТогда b Уничтожается в первую очередь. a Он еще жив, и a Уничтожен второй. Если приказ был отменен, bРазрушитель может попытаться использовать мертвого a Неопределенное поведение.
Для статических переменных разрушение происходит в обратном порядке строительства после main() Возвращается. Глобальные переменные, определенные в одном и том же блоке перевода, построены сверху вниз и уничтожены снизу вверх. Но в переводческих единицах порядок построения глобалов не уточняется — это фиаско статического порядка инициализации.
Фиаско со статической инициализацией является одной из самых известных ловушек в C++. Когда у вас есть глобальные или статические переменные в различных единицах перевода (различные файлы .cpp), стандарт C++ не указывает порядок их инициализации. Если один глобал зависит от другого перевода, он может быть использован до его создания.
Классическим исправлением является идиома Construct on First Use: заменить глобальную переменную функцией, которая возвращает ссылку на локальную статическую переменную. Локальная статика гарантированно инициализируется при первом вызове функции (и это безвредно для потоков с C++11).
- Порядок инициализации глобальных переводческих единиц не определен
- Если глобальный A (в файле 1.cpp) зависит от глобального B (в файле 2.cpp), B может еще не существовать, когда A построен.
- Исправление: используйте Construct on First Use idiom — оберните глобалы в функции с локальной статикой
- Локальная статическая инициализация безвредна, так как C++11 (известен как «магическая статика»)
Оригинальное название: First Use Idiom
Этот шаблон решает фиаско статического порядка инициализации, откладывая строительство до первого использования:
#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");
}Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.
- Область (где видно имя) и время жизни (когда существует объект) - это различные понятия, которые могут расходиться.
- C++ имеет четыре срока хранения: автоматический (стек), статический (срок службы программы), потоковый и динамический (куча)
- Встроенные типы по умолчанию имеют неопределенные значения — их чтение является неопределенным поведением.
- Инициализация значения с {} гарантирует нулевую инициализацию для встроенных типов
- Местные переменные уничтожаются в порядке LIFO — декларируют зависимости до их иждивенцев
- Используйте идиому Construct on First Use, чтобы избежать фиаско статического порядка инициализации в переводческих единицах.
Тест — проверьте свои знания
(15 XP)1. Каково значение локального `int x;` (без инициализатора) в C++?
2. Что такое фиаско статического порядка инициализации?
3. Чем `static int count = 0;` отличается от `int count = 0;`?