# Глава 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} ![Упрощенное представление памяти процесса](https://raw.githubusercontent.com/senjun-team/senjun-courses/cpp-chapter-9/illustrations/cpp/process_memory.jpg) {.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 фрейма: ![Фреймы стека вызовов](https://raw.githubusercontent.com/senjun-team/senjun-courses/cpp-chapter-9/illustrations/cpp/call_stack.jpg) {.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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!