На трьох попередніх уроках ми дізналися про передачу аргументів у функції по значенню, по посиланню і по адресі. На цьому уроці ми розглянемо повернення значень з функції у викликаючий об’єкт (caller) цими трьома способами.
Повернення значень (за допомогою оператора return) працює майже так само, як і передача значень в функцію. Ті ж самі плюси і мінуси. Основна відмінність полягає в тому, що потік даних рухається в протилежному напрямку. Однак тут є ще один нюанс — локальні змінні, які виходять з області видимості і знищуються, коли функція завершує своє виконання.
Повернення по значенню
Повернення по значенню — це найпростіший і найбезпечніший тип повернення. При поверненні по значенню, копія значення, що повертається, передається назад в caller. Як і у випадку з передачею по значенню, ви можете повертати літерали (наприклад, 7
), змінні (наприклад, x
) або вирази (наприклад, x + 2
), що робить цей спосіб дуже гнучким.
Ще однією перевагою є те, що ви можете повертати змінні (або вирази), в обчисленні яких задіяні і локальні змінні, оголошені в тілі самої функції. При цьому, можна не турбуватися про проблеми, які можуть виникнути з областю видимості. Оскільки змінні обчислюються до того, як функція здійснює повернення значення, то тут не повинно бути ніяких проблем з областю видимості цих змінних, коли закінчується блок, в якому вони оголошені. Наприклад:
1 2 3 4 5 |
int doubleValue(int a) { int value = a * 3; return value; // копія value повертається тут } // value виходить з області видимості тут |
Повернення по значенню ідеально підходить для повернення змінних, які були оголошені всередині функції, або для повернення аргументів функції, які були передані по значенню. Однак, подібно до передачі по значенню, повернення по значенню повільне при роботі зі структурами і класами.
Коли використовувати повернення по значенню:
при поверненні змінних, які були оголошені всередині функції;
при поверненні аргументів функції, які були передані в функцію по значенню.
Коли не використовувати повернення по значенню:
при поверненні стандартних масивів або вказівників (використовуйте повернення по адресі);
при поверненні великих структур або класів (використовуйте повернення по посиланню).
Повернення по адресі
Повернення по адресі — це повернення адреси змінної назад в caller. Подібно передачі по адресі, повернення по адресі може повертати тільки адресу змінної. Літерали і вирази повертати не можна, так як вони не мають адрес. Оскільки при поверненні по адресі просто копіюється адреса з функції в caller, то цей процес також дуже швидкий.
Проте цей спосіб має один недолік, який відсутній при поверненні по значенню: якщо ви спробуєте повернути адресу локальної змінної, то отримаєте несподівані результати. Наприклад:
1 2 3 4 5 |
int* doubleValue(int a) { int value = a * 3; return &value; // value повертається по адресі тут } // value знищується тут |
Як ви можете бачити, змінна value
знищується відразу після того, як її адреса повертається в caller. Кінцевим результатом буде те, що caller отримає адресу звільненої пам’яті (висячий вказівник), що, безсумнівно, викличе проблеми. Це одна з найпоширеніших помилок, яку роблять початківці. Більшість сучасних компіляторів видадуть попередження (а не помилку), якщо програміст спробує повернути локальну змінну по адресі. Однак є кілька способів обдурити компілятор, щоб зробити щось “погане”, не генеруючи при цьому попередження, тому вся відповідальність лежить на програмісті, який повинен гарантувати, що адреса, що повертається, буде коректною.
Повернення по адресі часто використовується для повернення динамічно виділеної пам’яті назад в caller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int* allocateArray(int size) { return new int[size]; } int main() { int *array = allocateArray(20); // Робимо що-небудь з array delete[] array; return 0; } |
Тут не виникне ніяких проблем, тому що динамічно виділена пам’ять не виходить з області видимості в кінці блоку, в якому вона оголошена, і все ще існуватиме, коли адреса повертатиметься в caller.
Коли використовувати повернення по адресі:
при поверненні динамічно виділеної пам’яті;
при поверненні аргументів функції, які були передані по адресі.
Коли не використовувати повернення по адресі:
при поверненні змінних, які були оголошені всередині функції (використовуйте повернення по значенню);
при поверненні великої структури або класу, які були передані по посиланню (використовуйте повернення по посиланню).
Повернення по посиланню
Подібно передачі по посиланню, значення, які повертаються по посиланню, повинні бути змінними (ви не зможете повернути посилання на літерал або вираз). При поверненні по посиланню в caller повертається посилання на змінну. Потім caller може її використовувати для продовження модифікації змінної, що може бути іноді корисно. Цей спосіб також дуже швидкий і при поверненні великих структур або класів.
Однак, як і при поверненні по адресі, ви не повинні повертати локальні змінні по посиланню. Розглянемо наступний фрагмент коду:
1 2 3 4 5 |
int& doubleValue(int a) { int value = a * 3; return value; // value повертається по посиланню тут } // value знищується тут |
Тут повертається посилання на змінну value
, яка знищиться, коли функція завершить своє виконання. Це означає, що caller отримає посилання на сміття. На щастя, ваш компілятор, найімовірніше, видасть попередження або помилку, якщо ви спробуєте зробити подібне.
Повернення по посиланню зазвичай використовується для повернення аргументів, переданих у функцію по посиланню. У наступному прикладі ми повертаємо (по посиланню) елемент масиву, який був переданий у функцію по посиланню:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> #include <array> // Повертаємо посилання на елемент масиву під індексом index int& getElement(std::array<int, 20> &array, int index) { // Ми знаємо, що array[index] не знищиться, коли ми будемо повертати дані в caller (так як caller сам передав цей array у функцію!) // Так що тут не повинно бути ніяких проблем з поверненням по посиланню return array[index]; } int main() { std::array<int, 20> array; // Присвоюємо елементу масиву під індексом 15 значення 7 getElement(array, 15) = 7; std::cout << array[15] << '\n'; return 0; } |
Результат виконання програми:
7
Коли ми викликаємо getElement(array, 15)
, то getElement() повертає посилання на елемент масиву під індексом 15, а потім main() використовує це посилання для присвоювання значення 7
цьому елементу.
Хоча цей приклад непрактичний, так як ми можемо напряму звернутися до 15 елементу масиву, але як тільки ми будемо розглядати класи, то ви виявите набагато більше застосувань для повернення значень по посиланню.
Коли використовувати повернення по посиланню:
при поверненні посилання-параметру;
при поверненні елементу масиву, який був переданий у функцію;
при поверненні великої структури або класу, який не знищується в кінці функції (наприклад, той, який був переданий у функцію).
Коли не використовувати повернення по посиланню:
при поверненні змінних, які були оголошені всередині функції (використовуйте повернення по значенню);
при поверненні стандартного масиву або значення вказівника (використовуйте повернення по адресі).
Змішування значень, що повертаються, з посиланнями
Хоча функція може повертати як значення, так і посилання, caller може неправильно це інтерпретувати. Подивимося, що станеться при змішуванні значень, що повертаються, з посиланнями на значення:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int returnByValue() { return 7; } int& returnByReference() { static int y = 7; // static гарантує те, що змінна y не знищиться, коли вийде з локальної області видимості return y; } int main() { int value = returnByReference(); // випадок A: все добре, обробляється як повернення по значенню int &ref = returnByValue(); // випадок B: помилка компілятора, так як 7 - це r-value, а r-value не може бути прив'язане до неконстантного посилання const int &cref = returnByValue(); // випадок C: все добре, час життя значення, що повертається, продовжується відповідно до часу життя cref } |
У випадку A ми присвоюємо посилання на значення, що повертається, змінній, яка сама не є посиланням. Оскільки value
не є посиланням, то значення, що повертається, просто копіюється в value
так, наче returnByReference() був поверненням по значенню.
У випадку B ми намагаємося ініціалізувати посилання ref
копією значення, що повертається з функції returnByValue(). Однак, оскільки значення, що повертається, не має адреси (це r-value), ми отримаємо помилку компіляції.
У випадку C ми намагаємося ініціалізувати константне посилання cref
копією значення, що повертається з функції returnByValue(). Оскільки константні посилання можуть бути ініціалізовані за допомогою r-values, то тут не повинно виникати ніяких проблем. Зазвичай r-values знищуються в кінці виразу, в якому вони створені, однак, при прив’язці до константного посилання, час життя r-value (в даному випадку, значення, що повертається з функції) продовжується відповідно до часу життя посилання (в даному випадку, cref
) .
Висновки
У більшості випадків ідеальним варіантом для використання є повернення по значенню. Це також найгнучкіший і найбезпечніший спосіб повернення даних у викликаючий об’єкт. Однак повернення по посиланню або по адресі також може бути корисним при роботі з динамічно виділеною пам’яттю. При використанні повернення по посиланню або по адресі переконайтеся, що ви не повертаєте посилання або адресу локальної змінної, яка вийде з області видимості, коли функція завершить своє виконання!
Тест
Напишіть прототипи для кожної з наступних функцій. Використовуйте найбільш підходящі параметри і типи повернення (по значенню, по адресі або по посиланню). Використовуйте const, якщо це необхідно.
Завдання №1
Функція sumTo(), яка приймає цілочисельний параметр, а повертає суму всіх чисел між 1
і числом, яке ввів користувач.
Відповідь №1
1 |
int sumTo(const int value); |
Завдання №2
Функція printAnimalName(), яка приймає структуру Animal
в якості параметру.
Відповідь №2
1 |
void printAnimalName(const Animal &animal); |
Завдання №3
Функція minmax(), яка приймає два цілих числа в якості вхідних даних, а повертає найменше та найбільше числа в якості окремих параметрів.
Підказка: Використовуйте параметри виводу.
Відповідь №3
1 |
void minmax(const int a, const int b, int &minOut, int &maxOut); |
Завдання №4
Функція getIndexOfLargestValue(), яка приймає цілочисельний масив (як вказівник) і його розмір, а повертає індекс найбільшого елементу масиву.
Відповідь №4
1 |
int getIndexOfLargestValue(const int *array, const int length); |
Завдання №5
Функція getElement(), яка приймає цілочисельний масив (як вказівник) і індекс і повертає елемент масиву під цим індексом (НЕ копію елементу). Припускається, що індекс коректний, а значення, що повертається, — константне.
Відповідь №5
1 |
const int& getElement(const int *array, const int index); |