Содержание
В предыдущей статье мы научились работать с двумерными массивами: заполнять матрицы, выводить их и находить элементы на диагоналях. До этого мы обращались к данным по имени переменной или по индексу элемента массива. Теперь посмотрим, что происходит с переменной в памяти и как обратиться к ней по адресу.
Представьте, что Вы вызываете такси. Фразы «заберите меня из университета и отвезите домой» недостаточно: водителю нужны адреса. В видео мы используем эту аналогию, чтобы понять работу с памятью. У переменной в программе тоже есть своё место, и указатель позволяет сохранить его адрес, а затем обратиться к переменной через него.
В этой статье разберём указатели на обычные переменные, операции & и *, указатель на указатель и значение nullptr. Все примеры можно запустить в C++17.
После этой статьи Вы сможете:
- объяснить, чем значение переменной отличается от её адреса;
- получить адрес переменной с помощью
&; - объявить указатель и обратиться к переменной через
*; - изменить значение переменной через указатель;
- прочитать объявление
int **и пройти два уровня разыменования; - объяснить, для чего нужен
nullptrи почему его нельзя разыменовывать.
Где хранятся переменные
Когда программа выполняется, процессор исполняет её инструкции, а данные размещаются в памяти. Например, программа создаёт две переменные, складывает их значения и выводит результат:
#include <iostream>
int main() {
int a = 7;
int b = 5;
int sum = a + b;
std::cout << sum;
return 0;
}
У переменной есть имя, тип и значение. В нашем примере имя первой переменной — a, тип — int, значение — 7. При этом сама переменная занимает определённое место в памяти.
Память удобно представлять как последовательность байтов. У каждого байта есть адрес, а одна переменная может занимать несколько байтов. Размер объекта в байтах можно узнать с помощью sizeof:
#include <iostream>
int main() {
int a = 7;
std::cout << sizeof(a);
return 0;
}
Как узнать адрес переменной: операция &
Чтобы получить адрес переменной, перед её именем ставят амперсанд — символ &. Выражение &a читается как «адрес переменной a».
#include <iostream>
int main() {
int a = 7;
std::cout << a << '\n';
std::cout << &a << '\n';
return 0;
}
В первой строке программа выведет значение 7, во второй — адрес переменной. Например, вывод может выглядеть так:
7
0x16fdff2ac
Запись с префиксом 0x обычно означает шестнадцатеричное число. Помимо цифр от 0 до 9, в этой системе используются буквы от a до f. Для работы с указателями переводить адрес в десятичную систему не требуется.
Если переменная занимает несколько байтов, какой адрес мы получаем? Для обычной переменной из наших примеров &a указывает на начало занимаемого ею участка памяти — на первый байт. Все байты одной переменной не превращаются в несколько отдельных указателей.
Что такое указатель
Указатель — это переменная, в которой можно хранить адрес объекта. В наших примерах таким объектом будет другая переменная. Чтобы объявить указатель на int, используем тип int *:
int a = 7;
int *ptr = &a;
Разберём вторую строку по частям:
| Часть записи | Значение |
|---|---|
int * | Тип «указатель на int» |
ptr | Имя указателя |
= | Инициализация указателя |
&a | Адрес переменной a |
В ptr хранится адрес a. Число 7 при этом остаётся в самой переменной a. Указатель не создаёт её копию.
Имя указателя Вы выбираете самостоятельно. В видео мы использовали в том числе имя arbuz: int *arbuz = &a;. Для языка это такое же допустимое имя, как ptr. В рабочей программе лучше выбирать имя, которое помогает понять назначение переменной.
Разыменование: как получить значение через указатель
Мы сохранили адрес в ptr. Теперь нужно обратиться к переменной по этому адресу. Для этого перед именем указателя ставят *. Эта операция называется разыменованием.
#include <iostream>
int main() {
int a = 7;
int *ptr = &a;
std::cout << a << '\n';
std::cout << *ptr << '\n';
return 0;
}
Результат:
7
7
В первой строке мы читаем a по имени. Во второй — обращаемся к той же переменной через ptr. В обоих случаях получаем число 7.
| Выражение | Что оно обозначает в этом примере |
|---|---|
a | Переменную со значением 7 |
&a | Адрес переменной a |
ptr | Указатель, в котором хранится адрес a |
*ptr | Ту же переменную a, доступную через указатель |
&ptr | Адрес самого указателя ptr |
Размер самого указателя можно проверить выражением sizeof(ptr). На распространённых 64-битных системах указатели на int, double и char обычно занимают 8 байт, хотя сами эти типы могут иметь разный размер. C++ не устанавливает универсальное правило «любой указатель всегда равен 8 байтам» для всех платформ.
Как изменить переменную через указатель
Разыменование позволяет не только прочитать значение, но и записать новое. Посмотрите на строку *ptr = 12;:
#include <iostream>
int main() {
int a = 7;
int *ptr = &a;
*ptr = 12;
std::cout << a << '\n';
std::cout << *ptr << '\n';
return 0;
}
12
12
Сначала ptr хранит адрес a. Выражение *ptr обращается к a, а присваивание записывает в неё число 12. Поэтому изменился результат вывода самой переменной a.
Можно ли направить указатель на другую переменную
Да. В указатель можно записать адрес другой подходящей переменной. Сравните присваивание самому указателю и присваивание через указатель:
#include <iostream>
int main() {
int a = 7;
int b = 20;
int *ptr = &a;
*ptr = 12; // меняем a
ptr = &b; // теперь ptr указывает на b
*ptr = 30; // меняем b
std::cout << a << ' ' << b;
return 0;
}
12 30
После ptr = &b; в ptr хранится новый адрес. Переменная a остаётся равной 12. Следующее разыменование обращается уже к b.
Указатель на указатель: int **
Мы уже выяснили, что указатель сам является переменной. Значит, можно получить его адрес и сохранить этот адрес в другом указателе.
int a = 7;
int *ptr_a = &a;
int **ptr_na_ptr = &ptr_a;
Тип int ** читается как «указатель на указатель на int». У ptr_na_ptr есть дополнительный уровень: сначала он ведёт к ptr_a, а уже ptr_a — к a.
| Выражение | Результат в нашем примере |
|---|---|
ptr_na_ptr | Адрес указателя ptr_a |
*ptr_na_ptr | Сам указатель ptr_a, который хранит адрес a |
**ptr_na_ptr | Переменная a со значением 7 |
Две звёздочки в выражении означают два последовательных разыменования: **ptr_na_ptr — это *(*ptr_na_ptr). Через такую цепочку тоже можно изменить исходную переменную:
#include <iostream>
int main() {
int a = 7;
int *ptr_a = &a;
int **ptr_na_ptr = &ptr_a;
**ptr_na_ptr = 12;
std::cout << a << '\n';
std::cout << *ptr_a << '\n';
std::cout << **ptr_na_ptr << '\n';
return 0;
}
12
12
12
Все три строки читают одну и ту же переменную a, но обращаются к ней разными способами. Не пытайтесь запомнить цепочку механически: нарисуйте три ячейки и последовательно переходите по стрелкам.
nullptr: когда указатель пока никуда не указывает
Иногда указатель нужен сейчас, а адрес подходящей переменной появится позже. В таком случае его можно инициализировать значением nullptr:
int *ptr = nullptr;
nullptr задаёт нулевое значение указателя: он не указывает на объект. Это не то же самое, что указатель на переменную, внутри которой хранится число 0.
#include <iostream>
int main() {
int a = 0;
int *ptr = nullptr;
ptr = &a;
std::cout << *ptr;
return 0;
}
После ptr = &a; указатель ведёт к существующей переменной, поэтому прочитать *ptr можно. Программа выведет её значение — 0.
Если адрес ещё не присвоен, сначала проверяем указатель:
#include <iostream>
int main() {
int *ptr = nullptr;
if (ptr != nullptr) {
std::cout << *ptr;
} else {
std::cout << "No object";
}
return 0;
}
Здесь условие ложно, поэтому разыменование не выполняется. Программа выводит No object.
Ещё одна опасная запись — локальный указатель без начального значения: int *ptr;. Такой указатель не становится нулевым автоматически. Сразу задавайте известное значение: адрес существующего объекта или nullptr.
Частые ошибки
| Ошибка | Как рассуждать правильно |
|---|---|
Путать a и &a | Первое — переменная, второе — её адрес. |
Ожидать значение 7 от вывода ptr | Чтобы прочитать значение a через указатель, используйте *ptr. |
Считать, что *ptr = 12; изменяет адрес | Эта запись изменяет объект по адресу. Сам ptr продолжает указывать туда же. |
Путать ptr и &ptr | Указатель хранит один адрес и сам имеет собственный адрес. |
Разыменовывать nullptr | Сначала указатель должен указывать на существующий подходящий объект. |
| Считать код 139 обязательным результатом ошибки | Разыменование нулевого указателя — неопределённое поведение. Конкретный исход не гарантируется. |
Подведём итоги
У переменной есть значение и адрес в памяти. Операция & получает её адрес, указатель сохраняет этот адрес, а операция * позволяет обратиться к переменной через указатель. При разыменовании мы работаем с исходной переменной, поэтому можем и прочитать её значение, и изменить его.
int *ptr = &a;— объявить указатель наintи сохранить адресa;*ptr— обратиться к объекту по адресу, который хранится вptr;ptr = &b;— направить указатель на другую переменную;int **pp = &ptr;— объявить указатель на указатель;nullptr— нулевое значение указателя, которое нельзя разыменовывать.
Теперь попробуйте закрепить материал в контесте к статье. В нём пять коротких заданий: определение указателя, соответствия между записями и их смыслом, пропуски в коде, изменение значения через два указателя и работа с nullptr. Сначала попробуйте ответить самостоятельно, а затем вернитесь к схеме, на которой возник вопрос.
Использованная литература
- C++ working draft: Compound types [Электронный ресурс]. — URL: https://eel.is/c++draft/basic.compound (дата обращения: 06.11.2026).
- C++ working draft: Pointers [Электронный ресурс]. — URL: https://eel.is/c++draft/dcl.ptr (дата обращения: 06.11.2026).
- C++ working draft: Unary operators [Электронный ресурс]. — URL: https://eel.is/c++draft/expr.unary.op (дата обращения: 06.11.2026).
- C++ working draft: Pointer conversions [Электронный ресурс]. — URL: https://eel.is/c++draft/conv.ptr (дата обращения: 06.11.2026).