В предыдущем листке была задача вычисления числа сочетаний из n элементов по k, для чего необходимо вычисление факториалов трех величин: n, k и n - k. Для этого можно сделать три цикла, что приводит к увеличению размера программы за счет трехкратного повторения похожего кода. Вместо этого лучше сделать одну функцию, вычисляющую факториал любого данного числа n и трижды использовать эту функцию в своей программе. Соответствующая функция может выглядеть так:
int factorial(int n) { int f = 1, i; for (i = 2; i <= n; ++i) { f = f * i; } return f; }
Этот текст должен идти до основной программы, то есть до функции main()
.
Первая строчка этого примера является описанием нашей функции. factorial
– идентификатор,
то есть имя нашей функции. После идентификатора в круглых скобках идет список параметров,
которые получает наша функция. Список состоит из перечисленных через запятую типов параметров и их
идентификаторов. В нашем случае список состоит из одной величины n
, имеющей тип int
:
наша функция вычисляет факториал целого числа. Функция вычисляет целочисленную величину,
поэтому функция будет возвращать значение типа int
, что указывается до идентификатора функции.
Функция может не возвращать никакого значения, тогда в качестве типа возвращаемого значения должно быть
указано слово void
.
Далее идет тело функции в фигурных скобках. Внутри функции вычисляется значение факториала числа n
и оно сохраняется в переменной f
. Функция завершается инструкцией return f
, которая
завершает работу функции и возвращает значение переменной f
. Инструкция return
может встречаться
в произвольном месте функции, ее исполнение завершает работу функции и возвращает указанное значение в место вызова.
Если функция не возвращает значения, то инструкция return
используется без возвращаемого значения, также
в функциях, не возвращающих значения, инструкция return
может отсутствовать.
Функция должна быть описана до начала основной программы. Сама же основная программа, как можно догадаться,
также является функцией с именем main
, не получающая никаких параметров и возвращающее значение типа int
.
Теперь мы можем использовать нашу функцию factorial
в основной программе нахождения числа сочетаний:
int main() { int n, k; cin >> n >> k; cout << factorial(n) / (factorial(k) * factorial(n - k)) << endl; return 0; }
В этом примере мы трижды вызываем функцию factorial
для вычисления трех факториалов:
factorial(n)
, factorial(k)
, factorial(n - k)
.
Мы также можем объявить функцию binomial
, которая принимает два целочисленных параметра
n
и k
и вычисляет число сочетаний из n
по k
:
int binomial(int n, int k) { return factorial(n) / (factorial(k) * factorial(n - k)); }
Тогда в нашей основной программе мы можем вызвать функцию binomial
для нахождения числа сочетаний:
cout << binomial(n,k) << endl;
Поскольку в этом случае функция main
вызывает функцию binomial
, а функция binomial
вызывает функцию factorial
, а каждая функция должна быть описана до ее вызова из другой функции,
то порядок описания функций в программе должен быть такой:
int factorial(int n)
int binomial(int n, int k)
int main()
Вернемся к задаче нахождения наибольшего из двух или трех чисел. Напишем функцию, находящую максимум из двух данных чисел:
int max(int a, int b) { if (a > b) { return a; } else { return b; } }
Теперь мы можем реализовать функцию max
, находящую максимум трех чисел:
int max(int a, int b, int c) { return max(max(a, b), c); }
В данном примере написаны две различные функции max
: первая с двумя параметрами,
вторая с тремя параметрами. Несмотря на то, что функции имеют одинаковые имена, по количеству
передаваемых параметров ясно, какая из них имеется в виду. В нашем случае функция
max(int a, int b, int c)
дважды вызывает функцию max
для двух чисел:
сначала, чтобы найти максимум из a
и b
, потом чтобы найти максимум из этой
величины и c
.
В каждой функции объявляется свой набор переменных. Переменные, определенные внутри каждой из функций, называются локальными переменными данной функции. Если в двух разных функциях есть локальные переменные с одинаковыми именами, то это — различные переменные. Если в одной из функций поменять значение этой переменной, то в другой функции оно останется неизменным. Пример:
void f1() { int a; a = 1; return; } int main() { int a; a = 2; f1(); cout << a << endl; return 0; }
На экран будет выведено значение 2, т.к. изменение локальной переменной a
внутри функции f1
не приводит
к изменению значения локальной переменной a
для функции main
.
Переменная может быть объявлена и вне всякой функции, тогда она будет доступной из всех функций и в каждой функции это будет одна и та же переменная. Такие переменные называются глобальными.
int a; void f1() { a = 1; return; } int main() { a = 2; f1(); cout << a << endl return 0; }
В этом примере на экран будет выведено значение 1, поскольку вызов функции f1
изменил значение глобальной переменной a
.
Одно из свойств глобальных переменных — их значения по умолчанию проинициализированы нулем, в отличии от локальных переменных, чьи значения по умолчанию инициализируются мусором.
Широкое использование глобальных переменных не принято в современном программировании, вместо глобальных переменных лучше использовать локальные переменные, а для обмена информацией между функциями нужно использовать передаваемые по ссылке параметры.
Раньше у нас была задача, в которой требовалось поменять значения двух переменных. Поскольку такое нужно будет довольно часто, попробуем написать для этого функцию.
void Swap(int a, int b) { int t = a; a = b; b = t; return; } int main() { int a = 1, b = 2; Swap(a, b); cout << a << " " << b << endl; return 0; }
Эта программа выведет 1 2
, то есть значения переменных a
и b
не поменяются. Причина в том, что передаваемые
в функцию параметры являются локальными переменными, когда вызывается функция, то создаются две локальные переменные
a
и b
для этой функции, в них записываются значения локальных переменных
a
и b
функции main
, затем запускается функция Swap
,
которая меняет значения своих локальных переменных, при этом значения локальных переменных функции main
не меняются. Такая передача параметров функции называется передачей параметров по значению.
Можно было бы объявить a
и b
глобальными переменными, но тогда мы не смогли бы использовать
функцию для обмена значений двух переменных, отличных от a
и b
.
Для того, чтобы функция могла изменять значение переменной, переданной в качестве параметра при вызове, используется механизм передачи параметров по ссылке. Если переменная передается в функцию по ссылке, то функция работает с самой переменной, а не с ее локальной копией и может изменять ее значения. Для обозначения того, что параметр передается по ссылке, перед идентификатором переменной-параметра, в описании функции, необходимо поставить знак амперсанда &:
void Swap(int & a, int & b) { int t = a; a = b; b = t; return; }Теперь функцию
Swap
можно использовать для обмена значений любых двух переменных типа int
,
при этом называться они могут как угодно, например, можно вызывать функцию так: Swap(x, y)
.
Но нельзя вызывать функцию так: Swap(1, 2)
—передаваемые по ссылке параметры должны
быть переменными, а не числами или какими-то более сложными выражениями.
Механизм передачи параметров по ссылке используется также в случае, когда функция должна вернуть не одно значение, а несколько. Тогда в функцию необходимо по ссылке передать несколько переменных, в которые функция запишет результат своей работы.
Эпиграф: void ShortStory() { cout << "У попа была собака, он ее любил." << endl; cout << "Она съела кусок мяса, он ее убил," << endl; cout << "В землю закопал и надпись написал:" << endl; ShortStory(); }
Как мы видели выше, функция может вызывать другую функцию. Но функция также может вызывать и саму себя! Рассмотрим это на примере функции вычисления факториала. Хорошо известно, что \(0!=1\), \(1!=1\). А как вычислить величину \(n!\) для большого \(n\)? Если бы мы могли вычислить величину \((n-1)!\), то тогда мы легко вычислим \(n!\), поскольку \(n!=n(n-1)\)!. Но как вычислить \((n-1)!\)? Если бы мы вычислили \((n-2)!\), то мы сможем вычисли и \((n-1)!=(n-1)(n-2)!\). А как вычислить \((n-2)!\)? Если бы... В конце концов, мы дойдем до величины \(0!\), которая равна \(1\). Таким образом, для вычисления факториала мы можем использовать значение факториала для меньшего числа. Это можно сделать и в программе на C++:
int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } }
Подобный прием (вызов функцией самой себя) называется рекурсией, а сама функция называется рекурсивной.
Рекурсивные функции являются мощным механизмом в программировании. К сожалению, они не всегда эффективны (об этом речь пойдет позже). Также часто использование рекурсии приводит к ошибкам, наиболее распространенная из таких ошибок – бесконечная рекурсия, когда цепочка вызовов функций никогда не завершается и продолжается, пока не кончится свободная память в компьютере. Пример бесконечной рекурсии приведен в эпиграфе к этому разделу. Две наиболее распространенные причины для бесконечной рекурсии:
if (n == 0)
, то factorial(0)
вызовет factorial(-1)
,
тот вызовет factorial(-2)
и т.д.
factorial(n)
будет
вызывать factorial(n)
, то также получиться бесконечная цепочка.
Поэтому при разработке рекурсивной функции необходимо прежде всего оформлять условия завершения рекурсии и думать, почему рекурсия когда-либо завершит работу.