Є ще один спосіб передачі змінних у функцію в мові C++ — по адресу.
Передача по адресу
Передача аргументів по адресу — це передача адреси змінної-аргументу (а не вихідної змінної). Оскільки аргумент є адресою, то параметром функції повинен бути вказівник. Потім функція зможе розіменувати цей вказівник для доступу або зміни вихідного значення. Ось приклад функції, яка приймає параметр, який передається по адресу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> void boo(int *ptr) { *ptr = 7; } int main() { int value = 4; std::cout << "value = " << value << '\n'; boo(&value); std::cout << "value = " << value << '\n'; return 0; } |
Результат виконання програми:
value = 4
value = 7
Як ви можете бачити, функція boo() змінила значення аргументу (змінну value
) через параметр-вказівник ptr
. Передачу по адресу зазвичай використовують з вказівниками на звичайні масиви. Наприклад, наступна функція виведе всі значення масиву:
1 2 3 4 5 |
void printArray(int *array, int length) { for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } |
Ось приклад програми, яка викликає цю функцію:
1 2 3 4 5 |
int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; // пам'ятаєте, що масиви конвертуються у вказівники при передачі? printArray(array, 7); // тому що тут array - це вказівник на перший елемент масиву (в операторі & тут немає необхідності) } |
Результат:
9 8 6 4 3 2 1
Пам’ятайте, що фіксовані масиви конвертуються у вказівники при передачі у функцію, тому їх довжину потрібно передавати в якості окремого параметру. Перед розіменуванням параметрів, переданих по адресу, не зайвим буде перевірити — чи не є вони нульовими вказівниками. Розіменування нульового вказівника призведе до збою в програмі. Ось функція printArray() з перевіркою (виявленням) нульових вказівників:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void printArray(int *array, int length) { // Якщо користувач передав нульовий вказівник в якості array if (!array) return; for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; printArray(array, 7); } |
Передача по константному адресу
Оскільки printArray() все одно не змінює значення отриманих аргументів, то гарною ідеєю буде зробити параметр array
константою:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void printArray(const int *array, int length) { // Якщо користувач передав нульовий вказівник в якості array if (!array) return; for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; printArray(array, 7); } |
Так ми бачимо відразу, що printArray() не змінить переданий аргумент array
. Коли ви передаєте вказівник у функцію по адресу, то значення цього вказівника (адреса, на яку він вказує) копіюється з аргументу в параметр функції. Іншими словами, він передається по значенню! Якщо змінити значення параметру функції, то зміниться тільки копія, вихідний вказівник-аргумент не буде змінений. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> void setToNull(int *tempPtr) { // Ми присвоюємо tempPtr інше значення (ми не змінюємо значення, на яке вказує tempPtr) tempPtr = nullptr; // використовуйте 0, якщо не підтримується C++11 } int main() { // Спочатку ми присвоюємо ptr адресу six, тобто *ptr = 6 int six = 6; int *ptr = &six; // Тут виведеться 6 std::cout << *ptr << "\n"; // tempPtr отримає копію ptr setToNull(ptr); // ptr до сих пір вказує на змінну six! // Тут виведеться 6 if (ptr) std::cout << *ptr << "\n"; else std::cout << " ptr is null"; return 0; } |
У tempPtr
копіюється адреса вказівника ptr
. Незважаючи на те, що ми змінили tempPtr
на нульовий вказівник (присвоїли йому nullptr
), це ніяк не вплинуло на значення, на яке вказує ptr
. Отже, результат виконання програми:
6
6
Зверніть увагу, хоча сама адреса передається по значенню, ви все одно можете розіменувати її для зміни значення вихідного аргументу. Заплутано? Давайте розберемося детально:
При передачі аргументу по адресу в змінну-параметр функції копіюється адреса з аргументу. У цей момент параметр функції і аргумент вказують на одне і те ж значення.
Якщо параметр функції потім розіменувати для зміни початкового значення, то це призведе до зміни значення, на яке вказує аргумент, оскільки параметр функції і аргумент вказують на одне і те ж значення!
Якщо параметру функції присвоїти іншу адресу, то це ніяк не вплине на аргумент, оскільки параметр функції є копією, а зміна копії не призводить до зміни оригіналу. Після зміни адресу параметра функції, параметр функції і аргумент вказуватимуть на різні значення, тому розіменування параметру і подальша його зміна ніяк не вплинуть на значення, на яке вказує аргумент.
У наступній програмі це все добре проілюстровано:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> void setToSeven(int *tempPtr) { *tempPtr = 7; // ми змінюємо значення, на яке вказує tempPtr (і ptr також) } int main() { // Спочатку ми присвоюємо ptr адресу six, тобто *ptr = 6 int six = 6; int *ptr = &six; // Тут виведеться 6 std::cout << *ptr << "\n"; // tempPtr отримає копію ptr setToSeven(ptr); // tempPtr змінив значення, на яке вказував, на 7 // Тут виведеться 7 if (ptr) std::cout << *ptr << "\n"; else std::cout << " ptr is null"; return 0; } |
Результат виконання програми:
Передача адресів по посиланню
Виникає питання: «А що, якщо ми хочемо змінити адресу, на яку вказує аргумент, всередині функції?». Виявляється, це можна зробити дуже легко. Ви можете просто передати адресу по посиланню. Синтаксис посилання на вказівник може здатися трохи дивним, але все ж:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> // tempPtr тепер є посиланням на вказівник, тому будь-які зміни tempPtr призведуть і до зміни вихідного аргументу! void setToNull(int *&tempPtr) { tempPtr = nullptr; // використовуйте 0, якщо не підтримується C++11 } int main() { // Спочатку ми присвоюємо ptr адресу six, тобто *ptr = 6 int six = 6; int *ptr = &six; // Тут виведеться 6 std::cout << *ptr; // tempPtr є посиланням на ptr setToNull(ptr); // ptr було присвоєно значення nullptr! if (ptr) std::cout << *ptr; else std::cout << " ptr is null"; return 0; } |
Результат виконання програми:
6 ptr is null
Нарешті, наша функція setToNull() дійсно змінила значення ptr
з &six
на nullptr
!
Існує тільки передача по значенню
Тепер, коли ви розумієте основні відмінності між передачею по посиланню, по адресу і по значенню, давайте трохи поговоримо про те, що знаходиться “під капотом”.
На уроці про посилання ми згадували, що посилання насправді реалізуються за допомогою вказівників. Це означає, що передача по посиланню є просто передачею по адресу. І трохи вище ми говорили, що передача по адресу насправді є передачею адреси по значенню! З цього випливає, що C++ дійсно передає все по значенню!
Плюси і мінуси передачі по адресу
Плюси передачі по адресу:
Передача по адресу дозволяє функції змінити значення аргументу, що іноді корисно. В іншому випадку, використовуємо const для гарантії того, що функція не змінить аргумент.
Оскільки копіювання аргументів не відбувається, то швидкість передачі по адресу досить висока, навіть якщо передавати великі структури або класи.
Ми можемо повернути відразу декілька значень з функції, використовуючи параметри виводу.
Мінуси передачі по адресу:
Всі вказівники потрібно перевіряти, чи не є вони нульовими. Спроба розіменувати нульовий вказівник призведе до збою в програмі.
Оскільки розіменування вказівника виконується повільніше, ніж доступ до значення напряму, то доступ до аргументів, переданих по адресу, виконується також повільніше, ніж доступ до аргументів, переданих по значенню.
Коли використовувати передачу по адресу:
при передачі звичайних масивів (якщо немає ніяких проблем з тим, що масиви конвертуються у вказівники при передачі).
Коли не використовувати передачу по адресу:
при передачі структур або класів (використовуйте передачу по посиланню);
при передачі фундаментальних типів даних (використовуйте передачу по значенню).
Як ви можете бачити самі, передача по адресу і по посиланню мають майже однакові переваги і недоліки. Оскільки передача по посиланню зазвичай безпечніша, ніж передача по адресу, то в більшості випадків краще використовувати передачу по посиланню.
Правило: Використовуйте передачу по посиланню, замість передачі по адресу, коли це можливо.