Как использовать ссылочные параметры в C ++? - PullRequest
51 голосов
/ 02 апреля 2010

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

Как и почему вы хотите использовать ссылку? Что произойдет, если вы не сделаете параметр ссылкой, а вместо этого оставите & выключенным?

Например, в чем разница между этими функциями:

int doSomething(int& a, int& b);
int doSomething(int a, int b);

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

Ответы [ 8 ]

113 голосов
/ 02 апреля 2010

Думайте о ссылке как псевдоним . Когда вы вызываете что-то для ссылки, вы действительно вызываете это для объекта, на который ссылается ссылка.

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.

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

4 голосов
/ 02 апреля 2010

Ответ GMan дает вам минимальное количество ссылок. Я просто хотел показать вам очень простую функцию, которая должна использовать ссылки: swap, которая меняет две переменные. Вот для int с (как вы просили):

// 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 s, которая не нуждается во временной. Впрочем, здесь меня больше волнует ясность, чем умница.

Без ссылок (или указателей) 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 в памяти в местоположении 0» или «int в памяти в местоположении 0x1000». «a» также является «int в ячейке памяти 0x1000». Другие операции над указателями - сложение и вычитание, но это также тема для другого дня.

4 голосов
/ 02 апреля 2010

Давайте рассмотрим простой пример функции с именем 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

1 голос
/ 02 апреля 2010

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

Первый использует нормальную функцию, а второй использует ссылки:


Изменить - вот исходный код, если вам не нравятся ссылки:

Пример 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
}
1 голос
/ 02 апреля 2010
// 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);

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

0 голосов
/ 05 октября 2017

Поправьте меня, если я не прав, но ссылка - это только разыменованный указатель, или?

Разница с указателем в том, что вы не можете легко зафиксировать NULL.

0 голосов
/ 06 сентября 2015

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

0 голосов
/ 02 апреля 2010

Я не знаю, является ли это самым основным, но здесь идет ...

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, это может быть сделано без путаницы вызывающего абонента.

...