Функции с переменным числом параметров
Функции с переменным числом параметров
Как уже обсуждалось ранее, по умолчанию параметры передаются функции через стек. Поэтому, технически, нет ограничения на количество
передаваемых параметров – “запихать” можно сколько угодно. Проблема в том, как потом функция будет разбирать переданные параметры.
Функции с переменным числом параметров объявляются как обычные функции, но вместо недостающих аргументов ставится многоточие. Пусть мы хотим сделать функцию,
которая складывает переданные ей числа, чисел может быть произвольное количество. Необходимо каким-то образом передать функции число параметров. Во-первых, можно
явно передать число параметров обязательным аргументом. Во-вторых, последний аргумент может иметь некоторое «терминальное» значение, наткнувшись на которое функция
закончит выполнение.
Общий принцип работы следующий: внутри функции берём указатель на аргумент, далее двигаемся к следующему аргументу, увеличивая значение указателя.
OLD SCHOOL
Делаем всё вручную. Функция, которая складывает переданные ей аргументы
#include <stdio.h> #include <conio.h> #include <stdlib.h> #define UNSIGNED_OVERFLOW -4 unsigned summ(unsigned char num, unsigned first, ...) { unsigned sum = 0; unsigned testsum = 0; unsigned *p = &first; while (num--) { testsum += *p; if (testsum >= sum) { sum = testsum; } else { exit(UNSIGNED_OVERFLOW); } p++; } return sum; } void main() { int sum = summ(5, 1u, 2u, 3u, 4u, 5u); printf("summ = %u\n", sum); sum = summ(7, 0u, 27u, 0u, 4u, 5u, 60u, 33u); printf("summ = %u\n", sum); getch(); }
Первый параметр – число аргументов. Это обязательный параметр. Второй аргумент – это первое переданное число, это тоже обязательный параметр. Получаем указатель на первое число
unsigned *p = &first;
Далее считываем все числа и складываем их. В этой функции мы также при сложении проверяем на переполнение типа unsigned.
Можно сделать первый аргумент необязательным и «перешагнуть» аргумент unsigned char num, но тогда возникнет большая проблема: аргументы располагаются друг за другом, но не факт, что непрерывно. Например, в нашем случае первый аргумент будет сдвинут не на один байт, а на 4 относительно num. Это сделано для повышения производительности. На другой платформе или с другим компилятором, или с другими настройками компилятора могут быть другие результаты.
unsigned summ2(unsigned char num, ...) { unsigned sum = 0; unsigned testsum = 0; //Для увеличения скорости работы адреса кратны 4 байтам, //даже если реальный размер переменной меньше unsigned *p = (unsigned*)(&num+4); while (num--) { testsum += *p; if (testsum >= sum) { sum = testsum; } else { exit(UNSIGNED_OVERFLOW); } p++; } return sum; }
Поэтому лучше число параметров, если это аргумент, сделать типом int или unsigned int.
Можно сделать по-другому: в качестве «терминального» элемента передавать ноль и считать, что если мы встретили ноль, то больше аргументов нет. Пример
#include <stdio.h> #include <conio.h> #include <stdlib.h> #define UNSIGNED_OVERFLOW -4 unsigned summ(unsigned first, ...) { unsigned sum = 0; unsigned testsum = 0; unsigned *p = &first; while (*p) { testsum += *p; if (testsum >= sum) { sum = testsum; } else { exit(UNSIGNED_OVERFLOW); } p++; } return sum; } void main() { int sum = summ(1u, 2u, 3u, 4u, 5u, 0); printf("summ = %u\n", sum); sum = summ(1u, 27u, 1u, 4u, 5u, 60u, 33u, 0); printf("summ = %u\n", sum); getch(); }
Но теперь уже передавать нули в качестве аргументов нельзя. Здесь также есть один обязательный аргумент – первое переданное число. Если его не передавать, то мы не сможем найти адрес, по которому размещаются переменные в стеке. Некоторые компиляторы (Borland Turbo C) позволяют получить указатель на …, но такое поведение не является стандартным и его нужно избегать.
VA_ARG
Можно воспользоваться макросом va_arg библиотеки stdarg.h. Он делает практически то же самое, что и мы: получает указатель на первый аргумент а затем двигается по стеку. Пример, та же функция, только с va_arg
//требует подключения библиотеки <stdarg.h> unsigned summ3(unsigned num, ...) { //Переменная типа va_list – список аргументов va_list args; unsigned sum = 0; unsigned testsum = 0; //Устанавливаем указатель на первый элемент va_start(args, num); while (num--) { //Достаём следующий, указывая тип аргумента testsum += va_arg(args, unsigned); if (testsum >= sum) { sum = testsum; } else { exit(UNSIGNED_OVERFLOW); } } va_end(args); }
Первый аргумент – число параметров – также лучше делать типа int, иначе получим проблему со сдвигом, кратным 4.
Название | Описание |
---|---|
va_list |
Тип, который используется для извлечения дополнительных параметров функции с переменным числом параметров |
void va_start(va_list ap, paramN) |
Макрос инициализирует ap для извлечения дополнительных аргументов, которые идут после переменной paramN. Параметр не должен быть объявлена как register, не может иметь типа массива или указателя на функцию. |
void va_end(va_list ap) |
Макрос необходим для нормального завершения работы функции, работает в паре с макросом va_start. |
void va_copy(va_list dest, va_list src) |
Макрос копирует src в dest. Поддерживается начиная со стандарта C++11 |
Неправильное использование
Функции printf и scanf типичные примеры функций с переменным числом параметров. Они имеют один обязательный параметр типа const char* - строку формата и остальные необязательные. Пусть мы вызываем эти функции и передаём им неверное количество аргументов: Если аргументов меньше, то функция пойдёт дальше по стеку и покажет какое-то значение, которое лежит «ниже» последнего аргумента, например
printf("%d\n%d\n%d\n%d\n%d", 1, 2, 3);
Если передано больше аргументов, то функция выведет только те, которые ожидала встретить
printf("%d\n%d\n%d\n%d\n%d", 1, 2, 3, 4, 5, 6, 7);
Так как очистку стека производит вызывающая функция, то стек не будет повреждён. Получается, что если изменить схему вызова и сделать так, чтобы вызываемый объект
сам чистил стек после себя, то в случае неправильного количества аргументов стек будет повреждён. То есть, буде функция объявлена как __stdcall, в целях безопасности
она не может иметь переменного числа аргументов.
Однако, если добавить спецификатор __stdcall к нашей функции summ она будет компилироваться. Это связано с тем,
что компилятор автоматически заменит __stdcall на __cdecl.
Давайте убедимся в этом. Использование ... в объявлении функции не является обязательным. То есть, если вы передадите функции больше параметров, то IDE покажет замечание, но код останется вполне рабочим. Например
#include <conio.h> #include <stdio.h> void allmyvars(int num) { int *p = &num + 1; while (num--) { printf("%d ", *p); p++; } } void main() { allmyvars(4, 1, 2, 3, 4); _getch(); }
Теперь объявим явно функцию как stdcall. Так как мы не использовали символа ..., то не произойдёт автоподмены stdcall на cdecl. Функция отработает, но после завершения стек будет повреждён.
#include <conio.h> #include <stdio.h> void __stdcall allmyvars(int num) { int *p = &num + 1; while (num--) { printf("%d ", *p); p++; } } void main() { allmyvars(4, 1, 2, 3, 4); _getch(); }
Программа завершится с ошибкой вроде The value of ESP was not properly saved across a function call.
