Указатели в C++: адреса, разыменование и nullptr

Разберём адреса переменных, объявление указателей, операции & и *, указатели на указатели и nullptr. Схемы памяти, примеры C++ и пять задач для практики.

Содержание

В предыдущей статье мы научились работать с двумерными массивами: заполнять матрицы, выводить их и находить элементы на диагоналях. До этого мы обращались к данным по имени переменной или по индексу элемента массива. Теперь посмотрим, что происходит с переменной в памяти и как обратиться к ней по адресу.

Представьте, что Вы вызываете такси. Фразы «заберите меня из университета и отвезите домой» недостаточно: водителю нужны адреса. В видео мы используем эту аналогию, чтобы понять работу с памятью. У переменной в программе тоже есть своё место, и указатель позволяет сохранить его адрес, а затем обратиться к переменной через него.

В этой статье разберём указатели на обычные переменные, операции & и *, указатель на указатель и значение nullptr. Все примеры можно запустить в C++17.

После этой статьи Вы сможете:

  • объяснить, чем значение переменной отличается от её адреса;
  • получить адрес переменной с помощью &;
  • объявить указатель и обратиться к переменной через *;
  • изменить значение переменной через указатель;
  • прочитать объявление int ** и пройти два уровня разыменования;
  • объяснить, для чего нужен nullptr и почему его нельзя разыменовывать.

Где хранятся переменные

Когда программа выполняется, процессор исполняет её инструкции, а данные размещаются в памяти. Например, программа создаёт две переменные, складывает их значения и выводит результат:

main.cpp
C++
#include <iostream>

int main() {
    int a = 7;
    int b = 5;
    int sum = a + b;

    std::cout << sum;
    return 0;
}

У переменной есть имя, тип и значение. В нашем примере имя первой переменной — a, тип — int, значение — 7. При этом сама переменная занимает определённое место в памяти.

Память удобно представлять как последовательность байтов. У каждого байта есть адрес, а одна переменная может занимать несколько байтов. Размер объекта в байтах можно узнать с помощью sizeof:

main.cpp
C++
#include <iostream>

int main() {
    int a = 7;
    std::cout << sizeof(a);
    return 0;
}

Как узнать адрес переменной: операция &

Чтобы получить адрес переменной, перед её именем ставят амперсанд — символ &. Выражение &a читается как «адрес переменной a».

main.cpp
C++
#include <iostream>

int main() {
    int a = 7;

    std::cout << a << '\n';
    std::cout << &a << '\n';
    return 0;
}

В первой строке программа выведет значение 7, во второй — адрес переменной. Например, вывод может выглядеть так:

terminal
TXT
7
0x16fdff2ac

Запись с префиксом 0x обычно означает шестнадцатеричное число. Помимо цифр от 0 до 9, в этой системе используются буквы от a до f. Для работы с указателями переводить адрес в десятичную систему не требуется.

Если переменная занимает несколько байтов, какой адрес мы получаем? Для обычной переменной из наших примеров &a указывает на начало занимаемого ею участка памяти — на первый байт. Все байты одной переменной не превращаются в несколько отдельных указателей.

Что такое указатель

Указатель — это переменная, в которой можно хранить адрес объекта. В наших примерах таким объектом будет другая переменная. Чтобы объявить указатель на int, используем тип int *:

main.cpp
C++
int a = 7;
int *ptr = &a;

Разберём вторую строку по частям:

Часть записиЗначение
int *Тип «указатель на int»
ptrИмя указателя
=Инициализация указателя
&aАдрес переменной a

В ptr хранится адрес a. Число 7 при этом остаётся в самой переменной a. Указатель не создаёт её копию.

Указатель ptr хранит условный адрес 0x1000 переменной a со значением 7. Собственный адрес ptr равен 0x2000.
Рисунок 1 — Значение переменной, её адрес и указатель на неё

Имя указателя Вы выбираете самостоятельно. В видео мы использовали в том числе имя arbuz: int *arbuz = &a;. Для языка это такое же допустимое имя, как ptr. В рабочей программе лучше выбирать имя, которое помогает понять назначение переменной.

Разыменование: как получить значение через указатель

Мы сохранили адрес в ptr. Теперь нужно обратиться к переменной по этому адресу. Для этого перед именем указателя ставят *. Эта операция называется разыменованием.

main.cpp
C++
#include <iostream>

int main() {
    int a = 7;
    int *ptr = &a;

    std::cout << a << '\n';
    std::cout << *ptr << '\n';
    return 0;
}

Результат:

terminal
TXT
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;:

main.cpp
C++
#include <iostream>

int main() {
    int a = 7;
    int *ptr = &a;

    *ptr = 12;

    std::cout << a << '\n';
    std::cout << *ptr << '\n';
    return 0;
}
terminal
TXT
12
12

Сначала ptr хранит адрес a. Выражение *ptr обращается к a, а присваивание записывает в неё число 12. Поэтому изменился результат вывода самой переменной a.

int *ptr = &a объявляет указатель; *ptr = 12 изменяет a с 7 на 12.
Рисунок 2 — Звёздочка в объявлении и при разыменовании

Можно ли направить указатель на другую переменную

Да. В указатель можно записать адрес другой подходящей переменной. Сравните присваивание самому указателю и присваивание через указатель:

main.cpp
C++
#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;
}
terminal
TXT
12 30

После ptr = &b; в ptr хранится новый адрес. Переменная a остаётся равной 12. Следующее разыменование обращается уже к b.

Указатель на указатель: int **

Мы уже выяснили, что указатель сам является переменной. Значит, можно получить его адрес и сохранить этот адрес в другом указателе.

main.cpp
C++
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_a хранит адрес a; одно разыменование даёт ptr_a, два — a.
Рисунок 3 — Два уровня указателей
ВыражениеРезультат в нашем примере
ptr_na_ptrАдрес указателя ptr_a
*ptr_na_ptrСам указатель ptr_a, который хранит адрес a
**ptr_na_ptrПеременная a со значением 7

Две звёздочки в выражении означают два последовательных разыменования: **ptr_na_ptr — это *(*ptr_na_ptr). Через такую цепочку тоже можно изменить исходную переменную:

main.cpp
C++
#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;
}
terminal
TXT
12
12
12

Все три строки читают одну и ту же переменную a, но обращаются к ней разными способами. Не пытайтесь запомнить цепочку механически: нарисуйте три ячейки и последовательно переходите по стрелкам.

nullptr: когда указатель пока никуда не указывает

Иногда указатель нужен сейчас, а адрес подходящей переменной появится позже. В таком случае его можно инициализировать значением nullptr:

main.cpp
C++
int *ptr = nullptr;

nullptr задаёт нулевое значение указателя: он не указывает на объект. Это не то же самое, что указатель на переменную, внутри которой хранится число 0.

main.cpp
C++
#include <iostream>

int main() {
    int a = 0;
    int *ptr = nullptr;

    ptr = &a;
    std::cout << *ptr;
    return 0;
}

После ptr = &a; указатель ведёт к существующей переменной, поэтому прочитать *ptr можно. Программа выведет её значение — 0.

Проверка ptr != nullptr безопасна, а чтение *ptr при нулевом указателе приводит к неопределённому поведению.
Рисунок 4 — Нулевой указатель можно проверить, но нельзя разыменовывать

Если адрес ещё не присвоен, сначала проверяем указатель:

main.cpp
C++
#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. Сначала попробуйте ответить самостоятельно, а затем вернитесь к схеме, на которой возник вопрос.

Использованная литература

  1. C++ working draft: Compound types [Электронный ресурс]. — URL: https://eel.is/c++draft/basic.compound (дата обращения: 06.11.2026).
  2. C++ working draft: Pointers [Электронный ресурс]. — URL: https://eel.is/c++draft/dcl.ptr (дата обращения: 06.11.2026).
  3. C++ working draft: Unary operators [Электронный ресурс]. — URL: https://eel.is/c++draft/expr.unary.op (дата обращения: 06.11.2026).
  4. C++ working draft: Pointer conversions [Электронный ресурс]. — URL: https://eel.is/c++draft/conv.ptr (дата обращения: 06.11.2026).