Переменное число аргументов

Приводимые ранее функции имели фиксированное количество аргументов, однако в некоторых случаях заранее может быть не известно их количество.

В Си нет перегрузки, то есть нельзя создать несколько функций с одинаковым именем, но разными аргументами... да и предугадать все возможные комбинации заведомо провальная идея. Тем не менее, Си позволяет написать функцию с произвольным количеством аргументов (англ. variadic function), через троеточие (...). Но как в таком случае получить доступ к ним? Нам известно аргументы передаются по значению, т. е. внутри функции создаются локальные копии переменных, которые в свою очередь складываются на стек. Следовательно, зная адрес хотя бы одного аргумента мы интегрируясь по памяти можем обработать их все. Небольшой пример:

Для компилятора GCC с уровнем оптимизации -O0 вывод будет следующим:

Здорово! Но... во-первых, при любом уровне оптимизации отличным от -O0 такой фокус может, и скорее всего, не сработает. А во-вторых, поведение ... не регламентируется стандартом и значения будут лежать не там, где вы ожидаете.

Следующая проблема с нефиксированным количеством аргументов банальна и очевидна: кто знает сколько аргументов было передано в действительности? Самый простой способ разрешить незадачу -- указать напрямую.

Это не очень удобно, легко ошибиться. printf(), например, решает ту же самую проблему через маркеры в строке, которую вы передаёте (а точнее указатель на неё) в качестве аргумента:

Из работы с printf() вы уже видите ещё одну проблему -- можно передать абсолютно любой тип данных, но внутри функции он не известен. К слову, по историческим причинам все типы уже int расширяются до его размеров, а float расширяется до double. Как раз именно поэтому не важно что вы передадите в printf("%f", var) -- 2.2 или 2.2f, это все равно будет double на той стороне.

Итого, как же быть? Работать с переменным количеством аргументов следует через стандартную библиотеку, а именно через заголовочный файл <stdarg.h>. В нём определён специальный тип данных va_list для работы c ... и несколько макросов (вызывающие функции вашего компилятора):

Ниже приведён небольшой пример использования ... для подсчёта среднего арифметического:

А вот вопрос поинтереснее. Допустим вы хотите написать модуль логирования, который в зависимости от настроек отправляет заданную строку либо в UART, либо, например на SD-карту, при этом вам хотелось бы сохранить сигнатуру обычного printf. Будите писать собственный парсер строки? К счастью, в стандартной библиотеке уже имеются специальные функции, которые могут принимать в качестве аргумент va_list; у таких функций имеется прификс v, например vprintf() или vsprintf().

С такими функциями связана одна уязвимость, uncontrolled format string, позволяющая злоумышленнику получить доступ к стеку, менять значение переменных или, что ещё хуже, вызывать другой код.


1 Макрос va_end подчищает работу с va_list, если его забыть вызвать стек может быть повреждён.