Функции с переменным числом параметров
Функции с переменным числом параметров
Как уже обсуждалось ранее, по умолчанию параметры передаются функции через стек. Поэтому, технически, нет ограничения на количество передаваемых параметров – “запихать” можно сколько угодно. Проблема в том, как потом функция будет разбирать переданные параметры. Функции с переменным числом параметров объявляются как обычные функции, но вместо недостающих аргументов ставится многоточие. Пусть мы хотим сделать функцию, которая складывает переданные ей числа, чисел может быть произвольное количество. Необходимо каким-то образом передать функции число параметров. Во-первых, можно явно передать число параметров обязательным аргументом. Во-вторых, последний аргумент может иметь некоторое «терминальное» значение, наткнувшись на которое функция закончит выполнение.
Общий принцип работы следующий: внутри функции берём указатель на аргумент, далее двигаемся к следующему аргументу, увеличивая значение указателя.
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.
