JavaRush /Курсы /C++ SELF /Адрес в памяти: оператор &am...

Адрес в памяти: оператор &

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

1. Углубленно изучаем оператор &

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

Представьте библиотеку. «Значение» — это книга (например, “C++ для людей, которые хотят спать”), а «адрес» — это её точное место: стеллаж 3, полка 2, позиция 5. Книга может быть одна и та же, но если вы не знаете полку — вы её не найдёте. Так вот: оператор & в C++ — это способ спросить у программы: “Окей, а где лежит вот этот объект?”

Важно сразу договориться о двух вещах.

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

Второе: адрес — не «вечный ID» и не “уникальный номер навсегда”. Адрес зависит от конкретного запуска программы: запустили ещё раз — и адреса могут стать другими.

Оператор &: «возьми адрес этого объекта»

Сейчас мы познакомимся с оператором, который часто выглядит как “ещё один странный символ”, но на деле он очень прямолинейный. Оператор & (унарный, то есть стоящий перед выражением) означает: взять адрес объекта. Не «ссылку», не «копию», не «что-то про побитовые операции» (побитовое & — отдельная история и выглядит иначе по контексту). Сейчас нас интересует именно &x, где x — объект.

Самое важное: оператор & применяется к существующему объекту. То есть вы должны иметь переменную (или поле структуры, или элемент массива/вектора), которая реально живёт в памяти, а не просто «число в воздухе».

x и &x — это разные сущности

На первых порах полезно проговаривать вслух:

x — это значение.
&x — это адрес значения.

И это не философия, а прямой факт: у этих выражений разные типы.

Посмотрим на это таблично:

Что пишем Смысл по-человечески Пример типа (интуитивно)
x
«само число/объект»
int
&x
«адрес, где лежит x» «указатель на
int
» (детально — в следующей лекции)

Пока мы не углубляемся в T*, но уже сейчас важно почувствовать: &x — не число x и не «улучшенная версия x», это другая категория данных.

Мини-схема: что именно делает &

Сейчас самое время зафиксировать механику в виде схемы, чтобы мозг перестал воспринимать & как «случайный символ». Представим цепочку: объект → адрес.

flowchart LR
    A["Объект (переменная) x"] -->|"оператор &"| B["Адрес объекта &x"]
    B --> C["можно вывести"]
    B --> D["можно сравнить"]
    B --> E["можно сохранить (чуть позже)"]

Ключевой смысл: оператор & превращает «объект» в «значение-адрес». В формальном описании языка этот оператор относится к унарным операторам и отдельно уточняется в стандарте (раздел [expr.unary.op] упоминается даже в редакторских заметках по уточнению address-of-оператора).

2. Время жизни объекта и корректность адреса

Перед тем как брать адреса, полезно чуть приземлить модель. Внутри одного запуска программы объект существует во времени: появился — живёт — исчез. И пока объект жив, у него есть адрес (место в памяти), по которому можно к нему обратиться. Это ровно та идея, которая лежит в основе последующих тем про указатели.

Подчеркну важную мысль: адрес имеет смысл только в пределах времени жизни объекта. Если объект уничтожен, то «старый адрес» превращается в опасную бумажку с бывшими координатами: по ним может уже лежать что-то другое, или вообще ничего «вашего».

Давайте сделаем микропример: адрес существует, пока переменная в области видимости.

#include <iostream>

int main() {
    int x = 42;

    std::cout << "x  = " << x << '\n';
    std::cout << "&x = " << &x << '\n'; // адрес (в каждом запуске может быть разный)
}

Здесь x — это значение 42, а &x — «координаты» x в памяти в текущем запуске. Формально в стандарте это относится к описанию оператора взятия адреса в разделе про унарные операторы.

Что оператор & не делает

Очень важно заранее снять типичные ожидания, потому что именно из них потом рождаются самые неприятные баги. Оператор & не является способом «сделать переменную важнее» или «прикрепить к ней уникальный номер». Он просто получает адрес в памяти в текущем запуске.

Ещё & не «запоминает» объект навсегда. Адрес, полученный через &, может быть корректным только пока объект жив. Если объект уничтожен, адрес может стать опасным. Мы подробно будем говорить об этом позже, но даже сейчас полезно держать правило в голове: «адрес живёт столько же, сколько живёт объект».

Наконец, & — это не «оператор ссылки». В русском языке хочется сказать «взять ссылку», но в C++ слово «ссылка» — это отдельный механизм (T&), а & сегодня — именно «взять адрес».

3. Как выглядят адреса на практике

Очень хочется, впервые увидев адрес, спросить: «Почему это похоже на набор странных символов?» Потому что адрес — это не «красивое» число для человека. Это техническое значение, которое имеет смысл для машины. Его вывод в консоль — в первую очередь инструмент отладки и обучения, а не то, что пользователь должен видеть в финальном приложении.

Давайте научимся печатать адреса и не пугаться. Сравним два вывода: значение и адрес.

#include <iostream>

int main() {
    int x = 7;

    std::cout << "x   = " << x << '\n';   // x   = 7
    std::cout << "&x  = " << &x << '\n';  // например: 0x7ff... (будет отличаться)
}

Вы почти наверняка увидите что-то вроде 0x7ffee3... (формат зависит от компилятора и платформы). Главное: не пытайтесь запомнить это число. Оно «одноразовое», как чек из супермаркета: полезно сейчас, но не как постоянная идентичность объекта.

Адрес как значение: сравнение адресов

Пока мы говорили «адрес можно вывести», но адрес ещё можно использовать как обычное значение в выражениях. Самое понятное действие — сравнить адреса, чтобы выяснить: мы говорим об одном и том же объекте или о разных.

Это часто помогает при отладке: например, вы подозреваете, что функция «работает не с тем объектом», и хотите проверить, совпадают ли адреса.

#include <iostream>

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

    std::cout << std::boolalpha;

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

Здесь (&a == &b) отвечает на вопрос «a и b — это одна и та же переменная?» Конечно, нет. А (&a == &a) — всегда true, потому что это буквально один объект.

Обратите внимание: сравнение адресов — это именно сравнение «мест», а не значений. Если a == b, это означает «значения равны». А если &a == &b, это означает «это один и тот же объект».

4. Адреса составных объектов: struct и поля

До этого момента можно было думать, что адрес — это только для примитивов (int, double). Но в реальном коде мы работаем со структурами: «задача», «пользователь», «заказ», «точка на карте». У структуры есть свой адрес, а у каждого поля — тоже адрес, потому что поле лежит внутри объекта.

Возьмём маленький кусочек нашего учебного приложения. Пусть у нас есть Task — задача в списке дел (такой struct мы могли ввести раньше, когда учились моделировать данные):

#include <iostream>
#include <string>

struct Task {
    std::string title;
    int priority{};
};

int main() {
    Task t{"Read about pointers", 1};

    std::cout << "&t          = " << &t << '\n';
    std::cout << "&t.title    = " << &t.title << '\n';
    std::cout << "&t.priority = " << &t.priority << '\n';
}

Здесь происходит полезная для мозга вещь: вы видите, что &t и &t.priority — разные адреса. Поле — это часть объекта, но занимает своё место внутри него.

Можно представить это очень грубо как «контейнер внутри контейнера»:

Task t лежит в памяти где-то тут:
+-------------------------------+
| title (std::string)           |
+-------------------------------+
| priority (int)                |
+-------------------------------+

&t          -> адрес начала Task
&t.priority -> адрес внутри Task (смещение)

Это ещё не «низкоуровневая адресная арифметика» (мы её не изучаем сейчас), это просто визуальная идея: внутри объекта есть части.

Встраиваем в приложение: отладочный вывод «где лежит задача»

Теперь давайте сделаем практическую вещь: добавим маленькую «отладочную печать» для нашей структуры Task. Идея простая: иногда вы хотите убедиться, что функция получает именно тот объект, который вы ожидаете.

Мы пока не используем никаких «сложных указателей» — только & и обычную передачу по const T& (это вы уже проходили ранее, когда говорили про параметры функций).

#include <iostream>
#include <string>

struct Task {
    std::string title;
    int priority{};
};

void print_task_debug(const Task& t) {
    std::cout << "Task: " << t.title << '\n';
    std::cout << "  &t          = " << &t << '\n';
    std::cout << "  &t.priority = " << &t.priority << '\n';
}

int main() {
    Task t{"Read about pointers", 1};
    print_task_debug(t);
}

Здесь есть важный момент: параметр const Task& t — это ссылка, а не копия. Поэтому &t в функции — это адрес того же объекта, который живёт в main. Мы не углубляемся в устройство ссылок сейчас, но практическая польза видна: печать адресов помогает понять, «копируем мы объект или работаем с оригиналом».

Если вы позже увидите в коде ситуацию, где адрес «вдруг другой», это будет сильной подсказкой: вероятно, где-то случилась копия.

5. Типичные ошибки при работе с оператором &

Ошибка №1: путать x и &x и делать выводы «по ощущениям».
Самая частая история: студент видит &x и думает, что это «какая-то версия x», а значит, её можно складывать, сравнивать с числами и т.д. На практике полезно заставить себя проговаривать: «x — данные, &x — координаты». Как только вы начинаете так читать код, половина путаницы исчезает.

Ошибка №2: пытаться использовать адрес как логический идентификатор в программе.
Иногда хочется сделать «хитро»: раз у объекта есть адрес, значит, он уникальный — и можно хранить его как ID. Это плохая идея: адрес зависит от запуска и от деталей размещения объектов. Сегодня вы видите один адрес, завтра — другой. Адрес годится для отладки и низкоуровневой техники, но не как бизнес-ключ.

Ошибка №3: брать адрес у «чего-то временного», не думая о времени жизни.
Даже если компилятор разрешил взять адрес в каком-то хитром выражении, это не значит, что этот адрес будет полезен долго. Главный вопрос, который нужно задавать себе уже сейчас: «Сколько живёт объект, адрес которого я беру?» Если ответ «до конца этой строки» — это повод насторожиться и остановиться.

Ошибка №4: ожидать, что адреса будут одинаковыми между запусками программы.
Новички иногда запускают программу дважды и удивляются, что &x стал другим. Это абсолютно нормальное поведение: размещение в памяти зависит от запуска, окружения, компилятора и множества факторов. Смысл адреса — в рамках одного выполнения, а не как постоянная величина.

Ошибка №5: пытаться «понять память» по красивому числу адреса.
Адрес выглядит как число, и мозг сразу хочет найти закономерность: «О, вот тут больше, значит объект ‘правее’!». Иногда это действительно отражает порядок размещения, а иногда — нет, и главное: вам сейчас это почти никогда не нужно. Учитесь использовать адрес как маркер «тот же объект / другой объект», а не как материал для нумерологии.

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