Вызов функцииФункции с переменным количеством параметров
Ранее (см. 3.6) мы уже рассматривали, как используется стек для вызова функций, передачи параметров и хранения локальных (автоматических) переменных. Фактические параметры записываются в стек перед вызовом функции, начиная с последнего в списке. Поскольку аппаратный стек расположен "вверх дном" и "растет" от старших адресов к младшим, то этим обеспечивается прямой порядок размещения их в памяти. Формальные параметры представляют собой "ожидаемые" смещения в стеке, по которым должны после вызова находиться соответствующие фактические параметры. Таким образом, сам механизм вызова функции соответствие параметров устанавливает только "по договоренности" между вызывающей и вызываемой функциями, а транслятор при использовании прототипа проверяет эти соглашения.
Поскольку фактические параметры записываются в стек и извлекаются из него вызывающей функцией, а используются вызываемой по смещениям в стеке, то имеет место независимость формальных и фактических параметров. В частности, если в списке есть лишние параметры, то вызов будет корректен, а вызываемая функция просто их "не заметит". Отсюда один шаг к использованию переменного количества фактических параметров: достаточно установить некоторый указатель на область памяти (область стека), занимаемую "лишними" фактическими параметрами, и тогда с помощью его и операций адресной арифметики функция может просмотреть их последовательность.
Текущее количество фактических параметров, передаваемых при вызове, может быть указано:
-отдельным параметром -счетчиком;
-параметром ограничителем, значение которого отмечает конец списка параметров;
-форматной строкой, в которой перечислены спецификации параметров (например, функция printf).
Если уж быть совсем точным, то список фактических параметров должен представлять собой последовательность переменного формата, в которой значение текущей переменной может определять количество и типы последующих переменных списка. Способы работы с такими последовательностями с использованием указателей и преобразованием их типа рассмотрены в п.4.4.
Пример функции, работающей с параметром -счетчиком.
int sum(int n,...) // n - счетчик параметров
{ int s,*p; // Указатель на область параметров
p = &n+1; // назначается на область памяти
for (s=0; n > 0; n--) // вслед за счетчиком n
s += *p++;
return(s); }
void main(){ cout << sum(5,0,4,2,56,7) + sum(2,6,46); }
Пример функции, работающей с параметром -ограничителем.
int sum(int a,...)
{ int s,*p; // Указатель на область параметров
p = &a; // назначается на первый параметр
for (s=0; *p >= 0; ) // из переменного списка
s += *p++; // Ограничитель - отрицательное
return(s); // значение
}
void main() { cout << sum(0,4,2,56,7,-1) + sum(6,46,-1) ;}
Если в списке предполагается наличие параметров различных типов, то типы их должны быть переданы в функцию отдельной спецификацией (подобно форматной строке функции printf). В этом случае область фактических параметров представляет собой память, в которой последовательность типов переменных заранее не определена (см п.4.4). С такой памятью можно работать только используя явное преобразование типа указателя к типу той переменной, которая в данный момент под ним находится. Операция *p++ по отношению к каждому типу указателя будет выполнять чтение из памяти переменной соответствующего типа с переходом к началу следующей:
//------------------------------------------------------bk45-01.cpp
#include <stdio.h>
int printf(char *s,...)
{
int ii;
double dd;
char * p,*ss;
p = &s+1; // Указатель на начало списка параметров
while (*s != '\0') // Просмотр форматной строки
{
if (*s != '%')
putchar(*s++); // Копирование форматной строки
else
{ s++; // Спецификация параметра вида "%d"
switch(*s++)
{ // Извлечение параметра и переход к следующему
case 'c': puchar(*p++);
break;
case 'd': ii = *((int*)p)++;
break; // Преобразование и вывод целого
case 'f': dd = *((double*)pd)++;
break; // Преобразование и вывод плавающего
case 's': ss = *((char**)ps)++;
puts(ss); // Вывод строки по указателю
break;
}}}}
Этот механизм может быть скрыт от программиста соответствующими макрокомандами.
//------------------------------------------------------bk45-02.cpp
#include <stdarg.h>
int printf(char *s,...)
{
va_list PNT; // Аналог указателя на область стека
int ii;
double dd;
char *ss;
va_start(PNT,s+1); // Назначить указатель на начало
while (*s != '\0') // Просмотр форматной строки
{
if (*s != '%')
putchar(*s++); // Копирование форматной строки
else
{ s++; // Спецификация параметра вида "%d"
switch(*s++)
{ // Извлечение параметра по указателю
// и переход к следующему
case 'd': ii = va_arg(PNT,int);
break; // Преобразование и вывод целого
case 'f': dd = va_arg(PNT,double);
// Преобразование и вывод плавающего
break;
case 's': ss = va_arg(PNT, char*);
puts(ss); // Вывод строки по указателю
break;
}
}}
va_end(PNT);
}
Макрокоманда va_list определяет переменную -указатель на область параметров в стеке, va_start назначает его на область памяти, расположенную за последним явно указанным параметром, va_arg извлекает из памяти переменную указанного типа и продвигает указатель на ее размерность, va_end выполняет завершающие действия. Использование макрокоманд предпочтительнее, так как они учитывают все нюансы механизма передачи параметров в данной версии транслятора (например, выравнивание адреса параметра к границе машинного слова ).