c可变参数和stdarg.h

c语言支持在函数中使用可变长度的参数列表(也支持宏定义的可变长参数列表,这点在以后再做研究)。带可变参数的函数基本声明类似这样:

extern int printf (const char *__restrict__format, ...);

网上很多讲解函数可变长度参数列表的文章都使用prinft做例子,原因就是它比较常用吧。c语言标准ISO C11中在第七章 Library中,提出使用标准c库中的stdlib.h头文件来进行可变长参数列表的处理。文章第一部分说用法,第二部分讲原理,第三部分对一个奇怪的问题做一些研究和猜测。

1.   stdlib.h使用篇

stdarg.h提供的接口包括一个数据类型va-list和三个常用的宏(va_start、va_arg和va_end),还有一个不是特别常用的va_copy。这些接口的用法一般遵循以下几个步骤,各个步骤对应的代码在下面的printf简略定义中给出:

<Step 1> 在调用参数表之前,定义一个va_list 类型的变量(va_list   ap;)。

<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他使输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。

int printf (const char *__restrict__format, ...){

         va_list      ap;                                                  //<Step1>

         va_start(ap,__restrict __format);          //<Step2>

         ……

         intvalue1 = va_arg(ap,int);                        //<Step3>

         charvalue2 = va_arg(ap,char);                 //<Step3>

         ……

         va_end(ap);                                                    //<Step4>

         …….

}

 

2.   stdlib.h原理篇

我们介绍VC中的stdarg.h的实现。这部分内容的研究需要理解栈的基本结构,见上一篇博客。

va_*的声明如下:

 

typedef char *  va_list;

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址  

#define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数  

#define va_end(ap)      ( ap = (va_list)0 )  

 

_INTSIZEOF(n) 之所以要定义成( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),是为了使最终的类型大小关于int类型向上对齐,即在32位机上, _INTSIZEOF(n)的值永远是4的整数倍(向上取值,如7对齐到8)。这么做的目的是因为栈中参数位置总是以机器位数做对齐的。va_list其实就是一个指针;va_start初始化这个指针时,先得到v的指针,v的指针就是栈中最后一个固定参数(相对于可变参数)的栈位置,(va_list)&v + _INTSIZEOF(v) 自然就是第一个可变参数的位置了;va_arg则把ap指针向高位挪 _INTSIZEOF(type))字节,并把这 _INTSIZEOF(type))字节的数据解引用,这样ap就指向下一个参数了;va_end把指针清空。

 

 

3.   stdlib.h困惑篇

我最初研究这个头文件,发现它属于标准C库定义的接口之一(这里顺便推荐一个做得很精美的“Standard C 语言标准函数库速查”网站http://ganquan.info/standard-c/)。但是在Linux的/usr/include下并没有发现这个头文件,下图是ubuntu下的搜索结果,可以和stdlib.h的搜索结果作对比。


显然结果显示在gcc的目录下存在我们想要的stdarg.h头文件(我的系统装了两个版本的gcc,所以出现了4.4和4.6两项)。另外,glibc源码中也没有stdarg.h头文件,只有一个stdarg.h-data,由于我没有研究过glibc的源码,不知道它的作用是什么。而gcc目录下的stdarg.h头文件可以在gcc的源码中找到。

 

关于这种情况,网上找到的解释如下。

解释一:

va_list是这样定义的

typedef __gnuc_va_list va_list;

显然__gnuc_va_list是GCC编译器的一个内置类型。

别外几个东西的定义

#define va_start(v,l)     __builtin_va_start(v,l)

#define va_end(v)     __builtin_va_end(v)

#define va_arg(v,l)     __builtin_va_arg(v,l)

上述三个它们都是宏定义,即在编译后产生的,利用函数栈得到相关的地址位置。

这种方法和printf的实现是一样的,都是在编译期间产生。

不同编译于对于stdarg.h的实现是不一样的。

 

解释二:

因为gcc要支持很多种平台,所以把stdarg.h直接实现在编译器内了,没有提供库,可以直接使用。 比如对于精简指令集的CPU,一般寄存器都比较多(几十个通用寄存器),一般参数传递都用寄存器实现,这时候我们一般看到的用栈传递参数的stdarg.h实现就不能用了,gcc直接把stdarg.h放到编译器里就可以很好的(很容易的)支持各种平台。 

 

根据这些解释,可以认为c标准库规定了stdarg.h这样一套接口,具体的实现则交给了各个系统的c编译器。而不同编译器来实现在不同的ABI系统中完成这种接口的具体实现。gcc正是做到了这一点。正如解释一中所说,gcc目录下的stdarg.h内容如下:

typedef __gnuc_va_list va_list;

 

#define va_start(v,l)     __builtin_va_start(v,l)

#define va_end(v)     __builtin_va_end(v)

#define va_arg(v,l)     __builtin_va_arg(v,l)

以__builtin_开头的函数是gcc自定义的内建函数,由gcc负责实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值