可变参数函数

来源:互联网 发布:淘宝外卖不能分口袋吗 编辑:程序博客网 时间:2024/05/16 18:52

可变参数参数在编程中其实是经常用得到的,查询一些资料,整理了一下,最后写出一个简单的 宏定义的可变参数LOG,这个实例大家肯定可以经常用到。

可变参数定义

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

intprintf( const char* format, ...);

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

printf("%d",i);

printf("%s",s);

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

说明例子

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

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

#include<stdio.h>

#include<stdarg.h>

voidsimple_va_fun(int start, ...)

{

va_listarg_ptr;

intnArgValue =start;

intnArgCout=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;

}

intmain(int argc, char* argv[])

{

simple_va_fun(100,-1);

simple_va_fun(100,200,-1);

return0;

}

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

FILE*logfile;

intWriteLog(const char * format, ...)

{

va_listarg_ptr;

va_start(arg_ptr,format);

intnWrittenBytes = vfprintf(logfile, format, arg_ptr);

va_end(arg_ptr);

returnnWrittenBytes;

}

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

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

voidva_start( va_list arg_ptr, prev_param );

typeva_arg( va_list arg_ptr, type );

voidva_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。

剖析

 va_*宏定义

我们已经知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于硬件平台的不同和编译器的不同,所以定义的宏也有所不同。

以下VC++6.0中stdarg.h里的代码

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

typedefchar * va_list;

#define_INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#defineva_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

#defineva_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

#defineva_end(ap) ( ap = (va_list)0 )

linux中的定义

typedefchar *va_list;

#define__va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) *sizeof (int))

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

voidva_end (va_list);

#defineva_end(AP) (AP=(char *)0)

#defineva_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\

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

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

1.   栈的方向和参数的入栈顺序

2.   CPU的对齐方式

3.   内存地址的表达方式。

1.   

Intel 32位的CPU为分析基础

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

                             

从上面压栈前后的两个图可明显看到栈的生长方向,在Intel32位的CPU中,windown或linux都使用了它的保护模式,ss指定栈所有在的段,ebp指向栈基址,esp指向栈顶。显然执行push指令后,esp的值会减4,而pop后,esp值增加4。 栈中每个元素存放空间的大小决定push或pop指令后esp值增减和幅度。Intel32位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的汇编格式写相应的汇编代码,程序段如下:

voidcallee(int a, int b)

{

int c= 0;

c = a+b;

}

voidcaller()

{

callee(1,2);

}

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

图2

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

pushebp

movesp, ebp

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

subesp, 4

mov[ebp-4], 0

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

moveax , [ebp + 8] ;这里用eax存放第一个传递进来的参数,记住第一个参数与ebp的偏移量肯定为8addeax, [ebp + 12] ;第二个参数与ebp的偏移量为12,故计算eax =a+b

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

栈又有了新了变化,如图2(e)。至此,函数callee的计算指令执行完毕,但还要做一些事情:释放局部变量占用的栈空间,销除函数的stack-frame过程会生成如下指令:movesp, ebp;把局部变量占用的空间全部略过,即不再使用,ebp以下的空间全部用于局部变量pop ebp;弹出caller函数的stack-frame 基址在Intel CPU里上面两条指令可以用指令leave来代替,功能是一样。这样栈的内容如图2(f)所示。最后,要返回到caller函数,因此callee的最后一条指令是ret,ret指令用于把栈上的保存的断点弹出到EIP寄存器,新的栈内容如图2(g)所示。函数callee的调用与返回全部结束,跟着下来是执行callcallee的下一条语句。

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

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

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

2.   数据对齐问题

对于两个正整数 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))

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

3.   再看va_*宏定义

va_start(va_listap, last)

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

va_arg(va_litap, type)

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

va_end(va_listap)

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

 

可变参数宏定义

printf()和fprintf()这些输出函数的参数是可变的,在调试程序时,你可能希望定义自己的参数可变的输出函数,

那么可变参数宏会是一个选择。

C99中规定宏可以像函数一样带有可变参数,比如

#define LOG(format, ...)fprintf(stdout, format, __VA_ARGS__)

其中,...表示参数可变,__VA_ARGS__在预处理中为实际的参数集所替换

GCC中同时支持如下的形式

#define LOG(format, args...) fprintf(stdout,format, args)

其用法和上面的基本一致,只是参数符号有变化

有一点需要注意,上述的宏定义不能省略可变参数,尽管你可以传递一个空参数,这里有必要提到"##"连接符号的用法。

"##"的作用是对token进行连接,在上例中,format、__VA_ARGS__、args即是token,

如果token为空,那么不进行连接,所以允许省略可变参数(__VA_ARGS__和args),对上述变参宏做如下修改

#define LOG(format, ...)    fprintf(stdout, format, ##__VA_ARGS__)

#define LOG(format, args...) fprintf(stdout,format, ##args)

上述的变参宏定义不仅能自定义输出格式,而且配合#ifdef #else#endif在输出管理上也很方便,

比如调试时输出调试信息,正式发布时则不输出,可以这样

#ifdef DEBUG

#define LOG(format, ...) fprintf(stdout,">> "format"\n", ##__VA_ARGS__)

#else

#define LOG(format, ...)

#endif

在调试环境下,LOG宏是一个变参输出宏,以自定义的格式输出;

在发布环境下,LOG宏是一个空宏,不做任何事情。

 

 

写日志实例

#ifndef _HYC_LOG_H_

#define _HYC_LOG_H_

#include "base.h"

#include <stdarg.h>

#include <string>

#include <iostream>

//

#ifdef LOG_COUT

   #define LOG_TRACE(strMsg,...) \

   {\

     char ch[1024] ;\

     sprintf(ch,"%s %d %s %s",__FILE__,__LINE__,__DATE__,__TIME__);\

     std::cout << ch << " " << minprintf(strMsg,##__VA_ARGS__)<< std::endl ;\   

    }

#else

   //

   #define LOG_TRACE(strMsg,...) \

   {\

     char ch[1024] ;\

     sprintf(ch,"%s %d %s %s",__FILE__,__LINE__,__DATE__,__TIME__);\

     char fileName[] = "huangxw.log" ; \

     std::ofstream outFile(fileName,std::ios::out|std::ios::app);  \

     outFile << ch << " " <<minprintf(strMsg,##__VA_ARGS__) << std::endl; \

     outFile.close();  \

    }

#endif

//

//

std::string minprintf(char *fmt, ...)

{

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

   char *p, *sval;

   char tVal[128] ;

   int ival;

   double dval;

   std::string strTotal ;

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

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

       if (*p != '%') {

           //putchar(*p);

           strTotal += *p ;

           continue;

       }  

       switch (*++p) {

           case 'd':

                ival = va_arg(ap, int);

                sprintf(tVal,"%d",ival);

                strTotal += tVal;

                break;

           case 'x':

                ival=va_arg(ap,int);

               sprintf(tVal,"%#x",ival);

                strTotal += tVal;

                break;

           case 'f':

                dval = va_arg(ap, double);

                sprintf(tVal,"%f",dval);

                strTotal += tVal;

                break;

           case 's':

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

                    strTotal += *sval ;

                break;

           default:

                strTotal += *p ;

                break;

       }

    }

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

   return strTotal;

}

#endif

 

test程序:

#include "../hyc_log.h"

int main()
{
        LOG_TRACE("test:%d",10);

        LOG_TRACE("test:%s=%d","max",10);
        LOG_TRACE("test");


}

 

原创粉丝点击