Главная /
Курсы /
C++ по спирали /
Глава 9. Время жизни и область видимости /
Жизненный цикл переменной
# Глава 9.1. Жизненный цикл переменной
В этой главе мы разберём:
- Область видимости (scope): из каких мест в коде переменная доступна.
- Время жизни (lifetime): в какой момент переменная создаётся и разрушается.
Но сначала рассмотрим, как переменные располагаются в памяти программы.
## Где живут переменные {#block-memory}
Исполняемый файл — это бинарный файл, разбитый на секции. Когда программа запускается, ОС загружает его в оперативную память. Под программу выделяется _адресное пространство._ Это абстракция физической памяти, которую ОС отдаёт процессу.
Адресное пространство содержит состояние программы: её код, [кучу](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) {#block-static-heap-stack}
**Код программы** — это область памяти, в которой находятся:
- Инструкции для выполнения программы процессором.
- Значения всех используемых в коде литералов.
- Переменные со статическим временем жизни. Они инициализируются на старте программы и уничтожаются при её завершении.
**Куча** также известна как динамическая память. Ее выделение и освобождение организуется из кода программы.
**Стек** — это область памяти, устроенная по принципу [LIFO.](https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA) Здесь размещается цепочка вызовов функций вместе с их аргументами и локальными переменными. {#block-stack}
_Упрощенно_ представим виртуальное адресное пространство однопоточного процесса. Допустим, оно занимает 32 Кб. Код программы находится в начале адресного пространства. Это удобно, ведь его размер известен заранее и не меняется. Стек и куча, напротив, могут расти и уменьшаться. Поэтому они расположены в противоположных концах адресного пространства и растут друг навстречу другу. {#block-virtual-address}
 {.illustration}
Если программа работает в несколько потоков, её адресное пространство организовано сложнее. Об этом будет рассказано в следующих главах.
## Время жизни {#block-lifetime}
Раccмотрим, какое время жизни может быть у переменной в программе на C++.
- Автоматическое время жизни управляется автоматически компилятором.
- Статическое время жизни длится от запуска программы и до её завершения.
- Динамическое время жизни управляется в рантайме из кода программы.
- Thread-local время жизни длится от старта потока до его завершения.
К переменными с **автоматическим временем жизни** относятся: {#block-automatic-lifetime}
- Локальные переменные. Они создаются внутри блока кода и разрушаются при выходе из него.
- Аргументы функций. Они создаются при вызове функции и разрушаются при выходе из неё.
Такие переменные находятся на стеке. Их время жизни детерминировано и _автоматически_ управляется компилятором.
**Статическим временем жизни** обладают: {#block-static-lifetime}
- Глобальные переменные. Они создаются вне класса или функции.
- Статические переменные и поля классов. При их объявлении указывается квалификатор `static`.
Переменные со статическим временем жизни находятся в специальной области памяти, которая инициализируется _до_ входа в `main()`. Она располагается в секции с кодом программы и на картинке выше помечена зелёным.
Переменные с **динамическим временем жизни** размещаются в куче. Ее выделение и освобождение контролируется из кода программы. Следовательно, переменные с динамическим временем жизни требуют ручного управления. При их создании программист должен выделять под них память, а при уничтожении — освобождать.
Вы уже сталкивались с динамическими объектами, хоть и косвенно. Например, они есть под капотом у контейнеров. Напрямую с динамическими переменными вы поработаете в главе про указатели.
**Локальным для потока временем жизни** обладают переменные, объявленные с квалификатором `thread_local`. Это ключевое слово было введено в В C++11 для того, чтобы в многопоточной программе каждый поток работал со своей копией переменной. Подробнее рассмотрим такие переменные в главах про потоки и процессы.
## Локальные переменные
Локальными называют переменные, созданные внутри функции или блока кода. Блок кода — это инструкции, объединённые фигурными скобками. Блоком является любая [составная инструкция.](/courses/cpp/chapters/cpp_chapter_0030/#block-compound-statement)
Каждый блок кода создаёт новую область видимости. Объявленные внутри него переменные недоступны снаружи. Попытка обращения к ним извне приведёт к ошибке компиляции.
В этом примере область видимости локальной переменной `code` распространяется на цикл и все его вложенные блоки. За пределами `while` эта переменная недоступна.
```cpp {.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_0055/#block-constructors-destructors)
Что будет выведено в консоль? {.task_text}
В случае ошибки компиляции напишите `err`. {.task_text}
```cpp {.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_0091_task_0010}
```
Переменная `i` создана в блоке `try`, но обращение к ней происходит в блоке `catch`. {.task_hint}
```cpp {.task_answer}
err
```
Локальные переменные и значения параметров функций живут на стеке. Он хранит последовательность вызовов функций и методов. Запуск функции приводит к помещению в стек нового фрейма. Фрейм — это область на стеке, содержащая: {#block-stack-frame}
- Аргументы функции.
- Адрес возврата. Он нужен, чтобы при выходе из функции продолжить выполнения с места, откуда она была вызвана.
- Локальные переменные.
Допустим, в запущенной программе выполняется строка 5:
```cpp {.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_0055/#block-raii) предполагающая захват некоего ресурса в конструкторе объекта и его освобождение в деструкторе. Деструктор срабатывает _автоматически,_ и программисту не приходится вручную вызывать код, отвечающий за освобождение ресурса.
## Глобальные переменные
Переменные, созданные вне функции или класса, называют **глобальными.** Их область видимости — _как минимум_ начиная со строки с объявлением и до конца файла. Но в большинстве случаев она распространяется _вообще на всю программу._
Существует негласное, но распространённое правило: глобальные переменные объявляются после импорта библиотек и до определения классов и функций. Хотя никто не мешает объявить их в произвольном месте.
```cpp {.example_for_playground}
import std;
const int port = 8080;
int main()
{
std::println("{}", port);
}
```
```
8080
```
У глобальных переменных статическое время жизни. Они создаются в памяти и инициализируются _до_ входа в `main()`, а разрушаются _после_ выхода из `main()`.
В C++ вы можете завести переменную без присваивания значения:
```cpp
double rps;
```
Мы [предупреждали,](/courses/cpp/chapters/cpp_chapter_0021/#block-initialization) что это плохая практика, которую _следует избегать._ Дело в том, что локальные переменные, не имеющие конструктора, без инициализации могут быть заполнены любым мусором. Обращение к такой переменной приведёт к [UB.](/courses/cpp/chapters/cpp_chapter_0062/#block-ub)
В отличие от локальных, глобальные переменные можно не инициализировать. Хотя делать так _тоже не рекомендуется._ Если у глобальной переменной нет конструктора, то она заполняется нулями. Вне зависимости от того, является ли нулевое значение корректным для данного типа. {#block-zeroes}
Например, не инициализированная глобальная переменная `operation` обнуляется и выходит за рамки принимаемых [перечислением](/courses/cpp/chapters/cpp_chapter_0054/#block-enum) значений.
```cpp {.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_0053/) Это ограничит её область видимости, но не повлияет на время жизни.
```cpp {.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`:
```cpp
static int count = 0;
```
Компилятор инициализирует такую переменную единожды, а при повторном заходе в её блок воспользуется сохранённым значением. Так как у статических переменных статическое время жизни, они существуют до самого завершения программы.
Возьмем пример кода из параграфа про локальные переменные и сделаем переменную `n` статической.
```cpp {.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}
```cpp {.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_0091_task_0020}
```
Статическая переменная `n` инициализируется нулём. [Пре-инкремент](/courses/cpp/chapters/cpp_chapter_0022/#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}
```cpp {.task_source #cpp_chapter_0091_task_0030}
int fib(int n)
{
}
```
В качестве кеша подойдёт `std::array`, индексы которого соответствуют номерам чисел в последовательности Фибоначчи. {.task_hint}
```cpp {.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_0112/#block-static) в главе про сборку проекта.
----------
## Резюме
- Время жизни переменной бывает автоматическим, статическим, динамическим и локальным для потока.
- Статическое время жизни бывает у глобальных и статических переменных.
- Автоматическое время жизни бывает у локальных переменных и значений параметров функций.
- Временем жизни автоматических переменных управляет компилятор. Он вызывает деструктор, когда объект покидает свою область видимости. За счёт этого в C++ работают RAII-классы.
- Статической можно сделать и локальную, и глобальную переменную.
- Блок кода создаёт область видимости.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!