C++

Посилання (References)

Посилання в мові C++. Різниця між посиланнями та вказівниками. l-value та r-value. Передача аргументів за посиланням. Константні посилання.

Що таке посилання?

До цього моменту ми працювали з двома типами змінних:

  1. Звичайні змінні, які безпосередньо зберігають значення (int a = 5;).
  2. Вказівники, які зберігають адресу іншого значення в пам'яті (int *ptr = &a;).

Посилання (reference) — це третій базовий тип змінних у C++. Воно працює як псевдонім (alias) або альтернативне ім'я для вже існуючої змінної. Усе, що ви робите з посиланням, насправді відбувається з оригінальною змінною.

Посилання оголошується за допомогою амперсанда & між типом даних та іменем змінної:

#include <iostream>

using namespace std;

int main()
{
    int value = 7;
    int &ref = value;  // ref — це тепер псевдонім для value

    cout << "value: " << value << "\n";  // 7
    cout << "ref:   " << ref   << "\n";  // 7

    value = 8;
    cout << "ref (after value = 8): " << ref << "\n";  // 8

    ref = 9;
    cout << "value (after ref = 9): " << value << "\n"; // 9

    return 0;
}

Зверніть увагу:

  • У контексті оголошення (int &ref = ...) символ & означає "посилання на".
  • У контексті виразу (&value) символ & означає "отримати адресу".

Ініціалізація посилань

Головне правило посилань: вони обов'язково повинні бути ініціалізовані при створенні. Ви не можете створити «порожнє» посилання, на відміну від вказівника. Крім того, після ініціалізації посилання не можна перепризначити на іншу змінну.

int value1 = 5;
int value2 = 10;

int &ref1 = value1;  // ✅ Правильно
// int &ref2;        // ❌ Помилка: посилання має бути ініціалізоване!

ref1 = value2;       // ⚠️ Увага! Це НЕ перепризначення посилання!
                     // Це означає: "записати значення value2 туди, куди вказує ref1"
                     // Тепер value1 дорівнює 10.

Після створення посилання та оригінальна змінна стають «одним цілим» — у них навіть однакова адреса в пам'яті:

cout << &value1 << "\n";  // Наприклад: 0x0035FE58
cout << &ref1   << "\n";  // Точно та сама адреса: 0x0035FE58!

l-value та r-value (Коротко)

Щоб зрозуміти, з чим можуть працювати посилання, потрібно згадати концепції l-value та r-value:

  • l-value (left value) — це об'єкт, який займає визначене місце в пам'яті та існує довше, ніж поточний вираз (наприклад, звичайні змінні: x, value).
  • r-value (right value) — це тимчасове значення, яке не має конкретної адреси в пам'яті й існує лише під час обчислення виразу (наприклад, літерали 5, або результати виразів 2 + 3).

Звичайне посилання (T &) може бути ініціалізоване ЛИШЕ неконстантним l-value.

int a = 7;
int &ref1 = a;      // ✅ a — це l-value

const int b = 8;
// int &ref2 = b;   // ❌ Помилка: b — це константа (не можна змінювати)
// int &ref3 = 5;   // ❌ Помилка: 5 — це r-value (літерал)
// int &ref4 = a+2; // ❌ Помилка: (a+2) — це тимчасове r-value

Передача аргументів за посиланням

Найпопулярніше застосування посилань — їх використання як параметрів функцій (pass by reference).

Як ми пам'ятаємо з основ функцій, за замовчуванням аргументи передаються за значенням (копіюються). Функція працює з копією і не може змінити оригінал:

void tryToChange(int x) {
    x = 8;  // Змінюється лише ЛОКАЛЬНА КОПІЯ
}

int main() {
    int n = 7;
    tryToChange(n);
    cout << n;  // Залишиться 7
}

Але якщо ми зробимо параметр функції посиланням (додамо &), створення копії не відбудеться. Параметр стане псевдонімом для переданої змінної!

#include <iostream>

using namespace std;

// x — це тепер ПОСИЛАННЯ на переданий аргумент
void successfullyChange(int &x) {
    x = 8;  // Змінює ОРИГІНАЛЬНУ змінну безпосередньо!
}

int main() {
    int n = 7;
    
    cout << "Before: " << n << "\n";  // 7
    successfullyChange(n);            // Зверніть увагу: при виклику & не пишеться
    cout << "After:  " << n << "\n";  // 8

    return 0;
}
Дві головні причини використовувати передачу за посиланням:
  1. Коли функція повинна змінити оригінальну змінну.
  2. Коли аргумент — це велика структура даних (наприклад, об'єкт з тисячами полів). Передача за значенням означала б витратне копіювання усіх цих полів. Посилання ж просто передає "адресу" під капотом (без витрат часу і пам'яті).

Константні посилання (const T&)

Ми з'ясували, що звичайні посилання не працюють з константами та r-values. Це створює проблему:

void printValue(int &x) {
    cout << x << "\n";
}

int main() {
    int a = 5;
    printValue(a);   // ✅ Працює (a — l-value)
    
    // printValue(10);  // ❌ Помилка компіляції! 10 — r-value
    return 0;
}

Але функція printValue нічого не змінює, вона лише дивиться на значення. Логічно, що ми мали б право передати туди і просто число (літерал).

Рішення — константне посилання (const Type&). Додавши const, ми обіцяємо компілятору, що через це посилання ми не будемо змінювати значення. За таку гарантію компілятор дозволяє ініціалізувати константне посилання і змінними, і константами, і літералами (r-values):

#include <iostream>

using namespace std;

// Параметр — константне посилання. Швидко передається, гарантовано не змінюється.
void printValue(const int &x) {
    // x = 10; // ❌ Заборонено: x позначене як const
    cout << x << "\n";
}

int main() {
    int a = 3;
    const int b = 4;

    printValue(a);      // ✅ Працює (неконстантне l-value)
    printValue(b);      // ✅ Працює (константне l-value)
    printValue(5);      // ✅ Працює (літерал r-value)
    printValue(a + 2);  // ✅ Працює (вираз r-value)

    return 0;
}
Золотий стандарт С++: Якщо аргумент — це великий об'єкт (не int, double чи bool) і ви не плануєте його змінювати всередині функції — завжди передавайте його як константне посилання (const Type &). Це найефективніший і найбезпечніший шлях.

Продовження часу життя (Lifetime Extension)

Існує одна магічна особливість: коли r-value (тимчасове значення) прив'язується до локального константного посилання, його "час життя" продовжується до моменту знищення цього посилання.

{
    const int &ref = 3 + 4; // Результат 7 зазвичай знищується миттєво після обчислення.
                            // Але тут компілятор таємно створює тимчасову змінну "під капотом",
                            // і `ref` вказує на неї.
                            
    cout << ref;            // Працює, виведе 7
} // Тут знищується ref, і разом з ним невидима тимчасова змінна

Посилання для скорочення доступу

Крім параметрів функцій, посилання зручні для скорочення довгого та складного доступу всередині структур:

struct Profile { int age; };
struct User { Profile profile; };
struct App { User current_user; };

App myApp;
// ... ініціалізація ...

// ❌ Довго і незручно:
myApp.current_user.profile.age = 25;
cout << myApp.current_user.profile.age;

// ✅ Елегантно через посилання:
int &userAge = myApp.current_user.profile.age;

userAge = 25;       // Коротко! Змінює оригінал
cout << userAge;    // Легко читати

Посилання vs Вказівники

"Під капотом" компілятор зазвичай реалізує посилання через ті ж самі вказівники (вони зберігають адресу). Посилання — це просто вказівник, який:

  1. Автоматично та неявно розіменовується щоразу, коли ви до нього звертаєтесь.
  2. Не може бути змінений, щоб вказувати на інше місце після створення.
ОсобливістьПосилання (&)Вказівник (*)
ІніціалізаціяОбов'язкова при створенніМоже бути неініціалізованим
Нульве значенняНе буває null посиланьМоже бути nullptr
ПерепризначенняНеможливо (назавжди пов'язане з оригіналом)Може вказувати на іншу адресу
Синтаксис доступуЯк зі звичайною змінною (value)Потребує розіменування (*ptr)
БезпекаДуже безпечно (не буває завислих у пам'яті)Небезпечні (null, сміття, завислі)

Що коли обирати? У сучасному C++ є просте правило: "Використовуйте посилання скрізь, де це можливо, і вказівники — лише там, де це необхідно". (Вказівники потрібні для динамічної пам'яті, масивів C-стилю або коли значення ДОЗВІЛЯЄТЬСЯ бути "відсутнім" / nullptr).


Підсумок

📍 Псевдонім (&)

Посилання (int& ref = var;) — це друге ім'я для змінної. Всі операції над ref автоматично виконуються над var. Адреси у них однакові.

📥 Передача аргументів

Передача в функцію за посиланням (void func(int &x)) дозволяє уникнути копіювання даних та змінювати оригінальні змінні всередині функції.

🛡️ Константні посилання

const int& ref може приймати константи і r-values (літерали). Це найбезпечніший і найшвидший спосіб передачі великих структур у функції (тільки для читання).

🆚 Порівняно з вказівниками

Посилання безпечніші: вони ніколи не бувають nullptr і не можуть "відв'язатися" від своєї змінної.
Copyright © 2026