# Глава 15.1. Зачем нужны указатели
C++ — это высокоуровневый язык с низкоуровневыми возможностями. С одной стороны, в нём есть средства для построения абстракций. В первую очередь это классы и шаблоны. С другой стороны, вы можете спуститься на уровень, максимально близкий к аппаратному. На нём вы управляете памятью напрямую: вручную контролируете её выделение и освобождение. Но для чего это нужно?
## Области памяти программы
Для начала [вспомним,](/courses/cpp/chapters/cpp_chapter_0090/#block-memory) где живут переменные. Виртуальное адресное пространство процесса разбито на области, и переменные могут располагаться в одной из трёх областей:
- статической,
- автоматической,
- динамической.
 {.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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!