До цього моменту ми встигли розглянути 2 основних типи змінних:
звичайні змінні, які зберігають значення напряму;
вказівники, які зберігають адресу іншого значення (або 0
), для доступу до яких виконується операція розіменування вказівника.
Посилання — це третій базовий тип змінних в мові С++.
Посилання
Посилання — це тип змінної в мові C++, який працює як псевдонім іншого об’єкта або значення. Мова C++ підтримує 3 типи посилань:
Посилання на неконстантні значення (зазвичай їх називають просто «посилання» або «неконстантні посилання»), про що ми поговоримо на цьому уроці.
Посилання на константні значення (зазвичай їх називають «константні посилання»), про що ми поговоримо на наступному уроці.
У C++11 додали посилання r-value, про які ми поговоримо трішки пізніше.
Посилання (на неконстантне значення) оголошується з використанням амперсанда (&
) між типом даних і ім’ям посилання:
1 2 |
int value = 7; // звичайна змінна int &ref = value; // посилання на змінну value |
У цьому контексті амперсанд не означає «оператор адресу», він означає «посилання на».
Посилання в якості псевдонімів
Посилання зазвичай поводяться ідентично значенням, на які вони посилаються. У цьому сенсі посилання працює як псевдонім об’єкта, на який воно посилається, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { int value = 7; // звичайна змінна int &ref = value; // посилання на змінну value value = 8; // value тепер 8 ref = 9; // value тепер 9 std::cout << value << std::endl; // виведеться 9 ++ref; std::cout << value << std::endl; // виведеться 10 return 0; } |
Результат виконання програми:
9
10
У прикладі, наведеному вище, об’єкти ref
і value
оброблюються як одне ціле. Використання оператора адресу з посиланням призведе до повернення адреси значення, на яке посилається посилання:
1 2 |
std::cout << &value; // виведеться 0035FE58 std::cout << &ref; // виведеться 0035FE58 |
Короткий огляд l-value і r-value
Раніше ми вже розглядали, що таке l-value і r-value.
l-value — це об’єкт, який має певну адресу в пам’яті (наприклад, змінна x
) і зберігається за межами одного виразу.
r-value — це тимчасове значення без певної адреси в пам’яті і з областю видимості виразу (тобто зберігається в межах одного виразу). В якості r-values можуть бути як результати виразу (наприклад, 2 + 3
), так і літерали.
Ініціалізація посилань
Посилання повинні бути ініціалізовані при створенні:
1 2 3 4 |
int value = 7; int &ref = value; // коректне посилання: ініціалізоване змінною value int &invalidRef; // некоректне посилання: посилання повинно на щось посилатися |
На відміну від вказівників, які можуть містити нульове значення, посилання нульовими бути не можуть.
Посилання на неконстантні значення можуть бути ініціалізовані лише неконстантними l-values. Вони не можуть бути ініціалізовані константними l-values або r-values:
1 2 3 4 5 6 7 |
int a = 7; int &ref1 = a; // ок: a - це неконстантне l-value const int b = 8; int &ref2 = b; // не ок: b - це константне l-value int &ref3 = 4; // не ок: 4 - це r-value |
Зверніть увагу, в другому випадку ви не можете ініціалізувати неконстантне посилання константним об’єктом. В іншому випадку, ви б могли змінити значення константного об’єкта через посилання, що вже порушує саме поняття «константа».
Після ініціалізації змінити об’єкт, на який вказує посилання — не можна. Розглянемо наступний фрагмент коду:
1 2 3 4 5 |
int value1 = 7; int value2 = 8; int &ref = value1; // ок: ref - тепер псевдонім для value1 ref = value2; // присвоюємо 8 (значення змінної value2) для змінної value1. Тут НЕ змінюється об'єкт, на який посилається посилання! |
Зверніть увагу, в стейтменті ref = value2;
виконується не те, що ви могли б очікувати! Замість переприсвоювання ref
(посилатися на змінну value2
), значення з value2
присвоюється змінній value1
(на яке і посилається ref
).
Посилання в якості параметрів в функціях
Посилання найчастіше використовуються в якості параметрів у функціях. У цьому контексті посилання-параметр працює як псевдонім аргументу, а сам аргумент не копіюється при передачі в параметр. Це в свою чергу підвищує продуктивність, якщо аргумент занадто великий або витратний для копіювання.
На уроці про вказівники і масиви ми говорили, що передача аргумента-вказівника в функцію дозволяє функції при розіменуванні цього вказівника напряму змінювати значення аргументу.
Посилання працюють аналогічно. Оскільки посилання-параметр — це псевдонім аргументу, то функція, яка використовує посилання-параметр, може змінювати аргумент, переданий їй, також напряму:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> // ref - це посилання на переданий аргумент, а не копія аргументу void changeN(int &ref) { ref = 8; } int main() { int x = 7; std::cout << x << '\n'; changeN(x); // зверніть увагу, цей аргумент не обов'язково повинен бути посиланням std::cout << x << '\n'; return 0; } |
Результат виконання програми:
7
8
Коли аргумент x
передається в функцію, то параметр функції ref
стає посиланням на аргумент x
. Це дозволяє функції змінювати значення x
безпосередньо через ref
! Зверніть увагу, змінна x
не обов’язково повинна бути посиланням.
Порада: Передавайте аргументи в функцію через неконстантні посилання-параметри, якщо вони повинні бути змінені функцією в подальшому.
Основним недоліком використання неконстантних посилань в якості параметрів у функціях є те, що аргумент повинен бути неконстантним l-value (тобто константою або літералом він бути не може). Ми поговоримо про це докладніше (і про те, як це обійти) на наступному уроці.
Посилання як найлегший спосіб доступу до даних
Друге застосування посилань полягає в більш легкому способі доступу до вкладених даних. Розглянемо наступну структуру:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Something { int value1; float value2; }; struct Other { Something something; int otherValue; }; Other other; |
Припустимо, що нам потрібно працювати з полем value1
структури Something
змінної other
структури Other
(звучить важко, але таке також зустрічається на практиці). Зазвичай, доступ до цього поля здійснювався б через other.something.value1
. А що, якщо нам потрібно неодноразово отримувати доступ до цього члену? В такому випадку код стає громіздким і хаотичним. Посилання ж надають легший спосіб доступу до даних:
1 2 |
int &ref = other.something.value1; // ref тепер може використовуватися замість other.something.value1 |
Таким чином, наступні два стейтменти ідентичні:
1 2 |
other.something.value1 = 7; ref = 7; |
Посилання дозволяють зробити ваш код більш чистим і зрозумілим.
Посилання vs. Вказівники
Посилання — це той же вказівник, який неявно розіменовується при доступі до значення, на яке він вказує (“під капотом” посилання реалізовані за допомогою вказівників). Таким чином, в наступному коді:
1 2 3 |
int value = 7; int *const ptr = &value; int &ref = value; |
*ptr
і ref
оброблюються однаково. Тобто це одне і те ж:
1 2 |
*ptr = 7; ref = 7; |
Оскільки посилання повинні бути ініціалізовані коректними об’єктами (вони не можуть бути нульовими) і не можуть бути змінені пізніше, то вони, як правило, є безпечнішими за вказівники (так як ризик розіменування нульового вказівника відпадає). Однак, вони трохи обмежені в функціональності в порівнянні з вказівниками.
Якщо поставлене завдання можна вирішити за допомогою як посилань, так і вказівників, то краще використовувати посилання. Вказівники слід використовувати тільки в тих ситуаціях, коли посилання є недостатньо ефективними (наприклад, при динамічному виділенні пам’яті).
Висновки
Посилання дозволяють визначати псевдоніми для інших об’єктів або значень. Посилання на неконстантні значення можуть бути ініціалізовані лише неконстантними l-values. Вони не можуть бути переприсвоєні після ініціалізації. Посилання найчастіше використовуються в якості параметрів у функціях, коли ми хочемо змінити значення аргументу або хочемо уникнути його витратного копіювання.