透析C语言可变参数问题

来源:互联网 发布:淘宝卖家开花呗的条件 编辑:程序博客网 时间:2024/05/18 03:55

一、是什么

我们学习C语言时最经常使用printf()函数,但我们很少了解其原型。其实printf()的参数就是可变参数,想想看,我们可以利用它打印出各种类型的数据。下面我们来看看它的原型:

int printf( const char* format, ...);

它的第一个参数是format,属于固定参数,后面跟的参数的个数和类型都是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式:

printf("%d",i);

printf("%s",s);

printf("the number is %d ,string is:%s", i, s);

那么它的原型是怎样实现的呢?我今天在看内核代码时碰到了vsprintf,花了大半天时间,终于把它搞的有点明白了。

二、先看两个例子

不必弄懂,先大致了解其用法,继续往下看。

①一个简单的可变参数的C函数

在函数simple_va_fun参数列表中至少有一个整数参数,其后是占位符…表示后面参数的个数不定.。在这个例子里,所有输入参数必须都是整数,函数的功能只是打印所有参数的值。

#include <stdio.h>

#include <stdarg.h>

void simple_va_fun(int start, ...)

{

va_list arg_ptr;

int nArgValue =start;

int nArgCout=0; //可变参数的数目

va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。

do

{

++nArgCout;

printf("the %d th arg: %d\n",nArgCout,nArgValue); //输出各参数的值

nArgValue = va_arg(arg_ptr,int); //得到下一个可变参数的值

} while(nArgValue != -1);

return;

}

int main(int argc, char* argv[])

{

simple_va_fun(100,-1);

simple_va_fun(100,200,-1);

return 0;

}

②格式化到一个文件流,可用于日志文件

FILE *logfile;

int WriteLog(const char * format, ...)

{

va_list arg_ptr;

va_start(arg_ptr, format);

int nWrittenBytes = vfprintf(logfile, format, arg_ptr);

va_end(arg_ptr);

return nWrittenBytes;

}

稍作解释上面两个例子。

【这部分的引用地址http://www.cppblog.com/lmlf001/archive/2006/04/19/5874.html】

从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:

⑴在程序中用到了以下这些宏:

void va_start( va_list arg_ptr, prev_param );

type va_arg( va_list arg_ptr, type );

void va_end( va_list arg_ptr );

va在这里是variable-argument(可变参数)的意思.

这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.

⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。

⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数.

⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。

⑸设定结束条件,①是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。②是调用宏va_end。

三、剖析可变参数真相

1. va_* 宏定义

我们已经知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的

不同 2)编译器的不同,所以定义的宏也有所不同。下面看一下VC++6.0中stdarg.h里的代码

(文件的路径为VC安装目录下的\vc98\ include\stdarg.h)

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,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

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

再来看看linux中的定义

typedef char *va_list;

#define __va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

#define va_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG))

void va_end (va_list);

#define va_end(AP) (AP= (char *)0)

#define va_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\

*((TYPE *)(AP - __va_rounded_size (TYPE))))

要理解上面这些宏定义的意思,需要首先了解:

①栈的方向②参数的入栈顺序③CPU的对齐方式④内存地址的表达方式。

2.栈——以Intel 32位的CPU为分析基础

在Intel CPU中,栈的生长方向是向下的,即栈底在高地址,而栈顶在低地址;从栈底向栈顶看过去,地址是从高地址走向低地址的,因为称它为向下生长,如图。

【图1 引用自 http://www.yuanma.org/data/2008/0504/article_3027_1.htm,这部分内容,我认为作者讲的很详细,所以引来共享】

从上面压栈前后的两个图可明显看到栈的生长方向,在Intel 32位的CPU中,windown或linux都使用了它的保护模式,ss指定栈所有在的段,ebp指向栈基址,esp指向栈顶。显然执行push指令后,esp的值会减4,而pop后,esp值增加4。 栈中每个元素存放空间的大小决定push或pop指令后esp值增减和幅度。Intel 32位CPU中的栈元素大小为16位或32位,由定义堆栈段时定义。在Window和Linux系统中,内核代码已定义好栈元素的大小为32位,即一个字长(sizeof(int))。因此用户空间程栈元素的大小肯定为32位,这样每个栈元素的地址向4字节对齐。

C语言的函数调用约定对编写可变参数函数是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高级程序设计语言中,函数调用约定有如下几种,stdcall,cdecl,fastcall ,thiscal,naked call。cdel是C语言中的标准调用约定,如果在定义函数中不指明调用约定(在函数名前加上约定名称即可),那编译器认为是cdel约定,从上面的几种约定来看,只有cdel约定才可以定义可变参数函数。下面是cdel约定的重要特征:如果函数A调用函数B,那么称函数A为调用者(caller),函数B称为被调用者(callee)。caller把向callee传递的参数存放在栈中,并且压栈顺序按参数列表中从右向左的顺序;callee不负责清理栈,而是由caller清理。 我们用一个简单的例子来说明问题,并采用Nasm的汇编格式写相应的汇编代码,程序段如下:

void callee(int a, int b)

{

int c = 0;

c = a +b;

}

void caller()

{

callee(1,2);

}

来分析一下在调用过程发生了什么事情。程序执行点来到caller时,那将要执行调用callee函数,在跳到callee函数前,它先要把传递的参数压到栈上,并按右到左的顺序,即翻译成汇编指令就是push 2; push 1;

图2

函数栈如图中(a)所示。接着跳到callee函数,即指令call calle。CPU在执行call时,先把当前的EIP寄存器的值压到栈中,然后把EIP值设为callee(地址),这样,栈的图变为如图2(b)。程序执行点跳到了callee函数的第一条指令。C语言在函数调用时,每个函数占用的栈段称为stack frame。用ebp来记住函数stack frame的起始地址。故在执行callee时,最前的两条指令为:

push ebp

mov esp, ebp

经过这两条语句后,callee函数的stack frame就建好了,栈的最新情况如图2(c)所示。 函数callee定义了一个局部变量int c,该变量的储存空间分配在callee函数占用的栈中,大小为4字节(insizeof int)。那么callee会在如下指令:

sub esp, 4

mov [ebp-4], 0

这样栈的情况又发生了变化,最新情况如图2(d)所示。注意esp总是指向栈顶,而ebp作为函数的stack frame基址起到很大的作用。ebp地址向下的空间用于存放局部变量,而它向上的空间存放的是caller传递过来的参数,当然编译器会记住变量c相对ebp的地址偏移量,在这里为-4。跟着执行c = a + b语句,那么指令代码应该类似于:

mov eax , [ebp + 8] ;这里用eax存放第一个传递进来的参数,记住第一个参数与ebp的偏移量肯定为8

add eax, [ebp + 12] ;第二个参数与ebp的偏移量为12,故计算eax = a+b

mov [ebp -4], eax ;执行 c = eax, 即c = a+b

栈又有了新了变化,如图2(e)。至此,函数callee的计算指令执行完毕,但还要做一些事情:释放局部变量占用的栈空间,销除函数的stack-frame过程会生成如下指令:

mov esp, ebp;把局部变量占用的空间全部略过,即不再使用,ebp以下的空间全部用于局部变量

pop ebp;弹出caller函数的stack-frame 基址

在Intel CPU里上面两条指令可以用指令leave来代替,功能是一样。这样栈的内容如图2(f)所示。最后,要返回到caller函数,因此callee的最后一条指令是

ret

ret指令用于把栈上的保存的断点弹出到EIP寄存器,新的栈内容如图2(g)所示。函数callee的调用与返回全部结束,跟着下来是执行call callee的下一条语句。

从caller函数调用callee前,把传递的参数压到栈中,并且按从右到左的顺序;函数返回时,callee并不清理栈,而是由caller清楚传递参数所占用的栈(如上图,函数返回时,1和2还放在栈中,让caller清理)。栈元素的大小为4个字节,每个参数占用栈空间大小为4字节的倍数,并且任何两个参数都不能共用同一个栈元素。

从C语言的函数调用约定可知,参数列表从右向左依次压栈,故可变参数压在栈的地址比最后一个命名参数还大,如下图3所示:

由图3可知,最后一个命名参数a上面都放着可变参数,每个参数占用栈的大小必为4的倍数。因此:可变参数1的地址 = 参数a的地址 + a占用栈的大小,可变参数2的地址 = 可变参数1的地址 + 可变参数1占用栈的大小,可变参数3的地址 = 可变参数2的地址 + 可变参数2占用栈的大小,依此类推。如何计算每个参数占用栈的大小呢?

3.数据对齐问题

对于两个正整数 x, n 总存在整数 q, r 使得

x = nq + r, 其中 0<= r <n //最小非负剩余

q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q = x/n, r = x % n.

所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:

x = nq + r', 其中 -n < r' <=0 //最大非正剩余

nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:

x+n = qn + (n+r'),其中 0<n+r'<=n //最大正剩余

x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 <n //最小非负剩余

所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:

((x+n-1)/n)*n

若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:

(x+n-1) & (~(n-1))

【来自CSDN博客:http://blog.csdn.net/swell624/archive/2008/11/03/3210779.aspx】

根据这些推导,相信已经了解#define __va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))的涵义。

4.再看va_* 宏定义

va_start(va_list ap, last)

last为最后一个命名参数,va_start宏使ap记录下第一个可变参数的地址,原理与“可变参数1的地址 = 参数a的地址 + a占用栈的大小”相同。从ap记录的内存地址开始,认为参数的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址 += occupy_stack(type)

va_arg(va_lit ap, type)

这里是获得可变参数的值,具体工作是:从ap所指向的栈内存中读取类型为type的参数,并让ap根据type的大小记录它的下一个可变参数地址,便于再次使用va_arg宏。从ap记录的内存地址开始,认为存的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址 += occupy_stack(type)

va_end(va_list ap)

用于“释放”ap变量,它与va_start对称使用。在同一个函数内有va_start必须有va_end。

5.可变参数函数问题

考虑了参数大小和数据对齐问题,使得可变参数的类型不但可以是基本类型,同样适用于用户定义类型。值的注意的是,如果是用户定义类型,最好用typedef定义的名字作为类型名,这样就会减少在va_arg进行宏展开时出错的机率。

在可变参数函数中,由va_list变量来记录(或获得)可变参数部分,但是va_list中并没有记录下它们的名字,事实上也是不可能的。要想把可变参数部分传递给下一个函数,唯有通过va_list变量去传递,而原来定义的函数用"..."来表示可变参数部分,而不是用va_list来表示。为了方便程序的标准化,ANSIC在标准库代码中就作出了很好的榜样:在任何形如: type fun( type arg1, type arg2, ...)的函数,都同时定义一个与它功能完全一样的函数,但用va_list类型来替换"...",即type fun(type arg1, type arg2, va_list ap)。以printf函数为例:

int printf(const char *format, ...);

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

第一个函数用"..."表示可变参数,第二个用va_list类型表示可变参数,目的是用于被其它可变参数调用,两者在功能功能上是完全上一样。只是在函数名字相差一个'"v"字母。

四、可变参数函数的应用

一个<The C Programming Language>中的例子:一个简单的实现printf函数的例子:

#include <stdio.h>

#include <stdlib.h>

#include <stdarg.h>

/* minprintf: minimal printf with variable argument list */

void minprintf(char *fmt, ...)

{

va_list ap; /* points to each unnamed arg in turn */

char *p, *sval;

int ival;

double dval;

va_start(ap, fmt); /* make ap point to 1st unnamed arg */

for (p = fmt; *p; p++) {

if (*p != '%') {

putchar(*p);

continue;

}

switch (*++p) {

case 'd':

ival = va_arg(ap, int);

printf("%d", ival);

break;

case 'x':

ival=va_arg(ap,int);

printf("%#x",ival);

break;

case 'f':

dval = va_arg(ap, double);

printf("%f", dval);

break;

case 's':

for (sval = va_arg(ap, char *); *sval; sval++)

putchar(*sval);

break;

default:

putchar(*p);

break;

}

}

va_end(ap); /* clean up when done */

}

int main(int argc, char* argv[])

{

int i = 1234;

int j = 5678;

char *s="nihao";

double f=0.11f;

minprintf("the first test:i=%d\n",i,j);

minprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);

minprintf("the 3rd test:s=%s\n",s);

minprintf("the 4th test:f=%f\n",f);

minprintf("the 5th test:s=%s,f=%f\n",s,f);

system("pause");

return 0;

}

不使用va_*宏定义的实现:

void minprintf(char* fmt, ...) //一个简单的类似于printf的实现不过参数必须都是int 类型

{

char* pArg=NULL; //等价于原来的va_list

char c;

pArg = (char*) &fmt; //注意不要写成p = fmt !因为这里要对//参数取址,而不是取值

pArg += sizeof(fmt); //等价于原来的va_start

do

{

c =*fmt;

if (c != '%')

{

putchar(c); //照原样输出字符

}

else

{

//按格式字符输出数据

switch(*++fmt)

{

case 'd':

printf("%d",*((int*)pArg));

break;

case 'x':

printf("%#x",*((int*)pArg));

break;

default:

break;

}

pArg += sizeof(int); //等价于原来的va_arg

}

++fmt;

}while (*fmt != '\0');

pArg = NULL; //等价于va_end

return;

}

五、参考引用:

  1. http://www.cppblog.com/lmlf001/archive/2006/04/19/5874.html
  2. http://hi.baidu.com/phps/blog/item/1fe5768d628c6112b21bba87.html
  3. http://www.yuanma.org/data/2008/0504/article_3027_2.htm
  4. http://hi.baidu.com/vama/blog/item/f188603f2cd315cc9f3d62cf.html

感谢以上 !

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 五行缺木和水怎么办 八字火旺的人怎么办 综合旺衰得分负怎么办 妈妈误打死一只黄鼠狼怎么办 油笔画在白墙上怎么办 壁纸上的水彩笔怎么办 隐形拉链头脱了怎么办 拉链的一边掉了怎么办 帝豪gs加了乙醇汽油怎么办 命理五行缺木怎么办 微信改名含有特殊符号怎么办 户口名字打错了怎么办 寻仙会心几率差怎么办 注册商标下来了没收到怎么办 金融公司倒闭欠的钱怎么办 买车贷款被骗了怎么办 定投终止后钱怎么办 受到小贷公司催款威胁怎么办 合同保证金单据丢了怎么办 公司注销期间发现欠税怎么办 公司注销后银行账户怎么办 注销公司营业执照和公章丢失怎么办 工商核名过期了怎么办 核名后的许可没办下来怎么办 重庆公司核名有同名的怎么办 新电视不全屏怎么办左右有黑边 所学类别找不到音乐表演怎么办 公司口头通知不续签合同怎么办 雪纺衬衣皱了怎么办 狗打架受伤怎么办泰迪 大狗打架破了怎么办 舌头上长溃疡怎么办吃什么药 悠悠球不回弹怎么办啊 围棋遇到对方不停围堵怎么办? s围棋业余四段想提升怎么办 wps禁止创建分享链接怎么办 驾驶人开车违章不认可怎么办 京东白条退货分期服务费怎么办 新车年检标丢了怎么办 异地违章罚单丢了怎么办 异地现场违章罚单丢了怎么办