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

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

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], який згадують навіть у редакторських примітках щодо уточнення оператора взяття адреси.

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. Це погана ідея: адреса залежить від запуску й від деталей розміщення обʼєктів. Сьогодні ви бачите одну адресу, завтра — іншу. Адреса придатна для налагодження й низькорівневих технік, але не як надійний ID у логіці програми.

Помилка № 3: брати адресу тимчасового обʼєкта, не замислюючись про час життя.
Навіть якщо компілятор дозволив узяти адресу в якомусь складному виразі, це ще не означає, що ця адреса буде корисною довго. Головне запитання, яке варто ставити собі вже зараз: «Скільки живе обʼєкт, адресу якого я беру?» Якщо відповідь «до кінця цього рядка», це привід насторожитися й зупинитися.

Помилка № 4: очікувати, що адреси будуть однаковими між запусками програми.
Новачки інколи запускають програму двічі й дивуються, що &x став іншим. Це абсолютно нормальна поведінка: розміщення в памʼяті залежить від запуску, оточення, компілятора й багатьох чинників. Сенс адреси — у межах одного виконання, а не як сталої величини.

Помилка № 5: намагатися «зрозуміти памʼять» за самим числом адреси.
Адреса виглядає як число, і мозок одразу шукає закономірність: «О, тут більше — отже, обʼєкт „правіше“!». Іноді це справді відображає порядок розміщення, а іноді — ні. Та головне: вам зараз це майже ніколи не потрібно. Вчіться використовувати адресу як маркер «той самий обʼєкт / інший обʼєкт», а не як матеріал для нумерології.

1
Опитування
Памʼять програми, рівень 38, лекція 4
Недоступний
Памʼять програми
Памʼять програми
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ