Как использовать ссылочные параметры в С++? - программирование
Подтвердить что ты не робот

Как использовать ссылочные параметры в С++?

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Но выглядит как неуклюжий наш код. На С++ представлены ссылки для решения этой проблемы. Если мы можем просто перечислить переменную, мы получим код, который мы искали:

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, происходящий внутри функции, действительно происходит на переменные, которые псевдонимы вне функции.

(Обратите внимание: никогда не записывайте функцию 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-correct. Это означает, что если ваша функция не изменяет параметр, отметьте его как 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.

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

Ответ 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

Ответ 3

Ответ 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, которая не нуждается в временном. Тем не менее, здесь я больше забочусь о ясном, чем умном.

Без ссылок (или указателей) 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

// 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);

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

Ответ 5

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

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


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

Пример 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
}

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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