JavaRush /Курсы /C++ SELF /Разыменование *

Разыменование * p и доступ p -> field

C++ SELF
39 уровень , 1 лекция
Открыта

1. Базовая идея: p — это «где», *p — это «что»

Когда вы впервые видите указатели, возникает логичный вопрос: «Зачем мне хранить адрес, если я хочу работать с данными?» И это правильный вопрос. Адрес сам по себе обычно не интересен — он нужен как маршрут к данным. Разыменование — это как открыть дверь по адресу в навигаторе: у вас есть координаты (p), и вы хотите попасть внутрь квартиры (*p).

Главная идея сегодняшней лекции очень приземлённая:
p — это “где”, *p — это “что лежит по этому адресу”.
А когда объект — это struct, мы хотим не просто «войти в квартиру», а ещё и «открыть конкретный шкафчик» — тут появляется оператор ->.

p и *p: адрес и объект

Давайте начнём максимально спокойно. У нас есть переменная x. Она хранит число. У неё есть адрес. Мы можем взять этот адрес, положить в указатель, а потом через указатель снова добраться до x.

Ниже — табличка, которую полезно держать в голове (она спасает от половины типичных ошибок с указателями).

Выражение Пример «Что это?» Тип Что означает по смыслу
объект (значение)
x
данные
int
само число
адрес объекта
&x
координата в памяти
int*
где лежит x
указатель (переменная)
p
данные-координата
int*
адрес, сохранённый в p
разыменование
*p
данные по адресу
int
объект, на который указывает p

Теперь маленький пример: просто напечатаем значение, адрес и значение «через адрес».

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;

    std::cout << "x  = " << x << '\n';   // x  = 42
    std::cout << "&x = " << &x << '\n';  // &x = 0x.... (адрес будет другим)
    std::cout << "*p = " << *p << '\n';  // *p = 42
}

Обратите внимание на важный психологический момент: p — это НЕ «особый доступ к x». p — это просто переменная, в которой лежит число-адрес. Магия начинается только в момент *p.

2. Чтение и запись через *p

*p для чтения

Когда мы говорим «разыменовать указатель», мы делаем операцию *p и получаем доступ к объекту, который лежит по адресу p. На практике чаще всего первое действие — просто прочитать значение, чтобы убедиться, что мы смотрим туда, куда хотели (это очень человеческий дебаг).

Пример максимально простой: меняем x, и видим, что чтение через *p отражает текущее состояние x.

#include <iostream>

int main() {
    int x = 10;
    int* p = &x;

    std::cout << *p << '\n';  // 10
    x = 25;
    std::cout << *p << '\n';  // 25
}

Здесь полезно осознать: *p не «сохраняет копию». *p каждый раз идёт по адресу и смотрит на живой объект.

*p для записи

Теперь самое вкусное: *p можно использовать не только справа (чтение), но и слева (запись). Это и есть «управление объектом по адресу».

Пусть у нас есть x = 10, и указатель p на x. Если мы напишем *p = 99;, мы изменим сам x, потому что *p — это и есть x, только «достали его по адресу».

#include <iostream>

int main() {
    int x = 10;
    int* p = &x;

    *p = 99;                 // меняем x через указатель
    std::cout << x << '\n';  // 99
}

Это место, где многие новички начинают подозревать, что C++ — это не язык, а квест «угадай, что имелось в виду». Но здесь всё честно: *p — это объект, значит его можно менять.

Частая путаница: p = &x и *p = x

На этом шаге полезно специально проговорить два похожих выражения, которые означают совершенно разное. Потому что именно тут рождается много багов, которые потом выглядят как «оно само сломалось, честно».

Сравним:

#include <iostream>

int main() {
    int x = 5;
    int y = 100;

    int* p = &x;  // p хранит адрес x
    p = &y;       // теперь p хранит адрес y (мы ПЕРЕНАПРАВИЛИ указатель)

    *p = 7;       // теперь меняем y, потому что p указывает на y
    std::cout << x << '\n';  // 5
    std::cout << y << '\n';  // 7
}

Смысл такой:
p = &y; меняет адрес внутри указателя (куда указываем).
*p = 7; меняет данные по адресу (что лежит там, куда указываем).

Если коротко: одна строка меняет «стрелочку», вторая — «мишень».

3. nullptr и проверка перед разыменованием

Сейчас будет правило, которое звучит скучно, но экономит часы жизни:
Если указатель может быть nullptr, то разыменовывать его можно только после проверки.

Проверка обычно делается через if (p != nullptr).

#include <iostream>

int main() {
    int* p = nullptr;

    if (p != nullptr) {
        std::cout << *p << '\n';
    } else {
        std::cout << "p is null\n";  // p is null
    }
}

Почему так строго? Потому что nullptr — это не адрес объекта. Это «пустое состояние». Попытка сделать *nullptr — это как попытка открыть дверь, которой не существует: не «ошибка доступа по расписанию», а лотерея из серии «а что сегодня сломаем».

Отдельный стильный момент: в современном C++ принято использовать именно nullptr, а не 0 или NULL, и это не просто прихоть — такой стиль закреплялся и вычищался в стандартных текстах много лет.

4. Указатель на struct: оператор ->

Почему появляется ->, если уже есть *p

Когда мы работаем со структурой, у нас есть поля. Для обычного объекта мы обращаемся к полю через точку: obj.field. Но если у нас указатель p, то p — это не объект, а адрес. Значит точка не подходит: у адреса нет поля age (у него вообще нет полей, он «координата»).

Чтобы обратиться к полю структуры по указателю, используется оператор ->:

  • p->field читается как «возьми объект по адресу p и возьми его поле field».

Сделаем минимальный пример со структурой User.

#include <iostream>
#include <string>

struct User {
    std::string name;
    int age{};
};

int main() {
    User u{"Bob", 30};
    User* p = &u;

    std::cout << p->name << '\n';  // Bob
    p->age += 1;
    std::cout << u.age << '\n';    // 31
}

Это очень важная демонстрация: изменение через p->age меняет исходный объект u, потому что p указывает именно на него.

p->field и (*p).field

Теперь момент, который часто выглядит как «заклинание на латыни», но на деле вполне логичен.

p->field — это сокращение для (*p).field.

То есть сначала мы разыменовываем p, получаем объект, а потом берём у него поле через точку.

Однако есть нюанс: у операторов есть приоритет. Оператор . (точка) имеет высокий приоритет, поэтому выражение *p.field означает не то, что вы хотите (и обычно вообще не компилируется или компилируется не туда).

Правильно так:

(*p).age — сначала разыменовали p, потом взяли age
p->age — то же самое, но читается легче

Давайте прямо покажем эквивалентность на коде.

#include <iostream>
#include <string>

struct User {
    std::string name;
    int age{};
};

int main() {
    User u{"Ann", 20};
    User* p = &u;

    std::cout << p->age << '\n';     // 20
    std::cout << (*p).age << '\n';   // 20
}

Почему -> так любят? Потому что (*p).age выглядит как «я случайно задел клавиатуру локтем, и оно сработало». А p->age читается почти как английское “pointer to age”.

5. Мини-схема: что происходит при *p и p->field

Сейчас будет схема, чтобы мозг мог отдохнуть от текста и переключиться на «картинки». Представим, что p указывает на u.

flowchart LR
    P[p: User*] -->|содержит адрес| U[User u]
    U --> NAME[name]
    U --> AGE[age]

    P2["*p"] -->|это| U
    P3["p->age"] -->|это| AGE

Именно так полезно думать: *p — это «сам объект», p->age — «конкретное поле объекта».

6. Пример: «Заметки» и редактирование через указатель

Чтобы не было ощущения, что указатели — это «ну вот в учебнике так написано», давайте добавим очень маленький кусочек логики в наше условное консольное приложение “Заметки” (или “Список задач”). Мы не будем делать ничего большого: просто покажем, как указатель может означать «текущий выбранный объект», и как через -> можно менять поля.

Пусть у нас есть модель:

#include <string>

struct Note {
    int id{};
    std::string title;
    bool pinned{};
};

Теперь представим, что мы «выбрали» конкретную заметку n и хотим поменять pinned через указатель.

#include <iostream>
#include <string>

struct Note {
    int id{};
    std::string title;
    bool pinned{};
};

int main() {
    Note n{1, "Buy milk", false};
    Note* selected = &n;

    selected->pinned = true;
    std::cout << n.pinned << '\n'; // 1 (true печатается как 1)
}

Да, вывод true как 1 — это нормально. Мы позже привыкнем к std::boolalpha, но сейчас нам важно другое: поле реально поменялось.

Теперь чуть интереснее: покажем, что можно менять и строку, и булево поле, и всё это остаётся в исходном объекте.

#include <iostream>
#include <string>

struct Note {
    int id{};
    std::string title;
    bool pinned{};
};

int main() {
    Note n{2, "Read C++ book", false};
    Note* selected = &n;

    selected->title += " (chapter pointers)";
    selected->pinned = !selected->pinned;

    std::cout << n.title << '\n';  // Read C++ book (chapter pointers)
    std::cout << n.pinned << '\n'; // 1
}

Смысл этого фрагмента для нашего приложения такой: мы можем хранить “текущий выбранный объект” как указатель и работать с ним единым способом, независимо от того, где он создан, пока он жив.

Сегодня мы не углубляемся в то, как именно выбирать заметку из контейнера и как гарантировать, что адрес не «протухнет» — это отдельная тема про безопасность и время жизни, и до неё мы ещё дойдём. Сейчас нам достаточно отработать «механику рук»: *p и p->field.

7. Типичные ошибки при разыменовании и ->

Ошибка №1: разыменование nullptr.
Самый частый сценарий: указатель объявили, поставили nullptr «на всякий случай», а потом в каком-то месте забыли, что он может быть пустым. В итоге появляется *p или p->field без проверки. Лечится дисциплиной: если указатель по контракту может быть пустым, перед каждым разыменованием должен быть if с проверкой p != nullptr или другая явная гарантия.

Ошибка №2: путаница “меняю указатель” и “меняю объект”.
p = &x; — вы меняете адрес в указателе. *p = 10; — вы меняете данные по адресу. Если перепутать, программа начинает вести себя так, будто у неё есть настроение и фаза луны в зависимостях. Спасает привычка проговаривать: «я сейчас двигаю стрелку или меняю цель?».

Ошибка №3: попытка писать p.field у указателя.
Это обычно происходит по инерции: “я же всегда писал через точку”. Но точка работает только с объектом, а не с адресом. Для указателя либо p->field, либо (*p).field. Если вы видите точку рядом с переменной-указателем — это почти всегда баг.

Ошибка №4: забытые скобки в (*p).field.
Запись *p.field почти никогда не означает то, что хотелось автору. Правильно: (*p).field. На практике проще сразу писать p->field, и проблема исчезает вместе с лишними скобками и лишними седыми волосами.

Ошибка №5: вера в то, что “если указатель не null — значит всё безопасно”.
Проверка p != nullptr защищает только от пустого состояния. Но она не доказывает, что объект по адресу жив, не уничтожен и вообще существует. Сегодня мы это лишь фиксируем как осторожность, а подробно разберём уже в теме про безопасность указателей и время жизни объектов.

1
Задача
C++ SELF, 39 уровень, 1 лекция
Недоступна
Рычаг усиления
Рычаг усиления
1
Задача
C++ SELF, 39 уровень, 1 лекция
Недоступна
Обмен через пульты
Обмен через пульты
1
Задача
C++ SELF, 39 уровень, 1 лекция
Недоступна
Чертёж прямоугольника
Чертёж прямоугольника
1
Задача
C++ SELF, 39 уровень, 1 лекция
Недоступна
Выбор профиля
Выбор профиля
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ