MyTetra Share
Делитесь знаниями!
Возврат значений по ссылке, по адресу (указателю) и по значению в C++
09.10.2018
23:39
Автор: Юрий Ворон
Текстовые метки: c++, возврат, функция, метод, ссылка, адрес, указател, значение
Раздел: Компьютер - Программирование - Язык C++

В этом уроке мы рассмотрим возврат значений обратно из функции в вызывающий объект всеми этими тремя способами.

Возврат значений (оператор return) работает почти так же, как и передача значений в функцию. Все те же плюсы и минусы. Основное различие состоит в том, что поток данных двигается уже в противоположную сторону. Однако здесь есть еще один нюанс – локальные переменные, которые выходят из области видимости и уничтожаются, когда функция завершает свое выполнение. Об этом поговорим чуть ниже.


Возврат по значению

Возврат по значению — это самый простой и безопасный тип возврата. При возврате по значению, копия возвращаемого значения передается обратно в caller. Как и в случае с передачей по значению, вы можете возвращать литералы (например, 7), переменные (например, x) или выражения (например, x + 2), что делает этот способ очень гибким.

Еще одним преимуществом является то, что вы можете возвращать переменные (или выражения), в вычислении которых задействованы и локальные переменные, объявленные в теле самой функции, без необходимости беспокоиться о проблемах, которые могут возникнуть с областью видимости. Поскольку переменные вычисляются до того, как функция производит возврат и копия значения возвращается в caller, то здесь не должно быть никаких проблем с областью видимости этих переменных, когда заканчивается блок, в которым они объявлены.


int doubleValue(int a)

{

    int value = a * 3;

    return value; // копия value возвращается здесь

} // value выходит из области видимости здесь


Возврат по значению идеально подходит для возврата переменных, которые были объявлены внутри функции, или для возврата аргументов функции, которые были переданы по значению. Однако, подобно передаче по значению, возврат по значению медленный при работе со структурами и большими классами.

Когда использовать возврат по значению:

  при возврате переменных, которые были объявлены внутри функции;

  при возврате аргументов функции, которые были переданы в функцию по значению.

Когда не использовать возврат по значению:

  при возврате стандартных массивов или указателей (используйте возврат по адресу);

  при возврате больших структур или классов (используйте возврат по ссылке).


Возврат по адресу

Возврат по адресу (он же возврат по указателю) – это возврат адреса переменной обратно в caller. Подобно передаче по адресу, возврат по адресу может возвращать только адрес переменной. Не литерала и не выражения (они не имеют адресов). Поскольку при возврате по адресу просто копируется адрес из функции в caller, то этот процесс также очень быстрый.

Тем не менее, этот способ имеет один недостаток, который возврат по значению не имеет — если вы попытаетесь вернуть адрес локальной переменной функции, то можете получить неожиданные результаты. Например:


int* doubleValue(int a)

{

    int value = a * 3;

    return &value; // value возвращается по адресу здесь

} // value уничтожается здесь


Как вы можете видеть, value уничтожается сразу после того, как его адрес возвращается в caller. Конечным результатом будет то, что caller получит адрес освобожденной памяти (висячий указатель), что, несомненно, вызовет проблемы. Это одна из распространенных ошибок, которую делают новички. Большинство современных компиляторов выдадут предупреждение (а не ошибку), если программист попытается вернуть локальную переменную по адресу, однако есть несколько способов обмануть компилятор, чтобы сделать что-то плохое, не генерируя при этом предупреждения, поэтому вся ответственность лежит на программисте, который должен гарантировать, что возвращаемый адрес будет действителен.

Возврат по адресу часто используется для возврата динамически выделенной памяти обратно в caller:


int* allocateArray(int size)

{

    return new int[size];

}

int main()

{

    int *array = allocateArray(20);

    // делаем что-нибудь с array

    delete[] array;

    return 0;

}


Здесь не возникнет никакх проблем, так как динамически выделенная память не выходит из области видимости в конце блока, в котором объявлена и все еще будет существовать, когда адрес будет возвращаться в caller.

Когда использовать возврат по адресу:

  при возврате динамически выделенной памяти;

  при возврате аргументов функции, которые были переданы по адресу.

Когда не использовать возврат по адресу:

  при возврате переменных, которые были объявлены внутри функции (используйте возврат по значению);

  при возврате большой структуры или класса, который был передан по ссылке (используйте возврат по ссылке).


Возврат по ссылке

Подобно передаче по ссылке, значения, возвращаемые по ссылке, должны быть переменными (вы не сможете вернуть ссылку на литерал или выражение). При возврате по ссылке в caller возвращается ссылка на переменную. Затем caller может её использовать для продолжения изменения переменной, что может быть иногда полезно. Этот способ также очень быстрый и при возврате больших структур или классов.

Однако, как и в возврате по адресу, вы не должны возвращать локальные переменные по ссылке. Рассмотрим следующий фрагмент кода:


int& doubleValue(int a)

{

    int value = a * 3;

    return value; // value возвращается по ссылке здесь

} // value уничтожается здесь


В программе выше возвращается ссылка на переменную value, которая уничтожится, когда функция завершит свое выполнение. Это означает, что caller получит ссылку на мусор. К счастью, ваш компилятор, вероятнее всего, выдаст предупреждение или ошибку, если вы попытаетесь это сделать.

Возврат по ссылке обычно используется для возврата аргументов, переданных в функцию по ссылке. В следующем примере мы возвращаем (по ссылке) элемент массива, который был передан в функцию по ссылке:


#include <array>

#include <iostream>

// Возвращаем ссылку на индекс элемента массива

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 может неправильно это интерпретировать. Посмотрим, что произойдет при смешивании возвращаемых значений и ссылок на значения.


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).


Итог

В большинстве случаев идеальным вариантом будет использовать возврат по значению. Это также самый гибкий и безопасный способ возврата данных обратно в вызывающий объект. Однако возврат по ссылке или по адресу также может быть полезен при работе с динамически выделенными классами или структурами. При использовании возврата по ссылке или по адресу убедитесь, что вы не возвращаете ссылку или адрес локальной переменной, которая выйдет из области видимости, когда функция завершит свое выполнение!


Так же в этом разделе:
 
MyTetra Share v.0.52
Яндекс индекс цитирования