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

УжасноПлохоНормальноХорошоОтлично (3 голосов, средний: 3,33 из 5)
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="" highlight="">