Зона кода

А давайте немного попрограммируем...

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

C99Полезное

В языке C99, впрочем, как и в более ранних версиях C, поддерживаются функции с переменным числом аргументов. Использовать готовые такие функции очень просто. Первой стандартной библиотечной функцией, с которой сталкиваются начинающие изучать язык C, очень часто является printf(). А это и есть, как раз, функция с переменным числом аргументов.

Однако для создания функций с переменным числом аргументов требуются определённые представления о том, как в языке C организован механизм передачи параметров в функцию и доступ к ним из её тела. Также понадобится умение использовать макросы, определённые в стандартном заголовочном файле stdarg.h. Обо всём этом мы подробно поговорим в данной статье.

Статья адресована тем, кто имеет небольшой опыт программирования на C, но уже разбирается в макросах, указателях и динамическом распределении памяти.

Знакомство с функциями с переменным числом аргументов

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

В списке формальных параметров функции с переменным числом аргументов через запятую перечисляются имена и типы обязательных формальных параметров. После имени последнего обязательного параметра ставится запятая, за которой следуют 3 точки. Мы сейчас говорили о списке обязательных параметров, входящем в заголовок функции. В прототипе функции имена обязательных параметров, разумеется, могут быть опущены.

И ещё. Последний обязательный параметр не должен быть объявлен со спецификатором register.

Вот пример заголовка функции с переменным числом аргументов:

int fun(int i, double d, ...)

Эта функция принимает 2 обязательных параметра, первый из которых имеет тип int, а второй — double, и произвольное число необязательных. Следующие вызовы функции корректны:

int a = fun(50, 3.14);
int b = fun(50, 3.14, 60);
int c = fun(50, 3.14, 60, 'a', "Это строка");

А следующие — нет:

int a = fun(50);
int b = fun(3.14);
int c = fun();

Число необязательных формальных параметров и их типы становятся известны только во время вызова функции. Типы необязательных формальных параметров совпадают с типами значений соответствующих им фактических аргументов с одной оговоркой. Заключается она в том, что значениям фактических аргументов типа char и short соответствует тип формального параметра int, а значениям типа float — тип double.

Таким образом, необязательные формальные параметры не могут иметь типы char, short и float. Однако выражения этих трёх типов передавать функциям в качестве необязательных аргументов можно. Просто их значения будут приведены к типу int в первых двух случаях и к типу double в третьем.

В теле функции к обязательным формальным параметрам можно обращаться, как всегда, по именам. А необязательные параметры имён не имеют. О том, как получать их значения, мы сейчас и поговорим.

Доступ к необязательным параметрам в теле функции

Если бы язык C был бы максимально дружественен по отношению к программисту, то в нём были бы предусмотрены механизмы, позволяющие из тела функции с переменным числом аргументов узнать число и типы необязательных параметров и получить их значения. В этом случае смысла в данной статье не было бы.

Но язык C дружественным не является. По той причине, что за такого рода "дружественность" пришлось бы расплачиваться падением скорости работы программ. Поэтому в C нет встроенных механизмов, позволяющих выяснить число и тип необязательных параметров. Этим выяснением должен заниматься автор функции. И лишь зная число необязательных параметров и их типы, можно получить их значения.

Каким образом из тела функции можно получить информацию о числе и типах необязательных параметров? Обычно она передаётся в функцию через обязательные параметры. Но возможны и другие варианты. Например, можно эту информацию извлекать из глобальных переменных, из файлов или считывать с консоли. Но первый вариант, всё же, является наиболее предпочтительным.

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

Предполагается, что значения необязательных параметров содержатся в некотором хранилище. Для работы с этим хранилищем имеются инструменты, определённые в стандартном заголовочном файле stdarg.h. К ним относятся тип va_list и макросы va_start(), va_arg(), va_copy() и va_end().

С помощью этих инструментов мы можем получать значения необязательных параметров в том порядке, в котором соответствующие им фактические аргументы были перечислены в списке аргументов в момент вызова функции. Для доступа к ссылке на значение текущего параметра, посредством которой это значение можно получить, используется объект типа va_list. Его можно определить, например, так:

va_list argptr;

Для инициализации объекта argptr нужно вызвать макрос va_start(), которому в качестве первого параметра следует передать имя этого объекта, а в качестве второго — имя последнего обязательного параметра. Мы используем термин "вызвать" по отношению к макросу, имея в виду, конечно же, что макрос — это не функция и вместо вызова происходит макроподстановка. Но работаем мы с параметризованными макросами почти как с функциями, поэтому и будем в дальнейшем использовать данный термин.

Например, в теле функции fun() вызов макроса должен выглядеть так:

va_start(argptr, d);

Теперь объект argptr содержит ссылку (или адрес объекта, содержащего ссылку) на значение первого необязательного параметра. Чтобы получить это значение, нужно "вызвать" макрос va_arg(), которому в качестве первого параметра следует передать имя нашего объекта, а в качестве второго — тип первого необязательного параметра. Макрос va_arg() "вернёт" значение этого параметра. Пусть, например, первый необязательный параметр имеет тип int. Тогда вызов va_arg()может быть, например, таким:

int n = va_arg(argptr, int);

Теперь argptr содержит ссылку (или адрес объекта, содержащего ссылку) на второй необязательный параметр. Значение этого параметра может быть получено аналогично. Например, если его тип — double, то получить его значение можно так:

double r = va_arg(argptr, double);

Таким же образом можно получать значения остальных необязательных параметров. После получения значения последнего из них, содержимое argptr считается неопределённым. Впрочем, мы не обязаны обрабатывать с помощью макроса va_arg() все необязательные параметры. Всё зависит от наших целей.

После того, как макрос va_arg() вызван нужное количество раз, необходимо вызвать макрос va_end():

va_end(argptr);

Предполагается, что вызов макроса va_end() корректно завершает работу с хранилищем значений, в частности, освобождает, при необходимости, задействованные в этой работе ресурсы. Макрос необходимо вызывать до выхода из функции.

После вызова va_end(argptr) можно снова инициализировать тот же самый объект argptr посредством va_start() и снова "пройтись" по значениям с помощью va_arg(). Но затем снова должен быть вызван va_end().

После того, как объект argptr инициализирован, но до того, как для него вызван макрос va_end(), можно создать копию argptr с помощью макроса va_copy() (или копию объекта, на который указывает argptr). Сделать это можно, например, так:

va_list argptr2;
va_copy(argptr2, argptr);

Теперь значения необязательных параметров можно получать и с помощью argptr2 независимо от argptr. Но после того, как необходимые значения получены, для argptr2 также должен быть вызван макрос va_end().

Таким образом, объект типа va_list может быть инициализирован двумя способами: с помощью va_start() и с помощью va_copy(). Каждой такой инициализации должен соответствовать вызов макроса va_end() для этого объекта.

Ближе к делу

А теперь давайте обсудим особенности реализаций типа va_list и макросов, предназначенных для создания функций с переменным числом аргументов, в компиляторе MinGW64, работающем под управлением операционной системы Windows 10. Именно это программное обеспечение установлено на моём ноутбуке и именно его я буду использовать для запуска и компиляции программ, рассмотренных далее.

Но для начала поговорим о том, как значения параметров передаются в функции. Имеются 2 основных способа их передачи: через стек и через регистры процессора. При этом в ходе одного вызова функции часть значений может быть передана первым из этих способов, а оставшаяся часть — вторым. В нашем случае значения необязательных параметров передаются через стек, поэтому будем говорить только об этом способе.

Идея передачи очень проста. В момент вызова функции значения фактических аргументов вычисляются и заталкиваются в стек. Затем управление передаётся в вызываемую функцию. В ходе своей работы функция, при необходимости, обращается к значениям, находящимся в стеке, через формальные аргументы. После окончания её работы управление возвращается в вызывающую функцию, которая восстанавливает стек до того состояния, в котором он находился перед вызовом функции.

Формально, восстановление состояния означает выталкивание значений формальных параметров из стека (они могут уже отличаться от значений соответствующих фактических аргументов). Но фактически выталкивание не означает немедленного удаления из памяти этих значений. Просто указатель стека переключается на первоначальную позицию.

Таким образом, все значения необязательных формальных параметров располагаются в стеке ''один за другим". Для получения доступа к ним мы должны располагать их адресами. Именно для хранения этих адресов и предназначены переменные типа va_list, т. е. эти переменные являются указателями.

Если argptr — это переменная типа va_list, то значение выражения sizeof(*argptr) равно 1. Таким образом, можно считать, что базовым типом argptr является char, т. е. тип va_list эквивалентен типу char *.

С переменными типа va_list можно обращаться как с обычными указателями. Например, если мы хотим скопировать значение argptr в argptr2, то можем сделать это так:

va_list argptr2 = argptr;

Таким образом, в нашем случае без макроса va_copy() можно обойтись. Для чего тогда вообще нужен этот макрос? Дело в том, что в других реализациях языка C99 arptr может не быть указателем на char, а являться, например, массивом или указателем на объект, созданный динамически. В таких случаях приведённое выше присваивание, очевидно, будет либо некорректным, либо не даст желаемого результата, т. к. будет скопирован лишь адрес, а не сам объект. В данных ситуациях макрос va_copy() позволит создать второй объект, независимый от первого.

А вот с макросом va_end() ситуация складывается забавная. Похоже, что в нашем случае он вообще не нужен. Я проводил различные эксперименты, вызывая этот макрос до инициализации параметра, который ему передаётся, а также непосредственно перед вызовами va_arg(). Кроме того, я вообще убирал вызов этого макроса из функции с переменным числом аргументов. Иными словами, "хулиганил", как мог. И во всех этих многочисленных случаях функция работала корректно.

Так для чего, всё-таки, нужен va_end()? Я встречал различные объяснения. Например, в одном из источников говорилось, что макрос восстанавливает стек. Имеется в виду, надо полагать, что указатель стека перемещается по стеку в ходе вызовов va_start() и va_arg(), и после окончания работы его нужно вернуть в первоначальное положение. Но зачем вообще трогать указатель стека? Адрес значения текущего параметра прекрасно сохраняется в переменных типа va_list. Указатель стека здесь при чём?

Другое объяснение — макрос va_end() "сбрасывает" (не знаю, что под этим подразумевается) значение своего параметра, чтобы параметр снова можно было использовать в va_start(). Но, во-первых, эксперимент показал, что значение параметра, преданного макросу, не изменяется. А во-вторых, а зачем его вообще сбрасывать перед вызовом va_start()?

Единственное толковое объяснение, которое я встретил, было таким. Если необязательные параметры передаются в функцию не через стек, а через регистры, то макрос va_start() динамически выделяет память, в которую копируются значения параметров из регистров. Разумеется, после использования памяти её нужно освободить. Именно этим, как было сказано в источнике, и занимается va_end().

Кроме этого, можно рассмотреть ещё одну версию. Переменная типа va_list может являться указателем на объект, создаваемый динамически макросом va_start(). Удаление этого объекта (может, это и подразумевалось под "сбрасыванием"?) разумно поручить макросу va_end().

Последние две версии можно, кстати, объединить в третью.

Но в нашем случае ни одна из разумных версий не подходит, т. к. значения параметров передаются через стек, а переменная типа va_list — это простой указатель на char.

А теперь рассмотрим два примера функций с переменным числом аргументов. При их создании будем использовать макросы. Всё будем делать "по науке", как положено. Нужно использовать макрос va_end() — значит будем использовать.

Первый пример функции с переменным числом аргументов

Создадим функцию print() с переменным числом аргументов. Единственный обязательный формальный параметр str будет иметь тип char * и содержать адрес C-строки. Все остальные параметры будут необязательными. Предполагается, что значение каждого необязательного аргумента — это либо целое число, либо вещественное число, либо символ, либо адрес C-строки. Функция будет выводить на консоль значения необязательных параметров в том порядке, в котором они были переданы функции (с тем очевидным исключением, что вместо адресов C-строк будут выводиться сами C-строки).

Информация о количестве необязательных параметров, а также о том, что они собой представляют, должна содержаться в строке, адресуемой str. Количество символов в строке должно совпадать с числом необязательных фактических аргументов. Каждый символ в строке должен  соответствовать своему аргументу и указывать его тип. Разумеется, порядок символов в строке должен соответствовать порядку, в котором связанные с ними аргументы передаются в функцию.

Символ 'i' должен означать, что соответствующий ему параметр содержит значение типа int, символ 'd' — значение типа double, 'c' — символ, 's' — адрес C-строки.

Ниже приведён полный код программы, включающей в себя как функцию print(), так и функцию main(), из которой она вызывается.

 1.#include <stdio.h>
 2.#include <stdarg.h>
 3.
 4.void print(const char *str, ...)
 5.{
 6.    if (!str || !*str)
 7.        return;
 8.    int i;
 9.    double d;
10.    char c, *s;
11.    va_list argptr;
12.    va_start(argptr, str);
13.    do
14.        switch(*str)
15.        {
16.            case 'i':
17.                i = va_arg(argptr, int);
18.                printf("%d", i);
19.                break;
20.            case 'd':
21.                d = va_arg(argptr, double);
22.                printf("%g", d);
23.                break;
24.            case 'c':
25.                c = va_arg(argptr, int);
26.                printf("%c", c);
27.                break;
28.            case 's':
29.                s = va_arg(argptr, char *);
30.                printf("%s", s);
31.                break;
32.        }
33.    while (*++str);
34.    va_end(argptr);
35.}
36.
37.int main()
38.{
39.    print("sicdcd", "Пример: ", 123, '*', 4.56, '=', 123 * 4.56);
40.    return 0;
41.}

Как мы видим, к программе подключён заголовок <stdarg.h> (стр. 2).

Код функции print(), полагаю, достаточно прост. Если в качестве первого параметра передан нулевой адрес или адрес завершающего строку нулевого символа, то заканчиваем работу функции (стр. 6-7). В противном случае объявляем переменные, предназначенные для хранения значений необязательных параметров (стр. 8-10). Далее объявляем и инициализируем макросом va_start() указатель argptr (стр. 11-12).

Затем в цикле do while перебираем все символы строки, адресуемой str (стр. 13-33). В зависимости от значения символа с помощью оператора switch переходим к выполнению определённого набора действий. А именно: получаем из стека значение соответствующего символу типа с помощью макроса va_arg() и сохраняем его в переменной, после чего печатаем посредством функции printf() это значение, или, в случае адреса С-строки, саму C-строку.

Не забываем напоследок вызвать макрос va_end() (стр. 34).

Заметим, что переменные d, c, s используются лишь для наглядности. Можно было бы сразу передавать в функцию printf() результаты, возвращаемые макросом va_arg().

Функция print() вызывается из main(); первой из них, как мы видим, в качестве необязательных фактических аргументов передаются значения всех четырёх видов (но трёх типов, поскольку символы и целые имеют тип int).

В результате запуска программы получаем следующий консольный вывод:

Пример: 123*4.56=560.88

Обратите внимание на то, что символ мы получаем как значение типа int, несмотря на то, что помещаем его в переменную типа char (стр. 25). Я уже писал о том, что значение типа char передать в функцию в качестве необязательного фактического аргумента невозможно. Если бы строка 25 выглядела бы так:

                c = va_arg(argptr, char);

то в ходе компиляции мы получили бы предупреждение:

In function 'print':
[Warning] 'char' is promoted to 'int' when passed through '…'
[Note] (so you should pass 'int' not 'char' to 'va_arg')
[Note] if this code is reached, the program will abort

Программа скомпилировалась бы, но её запуск привёл бы к аварийному завершению.

Второй пример функции с переменным числом аргументов

В некоторых языках программирования, например, в C++ и Java поддерживается перегрузка функций. Под этим понимается сосуществование в одной области видимости функций, имеющих одинаковые имена, но различающихся количеством или (и) типами формальных параметров. Обычно одноимённые функции выполняют, по существу, близкие действия.

Язык C не поддерживает перегрузку функций. Однако, используя функцию с переменным числом аргументов, можно в какой-то степени сымитировать вызовы функций, имеющих одинаковые имена, но принимающих параметры разных типов. Разумеется, при этом каждый раз вызываться будет одна и та же функция.

В качестве очередного примера напишем функцию sum(), принимающую два обязательных параметра и произвольное число необязательных. Предполагается, что необязательные параметры — это либо целые числа, либо вещественные числа, либо адреса C-строк.

Первый обязательный формальный параметр type имеет тип char и содержит один из трёх символов: 'i', 'd' или 's'. Символ означает тип необязательных формальных параметров (int, double или char * соответственно).

Второй обязательный формальный параметр num имеет тип int и содержит число необязательных параметров.

Если необязательные параметры — это целые или вещественные числа, то функция вычисляет их сумму и получает результат в виде значения типа int в первом случае или типа double во втором. А если необязательные параметры — это адреса C-строк, то функция выполняет конкатенацию всех C-строк, получая новую строку. Теперь уже результатом будет адрес новой строки (т. е. значение типа char *).

А какого типа значение будет возвращать функция sum(), если известно, что при разных вызовах она получает результаты разных типов? Мы создадим специальный тип result, представляющий собой объединение. Оно будет иметь 3 поля; типы полей будут совпадать с типами результатов, возвращаемых функцией, т. е. типами полей будут int, double и char *.

Результат работы функции будет записываться в соответствующее поле переменной типа result, после чего значение переменной будет возвращаться в вызывающую функцию. Таким образом, функция sum() будет возвращать значение типа result.

Ниже приведён код программы, содержащей функцию sum().

 1.#include <stdio.h>
 2.#include <stdlib.h>
 3.#include <string.h>
 4.#include <stdarg.h>
 5.
 6.typedef union
 7.{
 8.    int i;
 9.    double d;
10.    char *s;
11.} result;
12.
13.result sum(char type, int num, ...)
14.{
15.    result res;
16.    va_list argptr;
17.    va_start(argptr, num);
18.    int sum1 = 0;
19.    double sum2 = 0.0;
20.    char *str;
21.    switch (type)
22.    {
23.        case 'i':
24.            for (int i = 0; i < num; i++)
25.                sum1 += va_arg(argptr, int);
26.            va_end(argptr);
27.            res.i = sum1;
28.            return res;
29.        case 'd':
30.            for (int i = 0; i < num; i++)
31.                sum2 += va_arg(argptr, double);
32.            va_end(argptr);
33.            res.d = sum2;
34.            return res;
35.        case 's':
36.            for (int i = 0; i < num; i++)
37.                sum1 += strlen(va_arg(argptr, char *));
38.            va_end(argptr);
39.            str = malloc (sum1 + 1);
40.            *str = '\0';
41.            va_start(argptr, num);
42.            for (int i = 0; i < num; i++)
43.                strncat(str, va_arg(argptr, char *), sum1);
44.            va_end(argptr);
45.            res.s = str;
46.            return res;
47.    }
48.    res.s = 0;
49.    return res;
50.}
51.
52.int main()
53.{
54.    printf("3 + 5 + 8 - 6 = %d\n", sum('i', 4, 3, 5, 8, -6).i);
55.    printf("1.2 + 3.4 + 5.6 - 7.8 - 9.1 = %g\n", sum('d', 5, 1.2, 3.4, 5.6, -7.8, -9.1).d);
56.    char *str;
57.    printf("\"Это\" + \" \" + \"строка\" + \"!\" = \"%s\"",
58.           str = sum('s', 4, "Это", " ", "строка", "!").s);
59.    free(str);
60.    return 0;
61.}

Полагаю, что, если вы разобрались с кодом предыдущей программы, то и код этой вам будет несложно понять.

В строке 15 создаётся переменная res типа result. Именно её значение и будет возвращено из функции. Переменные sum1 и sum2 (см. стр. 18, 19) — это аккумуляторы, предназначенные для "накапливания" сумм целых и вещественных чисел соответственно. Переменная str (стр. 20) — это указатель, предназначенный для хранения адреса динамически созданной строки, представляющей собой объединение текстовых строк. Очевидно, в результате одного вызова функции sum() будет задействована только одна из переменных sum1, sum2 и str.

Как и в предыдущей программе, выбор действия, в зависимости от типа необязательных параметров, указанного в переменной type, осуществляется с помощью оператора switch (стр. 21). Обратите внимание на то, что если необязательные параметры — указатели на строки (стр. 35), то значения параметров перебираются дважды. В ходе первого перебора вычисляется сумма длин строк (стр. 36-37). Затем на основании полученной информации выделяется память под результирующую строку (стр. 39). Наконец, в ходе второго перебора выделенная память заполняется самими строками (стр. 42-43).

Результаты, полученные функцией, записываются в поля переменной res в строках 27, 33, 45 непосредственно перед возвратом значения res. Если значение первого параметра функции не совпадает ни с одним из символов 'i', 'd' или 's', то в поле s переменной res записывается 0 (стр. 48), после чего значение res возвращается (стр. 49). Таким образом, в функции res отсутствуют ветви, не возвращающие результат.

Из функции main(), предназначенной для тестирования функции sum(), эта функция вызывается трижды: каждому типу значений необязательных аргументов соответствует свой вызов. После каждого вызова на консоль c помощью функции printf() выводится значение соответствующего поля возвращённого функцией результата (см. стр. 54, 55, 57, 58). Обратите внимание на то, что третий вызов приводит к динамическому созданию строки, поэтому данная строка после вывода на печать уничтожается (стр. 59).

В результате выполнения программы на консоль выводится следующий текст:

3 + 5 + 8 - 6 = 10
1.2 + 3.4 + 5.6 - 7.8 - 9.1 = -6.7
"Это" + " " + "строка" + "!" = "Это строка!"

А можно обойтись без макросов?

Да, можно, поскольку мы имеем представление о том, как "работают" макросы в нашем случае. Но нам потребуется некоторая дополнительная информация. А именно, нам нужно получить ответы на 2 вопроса:

  1. Перебор значений необязательных параметров с помощью макроса va_arg() происходит в порядке увеличения их адресов в памяти, или в порядке уменьшения?
  2. Память какого размера отводится на хранение значения необязательного параметра того или иного типа в стеке?

Заметим, что, вообще говоря, размер памяти, выделяемой в стеке под значение параметра, необязательно совпадает с размером значения, а может его превосходить.

Ответы на поставленные вопросы несложно получить с помощью экспериментов. Я немного модернизировал функцию print() первой программы, вставив код, распечатывающий значения argptr после каждого вызова макросов va_start() и va_arg(). Выяснилось, что перебор значений необязательных параметров происходит в порядке возрастания их адресов в памяти. Стало также понятно, что как для хранения адреса, так и для хранения значения типа int или double, в стеке отводится 8 байтов.

Заметим, что размер переменной типа int составляет 4 байта. Таким образом, при хранении значения этого типа используется только половина отведённой памяти. Оказалось, что задействованы младшие 4 байта из 8 (под "младшими" понимаются имеющие меньшие адреса).

Этой информации вполне достаточно, чтобы написать новую версию функции sum() первой программы. Приводим код новой версии программы целиком:

 1.#include <stdio.h>
 2.
 3.void print(const char *str, ...)
 4.{
 5.    if (!str || !*str)
 6.        return;
 7.    int i;
 8.    double d;
 9.    char c, *s;
10.    char *p = (char *) &str;
11.    do
12.    {
13.        p += 8;
14.        switch(*str)
15.        {
16.            case 'i':
17.                i = *(int *) p;
18.                printf("%d", i);
19.                break;
20.            case 'd':
21.                d = *(double *) p;
22.                printf("%g", d);
23.                break;
24.            case 'c':
25.                c = *(int *) p;
26.                printf("%c", c);
27.                break;
28.            case 's':
29.                s = *(char **) p;
30.                printf("%s", s);
31.                break;
32.        }
33.    }
34.    while (*++str);
35.}
36.
37.int main()
38.{
39.    print("sicdcd", "Пример: ", 123, '*', 4.56, '=', 123 * 4.56);
40.    return 0;
41.}

Как мы видим, файл stdarg.h уже не подключается за ненадобностью. Вместо объекта argptr мы используем p — указатель на char. Объявляем его и инициализируем адресом последнего обязательного параметра (см. стр. 10). Теперь, если мы увеличим его на 8, то получим уже адрес первого необязательного.

На каждой итерации цикла do while, вычисляя адрес очередного значения необязательного параметра посредством увеличения значения p на 8 (стр. 13), получаем само значение и сохраняем его в одной из переменных i, d, c или s (см. стр. 17, 21, 25 и 29). Значение получаем так: сначала приводим адрес, хранящийся в p, к типу указателя на нужный нам тип, после чего полученный адрес разыменовываем.

Можно сказать, что вызову va_start() в старой версии функции соответствует инициализация p и увеличение значения p на 8 в начале первой итерации цикла do while . Макросу va_arg() соответствует получение значения параметра в одной из строк 17, 21, 25 и 29 и увеличение значения p на 8 на следующей итерации цикла.

Поскольку после получения значения последнего необязательного параметра значение p уже не изменяется, по окончании цикла p содержит адрес последнего параметра. А в случае использования va_arg() по окончании цикла в argptr находился адрес первого байта, следующего за 8-байтовой ячейкой стека, содержащей значение последнего необязательного параметра.

Макросу va_end никакой код в новой версии функции не соответствует.

Функция main() остаётся без изменений, как не изменяется и результат работы программы, выводимый на консоль.

Преимущество кода, не использующего макросы, определённые в файле stdarg.h, заключается в том, что он очень гибок. При желании мы можем "перепрыгивать" с одного значения необязательного параметра на другое, двигаться по стеку в любом направлении любое количество раз. В случае использования макросов наши возможности несколько ограничены: мы можем двигаться по стеку лишь в одном направлении. Чтобы вернуться к какому-либо уже пройденному значению, необходимо начинать весь путь заново.

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

А вот код функции print(), приведённый в данном разделе, мало того, что платформозависим и компиляторозависим, так ещё может зависеть даже от настроек компилятора. Стоит только, например, изменить порядок заталкивания значений параметров в стек, так сразу код перестанет быть работоспособным.

Кстати, мы так и не установили, в каком порядке значения параметров заталкиваются в стек в нашем случае. Мы лишь выяснили, что перебор значений идёт в сторону увеличения адресов. Но адреса могут возрастать как в направлении от вершины стека к его дну, так и в обратном направлении.

Впрочем, для нас порядок заталкивания значений параметров в стек и не важен. Однако для интереса можно попробовать его установить. Можно, например, написать произвольную функцию fun(), принимающую 3 параметра типа int, после чего вызвать её следующим способом:

fun(printf("1"), printf("2"), printf("3"));

В нашем случае на консоль будет выведена строка "321". Если предположить, что значения фактических аргументов вычисляются в том же порядке, в котором они заталкиваются в стек, а это, скорее всего, так и есть, то можно сделать вывод о том, что заталкиваются они в обратном порядке (справа налево). Получается, что, перебирая значения необязательных параметров, мы движемся от вершины стека к его дну, а сам стек растёт в сторону уменьшения адресов.

Меры предосторожности

Как мы уже выяснили, в языке C не существует механизмов, позволяющих, "находясь" в теле функции, точно узнать число необязательных параметров и их типы. Извлекая эту информацию из обязательных параметров, мы вынуждены доверять вызывающей функции, которая всегда может нас "обмануть".

Разумеется, программист, вызывая функцию с переменным числом аргументов и передавая ей неверную информацию об их количестве и типах, обманывает, в первую очередь, сам себя, так что делать это ему невыгодно. Но такие "обманы" могут быть непредумышленными и допущенными по невнимательности, поэтому с функциями, принимающими переменное число аргументов, нужно работать аккуратно.

А вот предумышленный обман может произойти со стороны пользователя программы, при условии, что информация, полученная от него, без проверок передаётся функции с переменным числом аргументов. Такие рода ситуации описаны в статье хорошо известного Криса Касперски "Неизвестная уязвимость функции printf".

Коротко перескажу её содержание. Рассматривается программа, принимающая пароль, введённый пользователем, и сравнивающая его с истинным. Если пароль неверен, она печатает соответствующее сообщение, выводя на консоль неверный пароль, полученный от пользователя, причём последний выводится с помощью функции printf(), которой передаётся единственный аргумент.

Если пользователь в качестве неверного пароля введёт строку, содержащую спецификаторы формата, то, тем самым, "обманет" функцию printf(), в результате чего она будет выводить на печать, как ей "кажется", значения необязательных параметров, хотя они отсутствуют.

Но на самом деле печататься будет содержимое стека, которое вовсе не предназначено для чужих глаз. И вполне может оказаться так, что напечатан будет верный пароль. Описанный случай, как мне кажется, достаточно искусственный. Однако, теоретически такое случиться может. Статья интересная, советую её прочитать.

А у меня на этом всё.

Скачать исходный код