我将首先根据 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;
}
(这与 C++ 不同。在 C++ 中,允许但不要求所有标准库头相互包含。)
确实,printf 的实现(可能)使用stdarg.h 机制来访问它的参数,但这只是意味着源代码中的某些文件 C 库(可能是“printf.c”)需要包含stdarg.h 以及stdio.h;这不会影响您的代码。
stdio.h 声明了接受va_list 类型参数的函数也是如此。如果您查看这些声明,您会发现它们实际上使用了一个以两个下划线或一个下划线和一个大写字母开头的 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__) .编译器会让你这样做,但效果是不确定的。
现在我要谈谈你从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 应该定义的集合中。 (具体来说,在Issue 6 中,va_list 由stdio.h 定义为“XSI”扩展,而在Issue 7 中它是强制性的。)此代码定义va_list,但前提是程序已请求问题 6+ XSI 或第 7 期功能;这就是#if defined __USE_XOPEN || defined __USE_XOPEN2K8 的意思。请注意,它使用_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 的克隆,它假定它可以使用名为 _VA_LIST_DEFINED 的宏与 stdarg.h 通信,当且仅当 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 中,您会看到这段代码的镜像,以及一长串 other 宏表示“不要定义va_list,我已经这样做了”对于其他 GCC 可以或曾经可以使用的C 库。)
但如果 __GNUC__ 未定义,则 stdio.h 假定它确实不知道如何与 stdarg.h 通信。但它确实知道在同一个文件中包含两次stdarg.h 是安全的,因为 C 标准要求它工作。因此,为了定义va_list,它会继续并包含stdarg.h,因此,stdio.h 应该定义的va_* 宏也将被定义。
这就是 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 问题 6+XSI 或问题 7 模式下编译,但我们仍然在此列表中看到 stdarg.h — 不直接包含在 @ 987654398@,但通过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 */
所以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 内在函数,意思是“使用当前 ABI 适合 va_list 的任何类型”。
$ echo 'void foo(__builtin_va_list x) {}' |
gcc -xc -std=c11 -fsyntax-only -; echo $?
0
(是的,GNU libc 的 stdio 实现比它有任何借口要复杂得多。解释是在过去人们试图使其 FILE 对象直接用作一个 C++ filebuf。几十年来都没有工作过——事实上,我不确定它是否曾经工作过;它在 EGCS 之前就被放弃了,这可以追溯到我了解历史——但仍有许多尝试的痕迹仍然存在,要么是为了二进制向后兼容,要么是因为没有人清理它们。)
(是的,如果我没看错的话,GNU libc 的 stdio.h 将无法与 stdarg.h 未定义 __gnuc_va_list 的 C 编译器一起正常工作。这在抽象上是错误的,但无害;任何想要与 GNU libc 一起工作的闪亮的新的非 GCC 兼容编译器将会有更多的事情需要担心。)