【问题标题】:stdarg and printf() in CC 中的 stdarg 和 printf()
【发布时间】:2017-03-21 22:11:42
【问题描述】:

<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

我看不懂里面的大部分内容,但似乎包括&lt;stdarg.h&gt;

那么,如果printf() 使用&lt;stdarg.h&gt; 来接受可变数量的参数并且stdio.h 具有printf(),那么使用printf() 的C 程序就不需要包含&lt;stdarg.h&gt; 吗?

我尝试了一个程序,它有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;
}

如果包含&lt;stdarg.h&gt;,它可以正常工作,但否则会生成以下错误:

40484293.c:13:38: error: expected expression before ‘int’
         printf("%d\n", va_arg(vlist, int));//////error: expected expression before 'int'/////////
                                      ^~~

这是怎么回事?

还是printf() 不使用&lt;stdarg.h&gt; 来接受可变数量的参数? 如果是这样,它是如何完成的?

【问题讨论】:

  • va_arg 是定义在stdarg.h 中的宏。所以必须包含stdarg.h 或为您的项目提供宏.... va_list va_start 和 va_end

标签: c gcc printf variadic-functions


【解决方案1】:

考虑:

stdio.h:

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

你需要&lt;stdarg.h&gt;吗? 不,你不知道。 ... 是语言语法的一部分——它是“内置的”。但是,只要您想使用此类参数列表做任何有意义且可移植的事情,您就需要在其中定义名称: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() 用于接受可变数量的 arguments 和 stdio.h 有 printf(),使用 printf() 的 C 程序需要 不包括吗?

即使是这样,您不能依赖它,否则您的代码格式不正确:如果使用属于它们的名称,则无论如何都必须包含所有标题,无论实现是否是否已经这样做了。

【讨论】:

  • 有没有办法在我的电脑上查看这个 libc 共享库文件?我的意思是我知道我无法打开它,但只想看看它在哪里。我正在使用 Ubuntu 15.10
  • @J...S 编译一些虚拟 C 代码和ldd prog,你会看到libc.so。在我的系统上,它位于/usr/lib。
  • ldd 编?那是什么?对不起,我是新来的!
  • @J...S 打开终端并输入ldd,后跟 C 可执行文件的名称(我用 prog 来表示,但它可以是任何东西 :)
  • @cat:你是对的,但有另一个原因。我的观点是f(int arg...) 是有效的 C,但它只在 C++ 中。无论如何,那条评论毫无用处。
【解决方案2】:

我将首先根据 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

要了解这是做什么的,您需要知道三件事:

  1. 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 已经以某种方式可用。

  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 或兼容 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 兼容编译器将会有更多的事情需要担心。)

【讨论】:

  • 如果我正确理解你的反对意见,"它不知道如何告诉stdarg.h "oi,我已经定义了va_list,不要再这样做了"" ,好吧,它确实:宏(包括守卫或VA_LIST_DEFINED之类的东西)或仍然是编译器魔法。可能又错过了你的意思。
  • @black 是的,这就是它在 #define _VA_LIST_DEFINED 的情况下对 #define _VA_LIST_DEFINED 所做的事情。问题是 stdio.h 属于 glibc 但 stdarg.h 属于编译器。如果编译器不是 GCC(或者一个高度兼容的编译器,例如 clang,它也定义了 __GNUC__),glibc 不能假设 #define _VA_LIST_DEFINED 可以工作。
  • 看clang的stdarg,似乎确实适应了GCC的话。涵盖这方面的问题会很有趣。
  • @zwol 你说 “所有以两个下划线或一个下划线和一个大写字母开头的名称都保留用于实现 - stdio.h 允许公开尽可能多的此类名称它想要(而且你不应该使用它们中的任何一个)。” 这是否意味着我不能声明以 2 个下划线或一个下划线和一个大写字母开头的宏?
  • @J...S 有少数例外,但是是的,这是正确的。形式上,编译器会让你去做,但后果是不确定的。
【解决方案3】:

stdarg 头文件用于使函数接受未定义的数字 争论,对吧?

不,&lt;stdarg.h&gt; 只是公开了一个应该用于访问额外参数的 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() 这样的东西,而不包括&lt;stdarg.h&gt;:

int vprintf(const char *format, va_list ap);

最重要的是:

  • 用可变数量的参数声明函数的标头不应在内部包含&lt;stdarg.h&gt;。
  • 具有可变数量参数的函数的实现必须包含&lt;stdarg.h&gt; 并使用va_list API 来访问额外的参数。

【讨论】:

  • int vprintf(const char *format, va_list ap); 这是否意味着va_list 可以代替...(省略号)?有优势吗?
  • @J...S 不,你不能。 va_list 是一种类型。此类型旨在保存有关... 函数的额外参数的信息。您可以像上下文一样威胁它,允许您按顺序获取额外的参数。尽管如此,vprintf() 只接受两个参数,而 printf() - 一个或多个。
  • @J...S This 手册页可能对您有所帮助。
  • @J...S 也很难说vprintf() 或printf() 有一些优势。它们只是为不同的用例而设计的。 printf() 在大多数情况下很有用,但有时您可能希望将函数的额外参数传递给另一个函数,这里va_list 很有帮助。
  • OP 发现的代码似乎正在实现 POSIX.1-2008-only 要求 stdio.h 声明 va_list - 它与 vprintf 无关。
【解决方案4】:

不,要使用printf(),您只需要#include &lt;stdio.h&gt;。不需要 stdarg 因为 printf 已经编译。编译器只需要看到printf 的原型就知道它是可变参数的(源自原型中的省略号...)。如果您查看printf 的stdio 库源代码,您会看到&lt;stdarg.h&gt; 被包含在内。

如果您想编写自己的可变参数函数,您必须 #include &lt;stdarg.h&gt; 并相应地使用其宏。如您所见,如果您忘记这样做,编译器将不知道va_start/list/end 符号。

如果您想查看printf 的真正实现,请查看the code in FreeBSD's standard I/O source 以及source for vfprintf。

【讨论】:

  • 什么意思 printf() 已经编译了?有 printf() 函数定义的部分代码在编译其余代码之前编译?
  • @J...S 没有代码可以在&lt;stdio.h&gt; 中实现 printf()。它只是一个标题,带有函数和变量(以及类型等)的声明。代码已经编译,并且是 C 运行时库的一部分(在许多 Linux 系统上为glibc.so)。
  • @J...S 已经编译就是这个意思。 &lt;stdio.h&gt; 标头仅包含 printf 函数的原型;然而,printf 代码通常位于已编译代码库中,称为 C-Library,通常位于名为 libc.a 或 libc.so 的文件中,编译器在最终将程序链接到可执行文件时使用它。尝试使用gcc -v 来启用打印它运行的命令。
  • @unwind 我真的可以在我的计算机上看到 C 运行时库的文件吗?我正在使用 Ubuntu 15.10。
  • @J...S 除非您静态链接,否则不会“采用”任何代码。通常你有一个整个运行时库的实例,你的代码会在需要时跳转到那个实例,即当你调用printf()时。
【解决方案5】:

将模块拆分为头文件和源文件的基础知识:

  • 在头文件中,你只放了你模块的接口
  • 在源文件中,放置模块的实现

所以即使printf 的实现使用了va_arg,正如你推测的那样:

  • 在stdio.h中,作者只声明了int printf(const char* format, ...);
  • 在stdio.c中,作者使用va_arg实现了printf

【讨论】:

  • stdio.h 还声明了 vprintf 和其他 v-functions。为了声明这样的函数,用户代码需要 stdarg.h。 但 stdio.h 没有。
  • @nm:嗯,没错,但我仅指的是 OP 所做的假设,根据该假设,如果函数的实现依赖于某个定义,那么函数接口也需要该定义。
【解决方案6】:

stdio.h 的这个实现在使用 gcc 编译时不包括 stdarg.h。编译器编写者总是袖手旁观。

无论如何,您的 C 源文件都必须包含它们引用的每个系统头文件。这是C标准的要求。也就是说,如果您的源代码需要 stdarg.h 中存在的定义,则它必须直接包含 #include &lt;stdarg.h&gt; 指令,或者包含在它包含的 your 头文件之一中。它不能依赖 stdarg.h 包含在其他 standard 标头中,即使它们确实包含它。

【讨论】:

  • 这是我找到的 here。它说在 stddef.h 中定义的 size_t 可以被包括 stdlib.h 的程序使用,因为它包括 stddef.h 。 “它可以通过包含 stdlib.h 来进一步导入,因为这个文件内部子包含 stddef.h。” 而stddef.h 是一个标准 头文件,而不是一个自定义,对吧?
  • @J...S 维基百科文章没有说明链接器的答案所声称的内容。也许该陈述被发现是错误的,并被删除。我在历史上找不到它。无论如何,如果标准说header A包含header B,你可以依赖它;我在标准中找不到任何这样的声明。
【解决方案7】:

仅当您要实现可变数量的参数函数时,才需要包含&lt;stdarg.h&gt; 文件。不需要能够使用printf(3) 和朋友。只有当您要处理可变数量的 args 函数的参数时,您才需要 va_list 类型,以及 va_start、va_arg 和 va_end 宏。因此,只有这样您才需要强制包含该文件。

一般来说,您不保证 &lt;stdarg.h&gt; 将包含在仅包含 &lt;stdio.h&gt; 的情况下,确实,您引用的代码仅包含它,如果 __GNU_C__ 未定义(我怀疑是这种情况,因此不包括在您的情况中),如果您使用的是gcc 编译器,则定义此宏。

如果您要在代码中创建可变参数传递函数,最好的方法是不要期望另一个包含的文件包含它,而是自己做(作为请求功能的客户端你在)任何你使用va_list类型,或va_start,va_arg或va_end宏的地方。

过去,对双重包含有一些混淆,因为某些头文件没有受到双重包含的保护(包括两次或多次相同的包含文件产生关于双重定义的宏或类似的错误,您必须小心) 但今天,这不是问题,通常所有标准标头字段都受到保护,不会被双重包含。

【讨论】:

    【解决方案8】:

    好的,有“常规”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。

    【讨论】:

    • 这并没有回答所提出的问题,即是否包含&lt;stdio.h&gt; 足以允许使用&lt;stdarg.h&gt; 定义的可变参数宏。
    • @TobySpeight 自从我发布答案以来,“所问的问题”已经改变。而且 OP 似乎根本不知道他在使用宏。
    猜你喜欢
    • 2013-04-29
    • 1970-01-01
    • 2015-05-24
    • 2020-05-23
    • 1970-01-01
    • 1970-01-01
    • 2015-02-28
    • 2017-01-07
    • 1970-01-01
    相关资源
    最近更新 更多