11.3 – Передача аргументов по ссылке

Добавлено 13 июня 2021 в 10:43

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

Передача по ссылке

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

void addOne(int &ref) // ref - переменная-ссылка
{
    ref = ref + 1;
}

Когда функция вызывается, ref станет ссылкой на аргумент. Поскольку ссылка на переменную обрабатывается точно так же, как и сама переменная, любые изменения, внесенные в ссылку, передаются аргументу!

Следующий пример показывает это в действии:

void addOne(int &ref)
{
    ref = ref + 1;
}
 
int main()
{
    int value{ 5 };
 
    cout << "value = " << value << '\n';
    addOne(value);
    cout << "value = " << value << '\n';
    return 0;
}

Эта программа аналогична той, которую мы использовали в примере передачи по значению, за исключением того, что параметр foo теперь является ссылкой, а не обычной переменной. Когда мы вызываем addOne(value), ref становится ссылкой на переменную value из main. Этот код дает следующий вывод:

value = 5
value = 6

Как видите, функция изменила значение аргумента с 5 на 6!

Возврат нескольких значений через выходные параметры

Иногда нам нужно, чтобы функция возвращала несколько значений. Однако функции могут иметь только одно возвращаемое значение. Один из способов вернуть несколько значений – использовать параметры-ссылки:

#include <iostream>
#include <cmath>    // для std::sin() и std::cos()
 
void getSinCos(double degrees, double &sinOut, double &cosOut)
{
    // sin() и cos() принимают радианы, а не градусы, поэтому нам нужно преобразование
    static constexpr double pi { 3.14159265358979323846 }; // значение пи
    double radians{ degrees * pi / 180.0 };
    sinOut = std::sin(radians);
    cosOut = std::cos(radians);
}
 
int main()
{
    double sin{ 0.0 };
    double cos{ 0.0 };
 
    // getSinCos вернет sin и cos в переменных sin и cos
    getSinCos(30.0, sin, cos);
 
    std::cout << "The sin is " << sin << '\n';
    std::cout << "The cos is " << cos << '\n';
    return 0;
}

Эта функция принимает один параметр (по значению) в качестве входных данных и «возвращает» два параметра (по ссылке) в качестве выходных данных. Параметры, которые используются только для возврата значений вызывающей стороне, называются выходными параметрами. Мы назвали эти параметры с суффиксом out, чтобы обозначить, что эти параметры выходные. Это помогает напомнить вызывающему, что начальное значение, переданное этим параметрам, не имеет значения и что мы должны ожидать их перезаписи. По соглашению выходные параметры обычно являются крайними правыми параметрами.

Давайте рассмотрим, как это работает, более подробно. Сначала функция main создает локальные переменные sin и cos. Они передаются в функцию getSinCos() по ссылке (а не по значению). Это означает, что функция getSinCos() имеет доступ к реальным переменным sin и cos, а не к копиям. getSinCos() присваивает новые значения sin и cos (через ссылки sinOut и cosOut соответственно), которые перезаписывают старые значения в sin и cos. Затем main печатает эти обновленные значения.

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

Этот метод, хотя и работает, имеет несколько незначительных недостатков. Во-первых, вызывающий должен передать аргументы для хранения обновленных выходных данных, даже если он не намеревается их использовать. Что еще более важно, синтаксис немного неестественный: и входные, и выходные параметры объединяются в вызове функции. Со стороны вызывающего не очевидно, что sin и cos являются выходными параметрами и будут изменены. Это, наверное, самая опасная часть этого метода (так как может привести к ошибкам). Некоторые программисты и компании считают, что это достаточно большая проблема, чтобы посоветовать вообще избегать выходных параметров или использовать для выходных параметров вместо этого передачу по адресу (что имеет более четкий синтаксис, указывающий, можно ли изменять параметр или нет).

Лично мы рекомендуем по возможности полностью избегать использования выходных параметров. Если вы всё же используете их, обозначайте параметры (и выходные аргументы) суффиксом (или префиксом) "out", чтобы помочь прояснить, что значение может быть изменено.

Ограничения передачи по ссылке

Неконстантные ссылки могут ссылаться только на неконстантные l-значения (например, неконстантные переменные), поэтому параметр-ссылка не может принимать аргумент, который является константным l-значением или r-значением (например, литералы и результаты выражений).

Передача по константной ссылке

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

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

Вы уже знаете, что константная ссылка – это ссылка, которая не позволяет изменять переменную, на которую она ссылается. Следовательно, если мы используем константную ссылку в качестве параметра, мы гарантируем вызывающему, что функция не изменит аргумент!

Следующая функция вызовет ошибку компиляции:

void foo(const std::string &x) // x - ссылка на константное значение
{
    x = "hello";  // ошибка компиляции: нельзя изменить значение константной ссылки!
}

Использование const полезно по нескольким причинам:

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

Лучшая практика


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

Напоминание


Неконстантные ссылки не могут связываться с r-значениями. Функция с неконстантным параметром-ссылкой не может быть вызвана с литералами или временными значениями.

#include <string>
 
void foo(std::string& text) {}
 
int main()
{
  std::string text{ "hello" };
  
  foo(text); // ok
  foo(text + " world"); // недопустимо, неконстантные ссылки
                        // не могут связываться с r-значениями.
 
  return 0;
}

Ссылки на указатели

Указатель можно передать по ссылке, и функция в этом случае может изменить адрес, на который указывает указатель:

#include <iostream>
 
void foo(int *&ptr) // передать указатель по ссылке
{
	ptr = nullptr; //  это изменяет фактический переданный аргумент ptr, а не копию
}
 
int main()
{
	int x{ 5 };
	int *ptr{ &x };
	std::cout << "ptr is: " << (ptr ? "non-null" : "null") << '\n'; // выводит non-null
	foo(ptr);
	std::cout << "ptr is: " << (ptr ? "non-null" : "null") << '\n'; // выводит null
 
	return 0;
}

(Мы покажем еще один пример в следующем уроке)

Напоминаем, что вы можете передать по ссылке массив в стиле C. Это полезно, если в функции вам нужна возможность изменять массив (например, для функции сортировки) или вам нужен доступ к информации о типе фиксированного массива (для выполнения sizeof() или цикла for-each). Однако обратите внимание, что для того, чтобы это работало, в параметре вам необходимо явно указать размер массива:

#include <iostream>
 
// Примечание: вам нужно указать размер массива в объявлении функции
void printElements(int (&arr)[4])
{
  // теперь мы можем это сделать, поскольку массив
  // не распадается на указатель
  int length{ sizeof(arr) / sizeof(arr[0]) }; 
  
  for (int i{ 0 }; i < length; ++i)
  {
    std::cout << arr[i] << '\n';
  }
}
 
int main()
{
    int arr[]{ 99, 20, 14, 80 };
    
    printElements(arr);
 
    return 0;
}

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

Плюсы и минусы передачи по ссылке

Преимущества передачи по ссылке:

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

Недостатки передачи по ссылке:

  • Поскольку неконстантная ссылка не может быть инициализирована константным l-значением или r-значением (например, литералом или выражением), аргументы неконстантных ссылочных параметров должны быть обычными переменными.
  • Может быть трудно определить, предназначен ли аргумент, переданный неконстантной ссылкой, для входных данных, выходных данных или для того и другого. Разумное использование const и суффикса именования выходных переменных может помочь прояснить это.
  • По вызову функции невозможно сказать, может ли аргумент измениться. Аргументы, переданные по значению и переданные по ссылке, выглядят одинаково. Мы можем определить, передан ли аргумент по значению или по ссылке, только взглянув на объявление функции. Это может привести к ситуациям, когда программист не понимает, что функция изменит значение аргумента.

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

  • При передаче структур или классов (используйте const, если они доступны только для чтения).
  • Когда вам нужна функция для изменения аргумента.
  • Когда вам нужен доступ к информации о типе фиксированного массива.

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

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

Лучшая практика


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

Теги

C++ / CppLearnCppАргументДля начинающихОбучениеПрограммированиеСсылка / Reference (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.