Посилання (References)
Що таке посилання?
До цього моменту ми працювали з двома типами змінних:
- Звичайні змінні, які безпосередньо зберігають значення (
int a = 5;). - Вказівники, які зберігають адресу іншого значення в пам'яті (
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;
}
- Коли функція повинна змінити оригінальну змінну.
- Коли аргумент — це велика структура даних (наприклад, об'єкт з тисячами полів). Передача за значенням означала б витратне копіювання усіх цих полів. Посилання ж просто передає "адресу" під капотом (без витрат часу і пам'яті).
Константні посилання (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 Вказівники
"Під капотом" компілятор зазвичай реалізує посилання через ті ж самі вказівники (вони зберігають адресу). Посилання — це просто вказівник, який:
- Автоматично та неявно розіменовується щоразу, коли ви до нього звертаєтесь.
- Не може бути змінений, щоб вказувати на інше місце після створення.
| Особливість | Посилання (&) | Вказівник (*) |
|---|---|---|
| Ініціалізація | Обов'язкова при створенні | Може бути неініціалізованим |
| Нульве значення | Не буває null посилань | Може бути nullptr |
| Перепризначення | Неможливо (назавжди пов'язане з оригіналом) | Може вказувати на іншу адресу |
| Синтаксис доступу | Як зі звичайною змінною (value) | Потребує розіменування (*ptr) |
| Безпека | Дуже безпечно (не буває завислих у пам'яті) | Небезпечні (null, сміття, завислі) |
Що коли обирати?
У сучасному C++ є просте правило: "Використовуйте посилання скрізь, де це можливо, і вказівники — лише там, де це необхідно". (Вказівники потрібні для динамічної пам'яті, масивів C-стилю або коли значення ДОЗВІЛЯЄТЬСЯ бути "відсутнім" / nullptr).
Підсумок
📍 Псевдонім (&)
int& ref = var;) — це друге ім'я для змінної. Всі операції над ref автоматично виконуються над var. Адреси у них однакові.📥 Передача аргументів
void func(int &x)) дозволяє уникнути копіювання даних та змінювати оригінальні змінні всередині функції.🛡️ Константні посилання
const int& ref може приймати константи і r-values (літерали). Це найбезпечніший і найшвидший спосіб передачі великих структур у функції (тільки для читання).🆚 Порівняно з вказівниками
nullptr і не можуть "відв'язатися" від своєї змінної.Вказівники: основи
Що таке пам'ять та адреси. Вказівники в C++. Оператори & та *. Нульові вказівники (nullptr). Розмір вказівників та для чого вони потрібні.
Вказівники, const і масиви
Взаємодія вказівників із постійними значеннями (const). Вказівник на константу vs константний вказівник. Як масиви зводяться до вказівників. Символьні рядки.