
- •Лекция №5
- •8. Подпрограммы пользователя (функции)
- •8.1. Определение функций
- •8.2. Прототип функции
- •8.3. Вызов функции
- •Массив в качестве аргумента функции
- •8.4. Вызов функции с переменным числом параметров
- •8.5. Передача параметров функции main
- •8.6. Рекурсия
- •8.7. Перегрузка имен функций
- •8.8. Макроподстановка (inline-подстановка) функций
- •Операция расширения области видимости
8.6. Рекурсия
Любая функция в программе на языке С++ может быть вызвана рекурсивно, т.е. она может вызывать саму себя. Компилятор допускает любое число рекурсивных вызовов. При каждом вызове для формальных параметров и переменных с классом памяти auto и register выделяется новая область памяти, так что их значения из предыдущих вызовов не теряются, но в каждый момент времени доступны только значения текущего вызова.
Переменные, объявленные с классом памяти static, не требуют выделения новой области памяти при каждом рекурсивном вызове функции и их значения доступны в течение всего времени выполнения программы.
Классический пример рекурсии - это математическое определение факториала n! :
n! = 1 при n=0;
n*(n-1)! при n>1 .
Функция, вычисляющая факториал, будет иметь следующий вид:
long fakt(int n)
{
return ( (n==1) ? 1 : n*fakt(n-1) );
}
Хотя компилятор языка С++ не ограничивает число рекурсивных вызовов функций, это число ограничивается ресурсом памяти компьютера и при слишком большом числе рекурсивных вызовов может произойти переполнение стека.
8.7. Перегрузка имен функций
Как правило, давать разным функциям разные имена - мысль хорошая, но когда некоторые функции выполняют одинаковую работу над объектами разных типов, может быть более удобно дать им одно и то же имя. Использование одного имени для различных действий над различными типами называется перегрузкой (overloading). Допускается перегружать как операторы, так и функции. Так, например, у сложения существует только одно имя, +, но его можно применять для сложения значений целых, плавающих и указательных типов. Эта идея легко расширяется на обработку операций, определенных пользователем, то есть, функций. Это значит, что имя может использоваться более чем для одной функции.
Например:
void vvod(int); void vvod(char*);
Что касается компилятора, единственное общее, что имеют функции с одинаковым именем, это имя. Предположительно, они в каком-то смысле похожи, но в этом язык ни стесняет программиста, ни помогает ему. Таким образом, перегруженные имена функций - это главным образом удобство записи. Это удобство значительно в случае функций с общепринятыми именами вроде sqrt, print и open. Когда имя семантически значимо, как это имеет место для операций вроде +, * и << и в случае конструкторов, это удобство становится существенным. Когда вызывается перегруженная функция f(), компилятор должен понять, к какой из функций с именем f следует обратиться. Это делается путем сравнения типов фактических параметров с типами формальных параметров всех функций с именем f. Поиск функции, которую надо вызвать, осуществляется за три отдельных шага:
1. Искать функцию соответствующую точно, и использовать ее, если она найдена.
2. Искать соответствующую функцию, используя встроенные преобразования, и использовать любую найденную функцию.
3. Искать соответствующую функцию, используя преобразования, определенные пользователем, и если множество преобразований единственно, использовать найденную функцию.
Например:
print(double), print(int);
void f(); {
print(1); print(1.0);
}
Правило точного соответствия гарантирует, что f напечатает 1 как целое и 1.0 как число с плавающей точкой. Ноль, char или short точно соответствуют параметру. Аналогично, float точно соответствует double.
К параметрам функций с перегруженными именами стандартные С++ правила неявного преобразования типа применяются не полностью. Преобразования, которые могут уничтожить информацию, не выполняются. Остаются int в long, int в double, ноль в long, ноль в double и преобразования указателей: преобразование ноль в указатель void*, и указатель на производный класс в указатель на базовый класс.
Вот пример, в котором преобразование необходимо:
print(double), print(long);
void f(int a); {
print(a);
}
Здесь a может быть напечатано или как double, или как long. Неоднозначность разрешается явным преобразованием типа (или print(long(a)) или print(double(a))).
Перегруженные функции должны отличаться или типом возвращаемого значения, или типами аргументов.
Пример:
//недопустимая перегрузка
int sum(int a){return(a+a);}
int sum(int &a){return(a+a);}//аргументы имеют одинаковый тип
Нужно быть осторожным, т.к. в данном примере компилятор не сигнализирует о наличие ошибок, однако при запуске на выполнение происходит аварийное завершение работы программы.
В следующих примерах показана допустимая перегрузка функций, т.к. перегруженные функции отличаются типами аргументов в первом случае и типом возвращаемого значения – во втором.
Пример:
//допустимая перегрузка
int sum(int a){return(a+a);}
int sum(char a){return(int)a;}
Пример:
//допустимая перегрузка
int sum(int a);
float sum(int a);
Допускается перегружать стандартные функции.
Пример:
#include<stdio.h>
void printf(int a){printf("%d",a);};
void printf(char a){printf("%c",a);};
void main()
{
printf(1);
printf('f');
}
Пример:
#include<stdio.h>
#include<math.h>
void sqrt(int a){printf("%d\n",a);};
void sqrt(char a){printf("%c\n",a);};
void sqrt(float a){printf("%f\n",a);};
//недопустимо описать void sqrt(double a){printf("%f\n",a);}; , т.к.
//void sqrt(double );уже существует в библиотеке math.h(является стандартной)
void main()
{
sqrt(1);
sqrt(11.0f);//вызов функции пользователя
sqrt(11.0);//вызов стандартной функции вычисления корня квадратного
sqrt('f');
}