Урок 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. Перегрузка и область видимости
Здесь действует два простых правила:
- набор функций, объединенных общим именем, должен быть объявлен в одной области видимости для обеспечения перегрузки;
- локальное объявление функции с таким же именем, скрывает перегруженную функцию, объявленную на более высоком уровне с таким же именем.