Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Функции с переменным числом параметров






Если список формальных параметров функции заканчивается многоточием, это означает, что при ее вызове на этом месте можно указать еще несколько параметров. Проверка соответствия типов для этих параметров не выполняется, char и short передаются как int, a float — как double. В качестве примера можно привести функцию printf, прототип которой имеет вид:

int printf(const char*,...);

Это означает, что вызов функции должен содержать по крайней мере один параметр типа char* и может либо содержать, либо не содержать другие параметры:

printf(" Введитe исходные данные"); // один параметр

printf(" Сумма: %5.2f рублей", sum); // два параметра

printf(" %d %d %d %d", a, b, с, d): // пять параметров

Для доступа к необязательным параметрам внутри функции используются макросы библиотеки va_start, va_arg и va_end, находящиеся в заголовочном файле < stdarg.h>. Эти макросы описаны в приложении 5.

Поскольку компилятор не имеет информации для контроля типов, вместо функций с переменным числом параметров предпочтительнее пользоваться параметрами по умолчанию или перегруженными функциями (см. «Перегрузка функций»), хотя можно представить случаи, когда переменное число параметров является лучшим решением.

 

 

Рекурсивные функции

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

Классическим примером рекурсивной функции является вычисление факториала (это не означает, что факториал следует вычислять именно так). Для того чтобы получить значение факториала числа n, требуется умножить на n факториал числа (n-1). Известно также, что 0! =1 и 1! =1.

long fact(long n){

if (n==0 || n==l) return 1;

return (n * fact(n -1);

}

To же самое можно записать короче:

long fact(long n){

return (n> l)? n * fact(n - 1): 1;

}

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

Технология создания программ, использующих функции, рассмотрена в седьмом семинаре практикума [11].

Перегрузка функций

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

Компилятор определяет, какую именно функцию требуется вызвать, по типу фактических параметров. Этот процесс называется разрешением перегрузки (перевод английского слова resolution в смысле «уточнение»). Тип возвращаемого функцией значения в разрешении не участвует. Механизм разрешения основан на достаточно сложном наборе правил, смысл которых сводится к тому, чтобы использовать функцию с наиболее подходящими аргументами и выдать сообщение, если такой не найдется. Допустим, имеется четыре варианта функции, определяющей наибольшее значение:

// Возвращает наибольшее из двух целых:

int max(int, int);

// Возвращает подстроку наибольшей длины:

char* max(char*, char*);

// Возвращает наибольшее из первого параметра и длины второго:

int max (int, char*);

// Возвращает наибольшее из второго параметра и длины первого:

int max (char*, int);

void f(int a, int b. char* c, char* d){

cout < < max (a, b) < < max(c. d) < < max(a, c) < < max(c. b);

}

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

Если точного соответствия не найдено, выполняются продвижения порядковых типов в соответствии с общими правилами (см. приложение 3), например, bool и char в int, float в double и т. п. Далее выполняются стандартные преобразования типов, например, int в double или указателей в void*. Следующим шагом является выполнение преобразований типа, заданных пользователем, а также поиск соответствий за счет переменного числа аргументов функций. Если соответствие на одном и том же этапе может быть получено более чем одним способом, вызов считается неоднозначным и выдается сообщение об ошибке.

Неоднозначность может появиться при:

§ преобразовании типа;

§ использовании параметров-ссылок;

§ использовании аргументов по умолчанию.

Пример неоднозначности при преобразовании типа:

#include < iostream.h>

float f(float i){

cout < < " function float f(float i)" < < endl;

return i;

}

double f(double i){

cout < < " function double f(double i)" < < endl;

return i*2;

}

int main(){

float x = 10.09:

double у = 10.09;

cout < < f(x) < < endl; // Вызывается f(float)

cout < < f(y) < < endl; // Вызывается f(double)

/* cout < < f(10) < < endl; Неоднозначность - как преобразовать 10: во float или double? */

return 0;

}

Для устранения этой неоднозначности требуется явное приведение типа для константы 10.

Пример неоднозначности при использовании параметров-ссылок: если одна из перегружаемых функций объявлена как int f(int a, int b), а другая — как int f (int a, int & b), то компилятор не сможет узнать, какая из этих функций вызывается, так как нет синтаксических различий между вызовом функции, которая получает параметр по значению, и вызовом функции, которая получает параметр по ссылке. Пример неоднозначности при использовании аргументов по умолчанию:

#include < iostream.h>

int f(int a){return a; }

int f(int a, int b = 1){return a * b; }

int main(){

cout < < f(10, 2); // Вызывается f(int, int)

/* cout < < f(10): Неоднозначность - что вызывается: f(int, int) или f(int)? */

return 0;

}

Ниже приведены правила описания перегруженных функций.

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

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

§ Функции не могут быть перегружены, если описание их параметров отличается только модификатором const или использованием ссылки (например, int и const int или int и int&).

Шаблоны функций

Многие алгоритмы не зависят от типов данных, с которыми они работают (классический пример — сортировка). Естественно желание параметризовать алгоритм таким образом, чтобы его можно было использовать для различных типов данных. Первое, что может прийти в голову — передать информацию о типе в качестве параметра (например, одним параметром в функцию передается указатель на данные, а другим — длина элемента данных в байтах). Использование допол­нительного параметра означает генерацию дополнительного кода, что снижает эффективность программы, особенно при рекурсивных вызовах и вызовах во внутренних циклах; кроме того, отсутствует возможность контроля типов. Другим решением будет написание для работы с различными типами данных нескольких перегруженных функций, но в таком случае в программе будет несколько одинаковых по логике функций, и для каждого нового типа придется вводить новую.

В C++ есть мощное средство параметризации — шаблоны. Существуют шаблоны функций и шаблоны классов. С помощью шаблона функции можно определить алгоритм, который будет применяться к данным различных типов, а конкретный тин данных передается функции в виде параметра на этапе компиляции. Компилятор автоматически генерирует правильный код, соответствующий переданному типу. Таким образом, создается функция, которая автоматически перегружает сама себя и при этом не содержит накладных расходов, связанных с параметризацией.

Формат простейшей функции-шаблона:

template < class Type> заголовок{

/* тело функции */

}

Вместо слова Туре может использоваться произвольное имя.

В общем случае шаблон функции может содержать несколько параметров, каждый из которых может быть не только типом, но и просто переменной, например:

template < class A, class В, int i> void f(){... }

Например, функция, сортирующая методом выбора массив из n элементов любого типа, в виде шаблона может выглядеть так:

template < class Type>

void sort_vybor(Type *b, int n){

Type а; //буферная переменная для обмена элементов

for (int i = 0; i< n-l; i++){

int imin = i;

for (int j = i + 1; j< n; j++)

if (b[j] < b[imin]) imin = j;

а = b[i]; b[i] = b[imin]; b[imin] = a;

}

}

Главная функция программы, вызывающей эту функцию-шаблон, может иметь вид:

#include < iostream.h>

template < class Type> void sort_vybor(Type *b, int n);

int main(){

const int n = 20;

int I, b[n];

for (i = 0; i< n; i++) cin > > b[i];

sort_vybor(b. n); // Сортировка целочисленного массива

for (i = 0; i< n; i++) cout < < b[i] < < ' ';

cout < < end!;

double a[] = {0.22, 117, -0.08, 0.21, 42.5};

sort_vybor(a, 5); // Сортировка массива вещественных чисел

for (i = 0; i< 5; i++) cout < < a[i] < < ' ';

return 0;

}

Первый же вызов функции, который использует конкретный тип данных, приводит к созданию компилятором кода для соответствующей версии функции. Этот процесс называется инстанцированием шаблона (instantiation). Конкретный тип для инстанцирования либо определяется комрилятором автоматически, исходя из типов параметров при вызове функции, либо задается явным образом. При повторном вызове с тем же типом данных код заново не генерируется. На месте параметра шаблона, являющегося не типом, а переменной, должно указываться константное выражение.

Пример явного задания аргументов шаблона при вызове:

tempiate< class X, class Y, class Z> void f(Y, Z);

void g(){

f< int, char*. double> (" Vasia", 3.0);

f< int, char*> (" Vasia", 3.0); // Z определяется как double

f< int> (" Vasia", 3.0); // Y определяется как char*, a Z - как double

// f(" Vasia", 3.0); ошибка: Х определить невозможно

}

Чтобы применить функцию-шаблон к типу данных, определенному пользователем (структуре или классу), требуется перегрузить операции для этого типа данных, используемые в функции.

Как и обычные функции, шаблоны функций могут быть перегружены как с помощью шаблонов, так и обычными функциями.

Можно предусмотреть специальную обработку отдельных параметров и типов с помощью специализации шаблона функции. Допустим, мы хотим более эффективно реализовать общий алгоритм сортировки для целых чисел. В этом случае можно «вручную» задать вариант шаблона функции для работы с целыми числами:

void sort_vibor< int> (int *t>, int n){

... // Тело специализированного варианта функции

}

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

Перегрузке и шаблонам функций посвящен семинар 8 практикума [11].

Функция main()

Функция, которой передается управление после запуска программы, должна иметь имя main. Она может возвращать значение в вызвавшую систему и принимать параметры из внешнего окружения. Возвращаемое значение должно быть целого типа. Стандарт предусматривает два формата функции:

// без параметров;

тип main(){ /*... */}

// с двумя параметрами:

тип main(int argc, char* argv[]){ /*... */}

При запуске программы параметры разделяются пробелами. Имена параметров в программе могут быть любыми, но принято использовать argc и argv. Первый параметр (argc) определяет количество параметров, передаваемых функции, включая имя самой программы, второй параметр (argv) является указателем на массив указателей типа char*. Каждый элемент массива содержит указатель на отдельный параметр командной строки, хранящийся в виде С-строки, оканчивающейся нуль-символом. Первый элемент массива (argv[0]) ссылается на полное имя запускаемого на выполнение файла, следующий (argv[1]) указывает на первый параметр, argv[2] — на второй параметр, и так далее. Параметр argv[argc] должен быть равен 0.

Если функция main() ничего не возвращает, вызвавшая система получит значение, означающее успешное завершение. Ненулевое значение означает аварийное завершение. Оператор возврата из main() можно опускать.

#include < iostream.h>

void main(int argc, char* argv[]){

for (int i = 0: i< argc: i++) cout < < argv[i] < < '\n';

}

Пусть исполняемый файл программы имеет имя main.exe и вызывается из командной строки:

d: \cpp\main.exe one two three

На экран будет выведено:

D: \CPP\MAIN.EXE

one

two

three

Функции стандартной библиотеки

Любая программа на C++ содержит обращения к стандартной библиотеке, в которой находятся определения типов, констант, макросов, функций и классов. Чтобы использовать их в программе, требуется с помощью директивы #include включить в исходный текст программы заголовочные файлы, в которых находятся соответствующие объявления. Сами библиотечные функции хранятся в скомпилированном виде и подключаются к программе на этапе компоновки. В программах на C++ могут использоваться функции, унаследованные от библиотеки С. Использование классов стандартной библиотеки рассматривается в третьей части книги.

Функции библиотеки можно разбить на группы по их назначению: ввод/вывод, обработка строк, математические функции, работа с динамической памятью, поиск и сортировка и т. д. Список функций библиотеки приведен в приложении 6. Ниже приведен краткий обзор функций ввода/вывода в стиле С, функций работы со строками и символами и математических функций.






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.