Переменное число аргументов в функциях C
Полезная, но редко используемая и часто недопонимаемая возможность языка C — переменное число аргументов функции. Это такая особая сишная магия, позволяющая писать функции наподобие printf(). Обычно такие функции пишутся именно для форматированного вывода текста, хотя возможны и другие применения. В учебниках любят приводить в качестве примера функцию сложения произвольного числа аргументов, но вряд ли такое можно увидеть в реальном коде.
Выглядеть определение такой безразмерной функции может как-то так (три точки в списке формальных аргументов следует воспринимать буквально, то есть там именно три точки):
int myfunct(int arg1, double arg2, ...) { /* какой-то код */ }
Так как в языке C обычно придерживаются соглашения о вызовах cdecl, то аргументы, передаваемые функции, передаются через стек, причем кладутся туда, начиная с последнего (правого) аргумента и заканчивая первым. Вот вам примерный портрет стекового фрейма для вызова функции с N аргументами на процессоре x86:
Все наши знания о фрейме ограничены значениями регистров ESP и EBP. Та точка отсчета, от которой мы можем отталкиваться для доступа к аргументам — текущее значение регистра EBP. Например, первый аргумент лежит по адресу EBP+8, а второй — по адресу EBP+12. Ладно, а сколько нужно прибавить к EBP, чтобы получить адрес N-го аргумента в стеке? Для этого нужно знать:
- сколько всего передано аргументов;
- размеры аргументов (кто сказал, что они должны быть одинаковы?).
Ни то, ни другое в доступных нам ESP и EBP не хранится. Знаем мы только то, что именованные аргументы имеют меньшие адреса. Выходит, что размеры аргументов должны быть жестко определены в теле функции или же получены на основе значений именованных аргументов. Но как определить их количество? Рассчитывать на форматную строку, как в printf? Но кто гарантирует, что эта строка правильная?
Что, страшно уже? К счастью, всю низкоуровневую рутину с жоглированием указателями и размерами готов взять на себя компилятор, предоставляя нам удобный интерфейс в заголовочном файле stdarg.h. Правда, количество аргументов и их типы нам все-таки придется выяснять самостоятельно.
Правила нехитрые:
- функция должна иметь по крайней мере один именованный параметр (именованные идут в списке формальных параметров перед неименованными);
- перед доступом к аргументам нужно вызвать
va_start, чтобы инициализировать объект контекста (va_list); - используем
va_argдля последовательного доступа к неименованным аргументам; - по окончанию вызываем
va_endв той же функции, что иva_start.
Тут не обойтись без примера. Посмотрим на пресловутую функцию сложения нескольких чисел (спокойствие, дальше будет более жизненный пример).
#include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list args; int total = 0; va_start(args, count); while (count--) total += va_arg(args, int); va_end(args); return total; } int main() { printf("%d\n", sum(1, 33)); printf("%d\n", sum(4, 1, 2, 3, 4)); return 0; }
Пояснения здесь излишни. Кстати, va_list можно передавать в качестве аргумента другой функции. Надо только не забыть вызвать для него va_end в той же функции (а точнее, в том же вызове функции), где был вызван va_start. Не знаю точно, что делает va_end, но если он манипулирует значением EBP, то лучше бы это значение было тем же, что и раньше.
Зачем может понадобиться передача va_list? Обычно для написания оберток для функций семейства printf, например, в каком-нибудь логгере:
int msgOut(enum msgLevel level, const char *fmt, ...) { va_list ap; int ret; va_start(ap, fmt); ret = msgOutL(level, fmt, ap); va_end(ap); return ret; } int msgOutL(enum msgLevel level, const char *fmt, va_list vl) { if (level >= s_level) return vfprintf(s_stream ? s_stream : stdout, fmt, vl); else return 0; }
Напоследок хочется сказать вот что. Если вы пишете функцию с переменным числом аргументов не от нечего делать, а в реальном проекте, то использование stdarg.h — единственный вариант. Организация стека в x86, описанная выше, — это штука, на постоянство которой не стоит рассчитывать. Оптимизирующие компиляторы могут организовать передачу аргументов через регистры, а уж про перенос программы на другие платформы и говорить не приходится. Еще одна рекомендация — пишите такие функции только для форматирования текста. Другие применения обычно создают больше проблем, чем решают.




