我首先将根据C标准回答您的问题,因为它告诉您应该如何编写代码。
C标准要求 stdio.h
的行为“好像”没有包含stdarg.h
。换句话说,包括stdio.h
不需要提供宏va_start
、va_arg
、va_end
和va_copy
,以及类型va_list
。换句话说,这个程序不被允许编译:
#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
引用的东西。 再次展示:
typedef _G_va_list va_list;
要理解这个在做什么,你需要知道三件事:
POSIX.1的最新“问题”是关于Unix操作系统的官方规范,将va_list
添加到stdio.h
应该定义的内容中。(具体来说,在Issue 6中,va_list
被定义为“XSI”扩展,而在Issue 7中则是强制性的。)此代码定义了va_list
,但仅当程序请求Issue 6+XSI或Issue 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
不要再次定义,则以下代码将无法编译:
GCC附带了一个stdarg.h
的副本,但它没有附带stdio.h
的副本。您引用的stdio.h
来自GNU libc,这是GNU伞下的一个单独项目,由一个单独的(但有重叠的)人员组织维护。至关重要的是,GNU libc的头文件不能假定它们正在被GCC编译。
所以,你引用的代码定义了
va_list
。如果定义了
__GNUC__
,这意味着编译器是GCC或兼容的克隆版本,它会假设可以使用名为
_VA_LIST_DEFINED
的宏与
stdarg.h
进行通信,仅当
va_list
被定义时才定义该宏,但作为宏,你可以使用
#if
检查它。
stdio.h
可以自己定义
va_list
,然后定义
_VA_LIST_DEFINED
,那么
stdarg.h
就不会这样做。
将编译良好。(如果您查看GCC的stdarg.h,在您系统的/ usr / lib / gcc / something / something / include下可能会隐藏,您将看到这段代码的镜像,以及一长串其他宏,也表示“不要定义va_list,我已经这样做”为了其他可以或曾经可以与GCC一起使用的C库。)
但是,如果未定义__GNUC__,那么stdio.h会认为它不知道如何与stdarg.h通信。但它确实知道在同一文件中安全地包含stdarg.h,因为C标准要求此操作有效。因此,为了定义va_list,它只需继续包含stdarg.h,因此,stdio.h不应该定义的va_*宏也将被定义。
这是HTML5人们所说的C标准的“故意违规”:它是有意错误的,因为以这种方式出错比任何可用的替代方案更不太可能破坏实际代码。特别是,
比起
在真实代码中出现的可能性要压倒性地大得多
#include <stdio.h>
#define va_start(x, y)
因此,让第一个工作起来比第二个更重要,尽管两者都应该能够工作。
最后,你可能仍然想知道_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>
#define _IO_va_list _G_va_list
#define __need___va_list
#include <stdarg.h>
#ifdef __GNUC_VA_LIST
# undef _IO_va_list
# define _IO_va_list __gnuc_va_list
#endif
所以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. */
...在__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在使用不定义__gnuc_va_list的C编译器的stdarg.h时将无法正常工作。这在抽象上是错误的,但是无害的; 想要使用全新的非GCC兼容编译器与GNU libc一起工作的人将会有更多的事情需要担心。)
va_arg
is a macro defined intostdarg.h
. So it is mandatory to includestdarg.h
or to provide the macro for your project.... Same thing forva_list
va_start
andva_end
- LPs