# Глава 16.1. Основы работы с указателями
C++ — это высокоуровневый язык с низкоуровневыми возможностями. С одной стороны, в нем есть средства для построения абстракций. В первую очередь это классы и шаблоны. С другой стороны, вы можете спуститься на уровень, максимально близкий к аппаратному. C++ позволяет работать с памятью напрямую: вручную контролировать её выделение и освобождение, обращаться по конкретному адресу.
Управление памятью завязано на концепцию указателей. Они лежат в основе реализации динамических структур данных, размер которых может изменяться во время выполнения программы. Например, это списки, динамические массивы, деревья и хеш-таблицы. Вы найдёте указатели под капотом контейнеров стандартной библиотеки.
## Что такое указатель
[Указатель](https://en.cppreference.com/w/cpp/language/pointer.html) (pointer) — это переменная, которая хранит адрес в оперативной памяти. Отсюда и название: значение такой переменной как бы _указывает_ на область памяти. А адрес — это по сути число. Например, `0x55ae9a41c2a0`. Поэтому можно сказать, что указатель — это переменная, в которой лежит целое неотрицательное число, трактуемое компилятором как адрес.
Если указатель содержит адрес конкретной переменной, то через него можно получить к ней доступ. Такой доступ является _косвенным:_ вместо обращения к значению переменной напрямую сначала происходит обращение к указателю, а затем — по адресу, на который он указывает.
## Объявление указателя
При объявлении указателя между его типом и именем ставится символ звёздочки `*`. Так выглядит объявление указателя `name` на переменную типа `T`:
```cpp
T * name;
```
Здесь символ `*` — это часть типа `T *`, а вовсе не оператор умножения. Как и у многих других символов, его смысл зависит от контекста.
Расставлять пробелы можно как угодно:
```cpp
T* name;
T * name;
T *name;
```
Разберем создание указателя на вектор:
```cpp
std::vector * p;
```
Что происходит на этой строке?
- Компилятор выделяет память под указатель `p`, но не под объект типа `std::vector`, на который указывает `p`. Вектор может уже существовать или создаваться дальше по коду.
- Компилятор не инициализирует выделенную память, ведь мы не присвоили указателю никакого значения. Как и при [инициализации по умолчанию](/courses/cpp/chapters/cpp_chapter_0131/#block-default-initialization) других типов, не имеющих конструктора, объект `p` может содержать любой мусор. Чтобы указатель ссылался на адрес переменной, нужно этот адрес получить.
Напишите, как выглядит объявление указателя `node` на объект класса `Node` без инициализации. {.task_text}
```consoleoutput {.task_source #cpp_chapter_0161_task_0010}
```
Нужно создать указатель типа `Node *` с именем `node`. Не забудьте `;` в конце. {.task_hint}
```cpp {.task_answer}
Node * node;
```
## Оператор взятия адреса
[Оператор взятия адреса](https://en.cppreference.com/w/cpp/language/operator_member_access.html#Built-in_address-of_operator) (address-of operator) `&` — это унарный оператор, который ставится перед своим операндом и возвращает его адрес:
```cpp {.example_for_playground .example_for_playground_001}
bool b = true;
// Инициализируем указатель b_ptr адресом переменной b
bool * b_ptr = &b;
```
 {.illustration}
В данном контексте символ `&` не имеет ничего общего с объявлением ссылки или логическим «И». При объявлении ссылки символ `&` относится к типу. А при взятии адреса он ставится перед уже существующей переменной. Сравните:
```cpp {.example_for_playground .example_for_playground_002}
int x = 5;
int & ref = x; // Это - объявление ссылки на x
int * ptr = &x; // А это - инициализация указателя адресом x
```
### Как вывести в консоль адрес объекта
Чтобы вывести адрес объекта в консоль, нужно явно привести его к типу указателя `void *`.
```cpp
std::println("{}", static_cast<void *>(&x));
```
```
0x7ffe4f884814
```
Тип `void *` означает, что указатель [может ссылаться](https://timsong-cpp.github.io/cppwp/n4868/basic.compound#5) на адрес объекта любого типа. Явное приведение типа неудобно, зато подчёркивает намерение получить адрес:
```cpp {.example_for_playground .example_for_playground_003}
int val = 256;
int * ptr = &val;
std::println("val={}. Address: {}", val, static_cast<void *>(&val));
std::println("ptr={}", static_cast<void *>(ptr));
```
```
val=256. Address: 0x7ffe4f884814
ptr=0x7ffe4f884814
```
[Спецификатор](https://en.cppreference.com/w/cpp/utility/format/spec.html) `p` добавляется в строку форматирования, чтобы указать: в данном месте будет подставлен указатель.
```cpp
std::println("ptr={:p}", static_cast<void *>(ptr));
```
```
ptr=0x7ffd6df3c614
```
А чтобы вывести адрес в верхнем регистре, используется спецификатор `P`.
```cpp
std::println("ptr={:P}", static_cast<void *>(ptr));
```
```
ptr=0X7FFDA8185ED4
```
Реализуйте функцию `check_eq()`, которая принимает _по ссылке_ два параметра типа `double`. {.task_text}
Функция возвращает строку. Строка содержит один либо два разделённых пробелом адреса в зависимости от того, являются ли аргументы функции одним и тем же объектом в памяти. {.task_text}
Например, если первый аргумент расположен по адресу `0x7ffce00d8bc0`, а второй — по адресу `0x7ffce00d8bb8`, то функция должна вернуть строку `"0x7ffce00d8bc0 0x7ffce00d8bb8"`. {.task_text}
Для форматирования строки вам понадобится функция [std::format()](https://en.cppreference.com/w/cpp/utility/format/format.html).
```cpp {.task_source #cpp_chapter_0161_task_0020}
// Ваша реализация check_eq()
```
Чтобы выяснить, являются ли аргументы одним и тем же объектом в памяти, нужно проверить, совпадают ли их адреса. Для возврата форматированной строки из функции вызовите `std::format()`. Например, `std::format("{:p}", static_cast<void *>(&a))`. {.task_hint}
```cpp {.task_answer}
std::string check_eq(double & a, double & b)
{
return &a == &b ?
std::format("{:p}", static_cast<void *>(&a)) :
std::format("{:p} {:p}", static_cast<void *>(&a), static_cast<void *>(&b));
}
```
## Оператор разыменования
Чтобы обратиться к переменной, на которую ссылается указатель, перед ним ставится [оператор разыменования](https://en.cppreference.com/w/cpp/language/operator_member_access.html#Built-in_indirection_operator) (dereference operator) `*`. Он также известен как оператор косвенного доступа (indirection operator).
```cpp {.example_for_playground .example_for_playground_004}
int x = 504;
int * x_ptr = &x;
// Через указатель x_ptr косвенно обращаемся к x
std::println("{}", *x_ptr);
```
```
504
```
Применение оператора `*` к указателю называется **разыменованием указателя.** Так, `*x_ptr` читается как «разыменование указателя `x_ptr`» или «обращение по указателю `x_ptr`».
Указатель типа `void *` нельзя разыменовывать: компилятору неизвестно, на переменную какого размера ссылается такой указатель, и поэтому не может корректно работать с её значением.
Разыменование необходимо и для чтения, и для _записи_ данных, адрес которых хранит указатель:
```cpp {.example_for_playground .example_for_playground_005}
// Изменяем значение переменной, на которую указывает x_ptr
*x_ptr = 200;
std::println("{}", x);
```
```
200
```
Что выведет этот код? {.task_text}
```cpp {.example_for_playground .example_for_playground_006}
std::uint16_t code = 100;
std::uint16_t * p = &code;
*p += *p * 3;
std::println("{}", *p);
```
```consoleoutput {.task_source #cpp_chapter_0161_task_0030}
```
Мы завели переменную `code` со значением `100` и указатель `p`, хранящий её адрес. Затем мы разыменовали указатель, чтобы работать с оригитальной переменной. Выражение `*p * 3` означает, что значение переменой, на которую указывает `p`, мы умножаем на 3. Запись `*p += ...` означает, что полученный результат мы прибавляем к оригинальной переменной. Строку `*p += *p * 3;` можно переписать так: `code += code * 3;`. {.task_hint}
```cpp {.task_answer}
400
```
В прошлой главе вы уже [реализовывали](/courses/cpp/chapters/cpp_chapter_0150/#block-swap) функцию `swap()`. На этот раз нужно написать немного странный и избыточный вариант `swap_via_pointers()`, в котором обмен значениями двух параметров происходит через указатели. {.task_text}
Для вас уже заведены два указателя. Пользуйтесь ими и временной переменной. Обращаться к параметрам `a` и `b` нельзя. {.task_text}
```cpp {.task_source #cpp_chapter_0161_task_0040}
void swap_via_pointers(double & a, double & b)
{
double * x = &a;
double * y = &b;
// Ваш код. К параметрам a и b обращаться нельзя.
}
```
Заведите временную переменную типа `double`. Сохраните в неё значение переменной, на которую указывает `x`. Затем разыменуйте `x`, чтобы сохранить значение разыменованного указателя `y`. И, наконец, разыменуйте `y`, чтобы сохранить в него значение временной переменной. {.task_hint}
```cpp {.task_answer}
void swap_via_pointers(double & a, double & b)
{
double * x = &a;
double * y = &b;
double tmp = *x;
*x = *y;
*y = tmp;
}
```
Приоритет оператора взятия адреса `&` и оператора разыменования `*` одинаковый. Он [такой же,](https://en.cppreference.com/w/cpp/language/operator_precedence.html) как у пре-инкремента, и ниже, чем у пост-инкремента. Поэтому не забывайте правильно расставлять скобки.
```cpp {.example_for_playground .example_for_playground_007}
int x = 200;
int * x_ptr = &x;
// Делаем инкремент значения, на которое указывает x_ptr
(*x_ptr)++;
std::println("{}", x);
```
```
201
```
Что выведет этот код? Введите `err` в случае ошибки компиляции. {.task_text}
```cpp {.example_for_playground .example_for_playground_008}
int a = 4;
int b = *&a; // То же самое, что *(&a)
std::println("{}", b == 4);
```
```consoleoutput {.task_source #cpp_chapter_0161_task_0050}
```
Чтобы было проще читать конструкцию `int b = *&a;`, можно расставить скобки: `int b = *(&a);`. Мы берём адрес переменной `a` и к получившемуся значению применяем разыменование, то есть обращаемся к переменной по адресу. При последовательном применении к переменной операторов `&` и `*` мы получаем исходную переменную. {.task_hint}
```cpp {.task_answer}
true
```
## Размер указателя
Сколько памяти выделяется под указатель? Ее должно быть достаточно, чтобы указатель мог хранить _любой_ адрес оперативной памяти. Но конкретный размер указателя зависит в основном от целевой ОС и архитектуры. В большинстве реализаций длина указателя равна длине [машинного слова:](https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%88%D0%B8%D0%BD%D0%BD%D0%BE%D0%B5_%D1%81%D0%BB%D0%BE%D0%B2%D0%BE) для 32-битных платформ это 4 байта, а для 64-битных — 8 байт. Есть особые случаи, когда указатель занимает больше. Про них вы узнаете позже.
Размер указателя **не зависит** от типа, на который он указывает.
Сколько байт занимает указатель в нашем [плэйграунде?](https://senjun.ru/playground/cpp/) Чтобы узнать, откройте плэйграунд и примените оператор `sizeof` у указателю. {.task_text}
```consoleoutput {.task_source #cpp_chapter_0161_task_0060}
```
Пример: `std::println("{}", sizeof(int *))`. {.task_hint}
```cpp {.task_answer}
8
```
Что выведет этот код? {.task_text}
```cpp {.example_for_playground .example_for_playground_009}
std::vector<int> * p_vec;
bool * p_bool;
std::println("{}", sizeof(p_vec) == sizeof(p_bool));
```
```consoleoutput {.task_source #cpp_chapter_0161_task_0070}
```
Размер указателя не зависит от типа, на который он указывает. {.task_hint}
```cpp {.task_answer}
true
```
## Нулевой указатель
В отличие от ссылок, указатель не требуется инициализировать конкретным адресом. Его можно присвоить и после создания.
```cpp {.example_for_playground .example_for_playground_010}
std::size_t a = 16;
std::size_t b = 32;
std::size_t * ptr;
ptr = &a; // присваиваем адрес после объявления
ptr = &b; // переназначаем адрес
```
Главное — помнить, что указатель, как и любая другая переменная, должен быть инициализирован:
```cpp
int * ptr; // В ptr может лежать любой мусор
std::println("{}", *ptr); // UB
```
Если указатель не ссылается на конкретный объект, обязательно присваивайте ему значение [nullptr](https://en.cppreference.com/w/cpp/language/nullptr.html). Это ключевое слово и одновременно литерал, означающий, что указатель не хранит адрес объекта:
```cpp
int * ptr = nullptr;
```
Литерал `nullptr` появился в C++11. В более старом коде вместо него используется макрос [NULL](https://cppreference.com/w/c/types/NULL.html):
```cpp
int * ptr = NULL;
```
Определение `NULL` зависит от реализации. Оно может быть таким:
```cpp
#define NULL 0
```
Или, например, таким:
```cpp
#define NULL nullptr
```
У `nullptr` тип [std::nullptr_t](https://en.cppreference.com/w/cpp/types/nullptr_t.html), а `NULL` может быть целым числом `int`. В любом случае они совместимы между собой благодаря [неявному приведению типов.](/courses/cpp/chapters/cpp_chapter_0010/#block-implicit-cast)
```cpp {.example_for_playground}
import std;
#include <cstddef> // Хедер содержит объявление макроса NULL
int main()
{
int * ptr = nullptr;
std::println("{}", ptr == NULL);
}
```
```
true
```
В современном C++ предпочтение отдаётся `nullptr`: его использование явно разграничивает, в каком случае идёт работа с числами, а в каком — с указателем.
Что выведет этот код? Введите `err` в случае ошибки компиляции или `ub`, если поведение не определено. {.task_text}
```cpp {.example_for_playground}
import std;
int main()
{
void * self = &self;
std::println("{}", static_cast<bool>(self));
}
```
```consoleoutput {.task_source #cpp_chapter_0161_task_0080}
```
В момент инициализации `self` своим же адресом эта переменная [уже существует,](https://timsong-cpp.github.io/cppwp/std23/basic.scope.pdecl#1) поэтому ошибки компиляции нет. В `self` сохраняется её же адрес, и он точно не равен `nullptr`. А приведение любого не нулевого числа к `bool` — это `true`. {.task_hint}
```cpp {.task_answer}
true
```
Указатель, равный `nullptr` или `NULL`, называется **нулевым указателем.** Если указатель не хранит адрес конкретного объекта, всегда делайте его нулевым. Но помните, что разыменование нулевого указателя — это тоже UB:
```cpp
int * ptr = nullptr;
std::println("{}", *ptr); // Так делать нельзя
```
Если по логике программы указатель _может_ оказаться нулевым, перед его разыменованием добавляйте проверку на `nullptr`.
```cpp {.example_for_playground .example_for_playground_011}
int * ptr = nullptr;
// ...
if (ptr == nullptr)
std::println("Null pointer");
else
std::println("{}", *ptr);
```
Обратите внимание, что с `nullptr` сравнивается сам указатель `ptr`, а не объект `*ptr`, на который он указывает. Ведь к указателям применимо сравнение операторами `>`, `>=`, `<`, `<=`, `==` и `!=`. При этом происходит _сравнение адресов,_ на которые они указывают.
Иногда проверку вида `ptr != nullptr` записывают более лаконично:
```cpp {.example_for_playground .example_for_playground_012}
if (ptr)
{
std::println("Valid pointer");
}
else
{
std::println("Null pointer");
}
```
Этот вариант работает благодаря неявному приведению `nullptr` к `false` и остальных значений — к `true`. Однако мы рекомендуем более явную проверку вида `ptr == nullptr` и `ptr != nullptr`. Она подчёркивает, что перед вами указатель, а не `bool`, число или какой-то другой тип.
## Оператор доступа к полям и методам класса
Допустим, у нас есть указатель на объект класса `std::pair`:
```cpp
std::pair<std::string, bool> res = {"/", true};
std::pair<std::string, bool> * res_ptr = &res;
```
Обращение к полям объекта осуществляется через оператор `.`. Его приоритет выше, чем у оператора разыменования `*`. Но чтобы обратиться к полю объекта, нужно сначала получить к нему косвенный доступ, разыменовав указатель. Чтобы поменять порядок применения операторов, используем скобки:
```cpp {.example_for_playground .example_for_playground_013}
(*res_ptr).first = "/etc/search.yaml"; // обращаемся к полю
std::println("{} {}", (*res_ptr).first, (*res_ptr).second);
```
```
/etc/search.yaml true
```
Аналогично выглядит вызов метода объекта:
```cpp {.example_for_playground .example_for_playground_014}
std::string uuid = "a674b109-b08d-433a-aed4-7e03861345d0";
std::string * p = &uuid;
std::size_t len = (*p).size(); // вызываем метод строки
std::println("{}", len);
```
```
36
```
Синтаксис вида `(*p).field` слишком громоздкий. Удобнее использовать оператор `->` для косвенного доступа к полю или методу класса через указатель. Считайте `p->field` синтаксическим сахаром, по сути не отличающимся от `(*p).field`.
```cpp {.example_for_playground .example_for_playground_015}
std::pair<std::string, bool> res = {"/", true};
std::pair<std::string, bool> * res_ptr = &res;
res_ptr->first = "/etc/search.yaml"; // обращаемся к полю
```
```cpp {.example_for_playground .example_for_playground_016}
std::string uuid = "a674b109-b08d-433a-aed4-7e03861345d0";
std::string * p = &uuid;
std::size_t len = p->size(); // вызываем метод строки
```
### Указатели и рекурсивные структуры данных
Итак, через указатели мы можем работать с объектами классов и структур. А они в свою очередь могут иметь поля с типом «указатель». Если тип такого поля совпадает с типом исходного класса, то перед вами рекурсивная структура данных.
Например, так выглядит структура, реализующая элемент [односвязного списка](https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA) целых чисел:
```cpp {.example_for_playground .example_for_playground_017}
struct ListNode
{
// Конструкторы
ListNode() {}
explicit ListNode(int value) : val(value) {}
explicit ListNode(int value, ListNode * next_node)
: val(value), next(next_node) {}
// Значение элемента списка
int val = 0;
// Указатель на следующий элемент списка
ListNode * next = nullptr;
};
```
Нам даже не обязательно заводить класс `List`. Структуры `ListNode` достаточно, чтобы собрать список и работать с ним:
```cpp {.example_for_playground .example_for_playground_018}
// Составляем список из 3-х элементов
ListNode tail{5, nullptr};
ListNode middle{3, &tail};
ListNode head{1, &middle};
// Итерируемся по списку
for(ListNode * node = &head; node != nullptr; node = node->next)
std::println("Node value: {}", node->val);
```
```
Node value: 1
Node value: 3
Node value: 5
```
Заведите структуру `BinTreeNode`, представляющую собой узел [бинарного дерева.](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE) Она должна хранить значение `val` типа `std::size_t`, указатели на левое и правое поддерево `left` и `right`. По умолчанию поля должны принимать значения 0 и `nullptr` соответственно. {.task_text}
У структуры должно быть два параметризованных конструктора со [спецификатором](/courses/cpp/chapters/cpp_chapter_0131/#block-explicit) `explicit`: {.task_text}
- для инициализации переданным значением `val`,
- для инициализации `val`, `left` и `right`. Именно в таком порядке.
```cpp {.task_source #cpp_chapter_0161_task_0090}
// Ваша реализация BinTreeNode
```
Для инициализации полей воспользуйтесь [DMI](/courses/cpp/chapters/cpp_chapter_0132/#block-dmi) (default member initialization, прямая инициализация полей). Это инициализация поля по месту его объявления. Затем заведите два конструктора: для инициализации `val` и для инициализации всех полей. В этих конструкторах используйте [список инициализации полей.](/courses/cpp/chapters/cpp_chapter_0132/#block-member-initializer-list) {.task_hint}
```cpp {.task_answer}
struct BinTreeNode
{
// Конструкторы
BinTreeNode()
{ }
explicit BinTreeNode(std::size_t x) : val(x)
{ }
explicit BinTreeNode(std::size_t x, BinTreeNode * l, BinTreeNode * r)
: val(x), left(l), right(r)
{ }
// Значение узла
std::size_t val = 0;
// Указатели на левое и правое поддерево
BinTreeNode * left = nullptr;
BinTreeNode * right = nullptr;
};
```
## Передача параметров по указателю
В C++ существует три способа передачи в функцию параметров:
- По значению (by value).
- По ссылке (by reference).
- По указателю (by pointer).
В прошлой главе мы [рассмотрели](/courses/cpp/chapters/cpp_chapter_0150/#block-func) передачу по значению и по ссылке. При передаче по значению функция работает с копией объекта, а при передаче по ссылке — с исходным объектом.
Как и передача по ссылке, передача по указателю нужна, чтобы избежать копирования: функция получает указатель и через него косвенно обращается к исходному объекту. Но передача по указателю означает, что он может быть равен `nullptr`. Поэтому если чётко не установлено, что такого быть не может, обязательно проверяйте, является ли указатель нулевым.
```cpp {.example_for_playground}
import std;
struct ConfValue
{
std::string section;
std::string name;
std::string value;
};
bool is_set(ConfValue * conf_val) // Принимаем conf_val по указателю
{
return conf_val != nullptr && !conf_val->value.empty();
}
int main()
{
ConfValue v{"net", "ip", "127.0.0.1"};
std::println("{}", is_set(&v)); // Передаем адрес v
ConfValue * v_ptr = &v;
std::println("{}", is_set(v_ptr)); // Передаем указатель v_ptr
std::println("{}", is_set(nullptr));
}
```
```
true
true
false
```
Под капотом передача параметра по ссылке и по указателю сводится к одному и тому же: в функцию попадает адрес объекта, и функция работает с этим объектом.
Перед вами классическая задача с собеседований. Нужно написать функцию `has_cycle()`, которая принимает указатель на первый элемент односвязного списка и определяет, есть ли в списке цикл. {.task_text}
Список содержит цикл, если есть такой узел, до которого можно _повторно_ добраться, непрерывно перебирая указатели `next`. {.task_text}
Решите эту задачу, используя `O(1)` дополнительной памяти. У вашего решения должна быть линейная сложность `O(N)`. Если вы хотите узнать простой алгоритм решения, то можете воспользоваться подсказкой. {.task_text}
Пример списка с циклом: {.task_text}
 {.illustration}
```cpp {.task_source #cpp_chapter_0161_task_0100}
struct ListNode
{
ListNode() {}
explicit ListNode(int value) : val(value) {}
explicit ListNode(int value, ListNode * next_node)
: val(value), next(next_node) {}
int val = 0;
ListNode * next = nullptr;
};
bool has_cycle(ListNode * head)
{
}
```
[Алгоритм «Черепаха и заяц»](https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%86%D0%B8%D0%BA%D0%BB%D0%B0#%D0%A7%D0%B5%D1%80%D0%B5%D0%BF%D0%B0%D1%85%D0%B0_%D0%B8_%D0%B7%D0%B0%D1%8F%D1%86) работает с двумя указателями: медленный указатель (черепаха) сдвигается по списку на один элемент, а быстрый (заяц) — на два элемента. Если они встретятся, то найден цикл. В своём решении не забывайте вовремя проверять указатель на равенство `nullptr`. {.task_hint}
```cpp {.task_answer}
struct ListNode
{
ListNode() {}
explicit ListNode(int value) : val(value) {}
explicit ListNode(int value, ListNode * next_node)
: val(value), next(next_node) {}
int val = 0;
ListNode * next = nullptr;
};
bool has_cycle(ListNode * head)
{
ListNode * slow = head;
ListNode * fast = head;
while (fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true;
}
return false;
}
```
### Низведение массива (array to pointer decay)
Как вы [помните,](/courses/cpp/chapters/cpp_chapter_0142/#block-array-to-pointer-decay) при передаче сишного массива в функцию теряется информация о его длине. Вместе с массивом приходится передавать дополнительный параметр — количество его элементов. А виной всему механизм под названием низведение массива (array to pointer decay).
Теперь вам легко понять, в чем его суть, ведь вы узнали про указатели. Сишный массив — это непрерывная область памяти, выделенная под фиксированное количество элементов типа `T`. Но в функцию вместо самого массива попадает указатель на нулевой элемент. Происходит низведение, то есть неявное приведение массива к указателю. Оно позволяет избежать лишнего копирования.
Так выглядит передача сишного массива в функцию:
```cpp
double get_median(double data[], std::size_t len);
```
А это — эквивалентный вариант записи:
```cpp
double get_median(double * data, std::size_t len);
```
В этих двух вариантах передача массива в функцию и работа с ним внутри функции совершенно не отличаются.
При этом оба способа нежелательно использовать в новом коде. Запись `double * data` запутывает: перед нами массив или указатель на единственное значение? Запись `double data[]` понятнее, но лучший совет по работе с сишными массивами — [избегать их.](/courses/cpp/chapters/cpp_chapter_0142/#block-advice) Поэтому самый безопасный и современный [вариант](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rr-ap) передачи массива в функцию — это `std::span`:
```cpp
double get_median(std::span<double> data);
```
## Указатели и константность
В главе про ссылки мы [говорили,](/courses/cpp/chapters/cpp_chapter_0150/#block-const) что константная ссылка и ссылка на константный объект — это одно и то же. В случае с указателями все разнообразнее. Константный указатель и указатель на константный объект — это разные вещи.
**Константный указатель** нельзя «перевесить» на другой объект, зато сам объект изменять через него можно:
```cpp {.example_for_playground .example_for_playground_019}
bool is_authorized = true;
bool * const p = &is_authorized; // Константный указатель
*p = false; // Ок
p = nullptr; // ошибка
```
```
main.cpp:9:7: error: cannot assign to variable 'p' with const-qualified type 'bool *const'
9 | p = nullptr;
| ~ ^
```
**Указатель на константный объект** можно переназначить, но объект через него модифицировать не получится:
```cpp {.example_for_playground .example_for_playground_020}
bool enable_encoding = true;
const bool * p = &enable_encoding; // Указатель на константу
*p = false; // Ошибка
p = nullptr; // Ок
```
```
main.cpp:8:8: error: read-only variable is not assignable
8 | *p = false;
| ~~ ^
```
Из этого примера видно, что указатель на константный объект можно использовать для доступа к неконстантной переменной. Просто у вас не получится изменить эту переменную через такой указатель. А получить неконстантый указатель на константный объект нельзя.
И, наконец, **константный указатель на константный объект** не разрешает ни переприсваивать указатель, ни изменять объект:
```cpp {.example_for_playground .example_for_playground_021}
bool has_focus = true;
const bool * const p = &has_focus; // Константный указатель на константу
*p = false; // Ошибка
p = nullptr; // Ошибка
```
Правило расстановки `const` при объявлении указателей легко запомнить:
- Если константность относится к объекту, то `const` ставится слева от `*`. То есть рядом с типом объекта.
- Если константность относится к указателю, то `const` ставится справа.
 {.illustration}
На какой строке кода допущена ошибка? Введите `0`, если ошибок нет и код скомпилируется. {.task_text}
```cpp {.example_for_playground .example_for_playground_022}
std::vector<int> v = {1, 5, 10}; // 1
std::vector<int> * const ptr = &v; // 2
ptr->clear(); // 3
ptr = nullptr; // 4
```
```consoleoutput {.task_source #cpp_chapter_0161_task_0110}
```
Указатель `ptr` является константным. По нему можно изменять объект. Но сам указатель изменять нельзя. {.task_hint}
```cpp {.task_answer}
4
```
В промышленной разработке константные указатели встречаются довольно редко. Чаще всего используются указатели на константные объекты. Поэтому в обиходе под константными указателями понимаются именно они.
Попробуем вывести в консоль указатель, у которого есть константность.
```cpp {.example_for_playground .example_for_playground_023}
int val = 101;
const int * ptr = &val;
std::println("{}", static_cast<void *>(ptr));
```
```
main.cpp:8:25: error: static_cast from 'const int *' to 'void *' is not allowed
8 | std::println("{}", static_cast<void *>(ptr));
| ^~~~~~~~~~~~~~~~~~~~~~~~
```
Ошибка компиляции говорит о том, что нельзя привести тип `const T *` к `void *`, то есть убрать константность. Поэтому нужно правильно расставить `const` в результирующем типе `void *`:
```cpp
std::println("{:p}", static_cast<const void *>(ptr));
```
```
0x7ffc36211234
```
А вот — ещё одна популярная задача про указатели с собеседований. Нужно написать функцию `get_intersection()`, которая принимает указатели на первые элементы двух списков. Функция должна вернуть указатель на элемент, на котором эти списки пересекаются. Если пересечения нет, функция возвращает `nullptr`. {.task_text}
Решите эту задачу, используя `O(1)` дополнительной памяти. У вашего решения должна быть линейная сложность `O(N + M)`, где `N` и `M` — длины списков. Чтобы посмотреть алгоритм, воспользуйтесь подсказкой. {.task_text}
Пересечение — это указатель, хранящий адрес элемента, который присутствует в обоих списках. В этом примере пересечение — это указатель на элемент со значением `8`: {.task_text}
 {.illustration}
```cpp {.task_source #cpp_chapter_0161_task_0120}
struct ListNode
{
ListNode() {}
explicit ListNode(int value) : val(value) {}
explicit ListNode(int value, ListNode * next_node)
: val(value), next(next_node) {}
int val = 0;
ListNode * next = nullptr;
};
const ListNode * get_intersection(const ListNode * head_a, const ListNode * head_b)
{
}
```
Алгоритм называется «Метод двух указателей с переключением». Его суть в том, что два указателя перемещаются по своим спискам. При достижении конца они переключаются на начало _другого_ списка. Это уравнивает количество элементов, перебираемое указателями. Если списки не пересекаются, указатели достигают конца одновременно. Если пересекаются, то указатели одновременно устанавливаются на общий элемент. Инициализируем указатели `ptr_a` и `ptr_b`, чтобы они указывали на начало двух списков. Пока эти указатели не равны, смещаем их по соответствующим спискам. Если какой-то из указателей достиг конца списка, перевешиваем его на другой список. Если пересечения нет, указатели _одновременно_ станут нулевыми. А если у списков есть пересечение, то указатели встретятся максимум через два обхода списков. {.task_hint}
```cpp {.task_answer}
struct ListNode
{
ListNode() {}
explicit ListNode(int value) : val(value) {}
explicit ListNode(int value, ListNode * next_node)
: val(value), next(next_node) {}
int val = 0;
ListNode * next = nullptr;
};
const ListNode * get_intersection(const ListNode * head_a, const ListNode * head_b)
{
const ListNode * ptr_a = head_a;
const ListNode * ptr_b = head_b;
while(ptr_a != ptr_b)
{
ptr_a = (ptr_a == nullptr) ? head_b : ptr_a->next;
ptr_b = (ptr_b == nullptr) ? head_a : ptr_b->next;
}
return ptr_a;
}
```
## Когда использовать указатели, а когда — ссылки
Указатели — довольно опасный инструмент. И в следующих главах мы подробно обсудим, почему. Речь пойдёт о том, что в основном указатели применяются для адресной арифметики и управления динамической памятью. В остальных же случаях [предпочитайте](https://isocpp.org/wiki/faq/references#refs-vs-ptrs) ссылки. Нужно, чтобы функция модифицировала параметр? Передавайте его по ссылке. Нужно избежать копирования? Передавайте объект по константной ссылке.
Есть несколько сценариев, при которых потребуется указатель, а не ссылка:
- Обнуление или переназначение на другой объект.
- Адресная арифметика.
- Динамическое управление памятью.
- Предоставление си-интерфейса для кода на другом языке. В Си есть указатели и нет ссылок.
----------
## Резюме
- Указатель — это переменная, хранящая адрес в оперативной памяти.
- В типе указателя присутствует символ `*`. Например, `std::string *` — это указатель на строку.
- Чтобы присвоить указателю адрес переменной, к ней применяется оператор взятия адреса `&`. Например, `&val` читается как «взятие адреса `val`».
- Чтобы через указатель косвенно обратиться к переменной, перед ним ставится оператор разыменования `*`. Так, `*p` читается как «разыменование `p`».
- Размер указателя не зависит от типа, на который он указывает.
- Указатель, который не хранит конкретный адрес, нужно делать нулевым: `p = nullptr`.
- Для доступа к полям и методам класса через указатель используется оператор `->`.
- Константный указатель не даёт переназначать указателю новый адрес, а указатель на константный объект не даёт изменять объект.
- Функция может принимать параметры по значению, по ссылке или по указателю.
- Если в функцию передаётся сишный массив, происходит низведение массива: он неявно приводится к указателю на нулевой элемент.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!