C中的stdarg和printf()

时间:2016-11-08 10:16:08

标签: c gcc printf variadic-functions

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

因此,printf()的{​​{1}}功能必须使用<stdio.h>来接受大量的论点(如果我误会,请纠正我)。

我在gcc的stdio.h文件中找到了以下行:

<stdarg.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.h,则使用printf()的C程序无需包含{{ 1}}做到了吗?

我尝试了一个程序,其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'///////// ^~~ 来接受可变数量的参数? 如果是这样,它是如何完成的?

8 个答案:

答案 0 :(得分:13)

考虑:

stdio.h中:

Button

您需要<Button Background="Transparent" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"> 吗? ,你没有。 int my_printf(const char *s, ...); 是语言语法的一部分 - 它是“内置的”。但是,只要您想使用此类参数列表执行任何有意义且可移植的操作,就需要在其中定义名称<stdarg.h>...等等。< / p>

stdio.c:

va_list

但实际上,这在libc的实现中是必要的,除非你自己编译库,否则你看不到它。你得到的是libc共享库,它已被编译为机器代码。

  

因此,如果printf()用于接受变量数   参数和stdio.h有printf(),一个使用printf()需要的C程序   不包括吗?

即使是这样,你也不能依赖它,否则你的代码就是格式错误:如果使用属于它们的名称,你必须包括所有的标题,无论是否实现已经这样做了。

答案 1 :(得分:10)

  

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来访问额外的参数。

答案 2 :(得分:10)

我首先要按照C标准回答您的问题,因为这就是告诉您应该如何编写代码的原因。

C标准要求stdio.h至&#34;表现为 - 如果&#34; 包括stdarg.h。换句话说,宏va_startva_argva_endva_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库的源代码(&#34; printf.c&#34;或许)需要包含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和{{ 1}})。编译器可以让你这样做,但效果是未定义的。

现在我要谈谈你引用__GNUC__的内容。这又是:

stdio.h

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

  1. 最近&#34;问题&#34; POSIX.1的正式规范,对于&#34; Unix&#34;在操作系统中,将#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 添加到应该定义的事物集va_list中。 (具体来说,在Issue 6中,stdio.hva_list定义为&#34; XSI&#34;扩展名,Issue 7是强制性的。{)此代码定义stdio.h,但仅限于程序已请求问题6 + XSI或问题7功能;这是va_list的含义。请注意,它使用#if defined __USE_XOPEN || defined __USE_XOPEN2K8来定义_G_va_list,就像在其他地方使用va_list来声明_G_va_list一样。 vprintf已经以某种方式提供。

  2. 您不能在同一翻译单元中两次编写相同的_G_va_list。如果typedef定义stdio.h但未通知va_list不再执行此操作,

    stdarg.h

    无法编译。

  3. GCC附带#include <stdio.h> #include <stdarg.h> 的副本,但附带stdarg.h的副本。您引用的stdio.h来自GNU libc,这是GNU保护伞下的一个单独项目,由一组独立(但重叠)的人员维护。至关重要的是,GNU libc的头文件不能假设它们是由GCC编译的。

  4. 因此,您引用的代码定义了stdio.h。如果定义了va_list,这意味着编译器是GCC或与quirk兼容的克隆,它假定它可以使用名为__GNUC__的宏与stdarg.h进行通信,该宏定义为和只有定义了_VA_LIST_DEFINED - 但作为一个宏,您可以使用va_list进行检查。 #if可以自定义stdio.h,然后定义va_list,然后_VA_LIST_DEFINED不会这样做,

    stdarg.h

    编译正常。 (如果您查看GCC的#include <stdio.h> #include <stdarg.h> ,它可能隐藏在您系统的stdarg.h中,您将看到此代码的镜像,以及一个非常长的列表其他宏也意味着“不要定义/usr/lib/gcc/something/something/include,我已经为#em>其他 C库提供了GCC可以或者可以一次,与。一起使用。)

    但如果未定义va_list,则__GNUC__会假定知道如何与stdio.h进行通信。但它确实知道在同一个文件中包含stdarg.h两次是安全的,因为C标准要求它能够工作。因此,为了获得stdarg.h定义,它只是继续并包含va_list,因此,stdarg.h 不是va_*的宏应该定义也将被定义。

    这就是HTML5用户所谓的“故意违规行为”#34; C标准:故意是错误的,因为以这种方式出错是不可能破坏现实世界的代码而不是任何可用的替代方案。特别是,

    stdio.h

    绝对比实际代码更容易出现

    #include <stdio.h>
    #include <stdarg.h>
    

    所以让第一个工作比第二个工作更重要,即使两者都应该工作。

    最后,你可能仍然想知道he #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 来确保我在POSIX Issue 6 + XSI和Issue 7模式下进行编译,但我们仍然在此列表中看到-std=c11 - 不是由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而言。 名称​​由<{1}}定义

    stdarg.h

    最后,/* Define __gnuc_va_list. */ #ifndef __GNUC_VA_LIST #define __GNUC_VA_LIST typedef __builtin_va_list __gnuc_va_list; #endif 是一个未记录的GCC内在词,意思是&#34;任何类型适用于__builtin_va_list当前的ABI&#34;。

    va_list

    (是的,GNU libc的stdio实现方式比没有任何理由更复杂。解释就是在人们试图使其成为{{1>对象可以直接用作C ++ $ echo 'void foo(__builtin_va_list x) {}' | gcc -xc -std=c11 -fsyntax-only -; echo $? 0 。这几十年没有用过 - 实际上,我不确定它是否曾经工作;它已经被放弃了在EGCS之前,这可以追溯到我所知道的历史 - 但是尝试还有很多很多痕迹仍然存在,无论是为了二进制向后兼容还是因为没有人到处都是清理它们。)

    (是的,如果我正确地阅读此内容,GNU libc&#39; FILE无法使用filebuf无法定义的C编译器stdio.h。这是抽象的错误,但无害;任何想要一个闪亮的新的非GCC兼容编译器与GNU libc一起工作的人都会有更多的事情需要担心。)

答案 3 :(得分:9)

不,使用printf()只需要#include <stdio.h>。不需要stdarg因为已经编译了printf。编译器只需要查看printf的原型就知道它是variadic(从原型中的省略号...派生)。如果您查看printf stdio库源代码,您会看到<stdarg.h>被包含在内。

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

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

答案 4 :(得分:2)

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

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

因此,即使printf的实施在您推测时使用了va_arg

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

答案 5 :(得分:2)

使用gcc编译时,Start_Course()的此实现不包括stdio.h。它的工作原理是编译器编写者总是袖手旁观。

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

答案 6 :(得分:1)

仅当要实现可变数量的参数函数时,才需要包含<stdarg.h>文件。它不需要能够使用printf(3)和朋友。仅当您要处理可变数量的args函数的参数时,您才需要va_list类型以及va_startva_argva_end宏。所以,只有这样你才需要强行包含该文件。

一般情况下,我们不保证<stdarg.h>仅包括<stdio.h>__GNU_C__实际上,只引用 的代码包括gcc 未定义(我怀疑是这种情况,所以它不包含在您的情况下)如果您使用的是va_list编译器,则会定义此宏。

如果要在代码中创建变量参数传递函数,最好的方法是不要期望另一个包含的文件包含它,但是自己动手(作为所请求功能的客户端)您在任何地方使用va_start类型,或va_argva_end<ul> {{for row in rows:}} <li>{{=row}}</li> {{pass}} </ul> 宏。

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

答案 7 :(得分:-1)

好的,有#34;常规&#34; 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。