Stdarg и printf() в C
Заголовочный файл <stdarg.h>
используется, чтобы заставить функции принимать undefined количество аргументов, правильно?
Итак, printf()
funtion <stdio.h>
должен использовать <stdarg.h>
для принятия переменного количества аргументов (пожалуйста, исправьте меня, если я ошибаюсь).
Я нашел следующие строки в файле stdio.h файла gcc:
#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
# ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
# define _VA_LIST_DEFINED
# endif
# else
# include <stdarg.h>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif
Я не могу понять большую часть того, что в нем, но он включает в себя <stdarg.h>
Итак, если printf()
использует <stdarg.h>
для принятия переменного количества аргументов, а stdio.h
имеет printf()
, программа C, использующая printf()
, не должна включать <stdarg.h>
, это?
Я попробовал программу с printf()
и определяемую пользователем функцию, принимающую переменное число аргументов.
Программа, которую я пробовал, это:
#include<stdio.h>
//#include<stdarg.h>///If this is included, the program works fine.
void fn(int num, ...)
{
va_list vlist;
va_start(vlist, num);//initialising va_start (predefined)
int i;
for(i=0; i<num; ++i)
{
printf("%d\n", va_arg(vlist, int));
}
va_end(vlist);//clean up memory
}
int main()
{
fn(3, 18, 11, 12);
printf("\n");
fn(6, 18, 11, 32, 46, 01, 12);
return 0;
}
Он отлично работает, если включен <stdarg.h>
, но в противном случае генерируется следующая ошибка:
40484293.c:13:38: error: expected expression before ‘int’
printf("%d\n", va_arg(vlist, int));//////error: expected expression before 'int'/////////
^~~
Как это?
Или это то, что printf()
не использует <stdarg.h>
для принятия переменного количества аргументов?
Если да, то как это делается?
Ответы
Ответ 1
Рассмотрим:
stdio.h:
int my_printf(const char *s, ...);
Вам нужно <stdarg.h>
? Нет, вы этого не делаете. ...
является частью грамматики языка - он "встроен". Однако, как только вы захотите сделать что-нибудь значимое и портативное с таким списком аргументов, вам нужны имена: va_list
, va_start
и т.д.
stdio.c:
#include "stdio.h"
#include "stdarg.h"
int my_printf(const char *s, ...)
{
va_list va;
va_start(va, s);
/* and so on */
}
Но это будет необходимо, по сути, в реализации вашего libc, чего вы не видите, если не скомпилируете библиотеку самостоятельно. Вместо этого вы получаете библиотеку libc, которая уже скомпилирована для машинного кода.
Итак, если printf() использует для принятия переменного числа аргументы и stdio.h имеет printf(), программу C с использованием функции printf() не включать?
Даже если бы это было так, вы не можете полагаться на это, иначе ваш код будет плохо сформирован: вы должны включать все заголовки в любом случае, если используется имя, принадлежащее им, независимо от того, выполняется ли реализация уже делает это или нет.
Ответ 2
Файл заголовка stdarg используется для принятия функций accept undefined number аргументов, правильно?
Нет, <stdarg.h>
просто предоставляет API, который должен использоваться для доступа к дополнительным аргументам. Нет необходимости включать этот заголовок, если вы хотите просто объявить функцию, которая принимает переменное количество аргументов, например:
int foo(int a, ...);
Это языковая функция и не требует дополнительных деклараций/определений.
Я нашел следующие строки в файле stdio.h файла gcc:
#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
# ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
# define _VA_LIST_DEFINED
# endif
# else
# include <stdarg.h>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif
Я предполагаю, что этот материал требуется только для объявления таких вещей, как vprintf()
без внутреннего включения <stdarg.h>
:
int vprintf(const char *format, va_list ap);
В довершение:
- Заголовок, объявляющий функцию с переменным числом аргументов, не должен включать
<stdarg.h>
внутренне.
- Реализация функции с переменным числом аргументов должна включать
<stdarg.h>
и использовать API va_list
для доступа к дополнительным аргументам.
Ответ 3
Нет, чтобы использовать printf()
все, что вам нужно, это #include <stdio.h>
. Нет необходимости в stdarg, потому что printf
уже скомпилирован. Компилятору нужно только увидеть прототип для printf
, чтобы знать, что он является вариационным (полученным из эллипсиса ...
в прототипе). Если вы посмотрите на исходный код библиотеки stdio для printf
, вы увидите включенную <stdarg.h>
.
Если вы хотите написать свою собственную вариационную функцию, вы должны #include <stdarg.h>
и соответственно использовать свои макросы. Как вы можете видеть, если вы забудете это сделать, символы va_start/list/end
неизвестны компилятору.
Если вы хотите увидеть реальную реализацию printf
, посмотрите код в стандартном источнике ввода/вывода FreeBSD, а также источник для vfprintf
.
Ответ 4
Сначала я отвечу на ваш вопрос в терминах стандарта C, потому что это то, что говорит вам, как вы должны писать свой код.
Для стандарта C требуется stdio.h
"вести себя как-будто", он не включает stdarg.h
. Другими словами, макросы va_start
, va_arg
, va_end
и va_copy
и тип va_list
должны быть недоступны, включив stdio.h
. Другими словами, эта программа не требуется компилировать:
#include <stdio.h>
unsigned sum(unsigned n, ...)
{
unsigned total = 0;
va_list ap;
va_start(ap, n);
while (n--) total += va_arg(ap, unsigned);
va_end(ap);
return total;
}
(Это отличие от С++. В С++ все стандартные заголовки библиотек разрешены, но не обязательно, чтобы включать друг друга.)
Верно, что реализация printf
(возможно) использует механизм stdarg.h
для доступа к своим аргументам, но это просто означает, что некоторые файлы в исходном коде для библиотеки C ( "printf.c
", возможно ) необходимо включить stdarg.h
, а также stdio.h
; что не влияет на ваш код.
Также верно, что stdio.h
объявляет функции, принимающие аргументы va_list
-typed. Если вы посмотрите на эти объявления, вы увидите, что они фактически используют имя typedef, которое начинается с двух символов подчеркивания или подчеркивания и заглавной буквы: например, с тем же stdio.h
, на который вы смотрите,
$ egrep '\<v(printf|scanf) *\(' /usr/include/stdio.h
extern int vprintf (const char *__restrict __format, _G_va_list __arg);
extern int vscanf (const char *__restrict __format, _G_va_list __arg);
Все имена, начинающиеся с двух символов подчеркивания или подчеркивания и заглавной буквы, зарезервированы для реализации - stdio.h
разрешено объявлять столько имен, сколько захочет. И наоборот, вам, программисту приложений, не разрешается объявлять какие-либо такие имена или использовать те, которые заявляет реализация (за исключением документированных подмножеств, таких как _POSIX_C_SOURCE
и __GNUC__
). Компилятор позволит вам сделать это, но эффекты undefined.
Теперь я расскажу о том, что вы указали из stdio.h
. Здесь он снова:
#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
# ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
# define _VA_LIST_DEFINED
# endif
# else
# include <stdarg.h>
# endif
#endif
Чтобы понять, что это делает, вам нужно знать три вещи:
-
Недавние "проблемы" POSIX.1, официальная спецификация того, что означает быть операционной системой Unix, добавить va_list
к множеству вещей stdio.h
. (В частности, в Проблема 6, va_list
определяется stdio.h
как расширение "XSI", а в Issue 7 обязателен.) Этот код определяет va_list
, но только если программа запросила функции Issue 6 + XSI или Issue 7; что означает #if defined __USE_XOPEN || defined __USE_XOPEN2K8
. Обратите внимание, что _G_va_list
использует _G_va_list
, чтобы определить va_list
, так же как и в других местах, он использовал _G_va_list
для объявления vprintf
. _G_va_list
уже доступен.
-
Вы не можете записать один и тот же typedef
дважды в одну и ту же единицу перевода. Если stdio.h
определено va_list
, не уведомив stdarg.h
не повторять это,
#include <stdio.h>
#include <stdarg.h>
не будет компилироваться.
-
GCC поставляется с копией stdarg.h
, но не поставляется с копией stdio.h
. Вы цитируете stdio.h
из GNU libc, который представляет собой отдельный проект под зонтиком GNU, поддерживаемый отдельной (но перекрывающейся) группа людей. Крайне важно, что заголовки GNU libc не могут предположить, что они скомпилированы GCC.
Итак, код, который вы указали, определяет va_list
. Если определено __GNUC__
, что означает, что компилятор является либо GCC, либо клоном, совместимым с quirk, он предполагает, что он может связываться с stdarg.h
с использованием макроса с именем _VA_LIST_DEFINED
, который определяется тогда и только тогда, когда va_list
а макрокоманда - с помощью #if
. stdio.h
может определить va_list
сам, а затем определить _VA_LIST_DEFINED
, а затем stdarg.h
не будет этого делать, а
#include <stdio.h>
#include <stdarg.h>
будет компилироваться отлично. (Если вы посмотрите на GCC stdarg.h
, который, вероятно, скрывается в /usr/lib/gcc/something/something/include
в вашей системе, вы увидите зеркальное изображение этого кода вместе с веселым длинным списком других макросов, которые также означают "не определять va_list
, я уже сделал это" для других библиотек C, которые GCC может использовать или использовать один раз с помощью.)
Но если __GNUC__
не определено, то stdio.h
предполагает, что он не знает, как связаться с stdarg.h
. Но он знает, что безопасно включать stdarg.h
дважды в один и тот же файл, потому что стандарт C требует, чтобы это работало. Поэтому, чтобы определить va_list
, он просто идет вперед и включает в себя stdarg.h
, и, таким образом, макросы va_*
, которые не должны определять stdio.h
, также будут определены.
Это то, что люди HTML5 назовут "преднамеренным нарушением" стандарта C: это неправильно, потому что неправильное в этом случае менее вероятно, нарушит реальный код, чем любая доступная альтернатива. В частности,
#include <stdio.h>
#include <stdarg.h>
в большей степени появляется в реальном коде, чем
#include <stdio.h>
#define va_start(x, y) /* something unrelated to variadic functions */
поэтому гораздо важнее сделать первую работу, чем вторую, хотя оба они должны работать.
Наконец, вы все еще можете задаться вопросом, откуда пришел черт _G_va_list
. Он не определен нигде в stdio.h
сам, поэтому он должен быть либо встроенным компилятором, либо быть определен одним из заголовков stdio.h
. Здесь вы узнаете все, что включает в себя заголовок системы:
$ echo '#include <stdio.h>' | gcc -H -xc -std=c11 -fsyntax-only - 2>&1 | grep '^\.'
. /usr/include/stdio.h
.. /usr/include/features.h
... /usr/include/x86_64-linux-gnu/sys/cdefs.h
.... /usr/include/x86_64-linux-gnu/bits/wordsize.h
... /usr/include/x86_64-linux-gnu/gnu/stubs.h
.... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h
.. /usr/lib/gcc/x86_64-linux-gnu/6/include/stddef.h
.. /usr/include/x86_64-linux-gnu/bits/types.h
... /usr/include/x86_64-linux-gnu/bits/wordsize.h
... /usr/include/x86_64-linux-gnu/bits/typesizes.h
.. /usr/include/libio.h
... /usr/include/_G_config.h
.... /usr/lib/gcc/x86_64-linux-gnu/6/include/stddef.h
.... /usr/include/wchar.h
... /usr/lib/gcc/x86_64-linux-gnu/6/include/stdarg.h
.. /usr/include/x86_64-linux-gnu/bits/stdio_lim.h
.. /usr/include/x86_64-linux-gnu/bits/sys_errlist.h
Я использовал -std=c11
, чтобы убедиться, что я не компилировался в режимах POSIX Issue 6 + XSI или Issue 7, но все равно мы видим stdarg.h
в этом списке - не включается непосредственно stdio.h
, а libio.h
, который не является стандартным заголовком. Давайте посмотрим там:
#include <_G_config.h>
/* ALL of these should be defined in _G_config.h */
/* ... */
#define _IO_va_list _G_va_list
/* This define avoids name pollution if we're using GNU stdarg.h */
#define __need___va_list
#include <stdarg.h>
#ifdef __GNUC_VA_LIST
# undef _IO_va_list
# define _IO_va_list __gnuc_va_list
#endif /* __GNUC_VA_LIST */
So libio.h
включает stdarg.h
в специальном режиме (здесь другой случай, когда макросы реализации используются для связи между заголовками системы) и ожидает, что он определит __gnuc_va_list
, но использует его для определения _IO_va_list
, не _G_va_list
. _G_va_list
определяется _G_config.h
...
/* These library features are always available in the GNU C library. */
#define _G_va_list __gnuc_va_list
... в терминах __gnuc_va_list
. Это имя определяется stdarg.h
:
/* Define __gnuc_va_list. */
#ifndef __GNUC_VA_LIST
#define __GNUC_VA_LIST
typedef __builtin_va_list __gnuc_va_list;
#endif
И __builtin_va_list
, наконец, является недокументированным GCC внутренним, что означает "любой тип подходит для va_list
с текущим ABI".
$ echo 'void foo(__builtin_va_list x) {}' |
gcc -xc -std=c11 -fsyntax-only -; echo $?
0
(Да, реализация GNU libc для stdio намного сложнее, чем у любого из нас есть оправдание. Объяснение заключается в том, что в старые времена люди пытались сделать свой объект FILE
непосредственно используемым как С++ filebuf
. не работал в течение десятилетий - на самом деле, я не уверен, что он когда-либо работал, он был оставлен до EGCS, который так же, как я знаю историю, но есть много, много остатков попытки, которая все еще висит вокруг, либо для двоичной обратной совместимости, либо потому, что никто не собирался их очищать.)
(Да, если я правильно это читаю, GNU libc stdio.h
не будет работать с компилятором C, чей stdarg.h
не определяет __gnuc_va_list
. Это абстрактно неправильно, но безвредно, любой, кто хочет блестящий новый компилятор, не совместимый с GCC для работы с GNU libc, будет иметь гораздо больше вещей, о которых можно беспокоиться.)
Ответ 5
Основы разделения модуля на файл заголовка и исходный файл:
- В файле заголовка вы помещаете только интерфейс своего модуля
- В исходном файле вы помещаете реализацию своего модуля
Таким образом, даже если реализация printf
использует va_arg
, поскольку вы предполагаете:
- В
stdio.h
автор объявил int printf(const char* format, ...);
- В
stdio.c
автор реализовал printf
с помощью va_arg
Ответ 6
Эта реализация stdio.h
не включает stdarg.h
при компиляции с gcc. По волшебству работает, что у авторов компилятора всегда есть рукава.
Ваши исходные файлы C должны включать в себя все системные заголовки, которые они ссылаются в любом случае. Это требование стандарта C. То есть, если ваш исходный код требует определений, присутствующих в stdarg.h, он должен содержать директиву #include <stdarg.h>
либо напрямую, либо в одном из ваших файлов заголовков, который он включает. Он не может полагаться на stdarg.h, который включен в другие стандартные заголовки, даже если они действительно включают его.
Ответ 7
Файл <stdarg.h>
должен быть включен только в том случае, если вы собираетесь реализовать переменное количество аргументов function. Не нужно было использовать printf(3)
и друзей. Только если вы собираетесь обрабатывать аргументы с переменным числом функции args, вам понадобится тип va_list
и макросы va_start
, va_arg
и va_end
. Итак, только тогда вам нужно будет принудительно включить этот файл.
В общем, вам не гарантируется, что <stdarg.h>
будет включен только с включением <stdio.h>
В самом деле, код, который вы цитируете, включает только его, если __GNU_C__
не определен (который я подозрительный, это так, поэтому он не включен в ваш случай), и этот макрос определяется, если вы используете компилятор gcc
.
Если вы собираетесь создавать функции передачи аргументов переменной в коде, лучший подход заключается не в том, чтобы включить в него другой включенный файл, но делать это самостоятельно (как клиент для запрошенной функции), где вы используете тип va_list
или макросы va_start
, va_arg
или va_end
.
Раньше была некоторая путаница в отношении двойного включения, поскольку некоторые файлы заголовков не были защищены от двойного включения (в том числе дважды или более раз тот же самый включенный файл приводил к ошибкам о двукратно определенных макросах или аналогичных, и вам приходилось с осторожностью), но сегодня это не проблема, и обычно все стандартные поля заголовков защищены от двойного включения.
Ответ 8
Хорошо, существует "обычное" семейство printf: printf, fprintf, dprintf, sprintf и snprintf.
А затем переменное количество аргументов printf family: vprintf, vfprintf, vdprintf, vsprintf и vsnprintf.
Чтобы использовать переменный список аргументов с помощью либо, вам нужно объявить stdarg.h.
stdarg.h определяет все используемые вами макросы: va_list, va_start, va_arg, va_end и va_copy.