Функции
Функция – это самостоятельная единица программы, которая спроектирована для реализации конкретной подзадачи. Функция может быть многократно вызвана из другого участка программы, это позволяет исключить повтор одних и тех же действий. Функции также помогают логически выстроить программу.
Функция определяется таким образом:
1. Тип возвращаемого значения;
2. Имя функции;
3. Информация о формальных аргументах;
4. Тело функции.
Формальный аргумент — это переменная в вызываемой функции.
Общая форма записи функции:
(тип1 agr1, тип2 agr2, . ) <
Тело функции;
>
Оператором возврата из функции в точку ее вызова является оператор return.
Давайте посмотри пример функции:
Функция SUM принимает два значения типа int и возвращает их сумму. a, b — формальные аргументы, а sum — локальная переменная.
Чтобы вызвать функцию, нужно указать ее имя и перечислить в скобках передаваемые фактические аргументы.
Фактический аргумент — это величина, которая присваивается формальному аргументу при вызове функции.
Пример вызова функции SUM:
В переменную s будет записано значение, которое вернет функция SUM.
Между формальными и фактическими параметрами при вызове функции должны соблюдаться правила соответствия по последовательности и по типам.
При передаче аргументов происходит их копирование. Это значит, что любые изменения, которые функция производит над переменными, имеют место быть только внутри функции. Чтобы изменить фактические значения, нужно передать указатель на этот элемент.
Например, функция sqr возвращает квадрат числа и изменяет фактическую переменную x на 5:
Функции могут и не возвращать значения, а просто выполнять некоторые вычисления. В этом случае указывается пустой тип возвращаемого значения void, а оператор return может либо отсутствовать, либо не возвращать никакого значения (return;):
Передача массива в функцию
Так как имя массива — это указатель, поэтому передача массива в функцию равна передачи указателя.
Пример передачи массива в функцию:
Так как мы передали указатель, то все изменения в функции будут распространяться и вне её. Аналогично и с двумерным массивом
Чтобы передать функции двумерный массив, необходимо четко указать размеры массива:
Если двумерным массив был выделен динамически, то в функцию нужно передать указатель на указатель:
Передача структуры в функцию
Передать структуру в функцию можно как по значению (тогда произойдет копирование структуры в функции), так и по адресу. Передавать адрес в функцию бывает гораздо выгодней по памяти. Дело в том, что размер указателя может быть намного меньше размера структурной переменной.
Если мы хотим передать структуру по значению, то изменяем строку 17 на F(P), а функцию так:
Передача массива структур полностью совпадает с передачей обычного массива:
При динамическом выделение памяти под структуру изменения не происходят.
Рекурсивные функции
Функция, которая вызывает сама себя, называется рекурсивной функцией.
Для рекурсивной функции обязательно нужно указать условие выхода из рекурсии, иначе функция будет бесконечно себя вызывать. Всё решение сводится к нахождению условия выхода.
При каждом вызове для формальных аргументов и локальных переменных выделяется новая область памяти, так что их значения из предыдущих вызовов не теряются, но в каждый момент времени доступны только значения текущего вызова (кроме указателей). Число рекурсивных вызовов ограничивается только ресурсом памяти компьютера и при слишком большом числе рекурсивных вызовов может произойти переполнение стека.
Когда выполняется условие выхода, то функция перестает вызывать себя и возвращает какое-то значение, в том числе и пустое. После возврата мы попадаем на предыдущий уровень рекурсии вместо последнего вызова функции.
Задача: дано натуральное число n > 1. Выведите все простые множители этого числа в порядке неубывания с учетом кратности.
Код на Си:
Чтобы подробнее разобраться с действием рекурсии, можно посмотреть как используется рекурсия в DFS — обход графа в глубину.
Как передать массив в функцию?
Если размер массива неизвестен, то можно использовать шаблон:
Если то что передается в функцию — это не массив, а указатель (т.е. не T[N] a T* ), то функция должна принимать указатель, а размер надо передавать дополнительным параметром или как-то еще.
Однако это очень ненадежное решение, нет никакой гарантии что будет передан правильный размер массива, или что ptr не равен nullptr .
По этому правильно — это использовать span<T> из C++ Core Guidelines
Как стать программистом
Это моя личная ссылка-приглашение на Stepik для вас. Регистрируясь по этой ссылке, записываясь на курсы и решая задачи, Вы помогаете автору данного сайта принять участие в конкурсе платформы Stepik! Подробности конкурса здесь: https://vk.cc/75rKuS
воскресенье, 6 октября 2013 г.
Занятие 18. Передача одномерных массивов в функцию. Возвращение массива из функции.
Добрый вечер друзья. Продолжаем изучать работу с массивами и указателями.
Сегодня научимся передавать массив в функцию и возвращать массив из функции.
Прочитайте улучшенную версию этого урока «Передача аргументов в функцию».
- Ещё более доступное объяснение
- Дополнительные материалы
- 12 задач на программирование с автоматической проверкой решения
Итак, начнем с первого пункта. Пусть нам необходимо передать массив в функцию и там его обработать. Ну допустим вывести его элементы на экран. Тут возможны два варианта.
1. У нас статический массив.
2. У нас динамический массив.
В зависимости от этого и нужно плясать.
Первый случай. Передача в функцию статического массива.
Для определенности будем передавать массив символов. Напишем функцию, которая принимает строку и выводит её на экран.
Давайте внимательно рассмотрим аргументы, которая эта функция принимает. Первый из них как раз и есть массив, который мы хотим передать. Как видите отличие от передачи обычной переменной лишь в том, что мы после имени пишем квадратные скобки. Именно они и указывают на то, что это не просто некая переменная а целый массив переменных данного типа.
В скобках, не нужно указывать размерность массива. С размерностью массива вообще все не так просто. Функции в Си не умеют самостоятельно определять размерность переданного им массива. Поэтому отдельным параметром нам необходимо передавать его размер. В нашей функции мы передаем размер массива с помощью переменной n.
Напишем самую простую программу, которая будет использовать эту функцию.
Обратите внимание, что передача массива в функцию в этом случае, по виду, ничем не отличается от передачи обычной переменной. Мы лишь указываем его имя и все. Это только на первый взгляд так кажется. Но об этом чуть позже.
Второй случай. Передача в функцию динамического массива.
Давайте решим такую задачу. имеется текстовый файл input.txt. Пусть в нем в первой строчке записано натуральное число N не превосходящее 100. А в следующих N строках записаны некоторые вещественные числа. Пусть мы хотим посчитать среднее арифметическое всех этих чисел и вывести.
Удобно для хранения чисел завести динамический массив. Хотя, на самом деле, в этой задаче мы могли бы просто читать числа из файла и сразу вычислять их среднее значение. Но как-то я не могу придумать пока более подходящего примера.
И так напишем требуемую программу.
Обратите внимание на аргументы функции. Как видите, если мы передаем динамический массив, то нам нужно явно указывать на то, что функция принимает указатель на первый элемент массива. В случае одномерного массива это не критично, и вы можете объявить аргументы как и в прошлый раз, т.е.
void sa_arr(float a[], int n)
но так делать не стоит. Приучайтесь сразу передавать динамические массивы таким образом.
Теперь вернемся к нашим отличиям а так же второму вопросу.
Если вы помните, то переменные в языке Си, при передаче их в функцию передаются по значению. То есть мы передаем не сами переменные, а только их значения. И если мы в функции будем их изменять, то настоящих переменных это никак не коснется. В нашей группе в вк, даже было небольшое задание на эту тему. Да и в уроке с указателями мы разбирали, как обойти это ограничение.
Основное отличие передачи массива в функцию в том, что массивы, в отличие от переменных всегда передаются по ссылке. Т.е. в функцию передается не копия массива, а сам массив. И если внутри функции мы будем как-то изменять массив, то эти изменения останутся и после того, как функция закончит свою работу.
Несколько изменим наше предыдущую программу. Изменим один из элементов внутри функции. И посмотрим, что будет с исходным массивом.
Массивы и функции
Массивы, также как остальные переменные, можно передавать в функции в качестве аргументов. Рассмотрим такую программу:
В теле функции main() объявляется массив, состоящий из 10 элементов. Далее вызывается функция arr_make() , которой передаются в качестве аргументов имя массива и два целых числа.
Если посмотреть на функцию arr_make() , то можно заметить, что ее первый параметр выглядит немного странно. Функция принимает массив неизвестно какого размера. Если предположить, что массивы передаются по значению, т.е. передаются их копии, то как при компиляции будет вычислен необходимый объем памяти для функции arr_make() , если неизвестно какого размера будет один из ее параметров?
На прошлом уроке мы выяснили, что имя массива — это константный указатель на первый элемент массива; т.е. имя массива содержит адрес. Выходит, что мы передаем в функцию копию адреса, а не копию значения. Как мы уже знаем, передача адреса приводит к возможности изменения локальных переменных в вызывающей функции из вызываемой. Ведь на одну и ту же ячейку памяти могут ссылаться множество переменных, и изменение значения в этой ячейке с помощью одной переменной неминуемо отражается на значениях других переменных.
Описание вида arr[] в параметрах функций говорит о том, что в качестве значения мы получаем указатель на массив, а не обычную (скалярную) переменную типа int, char, float и т.п.
Продолжим рассуждения. Если в функцию передается только адрес массива, то в теле функции никакого массива не существует, и когда там выполняется выражение типа arr[i] , то на самом деле arr — это не имя массива, а переменная-указатель, к которой прибавляется смещение. Поэтому цикл в функции arr_make() можно переписать на такой:
В теле цикла результат выражения справа от знака присваивания записывается по адресу, на который указывает arr. За это отвечает выражение *arr . Затем указатель arr начинает указывать на следующую ячейку памяти, т.к. к нему прибавляется единица ( arr++ ). Еще раз: сначала выполняется выражение записи значения по адресу, который содержится в arr; после чего изменяется адрес, содержащийся в указателе (сдвигается на одну ячейку памяти определенного размера).
Поскольку мы можем изменять arr, это доказывает, что arr — обычный указатель, а не имя массива. Тогда зачем в заголовке функции такой гламур, как arr[] ? Действительно, чаще используют просто переменную-указатель:
Хотя в таком случае становится не очевидно, что принимает функция — указатель на обычную переменную или все-таки на массив. В любом случае она будет работать.
Часто при передаче в функцию массивов туда же передают и количество его элементов в виде отдельного параметра. В примере выше N является глобальной константой, поэтому ее значение доступно как из функции main() , так и arr_make() . Иначе, более грамотно было бы написать функцию arr_make() так:
В данном случае параметр n — это количество обрабатываемых элементов массива.
Следует еще раз обратить внимание на то, что при передачи имени массива в функцию, последняя может его изменять. Однако такой эффект не всегда является желательным. Конечно, можно просто не менять значения элементов массива внутри функции, как в данном примере, где вычисляется сумма элементов массива; при этом сами элементы никак не изменяются:
Но если вы хотите написать более надежную программу, в которой большинство функций не должны менять значения элементов массивов, то лучше в заголовках этих функций объявлять параметр-указатель как константу, например:
В этом случае, любая попытка изменить значение по адресу, содержащемуся в таком константном указателе, будет приводить к ошибке и программист будет знать, что функция пытается изменить массив.
Усовершенствуем программу, которая была приведена в начале этого урока:
Теперь у пользователя запрашивается минимум и максимум, затем создается массив из элементов, значения которых лежат в указанном диапазоне. Массив выводится на экран с помощью функции arr_print() . Далее у пользователя запрашивается знак + или -. Вызывается функция arr_inc_dec() , которая в зависимости от введенного знака увеличивает или уменьшает на единицу значения элементов массива.
В функциях arr_make() и arr_print() используется нотация указателей. Причем в теле функций значения указателей меняются: они указывают сначала на первый элемент массива, затем на второй и т.д. В функции arr_inc_dec() используется вид обращения к элементам массива. При этом значение указателя не меняется: к arr прибавляется смещение, которое увеличивается на каждой итерации цикла. Ведь на самом деле запись arr[i] означает *(arr+i) .
При использовании нотации обращения к элементам массива программы получаются более ясные, а при использовании записи с помощью указателей они компилируются чуть быстрее. Это связано с тем, что когда компилятор встречает выражение типа arr[i] , то он тратит время на преобразование его к виду *(arr+i) . Однако лучше потратить лишнюю секунду при компиляции, но получить более читаемый код.
