Как я использую Параметры ссылки в C++?

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

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

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

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

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

51
задан Justin 19 July 2017 в 10:48
поделиться

6 ответов

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

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

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

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


Другой используется для удаления той копии, которая возникает при вызове функции. Предположим, у нас есть очень большой тип данных. Копирование этого объекта занимает много времени, и мы хотели бы этого избежать:

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.

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

115
ответ дан 7 November 2019 в 09:55
поделиться

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

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


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

Пример 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
ответ дан 7 November 2019 в 09:55
поделиться

Не знаю, является ли это самым простым, но вот ...

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 () необходимо изменить счетчик внутри, это можно сделать, не сбивая с толку вызывающего.

0
ответ дан 7 November 2019 в 09:55
поделиться

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

// 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
}

Существует более умная реализация swap для ints, которая не нуждается во временной. Однако, здесь я больше забочусь о ясности, чем об умности.

Без ссылок (или указателей) swap_noref не может изменять переданные ему переменные, а значит, просто не может работать. swap_ptr может изменять переменные, но он использует указатели, которые являются грязными (когда ссылки не подходят, указатели могут справиться с задачей). swap - самый простой.

Об указателях

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

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

Две основные операции над переменными - это получение текущего значения (делается просто с помощью имени переменной) и присвоение нового значения (оператор присваивания, '='). Значения хранятся в памяти (ячейка, в которой хранится значение, - это просто непрерывная область памяти). Например,

int a = 17;

получается что-то вроде (примечание: ниже "foo @ 0xDEADBEEF" означает переменную с именем "foo", хранящуюся по адресу "0xDEADBEEF". Адреса памяти были выдуманы):

             ____
a @ 0x1000: | 17 |
             ----

Все, что хранится в памяти, имеет начальный адрес, поэтому есть еще одна операция: получить адрес значения ("&" - оператор адреса). Указатель - это переменная, хранящая адрес.

int *pa = &a;

результаты:

              ______                     ____
pa @ 0x10A0: |0x1000| ------> @ 0x1000: | 17 |
              ------                     ----

Обратите внимание, что указатель просто хранит адрес памяти, поэтому у него нет доступа к имени того, на что он указывает. На самом деле, указатели могут указывать на вещи без имен, но это тема для другого дня.

Существует несколько операций над указателями. Вы можете разыменовать указатель (оператор "*"), что дает вам данные, на которые указывает указатель. Разыменование противоположно получению адреса: *&a - это тот же ящик, что и a, &*pa - это то же значение, что и pa, а *pa - это тот же ящик, что и a. В частности, pa в примере содержит 0x1000; * pa означает "int в памяти в месте pa", или "int в памяти в месте 0x1000". "a" также означает "int в месте памяти 0x1000". Другие операции над указателями - сложение и вычитание, но это тема для другого дня.

4
ответ дан 7 November 2019 в 09:55
поделиться

Давайте рассмотрим простой пример функции с именем increment , которая увеличивает свой аргумент. Рассмотрим:

void increment(int input) {
 input++;
}

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

int i = 1;
std::cout<<i<<" ";
increment(i);
std::cout<<i<<" ";

произведет 1 1 в качестве вывода.

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

void increment(int &input) { // note the & 
 input++;
}

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

4
ответ дан 7 November 2019 в 09:55
поделиться
// 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
ответ дан 7 November 2019 в 09:55
поделиться
Другие вопросы по тегам:

Похожие вопросы: