C中的stdarg和printf()

J..*_*..S 16 c printf gcc variadic-functions

<stdarg.h>头文件是用来做函数接受的参数未定义的数字,对不对?

所以,必须使用的printf()功能是接受大量的论点(如果我弄错了,请纠正我). 我在gcc的stdio.h文件中找到以下行:<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
Run Code Online (Sandbox Code Playgroud)

我无法理解其中的大部分内容,但它似乎包括在内 <stdarg.h>

所以,如果printf()使用<stdarg.h>接受可变数量的参数,并stdio.hprintf(),一个C程序中使用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;
}
Run Code Online (Sandbox Code Playgroud)

如果<stdarg.h>包含它,它工作正常,但否则会生成以下错误:

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

这怎么样?

或者它printf()<stdarg.h>用于接受可变数量的参数?如果是这样,它是如何完成的?

edm*_*dmz 13

考虑:

stdio.h中:

int my_printf(const char *s, ...); 
Run Code Online (Sandbox Code Playgroud)

你需要<stdarg.h>吗?,你没有....是语言语法的一部分 - 它是"内置的".但是,只要你想做的任何事情有意义和便携用的参数,例如列表,您需要在名称中有定义: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 */
}
Run Code Online (Sandbox Code Playgroud)

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

那么,如果printf()用于接受变量数量的参数而stdio.h有printf(),那么使用printf()的C程序不需要包含它吗?

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

  • @J ... S打开一个终端并键入`ldd`后跟一个C可执行文件的名称(我使用prog表示,但它可以是任何东西:) (2认同)

Ser*_*gio 10

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

不,<stdarg.h>只是暴露一个应该用于访问额外参数的API.如果你只想声明接受可变数量参数的函数,就没有必要包含那个头,如下所示:

int foo(int a, ...);
Run Code Online (Sandbox Code Playgroud)

这是一种语言功能,不需要额外的声明/定义.

我在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
Run Code Online (Sandbox Code Playgroud)

我想这些东西只需要声明vprintf()没有内部的东西,包括<stdarg.h>:

int vprintf(const char *format, va_list ap);
Run Code Online (Sandbox Code Playgroud)

最重要的是:

  • 声明具有可变数量参数的函数的标头不应包含在<stdarg.h>内部.
  • 具有可变数量的参数的函数的实现必须包括<stdarg.h>并使用va_listAPI来访问额外的参数.


zwo*_*wol 10

我首先要根据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;
}
Run Code Online (Sandbox Code Playgroud)

(这与C++不同.在C++中,允许所有标准库头,但不是必需的,以包含彼此.)

的确,在执行printf(可能)使用的stdarg.h机制来访问它的参数,但是这仅仅意味着一些文件在源代码中的C库(" printf.c",也许)需要包括stdarg.h以及stdio.h; 这不会影响您的代码.

stdio.h声明va_list带有-typed参数的函数也是如此.如果你查看这些声明,你会发现它们实际上使用的是一个以两个下划线开头的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);
Run Code Online (Sandbox Code Playgroud)

所有以两个下划线或下划线和大写字母开头的名称都保留用于实现 - 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
Run Code Online (Sandbox Code Playgroud)

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

  1. POSIX.1最近的"问题",即对"Unix"操作系统意味着什么的官方规范,添加va_list到一组事物stdio.h应该定义.(具体来说,在问题6中,va_list定义stdio.h为"XSI"扩展,在问题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>
    
    Run Code Online (Sandbox Code Playgroud)

    不会编译.

  3. GCC自带的副本stdarg.h,但它并没有配备的副本stdio.h.在stdio.h你引述来自GNU libc的,这是GNU旗下一个独立的项目,通过人们一个单独的(但重叠)组维护.至关重要的是,GNU libc的头文件不能假设它们是由GCC编译的.

所以,你引用的代码定义了va_list.如果__GNUC__定义了,这意味着编译器是GCC或与quirk兼容的克隆,它假定它可以stdarg.h使用名为的宏进行通信,_VA_LIST_DEFINEDva_list定义的宏定义时 - 只有定义了 - 但是作为宏,你可以检查它与#if. stdio.h可以定义va_list自己然后定义_VA_LIST_DEFINED,然后stdarg.h不会这样做,并且

#include <stdio.h>
#include <stdarg.h>
Run Code Online (Sandbox Code Playgroud)

将编译好.(如果你看看stdarg.h可能隐藏在/usr/lib/gcc/something/something/include你系统中的GCC ,你会看到这段代码的镜像,以及一个非常长的其他宏列表,这些宏也意味着"我没有定义va_list,我已经这样做了" GCC可以或可以一次使用的其他 C库.)

但是,如果__GNUC__没有定义,那么stdio.h假定它知道如何与沟通stdarg.h.但它确实知道stdarg.h在同一个文件中包含两次是安全的,因为C标准要求它能够工作.因此,为了得到va_list定义,它只是向前走,包括stdarg.h,因此,在va_*该宏stdio.h 应该定义也将被定义.

这就是HTML5用户称之为"故意违反"C标准的内容:故意,这是错误的,因为以这种方式出错是不太可能破坏现实世界的代码而不是任何可用的替代方案.特别是,

#include <stdio.h>
#include <stdarg.h>
Run Code Online (Sandbox Code Playgroud)

绝对更可能出现在实际代码中

#include <stdio.h>
#define va_start(x, y) /* something unrelated to variadic functions */
Run Code Online (Sandbox Code Playgroud)

因此,使第一个工作比第二个工作更重要,即使两者都应该起作用.


最后,你可能仍然想知道它_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
Run Code Online (Sandbox Code Playgroud)

我曾经-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 */
Run Code Online (Sandbox Code Playgroud)

因此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
Run Code Online (Sandbox Code Playgroud)

......就...而言__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
Run Code Online (Sandbox Code Playgroud)

而且__builtin_va_list,最后,是一个无证GCC固有的,意思是"任何一种适用于va_list当前ABI".

$ echo 'void foo(__builtin_va_list x) {}' |
    gcc -xc -std=c11 -fsyntax-only -; echo $?
0
Run Code Online (Sandbox Code Playgroud)

(是的,GNU libc的stdio实现方式比它有任何理由更复杂.解释是,在很久的时候,人们试图让它的FILE对象直接用作C++ filebuf.这在几十年内没有用 - 事实上,我不知道,如果它曾经工作过;它之前被废弃电梯群控系统,这是早在我所知道的历史-但也有很多很多遗迹的尝试还在闲逛,无论是二进制向后兼容,或者因为没有人去清理它们.)

(是的,如果我正确地阅读它,GNU libc stdio.h将无法正常使用stdarg.h未定义的C编译器__gnuc_va_list.这是抽象错误,但无害;任何人都想要一个闪亮的新的非GCC兼容的编译器来处理GNU libc将要担心更多的事情.)


Jen*_*ens 9

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

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

如果你想看到真正的实现printf,请查看FreeBSD标准I/O源代码以及源代码vfprintf.

  • @J ... S没有代码可以在`<stdio.h>`中实现*`printf()`.它只是一个标题,带有函数和变量(和类型等)的*声明*.代码已经编译,并且是C运行时库的一部分(许多Linux系统上的`glibc.so`). (3认同)
  • @J...S 已经编译就是这个意思。`&lt;stdio.h&gt;` 标头仅包含 printf 函数的原型;然而 printf 代码通常位于编译代码库中,称为 C 库,通常位于名为“libc.a”或“libc.so”的文件中,并在编译器最终将程序链接到可执行文件时使用。尝试使用“gcc -v”来启用其运行的命令的打印。 (2认同)
  • @J...S 没有代码被“采用”,除非您静态链接。通常,您有整个运行时库的一个实例,并且您的代码在需要时(即当您调用 printf() 时)跳转到该实例。 (2认同)