11.4 – Передача аргументов по адресу

Добавлено 13 июня 2021 в 23:33

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

Вот пример функции, которая принимает параметр, переданный по адресу:

#include <iostream>
 
void foo(int* ptr)
{
    *ptr = 6;
}
 
int main()
{
    int value{ 5 };
 
    std::cout << "value = " << value << '\n';
    foo(&value);
    std::cout << "value = " << value << '\n';
    return 0;
}

Приведенный выше фрагмент печатает:

value = 5
value = 6

Как видите, функция foo() изменила значение аргумента (значение переменной) через параметр-указатель ptr.

Передача по адресу обычно используется с указателями, которые чаще всего используются для указания на встроенные массивы. Например, следующая функция распечатает все значения в массиве:

void printArray(int* array, int length)
{
    for (int index{ 0 }; index < length; ++index)
    {
        std::cout << array[index] << ' ';
    }
}

Вот пример программы, которая вызывает эту функцию:

int main()
{
    // помним, что массивы распадаются на указатели
    int array[6]{ 6, 5, 4, 3, 2, 1 }; 
    // поэтому здесь array вычисляется в указатель
    // на первый элемент массива, и & не требуется
    printArray(array, 6);
}

Эта программа печатает следующее:

6 5 4 3 2 1

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

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

void printArray(int* array, int length)
{
    // если пользователь передал в array нулевой указатель, выходим раньше!
    if (!array)
        return;
 
    for (int index{ 0 }; index < length; ++index)
        std::cout << array[index] << ' ';
}
 
int main()
{
    int array[6]{ 6, 5, 4, 3, 2, 1 };
    printArray(array, 6);
}

Передача по адресу на константное значение

Поскольку printArray() не изменяет ни один из своих аргументов, лучше сделать параметр array константным:

void printArray(const int* array, int length)
{
    // если пользователь передал в array нулевой указатель, выходим раньше!
    if (!array)
        return;
 
    for (int index{ 0 }; index < length; ++index)
        std::cout << array[index] << ' ';
}
 
int main()
{
    int array[6]{ 6, 5, 4, 3, 2, 1 };
    printArray(array, 6);
}

Это позволяет нам с первого взгляда сказать, что printArray() не изменяет переданный аргумент array, и гарантирует, что мы не сделаем этого случайно.

Адреса фактически передаются по значению

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

Вот пример программы, которая это иллюстрирует.

#include <iostream>
 
void setToNull(int* tempPtr)
{
    // мы заставляем tempPtr указывать на что-то еще,
    // не изменяя значение, на которое указывает tempPtr.
    tempPtr = nullptr; // до C++11 используйте 0
}
 
int main()
{ 
    // Сначала мы устанавливаем ptr на адрес переменной five,
    // что означает *ptr = 5
    int five{ 5 };
    int* ptr{ &five };
	
    // Это напечатает 5
    std::cout << *ptr;
 
    // tempPtr получит копию ptr
    setToNull(ptr);
 
    // ptr всё еще установлен на адрес five!
 
    // Это напечатает 5
    if (ptr)
        std::cout << *ptr;
    else
        std::cout << " ptr is null";
 
    return 0;
}

tempPtr получает копию адреса, который удерживает ptr. Несмотря на то, что мы изменяем tempPtr, чтобы он указывал на что-то еще (nullptr), это не меняет значение, на которое указывает ptr. Следовательно, эта программа печатает:

55

Обратите внимание, что даже если сам адрес передается по значению, вы всё равно можете разыменовать этот адрес, чтобы изменить значение аргумента. Это обычная путаница, поэтому давайте проясним:

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

Следующая программа иллюстрирует это:

#include <iostream>
 
void setToSix(int* tempPtr)
{
    *tempPtr = 6; // мы меняем значение, на которое указывает tempPtr (и ptr)
}
 
int main()
{ 
    // Сначала мы устанавливаем ptr на адрес five, что означает *ptr = 5
    int five{ 5 };
    int* ptr{ &five };
	
    // Это напечатает 5
    std::cout << *ptr;
 
    // tempPtr получит копию ptr
    setToSix(ptr);
 
    // tempPtr изменил значение, на которое указывает, на 6,
    // поэтому теперь ptr указывает на значение 6
 
    // Это напечатает 6
    if (ptr)
        std::cout << *ptr;
    else
        std::cout << " ptr is null";
 
    return 0;
}

Это печатает:

56

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

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

Следующая программа иллюстрирует использование ссылки на указатель:

#include <iostream>
 
// tempPtr теперь является ссылкой на указатель, поэтому любые изменения,
// внесенные в tempPtr, также изменят аргумент!
void setToNull(int*& tempPtr)
{
    tempPtr = nullptr; // до C++11 используйте значение 0
}
 
int main()
{ 
    // Сначала мы устанавливаем ptr на адрес five, что означает *ptr = 5
    int five{ 5 };
    int* ptr{ &five };
	
    // Это напечатает 5
    std::cout << *ptr;
 
    // tempPtr устанавливается как ссылка на ptr
    setToNull(ptr);
 
    // ptr теперь изменен на nullptr!
 
    if (ptr)
        std::cout << *ptr;
    else
        std::cout << " ptr is null";
 
    return 0;
}

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

5 ptr is null

Это показывает, что вызов setToNull() действительно изменил значение ptr с &five на nullptr!

Существует только передача по значению

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

В уроке по ссылкам мы кратко упомянули, что ссылки обычно реализуются компилятором как указатели. Это означает, что за кулисами передача по ссылке – это, по сути, просто передача по адресу (с доступом к ссылке, выполняющим неявное разыменование).

И чуть выше мы показали, что передача по адресу на самом деле просто передача адреса по значению!

Таким образом, можно сделать вывод, что C++ на самом деле всё передает по значению! Свойства передачи по адресу (и ссылке) исходят исключительно из того факта, что мы можем разыменовать переданный адрес для изменения аргумента, чего мы не можем сделать с параметром обычного значения!

Передача по адресу делает изменяемые параметры явными

Рассмотрим следующий пример:

int foo1(int x);  // передаем по значению
int foo2(int& x); // передаем по ссылке
int foo3(int* x); // передаем по адресу
 
int i {};
 
foo1(i);  // не могу изменить i
foo2(i);  // могу изменить i
foo3(&i); // могу изменить i

Из вызова foo2() не очевидно, что функция может изменять переменную i, не так ли?

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

Однако у этого есть свой набор недостатков: вызывающий может подумать, что он может передать nullptr, когда он не должен так делать, и теперь вам нужно тщательно проверять на нулевые указатели.

Мы склоняемся к рекомендации передавать необязательные изменяемые параметры по ссылке. Более того, избегайте изменяемых параметров вообще.

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

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

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

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

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

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

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

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

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

Как видите, передача по адресу и передача по ссылке имеют практически одинаковые преимущества и недостатки. Поскольку передача по ссылке обычно более безопасна, чем передача по адресу, в большинстве случаев предпочтительнее использовать ее.

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

Если возможно, лучше передавать по ссылке, а не по адресу.

Теги

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

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

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