Главная / Курсы / C++ по спирали / Глава 15. Указатели / Зачем нужны указатели
# Глава 15.1. Зачем нужны указатели C++ — это высокоуровневый язык с низкоуровневыми возможностями. С одной стороны, в нём есть средства для построения абстракций. В первую очередь это классы и шаблоны. С другой стороны, вы можете спуститься на уровень, максимально близкий к аппаратному. На нём вы управляете памятью напрямую: вручную контролируете её выделение и освобождение. Но для чего это нужно? ## Области памяти программы Для начала [вспомним,](/courses/cpp/chapters/cpp_chapter_0090/#block-memory) где живут переменные. Виртуальное адресное пространство процесса разбито на области, и переменные могут располагаться в одной из трёх областей: - статической, - автоматической, - динамической. ![Упрощённое представление памяти процесса](https://raw.githubusercontent.com/senjun-team/senjun-courses/cpp-chapter-9/illustrations/cpp/process_memory.jpg) {.illustration} ### Статическая память В статической памяти находятся: - глобальные переменные, - переменные, помеченные спецификатором `static`. Это объекты со статическим временем жизни. ```cpp {.example_for_playground} import std; int err_code = -1; // Глобальная переменная int main() { static int retries = 0; // Статическая переменная } ``` Объём статической памяти определяется _при компиляции_ и зависит от того, сколько переменных какого размера туда попадёт. А максимальный объём зависит от целевой платформы, настроек компилятора и количества свободной памяти. На современных машинах он может быть очень большим. Но имейте ввиду: всё, что расположено в статической памяти, увеличивает размер бинарного файла программы. Так как объём статической памяти определяется при компиляции, в неё невозможно поместить объекты, чей размер изменяется в рантайме. Например, динамические массивы. Как в таком случае работает этот код? ```cpp {.example_for_playground} import std; std::vector<int> v{}; // Находится в статической памяти, // её размер определяется при компиляции int main() { v.push_back(1); // Изменяем размер вектора в рантайме! } ``` Ответ вы узнаете в этой главе. ### Автоматическая память В автоматическую память (стек) помещаются локальные переменные и аргументы функций. Их временем жизни управляет компилятор. С точки зрения разработчика оно регулируется автоматически. ```cpp {.example_for_playground .example_for_playground_001} int run_proc(int pid) // Параметр функции { int err_code = run(pid); // Локальная переменная return err_code; } ``` У стека есть максимально допустимый размер. Он зависит от целевой платформы и опций компилятора. Как правило, под Linux по умолчанию действует ограничение в 8 Мб, а под Windows — 1 Мб. Фактический размер стека меняется в рантайме: при вызове функции в стек добавляется новый стек-фрейм, а при выходе из неё — удаляется. При выполнении программы в автоматической памяти может оказаться больше данных, чем она может вместить. Тогда указатель вершины стека выйдет за его границы. Эта ошибка называется **переполнением стека** (stack overflow). К ней ведут два сценария: - Глубокая вложенность вызовов функций. Например, при бесконечной рекурсии память на стеке заканчивается из-за огромного количества фреймов. - Большой размер локальных переменных. На стек попадает фрейм, занимающий всю свободную память. Переполнение стека — один из подвидов **ошибки сегментации** (segmentation fault, segfault). Это ошибка обращения к памяти по некорректному адресу. Она приводит к аварийному завершению программы. Давайте получим переполнение стека в нашей песочнице: заведём большой массив, по случайным адресам заполним его случайными значениями, а затем прочитаем их. Элемент случайности необходим, чтобы компилятор не мог провести оптимизации. ```cpp {.example_for_playground} import std; void print_random_numbers() { // Подбираем такую длину массива, чтобы занять // весь объём стека const std::size_t n = 2 * 1024 * 1024; int arr[n] = {}; const std::size_t max_step = 10000; const std::size_t max_val = 100; std::vector<std::size_t> indexes; // По случайным индексам заполняем массив // случайными значениями for (std::size_t i = 0; i < n; i += std::rand() % max_step) { arr[i] = std::rand() % max_val; indexes.push_back(i); } // Обращаемся к случайным элементам массива for (std::size_t i : indexes) std::print("{} ", arr[i]); } int main() { std::srand(std::time({})); print_random_numbers(); } ``` ``` Segmentation fault (core dumped) ``` Программа аварийно завершилась. Этого бы не произошло, будь длина массива `n` хотя бы в два раза меньше. Но, как мы уже сказали, максимальный размер стека зависит от опций компилятора и целевой платформы. Подытожим: у вас не получится завести на стеке действительно много объектов. На стеке, как и в статической памяти, нельзя создать массив переменной длины. В примере выше вектор `indexes` живёт на стеке, но свои элементы хранит в другой области памяти. Эта область называется динамической памятью. ### Динамическая память Динамическая память (куча, heap) содержит переменные, память под которые выделяется _из кода программы._ Происходит это во время исполнения, то есть динамически. Допустим, мы хотим разместить в динамической памяти объект, поработать с ним, а затем уничтожить. Для этого в языке должны существовать механизмы, позволяющие: - Выделить память под объект на куче. - Получить доступ к выделенной памяти для создания, изменения и удаления объекта. - Освободить память, когда объект удалён. Для доступа к объектам в динамической памяти используются указатели. ## Что такое указатель {#block-pointer-definition} [Указатель](https://en.cppreference.com/w/cpp/language/pointer.html) (pointer) — это переменная, которая хранит адрес в оперативной памяти. Отсюда и название: значение такой переменной _указывает_ на область памяти. А адрес — это по сути число. Например, `0x55ae9a41c2a0`. Можно сказать, что **указатель** — это переменная, в которой лежит целое неотрицательное число, трактуемое компилятором как адрес. По этому адресу может находиться значение, переменная или блок не инициализированной памяти. Доступ к объекту через указатель считается _косвенным._ Вместо прямого обращения к значению переменной сначала происходит обращение к указателю, а затем — к адресу, на который он указывает. Указатели есть под капотом контейнера `std::vector`, который можно заполнять хоть десятками миллионов элементов. Когда внутри функции создаётся переменная типа `std::vector`, она размещается в автоматической памяти (на стеке). Но у вектора есть приватное поле — _указатель_ на область в динамической памяти. Там и находятся его элементы. Объект вектора со всеми полями лежит на стеке, но одно из полей ссылается на динамическую память. И управление этой памятью реализовано в методах вектора. Класс `std::vector` в этом плане не уникален. Вы найдёте указатели внутри _всех_ контейнеров стандартной библиотеки. Без указателей не обойтись при реализации списков, деревьев, хеш-таблиц и других динамических структур данных. ## Зачем уметь работать с указателями Управление динамической памятью неотделимо от работы с указателями. Понимание, _что_ происходит c указателями и динамической памятью внутри классов стандартной библиотеки, поможет: - Избегать лишнего выделения или копирования памяти. - Подбирать эффективные алгоритмы. Например, за счёт понимания, _почему_ у какого-то метода контейнера сложность — _амортизированная_ константа `O(1)`, а у другого — просто константа `O(1)`. Указатели — ключ к пониманию языка, к оптимизации потребления ресурсов и скорости выполнения. Поэтому любой C++ разработчик должен уметь работать с указателями. ## План действий В следующих главах мы по шагам разберём тему указателей: - Для начала просто научимся с ними работать. - Затем — перемещаться по памяти с помощью указателей. - И, наконец, этой памятью управлять: выделять её и освобождать. ---------- ## Резюме - Переменная может располагаться в одной из трёх областей памяти: в статической, автоматической, либо динамической памяти. - Размер статической и автоматической памяти ограничен. Для работы с большим количеством объектов предназначена динамическая память. - Работа с динамической памятью организуется через указатели. - Указатель — это переменная, хранящая адрес в оперативной памяти. - Указатели — одна из центральных концепций языка. И C++ разработчик обязан уметь её применять.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!