JavaRush /Курсы /C++ SELF /Указатель T*: хранит адрес и может быть nullptr

Указатель T*: хранит адрес и может быть nullptr

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

1. Погружаемся в T*

Когда вы впервые видите &x, кажется, что вопрос закрыт: «адрес получить можем, всё, победа». Но в реальных программах адрес редко нужен “на секунду”. Чаще хочется сохранить адрес, передать его дальше, сравнить, положить в переменную, сказать “объект есть/объекта нет”, или держать «ссылку на выбранный объект» как состояние. Для этого нам и нужен отдельный тип данных: указатель.

Можно думать об указателе как о записке с координатами: сам объект — это “квартира”, а указатель — “адрес на бумажке”. Бумажка может лежать у вас в кармане, вы можете дать её другу (передать в функцию), можете стереть адрес и написать новый. Но бумажка не является квартирой — это ключевой момент сегодняшней лекции.

Что такое T* по смыслу: «значение-адрес»

Указатель — это переменная, которая хранит адрес. Не значение объекта, не копию объекта, не «маленький объект внутри», а именно адрес, по которому объект находится.

  • int x = 10; — переменная x хранит число 10
  • int* p = &x; — переменная p хранит адрес x

Очень важно проговаривать это словами, когда читаете код. Не “p равно x”, а “p указывает на x” или “в p лежит адрес x”.

Небольшая схема (грубая, зато честная):

Stack (условно)
+-------------------+
| x: 10             |   <-- объект (значение)
+-------------------+
| p: 0x7ffd...      |   <-- указатель (адрес x)
+-------------------+

Адреса вида 0x7ffd... — это просто формат вывода (обычно шестнадцатеричный). И да: адреса будут разными при разных запусках программы, это нормально. Адрес — не “ID пользователя в вашей базе данных”, а техническая координата на время жизни объекта.

2. Синтаксис и дисциплина инициализации

Как объявить указатель T*

Синтаксис выглядит так:

int* p = nullptr;

Читается: “p — указатель на int”.

И тут новички обычно задают два отличных вопроса:

Первый: почему звёздочка *? Исторически это связано с операцией разыменования (мы её подробно разберём в следующей лекции). Пока воспринимайте * в объявлении как “значок указателя”.

Второй: к чему относится * — к типу или к имени? В C++ это часть объявления, и запись int* p; и int *p; эквивалентны. Лично я советую стиль, который легче читать глазами:

  • либо int* p (звёздочка рядом с типом: “указатель-на-int”),
  • либо int *p (звёздочка рядом с именем: “p — указатель”).

Главное — не мешайте стили в одном проекте, потому что мозг начнёт тратить силы на ерунду вместо логики.

Главное правило дисциплины: указатель всегда должен быть инициализирован

Самая опасная строка в начале пути указателей — вот такая:

int* p; // плохо: p не инициализирован

Неинициализированный указатель содержит мусорный адрес. Он может выглядеть как “нормальное число”, он может даже иногда “попасть” в какую-то память, но это уже территория UB и лотереи “повезёт/не повезёт”.

Правильная привычка для старта: указатель всегда в одном из двух состояний:

1) либо он равен nullptr (то есть “никуда не указывает”),
2) либо хранит адрес реального объекта, который сейчас существует.

Поэтому стартовая инициализация чаще всего такая:

int* p = nullptr;

Это как поставить переменную в состояние “пока никого не выбрали”. И это абсолютно нормально.

nullptr: корректное «пустое состояние» указателя

Слово nullptr можно перевести как “нулевой указатель”. Но на уровне смысла полезнее думать так: nullptr означает «указатель ни на что не указывает».

Это не ошибка. Это нормальная ситуация, как пустая строка или пустой vector: состояние “пока нет значения”.

Мини-пример:

#include <iostream>

int main() {
    int* p = nullptr;

    std::cout << std::boolalpha;
    std::cout << (p == nullptr) << '\n'; // true
}

Сравнение с nullptr — это базовая проверка “объект есть или нет”.

И ещё одно важное: в современном C++ мы стараемся использовать именно nullptr, а не 0 и не NULL. Потому что nullptr — это отдельное специальное значение для указателей, и компилятор с ним работает более предсказуемо.

3. Присваивание, копирование и сравнение указателей

Присваивание адреса: p = &x

Теперь соберём всё в одну картину: есть объект, и есть указатель, который хранит его адрес.

#include <iostream>

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

    p = &x; // теперь p хранит адрес x

    std::cout << "x  = " << x << '\n';   // x  = 10
    std::cout << "&x = " << &x << '\n';  // &x = 0x...
    std::cout << "p  = " << p << '\n';   // p  = 0x... (тот же адрес)
}

В этом примере полезно увидеть, что p и &x печатаются одинаково (как адрес), потому что внутри p и лежит &x.

Обратите внимание: мы пока не трогаем “что лежит по адресу”. Мы сегодня тренируем именно идею “указатель хранит адрес” и “может быть пустым”.

Копирование указателя — это копирование адреса

Один из самых частых когнитивных багов: студент видит p2 = p1 и подсознательно думает “скопировали объект”. Нет. Мы скопировали бумажку с адресом. Теперь две бумажки указывают на одну и ту же квартиру.

#include <iostream>

int main() {
    int x = 42;

    int* p1 = &x;
    int* p2 = p1; // копия адреса

    std::cout << std::boolalpha;
    std::cout << (p1 == p2) << '\n'; // true
}

Смысл этого равенства: “они указывают на один и тот же объект”.

Это свойство одновременно очень мощное и очень опасное. Мощное — потому что можно делиться доступом к одному объекту. Опасное — потому что можно случайно получить несколько “точек влияния” на один объект, забыть об этом и потом долго удивляться, почему что-то меняется “само”. Но это уже ближе к следующей лекции, где мы научимся читать/писать по адресу.

Указатели можно сравнивать: с nullptr и друг с другом

Сравнения — это ещё одна базовая операция для указателей. В рамках сегодняшней лекции нам нужны два вида сравнений.

Первый вид — сравнение с nullptr. Это проверка “есть объект или нет”.

#include <iostream>

int main() {
    int* p = nullptr;

    if (p == nullptr) {
        std::cout << "p is empty\n"; // p is empty
    }
}

Второй вид — сравнение указателей между собой. Это проверка “один и тот же объект или разные”.

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int* pa = &a;
    int* pb = &b;

    std::cout << std::boolalpha;
    std::cout << (pa == pb) << '\n'; // false
    std::cout << (pa == &a) << '\n'; // true
}

Мы не обсуждаем здесь “кто больше” (<, >). С указателями такие сравнения существуют, но для новичка это почти всегда лишняя территория боли и недопонимания. Сейчас нам важнее чёткая логика: “пусто / не пусто” и “тот же объект / другой объект”.

4. Типизация указателей: почему int* и double* не смешиваются

В C++ адрес не “просто адрес”. Он всегда несёт смысл типа: адрес int — это int*, адрес double — это double*.

Почему это важно? Потому что тип влияет на то, как язык будет потом интерпретировать доступ по адресу (разыменование). Если тип неправильный — вы просите компилятор сделать потенциально бессмысленную операцию. Поэтому C++ не даёт просто так присваивать указатели разных типов друг другу.

Пример (это должно не компилироваться — и это хорошо):

int main() {
    int x = 1;
    int* pi = &x;

    // double* pd = pi; // ошибка компиляции: разные типы указателей
}

И вот тут у новичка может возникнуть “гениальная” идея: “а я приведу тип!”. В большинстве учебных и прикладных сценариев это плохая идея. Если вы чувствуете желание сделать такое приведение, это часто сигнал: “модель данных спроектирована криво, остановись и подумай”. Приведения указателей — это не тема сегодняшней лекции, и мы сознательно туда не лезем.

5. Учебный сценарий: «выбранный пользователь» как указатель

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

Представим, что у нас есть struct User (мы умеем struct ещё с прошлых дней), и в main мы иногда выбираем пользователя, а иногда “сбрасываем выбор”. Для этого удобно завести переменную “может быть пользователь, а может не быть”.

Именно здесь nullptr выглядит максимально естественно.

#include <iostream>
#include <string>

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

int main() {
    User alice{"Alice", 20};
    User bob{"Bob", 30};

    User* selected = nullptr;      // пока никто не выбран

    selected = &alice;             // выбрали Alice
    std::cout << (selected == &alice) << '\n'; // 1 (true)

    selected = nullptr;            // сбросили выбор
    std::cout << (selected == nullptr) << '\n'; // 1 (true)

    selected = &bob;               // выбрали Bob
    std::cout << (selected == &bob) << '\n'; // 1 (true)
}

Обратите внимание: мы пока не читаем поля через указатель. Мы просто используем указатель как “маркер выбора” и учимся держать его в корректных состояниях. В следующей лекции мы научимся делать следующий шаг: читать/изменять объект по адресу.

6. Что можно делать с T* уже сейчас

Чтобы структурировать действия, полезно держать в голове простую таблицу. Она не заменяет понимание, но помогает не паниковать, когда видите указатели в коде.

Операция Пример Смысл
Объявить указатель
int* p = nullptr;
“p хранит адрес int, пока пусто”
Взять адрес
&x
получить адрес объекта x
Присвоить адрес
p = &x;
теперь p указывает на x
Скопировать указатель
p2 = p1;
копируем адрес, не объект
Проверить на пустоту
p == nullptr
есть объект или нет
Сравнить “тот же объект?”
p1 == p2
указывают ли на один объект

Если вы уверенно делаете эти вещи и понимаете смысл, то вы уже на хорошем пути. Дальше будет “доступ по адресу”, и там начнётся самое интересное.

7. Подготовка к следующей лекции: разыменование

Обычно мозг просит: “ну ладно, адрес, а как получить значение?”. Ответ: через разыменование *p. Но разыменование — это отдельная тема (следующая лекция), потому что там появляется главное правило безопасности: разыменовывать можно только валидный указатель.

Чтобы аккуратно показать “куда мы идём”, вот пример, где разыменование намеренно закомментировано:

#include <iostream>

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

    std::cout << p << '\n';   // 0x... (адрес)

    // std::cout << *p << '\n'; // 5  (разыменование — следующая лекция)
}

Пока достаточно запомнить: p — это адрес, а *p — это уже “то, что лежит по адресу”. И между ними стоит целая пропасть ответственности.

8. Типичные ошибки при работе с T* и nullptr

Ошибка №1: неинициализированный указатель (int* p;).
Это одна из самых опасных вещей, которые можно сделать новичку, потому что ошибка не обязана проявиться сразу. Такой p хранит “мусорный адрес”, и любая попытка использовать его позже может привести к UB. Правильная привычка — инициализировать сразу: int* p = nullptr; или int* p = &x;.

Ошибка №2: использование 0 или NULL вместо nullptr.
Исторически в C и старом C++ часто писали 0 или NULL. В современном C++ это хуже читается и может приводить к неоднозначностям при перегрузках. Если вы хотите обозначить “пустой указатель”, пишите nullptr и экономьте себе нервы на будущих темах.

Ошибка №3: ожидание, что p2 = p1 копирует объект.
Присваивание указателей копирует только адрес. В итоге две переменные начинают указывать на один и тот же объект, и это может неожиданно “связать” две части программы. Полезная привычка — мысленно проговаривать: “я копирую координаты, а не данные”.

Ошибка №4: путаница типа указателя (int* против double*).
Адрес в C++ типизирован. Если вы пытаетесь присвоить int* в double*, компилятор ругается не из вредности, а потому что иначе вы легко получите бессмысленную интерпретацию памяти. В начале пути стоит вообще избегать приведений указателей и относиться к ошибке компилятора как к подсказке: “ты пытаешься смешать разные сущности”.

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

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