Как использовать ссылочные параметры в C++?
Я пытаюсь понять, как использовать эталонные параметры. В моем тексте есть несколько примеров, однако они слишком сложны, чтобы я мог понять, почему и как их использовать.
Как и почему вы хотите использовать ссылку? Что произойдет, если вы не сделаете параметр ссылкой, а оставите &
выкл?
Например, в чем разница между этими функциями:
int doSomething(int& a, int& b);
int doSomething(int a, int b);
Я понимаю, что ссылочные переменные используются для того, чтобы изменить формальную -> ссылку, которая затем позволяет двусторонний обмен параметрами. Тем не менее, это степень моих знаний, и более конкретный пример был бы очень полезен.
8 ответов
Думайте о ссылке как о псевдониме. Когда вы вызываете что-то для ссылки, вы действительно вызываете это для объекта, на который ссылается ссылка.
int i;
int& j = i; // j is an alias to i
j = 5; // same as i = 5
Когда дело доходит до функций, подумайте:
void foo(int i)
{
i = 5;
}
Выше, int i
является значением, и переданный аргумент передается по значению. Это означает, что если мы скажем:
int x = 2;
foo(x);
i
будет копия x
, Таким образом, установка i
до 5 не влияет на x
потому что это копия x
быть измененным Однако, если мы сделаем i
ссылка:
void foo(int& i) // i is an alias for a variable
{
i = 5;
}
Потом говорю foo(x)
больше не делает копию x
; i
является x
, Так что, если мы скажем foo(x)
внутри функции i = 5;
точно так же, как x = 5;
, а также x
изменения.
Надеюсь, это прояснит немного.
Почему это важно? Когда вы программируете, вы никогда не хотите копировать и вставлять код. Вы хотите сделать функцию, которая выполняет одну задачу, и она делает это хорошо. Всякий раз, когда нужно выполнить эту задачу, вы используете эту функцию.
Допустим, мы хотим поменять местами две переменные. Это выглядит примерно так:
int x, y;
// swap:
int temp = x; // store the value of x
x = y; // make x equal to y
y = temp; // make y equal to the old value of x
Хорошо отлично Мы хотим сделать это функцией, потому что: swap(x, y);
намного легче читать. Итак, давайте попробуем это:
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
Это не сработает! Проблема в том, что это замена копий двух переменных. То есть:
int a, b;
swap(a, b); // hm, x and y are copies of a and b...a and b remain unchanged
В C, где ссылки не существуют, решением было передать адрес этих переменных; то есть используйте указатели *:
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int a, b;
swap(&a, &b);
Это хорошо работает. Тем не менее, это немного неудобно в использовании, и на самом деле немного небезопасно. swap(nullptr, nullptr)
, обменивает две ничто и разыменовывает нулевые указатели... неопределенное поведение! Исправлено с некоторыми проверками:
void swap(int* x, int* y)
{
if (x == nullptr || y == nullptr)
return; // one is null; this is a meaningless operation
int temp = *x;
*x = *y;
*y = temp;
}
Но выглядит как неуклюжий наш код получил. C++ вводит ссылки для решения этой проблемы. Если мы можем просто псевдоним переменной, мы получим код, который мы искали:
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
int a, b;
swap(a, b); // inside, x and y are really a and b
И простой в использовании, и безопасный. (Мы не можем случайно передать нулевое значение, нет нулевых ссылок.) Это работает, потому что перестановка, происходящая внутри функции, действительно происходит с переменными, псевдонимы которых находятся вне функции.
(Обратите внимание, никогда не пишите swap
функция.:) Один уже существует в шапке <algorithm>
и он настроен на работу с любым типом.)
Другое использование - удалить эту копию, которая происходит при вызове функции. Представьте, что у нас очень большой тип данных. Копирование этого объекта занимает много времени, и мы хотели бы избежать этого:
struct big_data
{ char data[9999999]; }; // big!
void do_something(big_data data);
big_data d;
do_something(d); // ouch, making a copy of all that data :<
Однако все, что нам действительно нужно, это псевдоним переменной, поэтому давайте укажем это. (Опять же, в C мы передавали адрес нашего большого типа данных, решая проблему копирования, но вводя неуклюжесть.):
void do_something(big_data& data);
big_data d;
do_something(d); // no copies at all! data aliases d within the function
Вот почему вы услышите, что вы должны все время передавать ссылки, если они не являются примитивными типами. (Поскольку внутренняя передача псевдонима, вероятно, выполняется с помощью указателя, как в C. Для небольших объектов копирование выполняется быстрее, чем с указателями.)
Имейте в виду, что вы должны быть правильными. Это означает, что если ваша функция не изменяет параметр, пометьте его как const
, Если do_something
выше только посмотрел, но не изменился data
мы бы отметили это как const
:
void do_something(const big_data& data); // alias a big_data, and don't change it
Мы избегаем копирования и говорим "эй, мы не будем изменять это". У этого есть другие побочные эффекты (с такими вещами как временные переменные), но вы не должны беспокоиться об этом сейчас.
В отличие от нашего swap
функция не может быть const
потому что мы действительно модифицируем псевдонимы.
Надеюсь, это прояснит еще немного.
* Грубый учебник указателей:
Указатель - это переменная, которая содержит адрес другой переменной. Например:
int i; // normal int
int* p; // points to an integer (is not an integer!)
p = &i; // &i means "address of i". p is pointing to i
*p = 2; // *p means "dereference p". that is, this goes to the int
// pointed to by p (i), and sets it to 2.
Итак, если вы видели функцию свопинга указатель-версия, мы передаем адрес переменных, которые мы хотим поменять, а затем делаем своп, разыменовывая, чтобы получить и установить значения.
Давайте рассмотрим простой пример функции с именем increment
что увеличивает его аргумент. Рассматривать:
void increment(int input) {
input++;
}
который не будет работать, так как изменение происходит в копии аргумента, переданного функции фактического параметра. Так
int i = 1;
std::cout<<i<<" ";
increment(i);
std::cout<<i<<" ";
будет производить 1 1
в качестве вывода.
Чтобы заставить функцию работать с фактическим переданным параметром, мы передаем ее reference
к функции как:
void increment(int &input) { // note the &
input++;
}
изменение, внесенное в input
внутри функции фактически делается фактический параметр. Это даст ожидаемый результат 1 2
Ответ GMan дает вам низкое на ссылки. Я просто хотел показать вам очень простую функцию, которая должна использовать ссылки: swap
, который меняет две переменные. Вот это для int
s (как вы просили):
// changes to a & b hold when the function exits
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
// changes to a & b are local to swap_noref and will go away when the function exits
void swap_noref(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
// changes swap_ptr makes to the variables pointed to by pa & pb
// are visible outside swap_ptr, but changes to pa and pb won't be visible
void swap_ptr(int *pa, int *pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main() {
int x = 17;
int y = 42;
// next line will print "x: 17; y: 42"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap can alter x & y
swap(x,y);
// next line will print "x: 42; y: 17"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap_noref can't alter x or y
swap_noref(x,y);
// next line will print "x: 42; y: 17"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap_ptr can alter x & y
swap_ptr(&x,&y);
// next line will print "x: 17; y: 42"
std::cout << "x: " << x << "; y: " << y << std::endl
}
Есть более умная реализация подкачки для int
с, которые не нуждаются во временном. Впрочем, здесь меня больше волнует ясность, чем умница.
Без ссылок (или указателей), swap_noref
не может изменить переданные ему переменные, что означает, что он просто не может работать. swap_ptr
может изменять переменные, но в нем используются указатели, которые являются беспорядочными (однако, ссылки не могут их сократить, однако указатели могут выполнять эту работу). swap
самый простой в целом.
На указатели
Указатели позволяют вам делать то же самое, что и ссылки. Однако указатели возлагают на программиста большую ответственность за управление ими и за память, на которую они указывают (тема, называемая " управление памятью" - но пока не беспокойтесь об этом). Как следствие, ссылки должны быть вашим предпочтительным инструментом на данный момент.
Думайте о переменных как об именах, привязанных к полям, в которых хранится значение. Константы - это имена, связанные непосредственно со значениями. Оба сопоставляют имена со значениями, но значение констант не может быть изменено. Хотя значение, содержащееся в блоке, может изменяться, привязка имени к блоку не может изменяться, поэтому ссылку нельзя изменить для ссылки на другую переменную.
Две основные операции над переменными - получение текущего значения (выполняется просто с помощью имени переменной) и присвоение нового значения (оператор присваивания, '='). Значения хранятся в памяти (поле, содержащее значение, является просто непрерывной областью памяти). Например,
int a = 17;
в результате получается что-то вроде (примечание: далее "foo @ 0xDEADBEEF" обозначает переменную с именем "foo", хранящуюся по адресу "0xDEADBEEF". Адреса памяти были составлены):
____
a @ 0x1000: | 17 |
----
Все, что хранится в памяти, имеет начальный адрес, поэтому есть еще одна операция: получить адрес значения ("&" является оператором address-of). Указатель - это переменная, которая хранит адрес.
int *pa = &a;
результаты в:
______ ____
pa @ 0x10A0: |0x1000| ------> @ 0x1000: | 17 |
------ ----
Обратите внимание, что указатель просто хранит адрес памяти, поэтому он не имеет доступа к названию того, на что он указывает. На самом деле, указатели могут указывать на вещи без имен, но это тема для другого дня.
Есть несколько операций над указателями. Вы можете разыменовать указатель (оператор "*"), который дает вам данные, на которые указывает указатель. Разыменование является противоположностью получению адреса: *&a
та же коробка, что и a
, &*pa
такое же значение, как pa
, а также *pa
та же коробка, что и a
, Особенно, pa
в примере держит 0x1000; * pa
означает "int в памяти в местоположении pa" или "int в памяти в местоположении 0x1000". "a" также является "int в ячейке памяти 0x1000". Другие операции над указателями - сложение и вычитание, но это также тема для другого дня.
// Passes in mutable references of a and b.
int doSomething(int& a, int& b) {
a = 5;
cout << "1: " << a << b; // prints 1: 5,6
}
a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b; // prints 2: 5,6
С другой стороны,
// Passes in copied values of a and b.
int doSomething(int a, int b) {
a = 5;
cout << "1: " << a << b; // prints 1: 5,6
}
a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b; // prints 2: 0,6
Или версия const:
// Passes in const references a and b.
int doSomething(const int &a, const int &b) {
a = 5; // COMPILE ERROR, cannot assign to const reference.
cout << "1: " << b; // prints 1: 6
}
a = 0;
b = 6;
doSomething(a, b);
Ссылки используются для передачи местоположений переменных, поэтому их не нужно копировать в стеке в новую функцию.
Простая пара примеров, которые вы можете запустить онлайн.
Первый использует обычную функцию, а второй использует ссылки:
Изменить - вот исходный код, если вам не нравятся ссылки:
Пример 1
using namespace std;
void foo(int y){
y=2;
}
int main(){
int x=1;
foo(x);
cout<<x;//outputs 1
}
Пример 2
using namespace std;
void foo(int & y){
y=2;
}
int main(){
int x=1;
foo(x);
cout<<x;//outputs 2
}
Я не знаю, является ли это самым основным, но здесь идет...
typedef int Element;
typedef std::list<Element> ElementList;
// Defined elsewhere.
bool CanReadElement(void);
Element ReadSingleElement(void);
int ReadElementsIntoList(int count, ElementList& elems)
{
int elemsRead = 0;
while(elemsRead < count && CanReadElement())
elems.push_back(ReadSingleElement());
return count;
}
Здесь мы используем ссылку для передачи нашего списка элементов в ReadElementsIntoList()
, Таким образом, функция загружает элементы прямо в список. Если мы не использовали ссылку, то elems
будет копия переданного списка, в который будут добавлены элементы, но затем elems
будет отброшен, когда функция вернется.
Это работает в обе стороны. В случае count
мы не делаем это ссылкой, потому что мы не хотим изменять переданное количество, вместо этого возвращая количество прочитанных элементов. Это позволяет вызывающему коду сравнить количество фактически прочитанных элементов с запрошенным числом; если они не совпадают, то CanReadElement()
должен был вернуться false
и сразу же попытаться прочитать еще немного, скорее всего, не удастся. Если они совпадают, то, возможно, count
было меньше, чем количество доступных элементов, и дальнейшее чтение будет уместным. Наконец, если ReadElementsIntoList()
необходимо изменить count
внутренне, это могло бы сделать это, не портя вызывающего абонента.
Поправьте меня, если я ошибаюсь, но ссылка - это только разыменованный указатель, или?
Отличие от указателя в том, что вы не можете легко зафиксировать NULL.
Как насчет метафоры: скажем, ваша функция считает бобы в банке. Ему нужна банка бобов, и вам нужно знать результат, который не может быть возвращаемым значением (по ряду причин). Вы можете отправить ему jar и значение переменной, но вы никогда не узнаете, изменяет ли это значение или нет. Вместо этого вам нужно отправить эту переменную через конверт с обратным адресом, чтобы он мог поместить значение в нее и знать, что записал результат в значение по указанному адресу.