Информатика и технология программирования

         

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


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

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

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



-отдельным параметром -счетчиком;



-параметром ограничителем, значение которого отмечает конец списка параметров;



-форматной строкой, в которой перечислены спецификации параметров (например, функция printf).

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


Пример функции, работающей с параметром -счетчиком.



int sum(int n,...) // n - счетчик параметров

{ int s,*p; // Указатель на область параметров

p = &#38n+1; // назначается на область памяти

for (s=0; n &#62 0; n--) // вслед за счетчиком n

s += *p++;
return(s); }
void main(){ cout &#60&#60 sum(5,0,4,2,56,7) + sum(2,6,46); }



Пример функции, работающей с параметром -ограничителем.



int sum(int a,...)
{ int s,*p; // Указатель на область параметров

p = &#38a; // назначается на первый параметр

for (s=0; *p &#62= 0; ) // из переменного списка

s += *p++; // Ограничитель - отрицательное

return(s); // значение

}
void main() { cout &#60&#60 sum(0,4,2,56,7,-1) + sum(6,46,-1) ;}



Если в списке предполагается наличие параметров различных типов, то типы их должны быть переданы в функцию отдельной спецификацией (подобно форматной строке функции printf). В этом случае область фактических параметров представляет собой память, в которой последовательность типов переменных заранее не определена (см п.4.4). С такой памятью можно работать только используя явное преобразование типа указателя к типу той переменной, которая в данный момент под ним находится. Операция *p++ по отношению к каждому типу указателя будет выполнять чтение из памяти переменной соответствующего типа с переходом к началу следующей:



//------------------------------------------------------bk45-01.cpp

&#35include &#60stdio.h&#62
int printf(char *s,...)
{
int ii;
double dd;
char * p,*ss;
p = &#38s+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

&#35include &#60stdarg.h&#62
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 выполняет завершающие действия. Использование макрокоманд предпочтительнее, так как они учитывают все нюансы механизма передачи параметров в данной версии транслятора (например, выравнивание адреса параметра к границе машинного слова ).


Содержание раздела