Урок №100. Возврат значений по ссылке, по адресу и по значению

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 90181

 ǀ   22 

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Результат выполнения программы:

7

Когда мы вызываем getElement(array, 15), то getElement() возвращает ссылку на элемент массива под индексом 15, а затем main() использует эту ссылку для присваивания этому элементу значения 7.

Хотя этот пример непрактичен, так как мы можем напрямую обратиться к 15 элементу массива, но как только мы будем рассматривать классы, то вы обнаружите гораздо больше применений для возврата значений по ссылке.

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

   при возврате ссылки-параметра;

   при возврате элемента массива, который был передан в функцию;

   при возврате большой структуры или класса, который не уничтожается в конце функции (например, тот, который был передан в функцию).

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

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

   при возврате стандартного массива или значения указателя (используйте возврат по адресу).

Смешивание возвращаемых значений и ссылок


Хотя функция может возвращать как значение, так и ссылку, caller может неправильно это интерпретировать. Посмотрим, что произойдет при смешивании возвращаемых значений и ссылок на значения:

В случае A мы присваиваем ссылку возвращаемого значения переменной, которая сама не является ссылкой. Поскольку value не является ссылкой, то возвращаемое значение просто копируется в value так, как если бы returnByReference() был возвратом по значению.

В случае B мы пытаемся инициализировать ссылку ref копией возвращаемого значения функции returnByValue(). Однако, поскольку возвращаемое значение не имеет адреса (это r-value), мы получим ошибку компиляции.

В случае C мы пытаемся инициализировать константную ссылку cref копией возвращаемого значения функции returnByValue(). Поскольку константные ссылки могут быть инициализированы с помощью r-values, то здесь не должно быть никаких проблем. Обычно r-values уничтожаются в конце выражения, в котором они созданы, однако, при привязке к константной ссылке, время жизни r-value (в данном случае, возвращаемого значения функции) продлевается в соответствии со временем жизни ссылки (в данном случае, cref).

Заключение

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

Тест


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

Задание №1

Функция sumTo(), которая принимает целочисленный параметр, а возвращает сумму всех чисел между 1 и числом, которое ввел пользователь.

Ответ №1

Задание №2

Функция printAnimalName(), которая принимает структуру Animal в качестве параметра.

Ответ №2

Задание №3

Функция minmax(), которая принимает два целых числа в качестве входных данных, а возвращает наименьшее и наибольшее числа в качестве отдельных параметров.

Подсказка: Используйте параметры вывода.

Ответ №3

Задание №4

Функция getIndexOfLargestValue(), которая принимает целочисленный массив (как указатель) и его размер, а возвращает индекс наибольшего элемента массива.

Ответ №4

Задание №5

Функция getElement(), которая принимает целочисленный массив (как указатель) и индекс и возвращает элемент массива по этому индексу (не копию элемента). Предполагается, что индекс корректен, а возвращаемое значение — константное.

Ответ №5

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (284 оценок, среднее: 4,89 из 5)
Загрузка...

Комментариев: 22

  1. Дмитрий:

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

    Если я не ошибаюсь , при уничтожении локальной переменной value ячейка памяти продолжает хранить значение этой переменной. Следовательно — в каллёр вернётся значение, которое нам нужно. Я прав?

    1. Макс:

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

  2. Павел:

    Для третьего задания

    вызов должен выглядеть как

    ?

    Количество аргументов же должно быть равно количеству параметров?

  3. Storyteller:

    Стал учить С++ по этому материалу, так как по учебнику, который предложили в колледже ничего не понятно. За 2 полных дня зачита дошел до этого места. И теперь меня терзает вопрос: какого черта я за эти 2 дня узнал в РАЗЫ больше, чем за 2 семестра? Конечно, сказывается факт того, что часть я уже знал, но все же! Не знаю, то ли тут так божественно изложен материал, то ли наша программа настолько бездарная, но огромное спасибо за адаптацию материала, очень помогли.

    1. Фото аватара Юрий:

      Пожалуйста, спасибо за отзыв))

  4. Макс:

    Статья супер. Автору спасибо.

  5. Дмитрий:

    Как на счет перегрузки операторов? Возможен ли возврат по указателю например для оператора +? Будет ли легальным следующий код:

  6. Sergey:

    Во втором тесте, VS(2019) при компиляции ругался когда была такая запись

    а когда добавил

    сразу скомпилировался. Почему?

    1. Артурка:

      Перед использованием структуры Animal требуется ее объявить до использования:

      Иначе компилятор не будет знать что такой тип данных существует.

  7. Константин:

    В случае смешивания возвращаемых значений очень хочется потрогать "за вымя" оператор static в функции int& returnByReference().
    Вот как он возвращает r-values без адреса в случае функции времени исполнения программы. На какой такой мнимой оси будут сидеть r-values ?

    1. Nikita:

      Так наша static переменная получается глобальной, потому у нее есть свой конкретный адрес, который не уничтожается при выходе из функции. Разве не так?

  8. Константин:

    Здравствуйте, огромное спасибо за уроки, очень доступно, последовательно и ясно.
    Вопрос:
    Если мы передали в функцию объект да ещё и расположенный в динамической памяти по ссылке либо по адресу, зачем его возвращать? Можно сделать функцию вида:

    и ни чего не возвращать. Можно в коде делать так и не париться с возвратами? (за возможные ошибки извините)

    1. bash:

      Во-первых, ошибка в селекторе. При передаче класса или структуры по указателю, селектор должен быть косвенный.
      Во-вторых, если это класс, то не факт, что это поле public и можно к нему обратиться.

  9. Дмитрий:

    Понимающие люди, поясните пожалуйста:
    1. Почему и с какой целью перед названием функции ставится "&" при передаче по ссылке и "*" при возврате по адресу? (это подразумевает, что все параметры этой функции принимаются по ссылке / адресу соответственно, я правильно понимаю?)
    2. Почему перед прототипом функции ставится const? (на примере 5-го задания)

    1. Анастасия:

      1. Почему и с какой целью перед названием функции ставится "&" при передаче по ссылке и "*" при возврате по адресу?
      это синтаксис ссылки и указателя соответственно. То есть если их не ставить, то результат функции будет передаваться по значению. В каком случае что использовать — объясняет данный урок.
      2. Почему перед прототипом функции ставится const? (на примере 5-го задания)
      в 5-м задании сказано, что возвращаемое значение должно быть константным, поэтому перед типом результата ставится const

  10. Юлиана:

    У меня тоже вопрос. Почему нельзя возвращать по значению большие структуры или классы? И массивы? (Хотя про массивы я начинаю догадываться: потому что имя массива — это указатель на его начало, а если возвращать по значению указатель, то мы вернем тупо адрес, а не то, на что этот адресс ссылается), верно?

    1. Анастасия:

      потому что, как уже было подмечено, чтобы вернуть что-то по значению, это что-то по сути копируется для возврата в вызывающую функцию. Так как структуры и классы — это, как правило, довольно объёмные штуки, лучше их не копировать, а передавать по ссылке. Если их в функции не надо менять, то ставить перед ними const

  11. Andrey:

    Первое: Больше спасибо за Вашу работу.
    Второе:
    Вопрос накопился. 🙂
    1 задание: зачем создавать копию value когда можно туда передать ссылку, все равно же константа?

    3 задание: вопрос такой же как и в первом:

    так же легче?

    в 4 и 5 ссылкой можно так же передавать длину и индекс массива?

    1. Danila:

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

      Передача по значению:
      mov rcx, 10 — помещаем в регистр занчение
      call sumTo — вызываем функцию

      Передача по ссылке:
      mov dword ptr ss:[rbp-4],A — помещаем значение в стек
      lea rax,qword ptr ss:[rbp-4] — получаем адрес в стеке — указатель/ссылка
      mov rcx,rax — помещаем этот адрес в регистр для передачи параметра
      call sumTo — вызываем функцию

      1. Upaut:

        На самом деле разницы в скорости особой нет как передавать: по значению, по адресу.
        При вызове функции все параметры функции, задом на перед, укладываются в стек, а только потом идет вызов call.
        операции mov, lea — 3 тактовые. call, push. pop — 2 такта.
        Перед вызовом функции вы при любом раскладе будете стек загружать. Нет разницы что вы кладете значение или адрес.
        Любая переменная хранится в оперативе и ассемблер в любом случае лезет к ней по адресу (обсолютный:относительный). Ему всеравно что забирать значение или адрес, по тактам это одинакого.
        Для максимальной скорости надо больше регистры общего назначения задействовать, они на уровне кэш памяти работают и стараться гонять значения между ними (eax, ebx, edx, ecx, esi, edi и т.д), и стек (хоть это и оперативная память, а не кеш), но доступ к стеку быстрее (за счет архитектуры микропроцессора: pop и push это 2 тактовые операции) чем просто области памяти

  12. Oleksiy:

    Выскажу свой опыт в понимании данной темы. В английском языке все просто: мы можем передать данные тремя способами: тупо по значению (by value), по указателю (by pointer) и по адресу (by address). Когда же читаешь тему по русски, то в сознании возникает каша, т.к. мозг не улавливает, в каком смысле используется слово "значение". Это слово имеет столько вариантов интерпретации (в отличии от английского), что затрудняет восприятие темы.

    1. Nikita:

      А это каши не вызывает?
      "по указателю (by pointer) и по адресу (by address)"
      Какая разница если указатель это и есть адрес? Или это типа ссылка? (опять таки на адрес)

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *