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

Теги: Функции с переменным числом параметров, неверное число параметров функции, vararg, va_arg, va_end. va_copy, va_list, __stdcall, __cdecl, __fastcall



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

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

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.

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Объединения и битовые поля