# Глава 9. Время жизни и область видимости
В этой главе мы разберем:
- Область видимости (scope): из каких мест в коде переменная доступна.
- Время жизни (lifetime): в какой момент переменная создается и разрушается.
Но сначала рассмотрим, как переменные располагаются в памяти программы.
## Где живут переменные
Исполняемый файл на C++ — это бинарный файл, разбитый на секции. Когда программа запускается, ОС загружает его в оперативную память. Под программу выделяется _адресное пространство._ Это абстракция физической памяти, которую ОС отдает процессу.
Адресное пространство содержит состояние программы: ее код, [кучу](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C)) и [стек.](https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA_%D0%B2%D1%8B%D0%B7%D0%BE%D0%B2%D0%BE%D0%B2)
**Код программы** — это область памяти, в которой находятся:
- Инструкции для выполнения программы процессором.
- Значения всех используемых в коде литералов.
- Переменные со статическим временем жизни. Они инициализируются на старте программы и уничтожаются при ее завершении.
**Куча** также известна как динамическая память. Ее выделение и освобождение организуется из кода программы.
**Стек** — это область памяти, устроенная по принципу [LIFO.](https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA) Здесь размещается цепочка вызовов функций вместе с их аргументами и локальными переменными.
_Упрощенно_ представим виртуальное адресное пространство однопоточного процесса. Допустим, оно занимает 32 Кб. Код программы находится в начале адресного пространства. Это удобно, ведь его размер известен заранее и не меняется. Стек и куча, напротив, могут расти и уменьшаться. Поэтому они расположены в противоположных концах адресного пространства и растут друг навстречу другу.
 {.illustration}
Если программа работает в несколько потоков, ее адресное пространство организовано сложнее. Об этом будет рассказано в следующих главах.
## Время жизни
Раccмотрим, какое время жизни может быть у переменной в программе на C++.
- Автоматическое время жизни управляется автоматически компилятором.
- Статическое время жизни длится от запуска программы и до ее завершения.
- Динамическое время жизни управляется в рантайме из кода программы.
- Thread-local время жизни длится от старта потока до его завершения.
К переменными с **автоматическим временем жизни** относятся:
- Локальные переменные. Они создаются внутри блока кода и разрушаются при выходе из него.
- Аргументы функций. Они создаются при вызове функции и разрушаются при выходе из нее.
Такие переменные находятся на стеке. Их время жизни детерминировано и _автоматически_ управляется компилятором.
**Статическим временем жизни** обладают:
- Глобальные переменные. Они создаются вне класса или функции.
- Статические переменные и поля классов. При их объявлении указывается квалификатор `static`.
Переменные со статическим временем жизни находятся в специальной области памяти, которая инициализируется _до_ входа в `main()`. Она располагаются в секции с кодом программы и на картинке выше помечена зеленым.
Переменные с **динамическим временем жизни** размещаются в куче. Ее выделение и освобождение контролируется из кода программы. Следовательно, переменные с динамическим временем жизни требуют ручного управления. При их создании программист должен занимать под них память, а при уничтожении — освобождать.
Вы уже сталкивались с динамическими объектами, хоть и косвенно. Например, они есть под капотом у контейнеров. Напрямую с динамическими переменными вы поработаете в главе про указатели.
**Локальным для потока временем жизни** обладают переменные, объявленные с квалификатором `thread_local`. Это ключевое слово было введено в В C++11 для того, чтобы в многопоточной программе каждый поток работал со своей копией переменной. Подробнее рассмотрим такие переменные в главах про потоки и процессы.
## Локальные переменные
Локальными называют переменные, созданные внутри функции или блока кода. Блок кода — это инструкции, объединенные фигурными скобками. Блоком является любая [составная инструкция.](/courses/cpp/chapters/cpp_chapter_0030/#block-compound-statement)
Каждый блок кода создает новую область видимости. Объявленные внутри него переменные не доступны снаружи. Попытка обращения к ним извне приведет к ошибке компиляции.
В этом примере область видимости локальной переменной `code` распространяется на цикл и все его вложенные блоки. За пределами `while` эта переменная недоступна.
```c++ {.example_for_playground .example_for_playground_001}
import std;
void handle_user_input()
{
while(true)
{
const std::string input = read_input();
const int code = parse_code(input);
if (is_valid(code))
{
std::println("Input: {}", code);
// ...
}
}
}
int main()
{
handle_user_input();
}
```
Область видимости локальной переменной — блок, в котором она заведена. Так как у локальных переменных автоматическое время жизни, то они существуют в памяти с момента объявления и до выхода из блока. При выходе из блока переменная удаляется. Для классов и структур при этом вызывается [деструктор.](/courses/cpp/chapters/cpp_chapter_0050/#block-constructors-destructors)
Что будет выведено в консоль? {.task_text}
В случае ошибки компиляции напишите `err`. {.task_text}
```c++ {.example_for_playground}
import std;
int main()
{
std::vector<std::string> snapshot_files = {
"/tmp/3881",
"/tmp/4074"
};
try
{
const std::size_t i = 2;
const std::string path = snapshot_files.at(i);
std::println(path);
}
catch(const std::out_of_range & e)
{
std::println("{}", i);
}
}
```
```consoleoutput {.task_source #cpp_chapter_0090_task_0010}
```
Переменная `i` создана в блоке `try`, но обращение к ней происходит в блоке `catch`. {.task_hint}
```cpp {.task_answer}
err
```
Локальные переменные и значения параметров функций живут на стеке. Он хранит последовательность вызовов функций и методов. Запуск функции приводит к помещению в стек нового фрейма. Фрейм — это область на стеке, содержащая:
- Аргументы функции.
- Адрес возврата. Он нужен, чтобы при выходе из функции продолжить выполнения с места, откуда она была вызвана.
- Локальные переменные.
Допустим, в запущенной программе выполняется строка 5:
```c++ {.example_for_playground .example_for_playground_002}
import std;
void handle_request(Request request)
{
std::println("Handling HTTP POST...");
// ...
}
void handle_requests()
{
for (Request r: get_requests())
handle_request(r);
}
int main()
{
handle_requests();
}
```
В этот момент стек вызовов будет содержать 4 фрейма:
 {.illustration}
Указатель вершины стека всегда смотрит на последний добавленный фрейм. При завершении функции он смещается к предыдущему фрейму, таким образом уменьшая количество фреймов в стеке.
Иногда говорят, что _стек растет вниз_, имея ввиду, что новые фреймы добавляются в память с меньшими адресами, а самый первый фрейм находится в памяти с наибольшим адресом.
Время жизни переменных на стеке известно заранее. Оно управляется компилятором. На это завязана магия [RAII,](/courses/cpp/chapters/cpp_chapter_0050/#block-raii) предполагающая захват некоего ресурса в конструкторе объекта и его освобождение в деструкторе. Деструктор срабатывает _автоматически,_ и программисту не приходится вручную вызывать код, отвечающий за освобождение ресурса.
## Глобальные переменные
Переменные, созданные вне функции или класса, называют **глобальными.** Их область видимости — _как минимум_ начиная со строки с объявлением и до конца файла. Но в большинстве случаев она распространяется _вообще на всю программу._
Существует негласное, но распространенное правило: глобальные переменные объявляются после импорта библиотек и до определения классов и функций. Хотя никто не мешает объявить их в произвольном месте.
```c++ {.example_for_playground}
import std;
const int port = 8080;
int main()
{
std::println("{}", port);
}
```
```
8080
```
У глобальных переменных статическое время жизни. Они создаются в памяти и инициализируются _до_ входа в `main()`, а разрушаются _после_ выхода из `main()`.
В C++ вы можете завести переменную без присваивания значения:
```c++
double rps;
```
Мы [предупреждали,](/courses/cpp/chapters/cpp_chapter_0030/#block-initialization) что это плохая практика, которую _следует избегать._ Дело в том, что локальные переменные, не имеющие конструктора, без инициализации могут быть заполнены любым мусором. Обращение к такой переменной приведет к [UB.](/courses/cpp/chapters/cpp_chapter_0060/#block-ub)
В отличие от локальных, глобальные переменные можно не инициализировать. Хотя делать так _тоже не рекомендуется._ Если у глобальной переменной нет конструктора, то она заполняется нулями. Вне зависимости от того, является ли нулевое значение корректным для данного типа.
Например, не инициализированная глобальная переменная `operation` обнуляется и выходит за рамки принимаемых [перечислением](/courses/cpp/chapters/cpp_chapter_0050/#block-enum) значений.
```c++ {.example_for_playground}
import std;
int request_count; // 0, ok
double rate; // 0.0, ok
bool is_valid; // false, ok
enum class Operation
{
INSERT = 1,
DELETE,
UPDATE
};
Operation operation; // 0, not ok!
int main()
{
std::println("{}", request_count);
std::println("{}", rate);
std::println("{}", is_valid);
std::println("{}", static_cast<int>(operation));
}
```
```
0
0
false
0
```
Глобальную переменную можно поместить в [пространство имен.](/courses/cpp/chapters/cpp_chapter_0050/#block-namespaces) Это ограничит ее область видимости, но не повлияет на время жизни.
```c++ {.example_for_playground}
import std;
namespace conf
{
const int port = 8080;
}
int main()
{
std::println("{}", conf::port);
}
```
```
8080
```
[Избегайте](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-global) в своем коде глобальных переменных. Изменение глобальной переменной внутри функции — это нежелательный [побочный эффект,](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D1%8B%D0%B9_%D1%8D%D1%84%D1%84%D0%B5%D0%BA%D1%82_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) который затрудняет ее правильное использование и тестирование. Глобальные переменные создают лишние зависимости между разными частями проекта и повышают риск возникновения ошибок.
## Статические переменные {#block-static}
Для создания статической переменной используется квалификатор `static`:
```c++
static int count = 0;
```
Компилятор инициализирует такую переменную единожды, а при повторном заходе в ее блок воспользуется сохраненным значением. Так как у статических переменных статическое время жизни, они существуют до самого завершения программы.
Возьмем пример кода из параграфа про локальные переменные и сделаем переменную `n` статической.
```c++ {.example_for_playground}
import std;
void f()
{
for (std::size_t i = 0; i < 3; ++i)
{
static std::size_t n = 0;
std::print("{} ", ++n);
}
std::println("");
}
int main()
{
f();
f();
}
```
```
1 2 3
4 5 6
```
Теперь при каждом следующем заходе в тело цикла компилятор использует сохраненное состояние `n` и не пересоздает ее. Вместо переменной с автоматическим временем жизни мы получили переменную со статическим временем жизни.
Что будет выведено в консоль? {.task_text}
В случае ошибки компиляции напишите `err`. {.task_text}
```c++ {.example_for_playground}
import std;
std::size_t f()
{
static int n = 0;
return ++n;
}
int main()
{
f();
std::println("{}", f());
}
```
```consoleoutput {.task_source #cpp_chapter_0090_task_0020}
```
Статическая переменная `n` инициализируется нулем. [Пре-инкремент](/courses/cpp/chapters/cpp_chapter_0020/#block-pre-increment) `n` сначала увеличивает ее на 1, а потом возвращает значение. {.task_hint}
```cpp {.task_answer}
2
```
Будьте осторожны при заведении тяжелых статических объектов, ведь память из-под них не будет высвобождена до конца работы программы.
Реализуйте функцию `fib()`, которая возвращает `n`-ное число последовательности Фибоначчи. Нумерация начинается с нуля. В последовательности Фибоначчи каждое следующее число равно сумме двух предыдущих. Начало последовательности выглядит так: 0, 1, 1, 2, 3, 5, 8, 13, ... {.task_text}
Примеры работы функции: `fib(0) == 0`, `fib(1) == 1`, `fib(6) == 8`. {.task_text}
**Функция должна кешировать** 40 первых чисел последовательности начиная со 2-го: 1, 2, 3, ... {.task_text}
```c++ {.task_source #cpp_chapter_0090_task_0030}
int fib(int n)
{
}
```
В качестве кеша подойдет `std::array`, индексы которого соответствуют номерам чисел в последовательности Фибоначчи. {.task_hint}
```c++ {.task_answer}
int fib(int n)
{
static std::array<int, 40> cache = {};
if (n <= 0)
return 0;
if (n == 1)
return 1;
if (n < cache.size())
{
std::size_t i = n - 2;
if (cache[i] != 0)
return cache[i];
cache[i] = fib(n - 1) + fib(n - 2);
return cache[i];
}
return fib(n - 1) + fib(n - 2);
}
```
Статическими можно делать не только локальные, но и глобальные переменные. Однако для глобальных переменных это имеет иное значение. Какое, вы [узнаете](/courses/cpp/chapters/cpp_chapter_0110/#block-static) в главе про сборку проекта.
## Затенение имен
Стандарт C++ позволяет заводить во вложенных областях видимости переменные с одинаковыми именами. При обращении к такой переменной происходит [затенение](https://en.wikipedia.org/wiki/Variable_shadowing) (перекрытие) имен: приоритет отдается переменной вложенного блока. Выбрать глобальную переменную можно, написав перед ней оператор разрешения имен `::` без указания пространства имен. Это означает обращение к глобальной области видимости:
В этом примере есть 3 переменные с именем `max_speed`: глобальная, локальная для функции `main()` и локальная во вложенном блоке.
```c++ {.example_for_playground}
import std;
double max_speed = 60.0;
int main()
{
double max_speed = 90.0;
{
double max_speed = 120.0;
std::println("{} {}", max_speed, ::max_speed);
}
std::println("{} {}", max_speed, ::max_speed);
}
```
```
120 60
90 60
```
Затенение имен приводит к массе непреднамеренных ошибок. Старайтесь его избегать.
## Вложенные блоки кода и RAII
Как вы [помните,](/courses/cpp/chapters/cpp_chapter_0050/#block-raii) идиома RAII применяется для автоматического управления ресурсами, требующими парных действий. Например, для открытия и закрытия сетевого соединения. Чтобы реализовать RAII-класс, в его конструкторе описывают захват ресурса, а в деструкторе — освобождение. И в некоторых случаях освободить ресурс требуется еще до выхода из функции, заранее.
В C++ распространена практика создания вложенного блока кода специально для контроля времени жизни переменной. Это выглядит как пара фигурных скобок, не относящихся к функции или управляющей конструкции:
```c++
int main()
{
// ...
{
int n = 1'000;
// ...
} // Здесь n разрушается
// ...
}
```
Рассмотрим типичный сценарий использования такого подхода. Он возникает при параллельной работе с переменной из нескольких потоков. Доступ к ней синхронизируется [мьютексом](https://ru.wikipedia.org/wiki/%D0%9C%D1%8C%D1%8E%D1%82%D0%B5%D0%BA%D1%81) `std::mutex`. Он гарантирует, что в каждый момент переменную читает или записывает максимум один поток. Но каждый раз вручную захватывать мьютекс _до обращения_ к переменной и отпускать его _после этого_ неудобно. Поэтому пользуются RAII-классом [std::unique_lock](https://en.cppreference.com/w/cpp/thread/unique_lock.html). В конструкторе он захватывает мьютекс, а в деструкторе отпускает. Для контроля времени жизни объекта `std::unique_lock` используется вложенный блок.
Допустим, у нас есть класс `TaskRunner`, объекты которого из нескольких потоков работают с очередью задач `task_queue`. Она защищена мьютексом `task_queue_mutex`. Тогда метод для выполнения задачи мог бы выглядеть так:
```c++
void TaskRunner::exec_task()
{
Task task;
{
std::unique_lock<std::mutex> lock(task_queue_mutex); // блокируем мьютекс
if (task_queue.empty())
return;
task = task_queue.pop();
} // разблокируем
exec(task);
}
```
Перед вами код для замера времени выполнения алгоритма `std::sort()`. Воспользуйтесь им, чтобы написать RAII-класс `MeasureTime`. В конструкторе он сохраняет текущее время. А в деструкторе выводит в консоль разницу между настоящим моментом и сохраненным. Чтобы увидеть пример использования класса, откройте задачу в плэйграунде. {.task_text}
```c++
std::vector<int> numbers = random_vector(1e6);
// https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now
auto start = std::chrono::high_resolution_clock::now();
std::sort(numbers);
auto finish = std::chrono::high_resolution_clock::now();
auto delta = std::chrono::duration_cast<std::chrono::milliseconds>
(finish-start).count();
std::println("Duration: {} ms", delta);
```
```c++ {.task_source #cpp_chapter_0090_task_0040}
class MeasureTime
{
};
```
В приватной секции класса заведите поле типа `std::chrono::time_point<std::chrono::high_resolution_clock>`. В конструкторе присвойте этому полю результат вызова `std::chrono::high_resolution_clock::now()`. В деструкторе заведите переменную, равную текущему значению времени. {.task_hint}
```c++ {.task_answer}
class MeasureTime
{
public:
MeasureTime()
{
start = std::chrono::high_resolution_clock::now();
}
~MeasureTime()
{
auto finish = std::chrono::high_resolution_clock::now();
auto delta = std::chrono::duration_cast<std::chrono::milliseconds>
(finish-start).count();
std::println("Duration: {} ms", delta);
}
private:
std::chrono::time_point<std::chrono::high_resolution_clock> start;
};
```
## Инициализаторы в if и switch
Инициализатор нужен, чтобы создать переменную и присвоить ей значение до выражения внутри круглых скобок управляющей конструкции. Вы уже [работали](/courses/cpp/chapters/cpp_chapter_0040/#block-for-explanation) с инициализатором в цикле `for`:
```c++
// инициализатор
// |--------|
for(int i = 0; i < n; ++i) { /* ... */ }
```
А в C++17 появилась возможность задавать инициализаторы в управляющих конструкциях `if` и `switch`:
```c++
if(init-statement; condition) { /* ... */ }
switch(init-statement; condition) { /* ... */ }
```
Какую проблему решают инициализаторы? Разберем это на примере кода, который обрабатывает введенную пользователем команду:
```c++ {.example_for_playground .example_for_playground_003}
std::string cmd = read_user_input();
if (cmd == "q")
{
std::println("Quitting...");
}
else
{
std::println("Handling command {}", cmd);
run_command(cmd);
}
// ...
```
У этого кода есть недостаток: область видимости переменной `cmd` больше, чем требуется. Переменная нужна только для условия, но объявлена _до_ него и доступна _после_ него.
Перепишем пример выше с использованием инициализатора:
```c++ {.example_for_playground .example_for_playground_004}
if (std::string cmd = read_user_input(); cmd == "q")
{
std::println("Quitting...");
}
else
{
std::println("Handling command {}", cmd);
run_command(cmd);
}
// ...
```
Теперь область видимости и время жизни `cmd` ограничены условием, в котором эта переменная используется. Вне его она недоступна.
Инициализаторы в управляющих конструкциях — всего лишь синтаксический сахар над созданием новой области видимости с помощью вложенного блока:
```c++ {.example_for_playground .example_for_playground_005}
// ...
{
std::string cmd = read_user_input();
if ( cmd == "q")
{
std::println("Quitting...");
}
else
{
std::println("Handling command {}", cmd);
run_command(cmd);
}
}
// ...
```
[Старайтесь минимизировать](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-scope) область видимости переменных: это делает код более надежным и лаконичным. [Используйте инициализаторы](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es6-declare-names-in-for-statement-initializers-and-conditions-to-limit-scope) там, где они вам в этом помогут.
Разумеется, инициализаторы опциональны: до этого момента вы использовали `if` и `switch` без них.
Перед вами функция `add_alias()` для добавления псевдонима консольной команды и функция `get_description()`, по команде или псевдониму возвращающая описание. Функции плохо спроектированы: они завязаны на глобальные переменные. Кроме того, в коде допущена ошибка, приводящая к накапливанию пустых описаний команд. {.task_text}
Перенесите логику из этого кода в класс `Commands`. Метод `get_description()` должен кидать исключение `std::out_of_range`, если команда не найдена. В условии `if` используйте инициализатор. {.task_text}
```c++
std::unordered_map<std::string, std::string> cmd_aliases =
{
{"rd", "rmdir"},
{"o", "less"}
};
std::unordered_map<std::string, std::string> cmd_descriptions =
{
{"rmdir", "remove empty directories"},
{"less", "display the contents of a file"},
{"sed", "stream editor for transforming text"}
};
void add_alias(std::string alias, std::string cmd)
{
cmd_aliases[alias] = cmd;
}
std::string get_description(std::string cmd)
{
auto it = cmd_aliases.find(cmd);
if (it != cmd_aliases.end())
return cmd_descriptions[it->second];
return cmd_descriptions[cmd];
}
```
```c++ {.task_source #cpp_chapter_0090_task_0050}
class Commands
{
public:
void add_alias(std::string alias, std::string cmd)
{
}
void add_command(std::string cmd, std::string description)
{
}
std::string get_description(std::string cmd)
{
}
private:
};
```
Вы можете освежить в памяти [варианты вставки](/courses/cpp/chapters/cpp_chapter_0070/#block-insert) элементов в ассоциативный контейнер. {.task_hint}
```c++ {.task_answer}
class Commands
{
public:
void add_alias(std::string alias, std::string cmd)
{
cmd_aliases[alias] = cmd;
}
void add_command(std::string cmd, std::string description)
{
cmd_descriptions.emplace(cmd, description);
}
std::string get_description(std::string cmd)
{
if (auto it = cmd_aliases.find(cmd); it != cmd_aliases.end())
return cmd_descriptions.at(it->second);
return cmd_descriptions.at(cmd);
}
private:
std::unordered_map<std::string, std::string> cmd_aliases;
std::unordered_map<std::string, std::string> cmd_descriptions;
};
```
----------
## Резюме
- Время жизни переменной бывает автоматическим, статическим, динамическим и локальным для потока.
- Статическое время жизни бывает у глобальных и статических переменных.
- Автоматическое время жизни бывает у локальных переменных и значений параметров функций.
- Временем жизни автоматических переменных управляет компилятор. Он вызывает деструктор, когда объект покидает свою область видимости. За счет этого в C++ работают RAII-классы.
- Статической можно сделать и локальную, и глобальную переменную.
- Блок кода создает область видимости.
- Для ограничения области видимости используются инициализаторы в конструкциях `if` и `switch`.
- При затенении имен приоритет отдается локальной переменной.
- Избегайте в своем коде глобальных переменных и затенения имен.
- Для доступа к глобальной области видимости используется оператор `::`.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!