с передача параметров в функции по значению и по адресу
C++. Функции. Передача параметров. Оператор return
Из данной статьи Вы узнаете о передаче параметров в функцию и о возврате результата с помощью оператора return. Рассмотрим теоретический материал.
Передача параметров в функцию
Обмен информацией между вызываемой и вызывающей функциями осуществляется с помощью механизма передачи параметров. Список_переменных, указанный в заголовке функции называется формальными параметрами или просто параметрами функции. Список_переменных в операторе вызова функции — это фактические параметры или аргументы.
Передача параметров выполняется следующим образом. Вычисляются выражения, стоящие на месте фактических параметров. Затем формальным параметрам присваиваются значения фактических. Выполняется проверка типов и при необходимости выполняется их преобразование.
Передача параметров в функцию может осуществляться по значению и по адресу.
При передачи данных по значению функция работает с копиями фактических параметров, и доступна к исходным значениям аргументов у нее нет. При передачи по адресу в функцию передается не переменная, а ее адрес, и, следовательно, функция имеет доступ к ячейкам памяти, в которых хранятся аргументов. Таким образом, данные, переданные по значению, функция изменить не может, в отличие от данных, переданных по адресу.
Если требуется запретить изменение параметра внутри функции, используют модификатор const. Заголовок функции в общем виде будет выглядеть так:
Функции. Передача аргументов по значению и по ссылке
Объявление и определение функций
При изучении работы функций важно понимать, что такое локальная и что такое глобальная переменные. В языке программирования C глобальные (внешние) переменные объявляются вне какой-либо функции. С их помощью удобно организовывать обмен данными между функциями, однако это считается дурным тоном, т.к. легко запутывает программу. Локальные переменные в языке программирования C называют автоматическими. Область действия автоматических переменных распространяется только на ту функцию, в которой они были объявлены. Параметры функции также являются локальными переменными.
В теле main() функция median() вызывается три раза. Результат выполнения функции не обязательно должен быть присвоен переменной.
Вышеописанную программу можно было бы записать так:
Хотя такой способ и экономит одну строчку кода, однако главная функция, в которой отражена основная логика программы, опускается вниз, что может быть неудобно. Поэтому первый вариант предпочтительней.
Напишите функцию, возводящую куб числа, переданного ей в качестве аргумента. Вызовите эту функцию с разными аргументами.
Статические переменные
Внешние статические переменные, в отличие от обычных глобальных переменных, нельзя использовать из других файлов в случае программы, состоящей не из одного файла. Они глобальны только для функций того файла, в котором объявлены. Это своего рода сокрытие данных, по принципу «не выставлять наружу ничего лишнего, чтобы ‘что-нибудь’ нечаянно не могло ‘испортить’ данные».
Статические переменные, объявленные внутри функций имеют такую же область действия, как автоматические. Однако в отличие от автоматических, значения локальных статических переменных не теряются, а сохраняются между вызовами функции:
В этом примере в функции hello() производится подсчет ее вызовов.
Передача аргументов по ссылке
В первом примере этого урока мы передавали в функцию аргументы по значению. Это значит, что когда функция вызывается, ей передаются в качестве фактических параметров (аргументов) не указанные переменные, а копии значений этих переменных. Сами переменные к этим копиям уже никакого отношения не имеют. В вызываемой функции эти значения присваиваются переменным-параметрам, которые, как известно, локальны. Отсюда следует, что изменение переданных значений никакого влияния на переменные, переданные в функцию при вызове, не оказывают. В примере выше даже если бы в функции median() менялись значения переменных n1 и n2, то никакого влияния сей факт на переменные num1 и num2 не оказал.
Однако можно организовать изменение локальной переменной одной функции с помощью другой функции. Сделать это можно, передав в функцию адрес переменной или указатель на нее. На самом деле в этом случае также передается копия значения. Но какого значения?! Это адрес на область памяти. На один и тот же участок памяти может существовать множество ссылок, и с помощью каждой из них можно поменять находящееся там значение. Рассмотрим пример:
Кроме того, следует знать, что функция может возвращать адрес.
Важно понять механизм так называемой передачи аргументов по ссылке, т.к. это понимание пригодится при изучении массивов и строк. Использовать указатели при работе с простыми типами данных не стоит. Лучше возвращать из функции значение, чем менять локальные переменные одной функции с помощью кода другой функции. Функции должны быть достаточно автономными.
Передача параметров в функцию
Об аргументах функции
П рактика показывает, что тема передачи аргументов в функции, особенно указателей, вызывает массу проблем. Давайте ещё раз обобщим наши знания и соберём информацию вместе.
Пусть у нас имеется какая-то структура и функция, которая работает с этой структурой.
Если мы передаём в функцию экземпляр структуры, то передаётся копия этой структуры.
В этом коде мы передаём в функцию foo копию переменной p. Значение кладётся на стек при вызове функции, и внутри функции мы можем с ним работать. Но изменяем мы только локальную копию. В main никакие изменения не видны. Проиллюстрируем это
Пусть переменная p имеет адрес AB00. По этому адресу в памяти компьютера располагается экземпляр структуры со значением x = 10. Мы передаём это значение в функцию foo, и локальная копия имеет адрес CD00. Потом мы изменяем значение внутри foo, и оно становится 100, но по тому же адресу CD00, а исходное значение не изменилось.
Мы уже знаем, что для изменения значения нужно передавать указатель. Вот пример, который работает, как мы и ожидаем
Пусть переменная p имеет адрес AB00. Передача по указателю – это тоже передача значения, просто в данном случае мы передаём адрес переменной. Внутри функции переменная point хранится по адресу CD00. Хранит она адрес переменной p, равный AB00. Далее мы изменяем значение содержимого, на которое ссылается переменная point. Поэтому изменение видно внутри main.
Если мы обозначим аргумент как const, то это защити его от возможного изменения. При попытке присвоить новое значение будет ошибка компиляции.
Более сложный пример, когда мы хотим изменить не содержимое объекта, а непосредственно сам объект. Типичная ошибка делать вот так
В этом случае изменения объекта не произойдёт, плюс, мы получим утечку памяти. Давайте разберёмся.
Пусть переменная p хранится по адресу AB00, а память под структуру была выделена по адресу AB04. p хранит этот адрес. В foo передаётся AB04 (содержимое p). Внутри foo мы локальной переменной point, которая хранится по адресу CD00, присваиваем новое значение СВ04 – адрес, который вернула нам функция malloc. Как видно, внутри main ничего не поменялось. Кроме того, после выхода из функции foo будет удалена переменная point, которая хранит адрес структуры на куче, а сама память не будет очищена.
Как обычно, p имеет адрес CD00 и хранит адрес CD04, по которому хранится структура. Мы передаём адрес переменной p – это AB00. Внутри foo теперь мы меняем содержимое по адресу AB00, и оно становится равным CD04. Иначе говоря, мы имеем две переменные, которые хранят один и тот же адрес на куче – это p внутри main, и point внутри foo. После выхода из функции локальная переменная point будет удалена, а значение, выделенное на куче, по адресу CD04 не пострадает.
Но заметьте, теперь у нас осталась «висячая» структура на куче по адресу AB04, на неё никто не ссылается и никто её не удаляет!
Внимательно следите за такими вещами.
Краткая памятка по использованию разных типов аргументов, на примере структуры point_t
Урок №99. Передача по адресу
Обновл. 13 Сен 2021 |
Есть еще один способ передачи переменных в функцию в языке C++ — по адресу.
Передача по адресу
Передача аргументов по адресу — это передача адреса переменной-аргумента (а не исходной переменной). Поскольку аргумент является адресом, то параметром функции должен быть указатель. Затем функция сможет разыменовать этот указатель для доступа или изменения исходного значения. Вот пример функции, которая принимает параметр, передаваемый по адресу:
Результат выполнения программы:
Вот пример программы, которая вызывает эту функцию:
Помните, что фиксированные массивы распадаются в указатели при передаче в функцию, поэтому их длину нужно передавать в виде отдельного параметра. Перед разыменованием параметров, передаваемых по адресу, не лишним будет проверить — не являются ли они нулевыми указателями. Разыменование нулевого указателя приведет к сбою в программе. Вот функция printArray() с проверкой (обнаружением) нулевых указателей:
Передача по константному адресу
Поскольку printArray() все равно не изменяет значения получаемых аргументов, то хорошей идеей будет сделать параметр array константным:
Обратите внимание, хотя сам адрес передается по значению, вы все равно можете разыменовать его для изменения значения исходного аргумента. Запутано? Давайте проясним:
При передаче аргумента по адресу в переменную-параметр функции копируется адрес из аргумента. В этот момент параметр функции и аргумент указывают на одно и то же значение.
Если параметр функции затем разыменовать для изменения исходного значения, то это приведет к изменению значения, на которое указывает аргумент, поскольку параметр функции и аргумент указывают на одно и то же значение!
Если параметру функции присвоить другой адрес, то это никак не повлияет на аргумент, поскольку параметр функции является копией, а изменение копии не приводит к изменению оригинала. После изменения адреса параметра функции, параметр функции и аргумент будут указывать на разные значения, поэтому разыменование параметра и дальнейшее его изменение никак не повлияют на значение, на которое указывает аргумент.
В следующей программе это всё хорошо проиллюстрировано:
Функции
Введение
Ч ем дальше мы изучаем си, тем больше становятся программы. Мы собираем все действия в одну функцию main и по несколько раз копируем одни и те же действия, создаём десятки переменных с уникальными именами. Наши программы распухают и становятся всё менее и менее понятными, ветвления становятся всё длиннее и ветвистее.
Но из сложившейся ситуации есть выход! Теперь мы научимся создавать функции на си. Функции, во-первых, помогут выделить в отдельные подпрограммы дублирующийся код, во-вторых, помогут логически разбить программу на части, в-третьих, с функциями в си связано много особенностей, которые позволят использовать новые подходы к структурированию приложений.
Функция – это именованная часть программы, которая может быть многократно вызвана из другого участка программы (в котором эта функция видна). Функция может принимать фиксированное либо переменное число аргументов, а может не иметь аргументов. Функция может как возвращать значение, так и быть пустой (void) и ничего не возвращать.
Мы уже знакомы с многими функциями и знаем, как их вызывать – это функции библиотек stdio, stdlib, string, conio и пр. Более того, main – это тоже функция. Она отличается от остальных только тем, что является точкой входа при запуске приложения.
Функция в си определяется в глобальном контексте. Синтаксис функции:
Самый простой пример – функция, которая принимает число типа float и возвращает квадрат этого числа
Внутри функции sqr мы создали локальную переменную, которой присвоили значение аргумента. В качестве аргумента функции передали число 9,3. Служебное слово return возвращает значение переменной tmp. Можно переписать функцию следующим образом:
В данном случае сначала будет выполнено умножение, а после этого возврат значения. В том случае, если функция ничего не возвращает, типом возвращаемого значения будет void. Например, функция, которая печатает квадрат числа:
в данном случа return означает выход из функции. Если функция ничего не возвращает, то return можно не писать. Тогда функция доработает до конца и произойдёт возврат управления вызывающей функции.
Если функция не принимает аргументов, то скобки оставляют пустыми. Можно также написать слово void:
Формальные и фактические параметры
П ри объявлении функции указываются формальные параметры, которые потом используются внутри самой функции. При вызове функции мы используем фактические параметры. Фактическими параметрами могут быть переменные любого подходящего типа или константы.
Например, пусть есть функция, которая возвращает квадрат числа и функция, которая суммирует два числа.
Обращаю внимание, что приведение типов просиходит неявно и только тогда, когда это возможно. Если функция получает число в качестве аргумента, то нельзя ей передать переменную строку, например «20» и т.д. Вообще, лучше всегда использовать верный тип или явно приводить тип к нужному.
Если функция возвращает значение, то оно не обязательно должно быть сохранено. Например, мы пользуемся функцией getch, которая считывает символ и возвращает его.
Передача аргументов
При передаче аргументов происходит их копирование. Это значит, что любые изменения, которые функция производит над переменными, имеют место быть только внутри функции. Например
Программы выведет
200
100
200
Понятно почему. Внутри функции мы работаем с переменной x, которая является копией переменной d. Мы изменяем локальную копию, но сама переменная d при этом не меняется. После выхода из функции локальная переменная будет уничтожена. Переменная d при этом никак не изменится.
Каким образом тогда можно изменить переменную? Для этого нужно передать адрес этой переменной. Перепишем функцию, чтобы она принимала указатель типа int
Вот теперь программа выводит
200
100
100
Здесь также была создана локальная переменная, но так как передан был адрес, то мы изменили значение переменной d, используя её адрес в оперативной памяти.
В программировании первый способ передачи параметров называют передачей по значению, второй – передачей по указателю. Запомните простое правило: если вы хотите изменить переменную, необходимо передавать функции указатель на эту переменную. Следовательно, чтобы изменить указатель, необходимо передавать указатель на указатель и т.д. Например, напишем функцию, которая будет принимать размер массива типа int и создавать его. С первого взгляда, функция должна выглядеть как-то так:
Но эта функция выведет ERROR. Мы передали адрес переменной. Внутри функции init была создана локальная переменная a, которая хранит адрес массива. После выхода из функции эта локальная переменная была уничтожена. Кроме того, что мы не смогли добиться нужного результата, у нас обнаружилась утечка памяти: была выделена память на куче, но уже не существует переменной, которая бы хранила адрес этого участка.
Для изменения объекта необходимо передавать указатель на него, в данном случае – указатель на указатель.
Вот теперь всё работает как надо.
Ещё подобный пример. Напишем функцию, которая принимает в качестве аргумента строку и возвращает указатель на область памяти, в которую скопирована эта строка.
В этом примере утечки памяти не происходит. Мы выделили память с помощью функции malloc, скопировали туда строку, а после этого вернули указатель. Локальные переменные были удалены, но переменная test хранит адрес участка памяти на куче, поэтому можно его удалить с помощью функции free.
Объявление функции и определение функции. Создание собственной библиотеки
В си можно объявить функцию до её определения. Объявление функции, её прототип, состоит из возвращаемого значения, имени функции и типа аргументов. Имена аргументов можно не писать. Например
Это смешанная рекурсия – функция odd возвращает 1, если число нечётное и 0, если чётное.
Содержимое файла исходного кода File1.c
Рассмотрим особенности каждого файла. Наш файл, который содержит функцию main, подключает необходимые ему библиотеки а также заголовочный файл File1.h. Теперь компилятору известны прототипы функций, то есть он знает возвращаемый тип, количество и тип аргументов и имена функций.
Заголовочный файл, как и оговаривалось ранее, содержит прототип функций. Также здесь могут быть подключены используемые библиотеки. Макрозащита #define _FILE1_H_ и т.д. используется для предотвращения повторного копирования кода библиотеки при компиляции. Эти строчки можно заменить одной
Файл File1.c исходного кода подключает свой заголовочный файл. Всё как обычно логично и просто. В заголовочные файлах принято кроме прототипов функций выносить константы, макроподстановки и определять новые типы данных. Кроме того, именно в заголовочных файлах можно обширно комментировать код и писать примеры его использования.
Передача массива в качестве аргумента
К ак уже говорилось ранее, имя массива подменяется на указатель, поэтому передача одномерного массива эквивалентна передаче указателя. Пример: функция получает массив и его размер и выводит на печать:
В этом примере функция может иметь следующий вид
Также напомню, что правило подмены массива на указатель не рекурсивное. Это значит, что необходимо указывать размерность двумерного массива при передаче
Если двумерный массив создан динамически, то можно передавать указатель на указатель. Например функция, которая получает массив слов и возвращает массив целых, равных длине каждого слова:
Можно вместо того, чтобы возвращать указатель на массив, передавать массив, который необходимо заполнить
На этом первое знакомство с функциями заканчивается: тема очень большая и разбита на несколько статей.