Как передать адрес переменной в функцию си
Перейти к содержимому

Как передать адрес переменной в функцию си

  • автор:

Передача параметров по указателю

Некоторые разработчики предпочитают наличие амперсанда в f(&obj) как подсказки о том, что значение obj может измениться внутри вызова. Правда, если obj уже и так указатель, то амперсанд не будет нужен.

Плюсы передачи по указателю

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

Это экономия одновременно и по памяти, и по скорости!

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

Можно использовать константный указатель: const T*

Минусы передачи по указателю

В Си существует неявное преобразование типа целого числа к типу указателя, о котором компилятор выдает лишь предупреждение, но не ошибку:

warning: passing argument 1 of ‘foo’ makes pointer from integer without a cast

Представьте себе, что foo довольно длинна, и везде, где употребляется x , нужна звёздочка, а вызывается foo 48 раз в разных местах программы — при этом иногда нужен амперсанд, иногда нет.

Итак, легко забыть звёздочку в теле функции foo или амперсанд — в её вызове, значит переданное число может случайно истолковаться как адрес числа. Обращение по непредсказуемому адресу приводит к ошибке Segmentation fault и завершению программы.

Поэтому передача аргументов по адресу опасна!

Как передать адрес переменной в функцию си

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

Например, пусть у нас будет простейшая функция, которая увеличивает число на единицу:

#include void increment(int x) < x = x + 1; printf("increment function: %d \n", x); >int main(void)

Здесь переменная n передается в качестве аргумента для параметра x. Передача происходит по значению, поэтому любое изменение параметра x в функции increment никак не скажется на значении переменной n. Что мы можем увидеть, запустим программу:

increment function: 11 main function: 10

Теперь изменим функцию increment, использовав в качестве параметра указатель:

#include void increment(int *x) < *x = *x + 1; printf("increment function: %d \n", *x); >int main(void)

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

*x = *x + 1;

Это изменяет значение, которое находится по адресу, хранимому в указателе x.

Поскольку теперь функция в качестве параметра принимает указатель, то при ее вызове необходимо передать адрес переменной: increment(&n); .

В итоге изменение параметра x также повлияет на переменную n:

increment function: 11 main function: 11

Еще один показательный пример применения указателей в параметрах — функция обмена значений:

#include void swap(int *a, int *b) < int temp = *a; *a = *b; *b=temp; >int main(void)

Функция swap() в качестве параметров принимает два указателя. Посредством переменной temp происходит обмен значениями.

При вызове функции swap в нее передаются адреса переменных x и y, и в итоге их значения будут изменены.

Константые параметры

Если необходимо запретить изменять значение параметра-указателя внутри функции, то можно определить такой параметра как константный:

#include // константный параметр int twice(const int *x) < //*x = *x * *x; // так нельзя, так как x - константный параметр int y = *x + *x; return y; >int main(void) < int n = 10; int m = twice(&n); printf("n = %d \n", n); // n = 10 printf("m = %d \n", m); // m = 20 return 0; >

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

Массивы в параметрах

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

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

#include void twice(int n, int p[]) < for(int i = 0; i < n; i++) < p[i]= p[i] * 2; >> int main(void) < int nums[] = ; // получаем количество элементов массива int length = sizeof(nums)/sizeof(nums[0]); twice(length, nums); for(int i=0; i return 0; >

Функция twice в качестве параметров принимает массив и число его элементов и в цикле увеличивает их в два раза.

В функции main передаем массив в функцию twice и затем выводим его на консоль. В результате мы увидим, что массив nums был изменен:

2 4 6 8 10

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

#include void twice(int n, int *p) < for(int i=0; i> int main(void) < int nums[] = ; int length = sizeof(nums)/sizeof(nums[0]); twice(length, nums); for(int i=0; i return 0; >

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

Передача указателей в функцию C++

Вопрос по C++. При передаче обычной переменной в функцию создается, как я понимаю, её копия. А что происходит, когда мы передаем указатель? Создается ли копия указателя или нет?

Отслеживать
44.8k 3 3 золотых знака 38 38 серебряных знаков 89 89 бронзовых знаков
задан 24 дек 2016 в 23:45
113 1 1 серебряный знак 5 5 бронзовых знаков

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Если у вас объявлена функция с параметром, как, например,

void f( T t ); 

где T — это некоторый тип, и эта функция вызывается с некоторым выражением, переданным ей в качестве аргумента, как

f( exp ); 

то инициализацию параметра можно представить следующим образом

void f( /* T t */ ) < T t = exp; //. 

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

Сравните две функции

void f( int *p ) < p = new int( 10 ); >void g( int * &p )
int main()

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

У функции g параметр имеет ссылочный тип, то есть эта ссылка на переданный функции аргумент. Поэтому функция имеет дело с исходным аргументом и меняет его в своем теле, присваивая ему адрес выделенной памяти.

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

void h( int **p )

Вызов функции будет выглядеть как

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

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

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

#include void foo(int* ptr) < *ptr = 6; >int main() < int value< 5 >; std::cout value = 5 value = 6

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

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

void printArray(int* array, int length) < for (int index< 0 >; index < length; ++index) < std::cout >

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

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 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 int main() < int array[6]< 6, 5, 4, 3, 2, 1 >; printArray(array, 6); >

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

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

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

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

#include 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 

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

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

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

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

#include void setToSix(int* tempPtr) < *tempPtr = 6; // мы меняем значение, на которое указывает tempPtr (и ptr) >int main() < // Сначала мы устанавливаем ptr на адрес five, что означает *ptr = 5 int five< 5 >; int* ptr< &five >; // Это напечатает 5 std::cout 

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

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

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

#include // 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 

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

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 – логически недопустимый аргумент (используйте передачу по ссылке).
  • При передаче структур или классов (используйте передачу по ссылке).
  • При передаче базовых типов (используйте передачу по значению).

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

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *