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

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

  • автор:

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

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

Передача данных по значению

Этот способ передачи данных в подпрограмму является основным и действует по-умолчанию. Фактический параметр вычисляется в вызывающей функции и его значение передаётся на место формального параметра в вызываемой функции. На этом связь между фактическим и формальным параметрами прекращается.

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

Пример 1 . Вычислить сумму ряда с заданной точностью ε =10 -5 :

Для вычисления суммы ряда используем функцию. В неё передадим по значению x и eps . Результат вернём через имя функции оператором return .

Возможный вариант реализации программы:

using namespace std;

double fsum(double x, double eps);

double x, s, eps = 1.0e-5;

double fsum(double x, double eps)

double s = x, p = x, i, t = x * x;

for(i = 3; fabs(p) > eps; i += 2)

p = -p * t / (i * (i — 1));

Не сложно убедиться, что всё нормально работает. Но что делать, когда выходных параметров два или более? Через имя функции можно вернуть только один объект, остальные придётся возвращать через список. Позволит ли этот способ (передача по значению) вернуть через список параметров изменённые значения? Нет, не позволит. Давайте проверим это на несложном примере.

Пример 2 . Даны два числа, хранящиеся в переменных a и b . Используя подпрограмму, выполнить обмен содержимого ячеек этих переменных.

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

Возможный вариант реализации программы:

using namespace std;

void Obmen(double x, double y);

double a = 2.5, b = 3.1;

void Obmen(double x, double y)

Результаты выполнения программы:

Do Obmen: a=2.5 b=3.1

Function Obmen start:

Function Obmen end:

Posle Obmen: a=2.5 b=3.1

Вывод на экран значений переменных показывает, что данные в функцию переданы правильно, перестановка в функции произведена, но это ни как не отразилось на значениях исходных переменных a и b после выхода из функции Obmen() .

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

Передача данных по адресу

По адресу в функцию всегда передаются массивы (рассмотрим это в следующих темах). Для массива это вообще единственный способ передачи данных в языках С/С++. Так же по адресу можно передать те простые объекты, которые являются выходными данными (или входными и выходными одновременно).

В случае передачи данных по адресу фактический параметр может быть только переменной (константа или выражение не имеют адреса!).

Вернёмся к предыдущему примеру.

Пример 3 . Даны два числа, хранящиеся в переменных a и b . Используя подпрограмму, выполнить обмен содержимого ячеек этих переменных.

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

Возможный вариант реализации программы:

using namespace std;

void Obmen(double *x, double *y);

double a = 2.5, b = 3.1;

void Obmen(double *x, double *y)

Результаты выполнения программы:

Do Obmen: a=2.5 b=3.1

Function Obmen start:

Function Obmen end:

Posle Obmen: a=3.1 b=2.5

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

Как это работает? Рассмотрим данный вопрос подробнее, используя пример с обменом данных. Для наглядности приведём рисунок:

В вызывающей функции (в нашем случае — в main() ) вычисляются адреса объектов, передаваемых по адресу ( у нас — адреса переменных a и b . Пусть это будут числа 1000 и 1008 ), и затем эти адреса копируются в ячейки памяти — указатели, память под которые выделена в функции Obmen() (это x и y ). Зная адрес переменной, например, адрес переменной a , который теперь хранится в указателе x , можно, пользуясь операцией разыменование, не только прочитать, но и изменить значение исходной переменной.

Ни какой реальной передачи данных (в смысле копирования) из подпрограммы Obmen() назад в main() не делается. Мы на самом деле через указатели работаем с исходными объектами! Поэтому после выхода из функции Obmen() имеем изменённые переменные a и b (если быть точнее, переменные изменятся ещё до выхода из функции,то есть в момент перестановки в самой функции Obmen() ).

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

Это ещё один из способов вернуть результат работы функции через список параметров. Напомним, что применяется только для С++. В языке С такого варианта нет.

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

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

Вернёмся снова к примеру обмена, только данные передадим по ссылке.

Пример 4 . Даны два числа, хранящиеся в переменных a и b . Используя подпрограмму, выполнить обмен содержимого ячеек этих переменных.

Возможный вариант реализации программы:

using namespace std;

double a = 2.5, b = 3.1;

Передача параметров в функцию

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

Пусть у нас имеется какая-то структура и функция, которая работает с этой структурой.

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

#include #include typedef struct point_tag < float x; float y; >point_t; void foo(point_t point) < point.x = 100; >int main() < point_t p = < 10.0, 20.0 >; printf("p.x = %.3f\n", p.x); foo(p); printf("p.x = %.3f", p.x); _getch(); return 0; >

В этом коде мы передаём в функцию foo копию переменной p. Значение кладётся на стек при вызове функции, и внутри функции мы можем с ним работать. Но изменяем мы только локальную копию. В main никакие изменения не видны. Проиллюстрируем это

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

Пусть переменная p имеет адрес AB00. По этому адресу в памяти компьютера располагается экземпляр структуры со значением x = 10. Мы передаём это значение в функцию foo, и локальная копия имеет адрес CD00. Потом мы изменяем значение внутри foo, и оно становится 100, но по тому же адресу CD00, а исходное значение не изменилось.

Мы уже знаем, что для изменения значения нужно передавать указатель. Вот пример, который работает, как мы и ожидаем

#include #include typedef struct point_tag < float x; float y; >point_t; void foo(point_t *point) < point->x = 100; > int main() < point_t p = < 10.0, 20.0 >; printf("p.x = %.3f\n", p.x); foo(&p); printf("p.x = %.3f", p.x); _getch(); return 0; >

Передача указателя на структуру

Пусть переменная p имеет адрес AB00. Передача по указателю – это тоже передача значения, просто в данном случае мы передаём адрес переменной. Внутри функции переменная point хранится по адресу CD00. Хранит она адрес переменной p, равный AB00. Далее мы изменяем значение содержимого, на которое ссылается переменная point. Поэтому изменение видно внутри main.

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

void foo(const point_t *point) < point->x = 100; //compilation error >

Более сложный пример, когда мы хотим изменить не содержимое объекта, а непосредственно сам объект. Типичная ошибка делать вот так

#include #include typedef struct point_tag < float x; >point_t; void foo(point_t *point) < point = malloc(sizeof(point_t)); point->x = 100; > int main() < point_t *p = malloc(sizeof(point_t)); p->x = 10; printf("p.x = %.3f\n", p->x); foo(p); printf("p.x = %.3f", p->x); _getch(); free(p); return 0; >

В этом случае изменения объекта не произойдёт, плюс, мы получим утечку памяти. Давайте разберёмся.

Модификация структуры не приводит к изменению этой структуры

Пусть переменная p хранится по адресу AB00, а память под структуру была выделена по адресу AB04. p хранит этот адрес. В foo передаётся AB04 (содержимое p). Внутри foo мы локальной переменной point, которая хранится по адресу CD00, присваиваем новое значение СВ04 – адрес, который вернула нам функция malloc. Как видно, внутри main ничего не поменялось. Кроме того, после выхода из функции foo будет удалена переменная point, которая хранит адрес структуры на куче, а сама память не будет очищена.

Для модификации объекта нужно передавать указатель , и в данном случае, для модификации указателя, нужно передать указатель на указатель.

#include #include typedef struct point_tag < float x; >point_t; void foo(point_t **point) < (*point) = malloc(sizeof(point_t)); (*point)->x = 100; > int main() < point_t *p = malloc(sizeof(point_t)); p->x = 10; printf("p.x = %.3f\n", p->x); foo(&p); printf("p.x = %.3f", p->x); _getch(); return 0; >

Указатель на указатель позволяет модифицировать указатель

Как обычно, p имеет адрес CD00 и хранит адрес CD04, по которому хранится структура. Мы передаём адрес переменной p – это AB00. Внутри foo теперь мы меняем содержимое по адресу AB00, и оно становится равным CD04. Иначе говоря, мы имеем две переменные, которые хранят один и тот же адрес на куче – это p внутри main, и point внутри foo. После выхода из функции локальная переменная point будет удалена, а значение, выделенное на куче, по адресу CD04 не пострадает.

Но заметьте, теперь у нас осталась «висячая» структура на куче по адресу AB04, на неё никто не ссылается и никто её не удаляет!

Утечка памяти при изменении указателя

Внимательно следите за такими вещами.

#include #include typedef struct point_tag < float x; >point_t; void foo(point_t **point) < free(*point); (*point) = malloc(sizeof(point_t)); (*point)->x = 100; > int main() < point_t *p = malloc(sizeof(point_t)); p->x = 10; printf("p.x = %.3f\n", p->x); foo(&p); printf("p.x = %.3f", p->x); _getch(); free(p); return 0; >

Краткая памятка по использованию разных типов аргументов, на примере структуры point_t

Таб. 1 Памятка по передаче параметров

foo(point_t) Только чтение
foo(point_t*) Модификация значения
foo(const point_t*) Только чтение, но при передаче большого объекта расходуется меньше ресурсов
foo(point_t **) Модификация содержимого и самого указателя

ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students

email

Всё ещё не понятно? – пиши вопросы на ящик

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

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

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

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

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

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

2 4 6 8 10

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

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

В итоге в данном случае не будет большой разницы, какой тип имеет параметр — массив или указатель. Компилятор в любом случае будет рассматривать параметр типа int array[] как указатель int* array

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

#include // функция ожидает получить массив как минимум из 4 элементов void twice(int numbers[4]) < for(size_t i = 0; i < 4; i++) < numbers[i]= numbers[i] * 2; printf("%d \t", numbers[i] * 2); >> int main(void) < int nums[5] = ; twice(nums); return 0; >

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

Начиная со стандарта C99 Си позволяет установить минимальную длину массива с помощью оператора static :

// ожидаем массив как минимум с 4 элементами void twice(int numbers[static 4])
Константный массив

Массив также можно передавать как константный — в этом случае значения его элементов нельзя изменить:

#include // массив p - константный void twice(size_t n, const int p[]) < for(size_t i = 0; i < n; i++) < // p[i]= p[i] * 2; // Так нельзя - массив константный printf("%d \t", p[i] * 2); >> int main(void) < int nums[] = ; // получаем количество элементов массива size_t length = sizeof(nums)/sizeof(nums[0]); twice(length, nums); return 0; >

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

Некоторые разработчики предпочитают наличие амперсанда в 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 и завершению программы.

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

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

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