Переменное число аргументов в функциях 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, описанная выше, — это штука, на постоянство которой не стоит рассчитывать. Оптимизирующие компиляторы могут организовать передачу аргументов через регистры, а уж про перенос программы на другие платформы и говорить не приходится. Еще одна рекомендация — пишите такие функции только для форматирования текста. Другие применения обычно создают больше проблем, чем решают.

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
Loading ... Loading ...

Оставьте свой отзыв

Если введен идентификатор OpenID, можно не задавать имя и почту (но имя лучше все же задать).

XHTML: Можно использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

Это не спам, честное слово