C语言中的stdarg和printf()函数

19

<stdarg.h>头文件用于使函数接受未定义数量的参数,对吧?

因此,<stdio.h>中的printf()函数必须使用<stdarg.h>来接受可变数量的参数(如果我错了,请纠正我)。

我在gcc的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>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif

我不能理解其中大部分的内容,但它似乎包括了<stdarg.h>

因此,如果printf()使用<stdarg.h>来接受可变数量的参数,并且stdio.hprintf(),那么使用printf()的C程序是否需要包含<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>来接受可变数量的参数吗?如果是这样,那么该如何实现?

3
va_arg is a macro defined into stdarg.h. So it is mandatory to include stdarg.h or to provide the macro for your project.... Same thing for va_list va_start and va_end - LPs
8个回答

13

请考虑:

stdio.h:

int my_printf(const char *s, ...); 

你需要<stdarg.h>吗?不需要,因为它是语言的一部分 - 它是“内置”的。但是,一旦你想要对这样的参数列表进行任何有意义且可移植的操作,你需要在其中定义名称va_listva_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的实现中是必要的,除非你自己编译库,否则你看不到它。相反,你得到的是已经被编译成机器码的共享库。

所以,如果printf()使用...来接受可变数量的参数,并且stdio.h有printf(),那么使用printf()的C程序是否需要包含...呢?

即使是这样,你不能依赖它,否则你的代码是不正确的:如果使用了它们属于的名称,则必须无论实现是否已经包含它们,都要包含所有的头文件。


1
有没有办法在我的电脑上查看这个libc共享库文件?我的意思是,我知道我不能打开它,但只是想看看它在哪里。我正在使用Ubuntu 15.10。 - J...S
1
编译一些虚拟的 C 代码并运行 ldd prog 命令,你会看到 libc.so。在我的系统中,它位于 /usr/lib 目录下。 - edmz
1
ldd prog?这是什么?抱歉,我对此很陌生! - J...S
2
@J...S 打开终端并输入 ldd,然后跟上一个 C 可执行文件的名称(我使用了 prog 来表示,但它可以是任何东西 :) - edmz
1
@cat:你是对的,但原因不同。我的观点是f(int arg...)在C语言中是有效的,但在C++中才是有效的。那个评论无论如何都是无用的。 - edmz
显示剩余2条评论

13

我首先将根据C标准回答您的问题,因为它告诉您应该如何编写代码。

C标准要求 stdio.h 的行为“好像”没有包含stdarg.h。换句话说,包括stdio.h不需要提供宏va_startva_argva_endva_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引用的东西。 再次展示:

#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

要理解这个在做什么,你需要知道三件事:

  1. 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

  2. 在同一翻译单元中不能写两个相同的typedef。如果stdio.h定义了va_list,而没有以某种方式通知stdarg.h不要再次定义,则以下代码将无法编译:

    #include <stdio.h>
    #include <stdarg.h>
    
  3. 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就不会这样做。
#include <stdio.h>
#include <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>
#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 */

所以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在使用不定义__gnuc_va_list的C编译器的stdarg.h时将无法正常工作。这在抽象上是错误的,但是无害的; 想要使用全新的非GCC兼容编译器与GNU libc一起工作的人将会有更多的事情需要担心。)


如果我正确理解了你的反对意见,"_它不知道如何告诉stdarg.h“嗨,我已经定义了va_list,请不要再次定义”_,那么它确实可以:使用宏(包括保护或类似VA_LIST_DEFINED的东西)或者仍然是编译器的魔法。也许我又错过了你的观点。 - edmz
@black 是的,这就是在 __GNUC__ 情况下使用 #define _VA_LIST_DEFINED 的作用。问题在于 stdio.h 属于 glibc,而 stdarg.h 属于编译器。如果编译器不是 GCC(或者是一个定义了 __GNUC__ 的兼容编译器,比如 clang),glibc 不能假设 #define _VA_LIST_DEFINED 能够正常工作。 - zwol
看了一下 clangstdarg,它似乎适应了 GCC 的用法。涉及到这个方面的问题会很有趣。 - edmz
1
@J...S 除了一小部分例外,是的,那是正确的。从正式的角度来看,编译器会让你这样做,但后果是未定义的。 - zwol
我觉得你的回答比我的更好,更全面,所以如果您不介意的话,我将请OP将您的回答标记为被接受的答案。 - edmz
显示剩余4条评论

11

stdarg头文件用于让函数接受未定义数量的参数,对吗?

不,<stdarg.h>只是提供了一个API,用于访问额外的参数。如果您只想声明接受可变数量参数的函数,则没有必要包含该头文件,像这样:

int foo(int a, ...);

这是一种语言特性,不需要额外的声明或定义。

我在gcc的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>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif
我猜这种东西仅仅是为了声明像vprintf()这样的函数,而不需要内部引用<stdarg.h>
int vprintf(const char *format, va_list ap);

更棒的是:

  • 声明可变参数函数的头文件不应该在内部包括<stdarg.h>
  • 实现可变参数函数必须包括<stdarg.h>,并使用va_list API来访问额外的参数。

int vprintf(const char *format, va_list ap); 这句话的意思是 va_list 可以替换成省略号(...)吗?这样做有什么优势吗? - J...S
@J...S 不行,你不能这样做。va_list 是一种类型。这种类型被设计用于保存“...”函数的额外参数信息。您可以将其视为允许您按顺序获取额外参数的上下文。然而,vprintf() 只接受两个参数,而 printf() 则需要一个或多个参数。 - Sergio
@J...S 这个页面可能对你有帮助。 - Sergio
@J...S,很难说vprintf()printf()有什么优势。它们只是为不同的用例而设计。printf()在大多数情况下都很有用,但有时您可能想将函数的额外参数传递给另一个函数,在这种情况下,va_list很有帮助。 - Sergio
OP找到的代码似乎是实现POSIX.1-2008-only对于stdio.h声明va_list的要求 - 它与vprintf无关。 - zwol

9
不,要使用printf(),你只需要#include <stdio.h>。因为printf已经被编译了,所以不需要stdarg。编译器只需要看到printf的原型就知道它是可变参数的(原型中的省略号...)。如果你查看printf的,你会看到<stdarg.h>被包含在其中。
如果你想编写自己的可变参数函数,你必须#include <stdarg.h>并相应地使用它的宏。如你所见,如果忘记这样做,va_start/list/end符号对编译器来说是未知的。
如果您想看到一个真正的printf实现,请查看FreeBSD标准I/O源代码中的代码,以及vfprintf的源代码

4
<stdio.h>头文件中,并没有实现printf()函数的代码,它只是一个包含函数、变量和类型等声明的头文件。相应的代码已经编译完成,并包含在C运行库中(在许多Linux系统上是glibc.so)。请注意,这里的"implement"翻译为"实现","declaration"翻译为"声明"。 - unwind
2
@J...S 已经编译的意思就是这样。<stdio.h>头文件只包含printf函数的原型;printf代码通常在一个已编译代码库中,称为C库,通常在名为libc.alibc.so的文件中,并在编译器将程序链接成可执行文件时使用。尝试使用gcc -v命令启用打印它运行的命令。 - Jens
1
@J...S 你的意思是什么?你是指运行时的源代码吗?如果是的话,当然可以浏览他们的git仓库。但请注意,C运行时不是一个小项目。 - unwind
2
@J...S,除非您进行静态链接,否则不会“拿走”任何代码。通常情况下,您会有整个运行时库的实例,并且在需要时,即调用printf()时,您的代码会跳转到其中。 - unwind
@J...S 你的预编译 C 库可能在文件 /lib/libc.so.6 中。你也可能有一个 libc.a 文件,它与之类似但是用于静态链接(如果你告诉编译器使用 libc.a,它将把 printf 和其他库函数的代码复制到你的程序中,而默认情况下,使用动态链接,你的程序将在运行时加载 libc.so 并使用其中的代码)。 - Chortos-2
显示剩余10条评论

2
模块拆分为头文件和源文件的基础知识:
  • 在头文件中,只需放置模块的接口
  • 在源文件中,放置模块的实现
因此,即使 printf 的实现使用了 va_arg(正如您猜测的那样):
  • stdio.h 中,作者仅声明了 int printf(const char* format, ...);
  • stdio.c 中,作者使用 va_arg 实现了 printf

stdio.h 还声明了 vprintf 和其他 v-函数。为了声明这样的函数,用户代码需要 stdarg.h。但是 stdio.h 没有这个需求。 - n. m.
@n.m.:嗯,确实如此,但我仅仅是指OP所做出的假设,即如果函数的实现依赖于某个定义,则函数接口也需要该定义。 - barak manos

2

当使用gcc编译时,这个stdio.h的实现不包含stdarg.h。它是通过编译器作者神奇的魔法实现的。

无论如何,您的C源文件必须包含其引用的每个系统头文件。这是C标准的要求。也就是说,如果您的源代码需要在stdarg.h中存在的定义,则必须直接或间接地包含#include <stdarg.h>指令,即在其中一个您的头文件中。即使其他标准头文件实际上包含了stdarg.h,它也不能依赖于stdarg.h被包含在其他标准头文件中。


这里有些我在这里找到的东西:https://dev59.com/DnE85IYBdhLWcg3w6oB0#2550799 。它说 stddef.h 中定义的 size_t 可以被包含了 stddef.h 的程序通过包含 stdlib.h 来使用。 “它可以通过包含 stdlib.h 进一步导入,因为此文件在内部子包含了 stddef.h。” 而且,stddef.h 是一个标准头文件而不是自定义头文件,对吧? - J...S
@J...S 维基百科文章并没有说连接器的答案所声称的那样。也许那个声明被发现是错误的,并被删除了。我在历史记录中找不到它。无论如何,如果标准规定头文件A包括头文件B,您可以依赖它;然而我在标准中找不到任何这样的声明。 - n. m.

1
<stdarg.h>文件仅在您需要实现可变参数函数时才需要包含。如果您只是使用printf(3)和相关函数,则不需要该文件。只有在处理可变参数函数的参数时,您才需要va_list类型和va_startva_argva_end宏。因此,只有在这种情况下,您才需要强制包含该文件。
一般情况下,您不能保证<stdarg.h>仅通过包含<stdio.h>就可以被包含。确实,您引用的代码仅在__GNU_C__未定义时(我怀疑这是您的情况),才会包含该文件;而这个宏在使用gcc编译器时被定义。
如果您在代码中创建可变参数传递函数,最好的方法不是期望另一个包含文件将其包含,而是自己编写(作为请求功能的客户端),在使用va_list类型、va_start、va_arg或va_end宏的任何地方都要这样做。过去,有些人对双重包含感到困惑,因为某些头文件没有保护免受双重包含(包含两次或更多次相同的包含文件会导致错误,如重复定义的宏等,您必须小心处理),但今天,这已经不是问题了,通常所有标准头文件都受到双重包含的保护。

-1

好的,这里有“常规”的printf系列:printf,fprintf,dprintf,sprintf和snprintf。 然后还有可变数量参数的printf系列:vprintf,vfprintf,vdprintf,vsprintf和vsnprintf。

要使用任一种的可变参数列表,您需要声明stdarg.h。 stdarg.h定义了您正在使用的所有宏:va_list,va_start,va_arg,va_end和va_copy。


这并没有回答问题,问题是是否包含<stdio.h>就足以允许使用由<stdarg.h>定义的可变参数宏。 - Toby Speight
@TobySpeight,“提问方式”自我回答后已经发生了变化。而且,原帖发布者似乎并不知道他在使用宏。 - Terry Wendt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接