Урок 9.8. Функции с переменным числом параметров
9.8. Функции с переменным числом параметров
В С++ можно создавать функции с переменным числом параметров. Параметры, их количество и типы, становятся известны только во время вызова функции. Формат описания функций с переменным числом параметров:
<тип> <имя> (<список явных параметров>, …) { <тело функции> } |
Здесь:
<тип> – тип возвращаемого значения,
<список параметров> – список параметров известных до вызова функции.
После списка явных параметров следует необязательная запятая и троеточие, которое сообщает компилятору, что контроль типов и количества параметров при вызове функции не следует.
При создании функций с переменным числом параметров необходимо предусмотреть способ определения количества параметров и их типов. Используется для этих целей два способа:
- один из параметров определяет число параметров функции;
- в списке явных параметров задается параметр, указывающий на конец списка параметров.
Если не один из этих способов не используется, тогда можно использовать специальный набор макроопределений.
Параметры функции помещаются в стек, при этом первый параметр оказывается в вершине стека. Переход от одного параметра к другому осуществляется с помощью указателей.
Пример 1. Задание числа дополнительных параметров с помощью первого параметра. Функция вычисляет сумму значений дополнительных параметров. Список явных параметров состоит из одного параметра, который задает число дополнительных параметров.
#include <stdio.h> #include <conio.h> #include <iostream> void main() { int sum(int n,...); //прототип функции с переменным //числом параметров cout << "\n 4+6=" << sum(2,4, 6); cout << "\n 1+2+3+4+5+6=" << sum(6,1,2,3,4,5,6); cout << "\n Parametrov net.Summa ravna:" << sum(0); getch(); } int sum(int n,...)//n-число суммируемых параметров { int s=0; int *p = &n; //получение адреса параметров в стеке for(int i=1; i<=n; i++) s+=*++p; //суммируем числа return s; } |
Для доступа к параметрам, которые один за другим попали в стек, используется адрес первого параметра (*p = &n;), таким образом, указатель устанавливается на начало списка параметров в стеке (в памяти). Затем в цикле при помощи указателя p перемещаемся по параметрам и суммируем их, извлекая (*p) из памяти. Все параметры должны иметь одинаковый тип.
Проверка соответствия типов для дополнительных параметров не выполняется, поскольку компилятор не имеет информации, необходимой для проведения проверки. При вызове функции дополнительные параметры типа char и short передаются как int, а float — как double.
Пример 2. Определение конца списка параметров с помощью параметра индикатора. Функция product() вычисляет произведение дополнительных параметров. Как только встречается нулевой параметр, вычисление произведения прекращается. Указатель p указывает на начало списка параметров (double*p=&a;).
#include <iostream> double product(double a, ...) { double b; if (a)//проверяем на отсутствие дополнительных параметров { b = 1.0; //b - произведение //цикл до тех пор, пока p не укажет на 0 for (double* p=&a; *p; p++) b *= *p; } else b = 0.0; return b; } void main() { double product(double a, ...);//прототип функции с // переменным числом параметров cout << "\n product (6e0, 5e0, 3e0, 0e0) = " << product (6e0, 5e0, 3e0, 0e0); cout << "\n product (1.5f, 2.0f, 0.0f, 3.0f, 0.0f) = " << product (1.5f, 2.0f, 0.0f, 3.0f, 0.0f); cout << "\n product (0.0) = " << product (0.0); cout << "\n product (1.0e-28,0.0) = " << product (1.0e-28,0.0); getch(); } |
Напомним, что при вызове функции дополнительные параметры типа float передаются как double, так что для параметров типа float надо использовать функцию с параметрами типа double.
Для использования в программе функций с переменным числом параметров, при вызове которых можно использовать параметры разных типов, удобно использовать специальный набор макроопределений (заголовочный файл stdarg.h). Макрокоманды для доступа к списку фактических параметров переменной длины, имеют следующий формат:
//связывание переменной param с первым параметром
void va_start(va_list param,<последний явный параметр>);
//получение значения очередного параметра типа type
type va_arg(va_list param, type);
//организации корректного выхода из функции
void va_end(va_list param);
Кроме перечисленных макросов, в файле stdarg.h определен специальный тип данных va_list, который является указателем. Именно такого типа должны быть первые операнды, используемые при обращении к макрокомандам va_start, va_arg, va_end.
Определим порядок использования макросов. В теле функции обязательно определяется переменная типа va_list: va_list f. С помощью макроса va_start переменная f связывается с первым необязательным параметром, т.е. с началом списка неизвестной длины:
va_start(f,<последний явный параметр>)
Теперь с помощью указателя f можно получить значение фактического параметра, задав его тип. Макрос
va_arg(f, type);
позволяет получить значение очередного параметра типа type. Кроме того, макрос меняет значение указателя f на адрес следующего фактического параметра в списке.
Макрокоманда va_end предназначена для организации корректного выхода из функции с переменным числом параметров: va_end(f).
Приведем пример использования макросов в функции конкатенации любого числа строк. Строки передаются функции с помощью списка указателей. В конце списка помещается нулевой указатель NULL.
#include <stdio.h> #include <conio.h> #include <stdarg.h> //для макросов переменного списка параметров #include <iostream> using namespace std; // функция конкатенации неопределенного числа строк char *concat(char *s1, ...) { va_list par; //указатель на параметры списка char *cp; int len = strlen(s1); //длина 1-го параметра va_start(par,s1); //начало переменного списка //цикл для определения общей длины параметров строк while(cp = va_arg(par, char *)) len += strlen(cp); //выделение памяти для результата char *stroka = new char[len + 1]; strcpy(stroka, s1); va_start(par, s1);//начало переменного списка //цикл конкатенации параметров строк while(cp = va_arg(par, char *)) strcat(stroka, cp); va_end(par); return stroka; } void main() { setlocale(LC_CTYPE, "Russian"); //прототип функции с переменным числом параметров char *concat(char *s1, ...); char *s; //указатель для результата s = concat("\nVerba"," volant, ","scripta ","manent\n", "(Слова исчезают, написанное остается.)", NULL); cout << s; _getch(); } |
В приведенной функции concat() тип параметров заранее известен и фиксирован (указатель на символы). В некоторых случаях параметры могут изменяться как по числу, так и по типу. В этих случаях необходимо, каким-то образом сообщать функции типы параметров для правильного их извлечения из стека. Например, в функциях printf() и scanf() в качестве первого операнда задается строка форматов, которая и содержит информацию о вводимых или выводимых данных.
Следующий пример, функция miniprintf(), показывает, как это можно сделать. В этой функции допускаются только два вида форматов %d и %f, которые позволяют макросу va_arg() правильно извлекать значения. Окончание вывода определяется завершением перебора форматов.
#include <conio.h> #include <stdarg.h> //для макросов переменного списка параметров #include <iostream> using namespace std; void miniprintf(char *format, ...) { va_list ap; //указатель на необязательный параметр char *p; //для просмотра строки format int ii; //целые параметры double dd; //параметры типа double va_start(ap, format); //настроились на первый параметр for(p = format; *p; p++) { if(*p != '%') {cout << *p; continue; } switch (*++p) { case 'd': ii = va_arg(ap, int); cout << ii; break; case 'f': dd = va_arg(ap, double); cout << dd; break; default: cout << *p; } }//конец цикла просмотра строки форматов va_end(ap); //подготовка к завершению функции } void main() { void miniprintf(char *format, ...); int k = 123; double d = 2.718282; miniprintf("\n Integer k = %d \n Double d = %f", k, d); _getch(); } |
Урок 9.7. Перегрузка функций
9.7. Перегрузка функций
9.7.1. Вызов функций при перегрузке
9.7.2. Перегрузка функций с несколькими аргументами
9.7.3. Перегрузка функций с аргументами по умолчанию
9.7.4. Перегрузка и область видимости
9.7. Перегрузка функций
Что такое перегрузка функций? В С++ несколько функций могут иметь одинаковые имена. Эта возможность используется, в основном, когда надо выполнить похожие действия, но над данными различных типов. Например:
int min(int, int);
long min(long, long);
double min(double, double);
Вызов функции min(): double a = min(34.78, 88.77); Компилятор сам определит, какой конкретно экземпляр функции вызвать. Такой механизм называется перегрузкой функции.
Будем называть сигнатурой функции тип и количество параметров, тип возвращаемого значения и имена параметров значения не имеют.
Функции не считаются перегруженными если:
- сигнатуры совпадают, в этом случае второе и все остальные объявления трактуются как переобъявления первого;
- сигнатуры функций совпадают, но возвращаемые значения различны, в этом случае последующие объявления считаются ошибочными;
int min(int, int);
long min(int, int); //Error
Если сигнатуры различны, тогда функция считается перегруженной, при условии, что её экземпляры объявлены в одной зоне видимости.
Чаще всего механизм перегрузки используется при объявлении конструкторов класса и при перегрузке операций.
9.7.1. Вызов функций при перегрузке
Функции, имеющие одинаковые имена и различные сигнатуры и определенные в одной области видимости, называются перегруженными.
Выбор экземпляра функции при вызове включает сравнение типов и числа фактических аргументов с формальными параметрами в объявлении каждого экземпляра.
Возможны три случая:
- Имеет место точное совпадение;
При этом char, short, 0 точно соответствуют типу int;
- Соответствие достигается при помощи преобразований;
При этом:
- фактический аргумент любого числового типа может быть приведен к любому числовому типу;
- константа 0 типа может быть преобразована к указателю;
- любой указатель может быть преобразован к void *;
- соответствие может быть достигнуто при помощи преобразований пользователя (в применении к фактическому параметру), т.е., если нет стандартных преобразований, тогда компилятор пытается применить преобразования определенные пользователем в классе.
- Соответствие достигается с более чем одним экземпляром функции, в этом случае выдается сообщение об ошибке. Например:
void f(long); void f(int); void f(double); unsigned int ui; |
Для вызова f(ui) есть 3 соответствия, каждое из которых достигается при помощи стандартного преобразования.
Пример. Применение комбинации преобразований – стандартных и определенных пользователем.
void f(void *); void f(double); class String{ // класс строк int sz; //длина строки char *str; //указатель на строку public: … //пользовательская перегруженная операция преобразования типа operator int(){ return sz;} }; Рассмотрим следующие ситуации: String s; f(5); //стандартное преобразование int->double, выбирается f(double). f(s); //1. преобразование пользователя в int, вызывается перегруженная // операция int //2. стандартное преобразование int->double, //3. выбирается f(double). Два уровня пользовательских преобразований не допускается. Примеры двусмысленных вызовов. 1. f(’a’); – ошибка, двусмысленная ситуация, точного соответствия нет, а символ ’a’ может быть преобразован и к long и к double, приоритетов преобразований нет. 2. class String{ Letter *str; public: // конструктор - преобразователь типа String(Letter); }; class Letter{ char ch; //преобразователь типа public: operator String(); } void f(String); Letter L; f(L); //Ошибка двусмысленности, в обоих классах есть преобразование //типа, компилятор затрудняется выбрать одно преобразование f(String(L)); //Ошибка двусмысленности, неизвестно, что вызывать //конструктор String или перегруженный оператор String. f(L.operator String()); //разрешение двусмысленной ситуации: //явный вызов операции преобразования типа |
В случае, когда одно из преобразований ведет к достижению точного соответствия, а другое требует ещё и стандартных преобразований, то двусмысленности не возникает.
9.7.2. Перегрузка функций с несколькими аргументами
При вызове функций с несколькими аргументами правила соответствия применяются для каждого аргумента. При перегрузке такой функции выбирается тот экземпляр, для которого достигнуто либо точное соответствие, либо достигнуто соответствие при помощи преобразований. Например:
void f(long *, int); void f(int, int); f(5, ’c’);//соответствует void f(int, int); |
В этом вызове, первый и третий параметры дают точное соответствие у обоих экземпляров функции. Второй аргумент вызываемой функции точно соответствует формальному параметру первого экземпляра, а для достижения соответствия со вторым экземпляром необходимо стандартное преобразование.
void f(int, double, long *); void f(int, long, void *); double a = 5, *pd; pd = &a; f(6, ’a’, pd);// соответствует void f(int, long, void *); |
В этом примере по первому и второму аргументу ни одна функция не имеет преимущества, но так как double * нельзя привести к long *, а к типу void * можно, то второй экземпляр функции имеет преимущество и будет вызван.
9.7.3. Перегрузка функций с аргументами по умолчанию
Если при перегрузке функций есть один или несколько экземпляров с аргументами по умолчанию, то вызываться будет тот, для которого обеспечены все или некоторое подмножество аргументов, к которым применимы правила соответствия. Например,
void f(int); void f(double, int = 0); f(5); //точно соответствует void f(int); f(5.7, 0); //точно соответствует void f(double, int = 0); f(7, 8); //соответствует void f(double, int ); //выбор по числу аргументов, 7 преобразуется в double f(34L); // ошибка: двусмысленность по параметру и нельзя //сделать выбор по числу аргументов f(34.56); //соответствует void f(double, int = 0); |
9.7.4. Перегрузка и область видимости
Здесь действует два простых правила:
- набор функций, объединенных общим именем, должен быть объявлен в одной области видимости для обеспечения перегрузки;
- локальное объявление функции с таким же именем, скрывает перегруженную функцию, объявленную на более высоком уровне с таким же именем.
Урок 9.6. Рекурсивные функции
9.6. Рекурсивные функции
В языке Си функции могут вызывать сами себя непосредственно или косвенно, т.е. могут быть рекурсивными. Если функция непосредственно вызывает саму себя – то это называется прямой рекурсией, а если функция вызывает какую-либо другую функцию, которая либо сама, либо посредством другой функции вызывает исходную функцию – то это называется косвенной рекурсией.
Каждая цепочка рекурсивных вызовов должна на каком-то шаге завершиться. Условие полного окончания работы рекурсивной функции должно находиться в самой функции (иначе произойдет зацикливание), а именно, любая рекурсивная функция должна содержать рекурсивный вызов внутри условного оператора.
Применять рекурсивные методы программирования стоит в тех задачах, где рекурсия использована в определении обрабатываемых данных. Это, как правило, связано с такими динамическими структурами данных как стеки, деревья, очереди и др. Многие задачи, решаемые при помощи рекурсии, более эффективно решаются либо с помощью итеративных алгоритмов либо с помощью подпрограмм. Например, вычисление факториала, которое мы рассмотрим, удобно для объяснения рекурсии, однако не дает никакого выигрыша в программной реализации. Более того, рекурсивный алгоритм вычисления факториала работает медленнее итеративного алгоритма, за счет накладных расходов на вызов функции и возврат значений.
Рассмотрим пример программы вычисляющей факториал положительного числа.
#include <stdio.h> #include <conio.h> #include <iostream> using namespace std; void main() { int f(int k); //функция вычисления факториала int fact = f(5); cout << "fact! = " << fact; getch(); } // Рекурсивная функция вычисления факториала int f(int k) { if (k < 0) return 0; if (k == 0) return 1; //т.к. 0! == 1 return k * f(k-1); } |
При первом вызове f(5) функция возвращает выражение 5*f(4), т.е. функция f() фактически не возвращает значение, а вызывает сама себя с другим значением. Рекурсивные вызовы будут продолжаться до тех пор, пока k не станет равным 0. Будет создана следующая цепочка возвращаемых выражений:
5*f(4), 4*f(3), 3*f(2), 2*f(1), 1*f(0)
Вызов f(0)не провоцирует дальнейших вызовов, а возвращает значение 1, произведение 1*1 будет возвращено предыдущему вызову и т.д. до вызова f(5), которому возвращается значение 120. Тем самым будут организованы следующие умножения:
1*1*2*3*4*5, а в общем случае 1*1*2*3*4*5*…*(k-1)*k
Рассмотрим еще один традиционный пример функции, представленной в форме прямой рекурсии, – печать чисел как последовательности символов.
#include <stdio.h> #include <conio.h> void main() { void printd(int n); int n; n = 135; printd(n); _getch(); } //Рекурсивная функция void printd(int n) // печать n { int i; if (n < 0) { putchar('-'); // печать минуса для отрицательных чисел n=-n; } if ((i = n / 10)!= 0) printd(i); putchar(n%10+'0'); // преобразование цифры в символ } |
Рассмотрим работу этой функции для числа n=153. Поскольку число положительно, то операторы печати знака минус putchar('-'); и смена знака числа n=-n; пропускаются.
Оператор if ((i=n/10)!=0) printd(i); проверяет целую часть частного 153/10 = 15 на ноль. Если целая часть не ноль, то с этим значением (15) идет вновь обращение к функции printd(), а после возврата из нее оператором putchar(n%10+'0'); печатается остаток от деления 153%10 = 3, т.е. 3. Аналогично, вторая printd() передает третьей 1 (та печатает ее), а затем сама печатает 5.
Таким образом, однократное обращение извне к функции printd(); вызовет трехкратное ее срабатывание.
Задание на дом: напишите рекурсивную функцию вычисления целой степени вещественного числа.
Решение:
#include <stdio.h> #include <conio.h> #include <iostream> using namespace std; void main() { double power(double a, int n);//прототип функции double a = 5; int n = - 3; double d = power(a, n); cout << a << "^" << n << " = " << d; _getch(); } // Рекурсивная функция вычисления целой степени вещественного числа double power(double a, int n) { if (n == 0) return 1; if (a == 0) return 0; if (n > 0) return a * power(a, n-1); if (n < 0) return power(a, n+1) / a; } |
Задание на дом: напишите рекурсивную функцию вычисления суммы элементов вектора. Тело функции должно состоять из одной строчки.
Решение (2 варианта summa() и summa1()):
#include <stdio.h> #include <conio.h> #include <iostream> using namespace std; void main() { double summa (int *v, int n); //прототипы функций double summa1(int *v, int n); int v[5] = {1, 2, 3, 4, 5}; double s = summa(v, 5); cout << "s = " << s << endl; double s1 = summa1(v, 5); cout << "s1 = " << s1 << endl; _getch(); } // Рекурсивные функции вычисления суммы элементов вектора double summa(int *v, int n) { return n > 0 ? summa(v, n-1) + v[n-1] : 0; } double summa1(int *v, int n) { return n > 0 ? summa(v+1, n-1) + *v : 0; } |
Рассмотрим пример косвенной рекурсии. Пример принадлежит к классу алгоритмов, работающих с данными, структура которых тоже определяется рекурсивно. Характерной задачей такого рода является преобразование простых выражений в соответствующую постфиксную форму (иначе Полиз – польскую инверсную запись), т.е. в форму, при которой знак операции следует за операндами. Выражение будем определять в виде РБНФ[*] так:
Выражение = Слагаемое{("+" I "-")Слагаемое}.
Слагаемое = Множитель{("*" I "/")Множитель}.
Множитель = Буква I "("Выражение")" I "["Выражение"]"
Обозначая выражение как Е, слагаемые как Т1, Т2, а множители как F1, F2, запишем правила преобразования:
T1+T2 --> T1T2+
T1-T2 --> T1T2-
F1*F2 --> F1F2*
F1/F2 --> F1F2/
(E) --> E
[E] --> E
___________________________________________________________________
[*]
РБНФ - расширенная форма Бэкуса-Наура, метанотация (формализм) для записи синтаксических конструкций. В данном примере использованы такие метасимволы:.,+,{,},(,),I. Эти метасимволы читаются следующим образом:
- А = ВС - синтаксическая конструкция А состоит из следущих друг за другом конструкций В и С.
- А = В I С - А состоит либо из В, либо из С.
- А = {B} - А может состоять из конкатенации любого числа (включая ноль) конструкций В.
Наша программа отражает структуру синтаксиса входных выражений. Поскольку синтаксис рекурсивен, рекурсивна и сама программа.
#include <stdio.h> #include <conio.h> #define EOL 0x0a char *ch = new char [100]; char *rez = new char [100]; char *rez1; void Mnoj(); // множитель void Slag(); // слагаемое void Vir(); // выражение void main() { rez1 = rez; //адрес rez будет меняться, а для вывода результата //нужно начало строки scanf("%30s", ch); //не более 100 символов Vir(); *rez = '\0'; puts("rez1="); puts(rez1); _getch(); } void Vir() /* выражение */ { char OpSl; Slag(); while( (*ch == '+') || (*ch == '-')) { OpSl =* ch++; // операция сложения Slag(); *rez++ = OpSl; } //*rez = '\0'; } // конец выражения void Slag() /* слагаемое */ { char OpUmn; Mnoj(); while ((*ch == '*') || (*ch == '/')) { OpUmn =* ch++; Mnoj(); *rez++ = OpUmn; } } void Mnoj() /* множитель */ { if (*ch == '(') { ch++; Vir(); while (*ch++ != ')'); return; } else if (*ch == '[') { ch++; Vir(); while (*ch != ']') ch++; } else while ((*ch < 'a') || (*ch > 'z')) ch++; *rez++ = *ch++; } // конец множителя //Результат работы: >a+b ab+ >a*b+c ab*c+ >a+b*c abc*+ >a*(b/[c-d]) abcd-/* |
При повторном входе в рекурсивную функцию в памяти машины создаются копии всего набора локальных переменных этой функции, организуемые в виде стека.
Рекурсии, как правило, не предусматривают никаких механизмов защиты памяти, так что иногда стек обрабатываемых величин может переполняться. Возможные ошибки такого рода должен предотвращать сам программист.