可变参数函数

文档下载地址

http://download.csdn.net/detail/xuehui869/6435849

 

《C和指针》  没有说这部分。  此在GNU中用的很多

一、###__VA_ARGS__[x1] 

转自:http://www.cnblogs.com/zhujudah/admin/EditPosts.aspx?opt=1

1.#[x2] 字符串化操作符#——将宏名转化为字符串
假如希望在字符串中包含宏参数,ANSI C允许这样作,在类函数宏的替换部分,#符号用作一个预处理运算符,它可以把语言符号转化程字符串。例如,如果x是一个宏参量,那么#x可以把参数名转化成相应的字符串。该过程称为字符串化(stringizing.
#incldue
#define PSQR(x) printf("the square of" #x "is%d.\n",(x)*(x))
int main(void)
{
    int y =4;
    PSQR(y);
    PSQR(2+4);
    return 0;
}
输出结果:
the square of y is 16.
the square of 2+4 is 36.
第一次调用宏时使用“y”代替#x;第二次调用时用“2+4"#x
2.##
[x3] 粘接操作符##——连接两个宏名

具体解释参见2。例如:
#define XNAME(n) x##n
这样宏调用:
XNAME(4)
展开后:
x4
程序:
#include
#define XNAME(n) x##n
#define PXN(n) printf("x"#n" = %d\n",x##n)
int main(void)
{
    int XNAME(1)=12;//int x1=12;
    PXN(1);//printf("x1 = %d\n", x1);
    return 0;
}
输出结果:
x1=12
3.
可变参数宏..._ _VA_ARGS_ _
__VA_ARGS__
是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。
实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏_ _VA_ARGS_ _就可以被用在替换部分中,替换省略号所代表的字符串。比如:
#define PR(...) printf(__VA_ARGS__)
int main()
{
    int wt=1,sp=2;
    PR("hello\n");
    PR("weight = %d, shipping = %d",wt,sp);
    return 0;
}
输出结果:
hello
weight = 1, shipping = 2
省略号只能代替最后面的宏参数。
#define W(x,...,y)
错误!

 

两类写法,具体怎么样的实现规则还不甚明朗。

 

 



典型用法:

 

##的作用1---------

显然,printf()中是使用了变参数宏VA_start()的,该函数包含在#include

 

执行结果:

 

 

―――――――――――2013年8月14日

注意:gcc中支持c99,不过要在编译选型中加入选项:-std=c99

 

 

 

##的作用2--------------

 

 

 

 

http://soft.chinabyte.com/database/288/12130788.shtml

 

 

二、详解:C语言预处理功能关于字符串化和符号粘贴

  在C语言开发中,宏定义是一个非常有用的工具,它可以使我们的代码更容易理解,更容易维护。如查一个常量在多处被使用,且今后可能会根据不同需要而修改的话,将其define一下那是再好不过了。除此之外,宏定义还有其他的功能,了解它,将更好地辅助我们开发C程序。先看个例子:

  #defineConn(x,y)

  x##y

  #defineToString(x)

  #x

  #defineToChar(x)

  #@x

  这几个宏定义中分别用到了“##”、“#”,“#@”它们的功能分别是:

  1、粘接操作符##——连接两个宏名,注意所连接的是宏名,而不是其所指代的值;

  如intConn(a,b);定义了一个int型变量ab,以后可以直接调用ab,而不必采用Conn(a,b)的形式;

  printf(Conn("ab","cd"));输出结果为:abcd

  但是:

  #define M0 #define var(x) Var_##x ... int var(M);//此处定义了什么?

  根据ANSI/ISOC,##操作符只是简单地粘接两个宏名,则intvar(M)定义的应该就是Var_M,据测试,这一点是没有问题的。但在一些比较古老的C编译环境中,也有可能定义的是Var_0,比如在TC2.0中测试发现“Var_0=0”可编译通过,而“Var_M=0”出现ERROR。

  2、字符串化操作符#——将宏名转化为字符串[x4] 

  如printf(ToString(var1));输出结果为:var1。var1可以是一个已定义的变量名,也可以是一个从未出现的字符组合

  类似地,若有:

  #defineSTR 0

  ...

  printf(Tostring(STR));//

  当前流行编译环境会输出STR,而TC 2.0则会输出0。

  3、字符化操作#@——将宏名转化为字符,注意:早期编译器可能不支持

  如:

  char c;

  c =ToChar(1);//c = '1'

  c =ToChar(a);//c = 'a'

  如果提供的宏中不止一个字符(注意,不能超过4个,否则编译器报错),则转化结果为最后一个字符,如

  c = ToChar(abc);//c= 'c'

  c =ToChar(abcd);//c= 'd'

  c =ToChar(abcde);//ERROR

  总结一下,关于其用法是自己总结的,肯定不全。

  1、使用中遵循ANSI C中规定,但要记得编译通不过是可能是早期编译器不支持C标准的问题;

  2、##操作可应用在变量定义中,若程序开发中遇到要定义一大堆变量,且这些变量具有相同的前缀时,##很显得尤为重要,它可以使代码更加整洁,且减少了出错的机率。如果一旦后来发现需要批量修改变量前缀,你会庆幸自己使用了这么一件利器;

  3、#操作符可用于调试时将变量名输出,可配合##一起使用,如定义#define CHECK_VAR(x,fmt) printf(#x " =" #fmt "\n", x),则CHECK_VAR(var1,%d)相当于printf("var = %d\n", var1);

  Tips:

  1、ANSI C中规定若宏定义名出现在引号(''或" ")中,则不进行替换,但有些早期编译器的处理可能有所不同,如#define CTRL(c) (‘c’ & 37),按照标准CTRL(d)被扩展成('c'& 37)。显然,这没有完成作者的本意,它在某些编译器下碰巧能工作不过是个意外,实际使用中应避免。

  2、字符串的连接不必使用##这么麻烦,实际中两个字符串常量可以直接写到一起,如printf("ab""cd")输出abcd。或在使用##宏定义时,可以用printf(ToString(str) "\n");输出字符串后换行,以前不敢这样用,后来试验了下发现还比较好使,当然直接用puts也可完成同样功能。

  3、若要使#或##转换的是宏字符常量的值也不是其名字,可以使用间接方法,如:

  #defineToString(x) #x

  #defineXstr(x) ToString(x)

  #defineSTR1 STR2 ...

printf(Xstr(STR1));//输出结果是STR2而非STR1

 

 

 

 

 

 

 

 

__VA_ARGS__用法

http://www.itqun.net/content-detail/72062.html

自定义调试信息的输出
  调试信息的输出方法有很多种,例如直接用printf,或者出错时使用perror, fprintf等将信息直接打印到终端上,Qt上面一般使用qDebug,而守护进程则一般是使用syslog将调试信息输出到日志文件中等等...
  使用标准的方法打印调试信息有时候不是很方便,例如Qt编程,在调试已有的代码时, 我想在打印调试信息的地方, 把代码位置也打印出来以方便定位错误,或者需要在调试信息前面加一个前辍,好方便在调试信息太多的时候可以用grep过滤一下,仅显示本模块的调试信息,这时就需要一个一个地修改已有的qDebug,使其成为以下形式:
qDebug("[模块名称]调试信息 File:%s, Line:%d", __FILE__, __LINE__ );
  这样的修改比较烦人,而且一不小心会遗漏某个没改的...
  为了能方便地管理调试信息的输出,一个比较简单的方法就是自已定义一个打印调试信息的宏,然后替换原来的,废话就不多说了,直接给出一个现成的,下面是一个例子,我用WiFi表示当前代码的模块名称,我要求在模块中的所有调试信息前面均带有[WiFi]前辍,这样我就能方便地只需使用命令行 | grep "\[WiFi\]"来过滤掉来自其它模块的调试信息了:

 

典型用法:
#defineqWiFiDebug(format, ...) qDebug("[WiFi] "format" File:%s,Line:%d, Function:%s", ##__VA_ARGS__, __FILE__, __LINE__ , __FUNCTION__);

 


  上面的宏是使用qDebug输出调试信息,在非Qt的程序中也可以改为printf,守护进程则可以改为syslog等等...其中,决窍其实就是这几个宏 ##__VA_ARGS__, __FILE__, __LINE____FUNCTION__,下面介绍一下这几个宏:
1) __VA_ARGS__是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错,你可以试试。
2)__FILE__ 宏在预编译时会替换成当前的源文件名
3)__LINE__宏在预编译时会替换成当前的行号
4)__FUNCTION__宏在预编译时会替换成当前的函数名称
  有了以上这几个宏,特别是有了__VA_ARGS__,调试信息的输出就变得灵活多了。
  有时,我们想把调试信息输出到屏幕上,而有时则又想把它输出到一个文件中,可参考下面的例子:
//debug.c
#include
#include
//
开启下面的宏表示程序运行在调试版本,否则为发行版本, 这里假设只有调试版本才输出调试信息
#define _DEBUG
#ifdef _DEBUG
    //
开启下面的宏就把调试信息输出到文件,注释即输出到终端
    #define DEBUG_TO_FILE
    #ifdef DEBUG_TO_FILE
        //
调试信息输出到以下文件
        #define DEBUG_FILE"/tmp/debugmsg"
        //
调试信息的缓冲长度
        #define DEBUG_BUFFER_MAX 4096
        //
将调试信息输出到文件中
        #define printDebugMsg(moduleName,format, ...) {\
            charbuffer[DEBUG_BUFFER_MAX+1]={0};\
            snprintf(buffer, DEBUG_BUFFER_MAX \
                   , "[%s] "format" File:%s, Line:%d\n", moduleName,##__VA_ARGS__, __FILE__, __LINE__ );\
             FILE*fd = fopen(DEBUG_FILE, "a");\
            if ( fd !=NULL ) {\
               fwrite( buffer, strlen(buffer), 1, fd );\
               fflush( fd );\
               fclose( fd );\
            }\
        }
    #else
        //
将调试信息输出到终端
        #define printDebugMsg(moduleName,format, ...) \
                 printf( "[%s] "format" File:%s, Line:%d\n", moduleName,##__VA_ARGS__, __FILE__, __LINE__ );
    #endif //end for #ifdef DEBUG_TO_FILE
#else
    //
发行版本,什么也不做
    #define printDebugMsg(moduleName, format, ...)
#endif //end for #ifdef _DEBUG
int main(int argc, char** argv)
{
    int data = 999;
    printDebugMsg( "TestProgram", "data =%d", data );
    return 0;
}

 

三、va_startva_end使用详解

  本文主要介绍va_start和va_end的使用及原理。

  在以前的一篇帖子FormatMessageBox详解中曾使用到va_start和va_end这两个宏,但对它们也只是泛泛的了解。

  介绍这两个宏之前先看一下C中传递函数的参数时的用法和原理:

1.在C中,当我们无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表

void foo(...);
void foo(parm_list,...);

这种方式和我们以前认识的不大一样,但我们要记住这是C中一种传参的形式,在后面我们就会用到它。

 

2.函数参数的传递原理

  函数参数是以数据结构:栈的形式存取,从右至左入栈。

  首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址,举个例子如下:
void func(int x, float y, char z);
  那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。

  下面是 里面重要的几个宏定义如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。

例如 int max(intn, ...); 其函数内部应该如此实现:
#include
void fun(int a, ...)
{
  int *temp = &a;

  temp++;

  for (int i = 0; i   {
    cout << *temp << endl;
    temp++;
  }
}
int main()
{
  int a = 1;
  int b = 2;
  int c = 3;
  int d = 4;
  fun(4, a, b, c, d);
  system("pause");
  return 0;
}

Output::
1
2
3
4

3:获取省略号指定的参数
  在函数体中声明一个va_list,然后用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束。像这段代码:
void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...)
{
va_list args;
va_start(args, pszFormat); //一定要“...”之前的那个参数
_vsnprintf(pszDest, DestLen, pszFormat, args);
va_end(args);
}

 

4.演示如何使用参数个数可变的函数,采用ANSI标准形式
#include 〈stdio.h〉
#include 〈string.h〉
#include 〈stdarg.h〉

/*函数原型声明,至少需要一个确定的参数,注意括号内的省略号*/
int demo( char, ... );
void main( void )
{
   demo("DEMO", "This", "is","a", "demo!", "");
}

/*ANSI标准形式的声明方式,括号内的省略号表示可选参数*/
int demo( char msg, ... )
{
       /*定义保存函数参数的结构*/
   va_list argp;
   int argno = 0;
   char para;
     /*argp指向传入的第一个可选参数,msg是最后一个确定的参数*/
   va_start( argp, msg );
   while (1)
       {
        para = va_arg( argp, char);
           if ( strcmp( para,"") == 0 )
               break;
           printf("Parameter #%dis: %s\n", argno, para);
           argno++;
}
va_end( argp );
/*将argp置为NULL*/
return 0;
}

 以上是对va_start和va_end的介绍。

 

四、再详解 va_start与va_end

2. 定义
大家先看几宏.
在VC++6.0的include有一个stdarg.h头文件,有如下几个宏定义:
#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 ) // 将指针置为无效
如果对以上几个宏定义不理解,可以略过,接这看后面的内容.
3. 参数在堆栈中分布,位置
在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码, 这个入栈过程,堆栈地址不断递减,一些黑客就是在堆栈中修改函数返回地址,执行自己的代码来达到执行自己插入的代码段的目的.
总之,函数在堆栈中的分布情况是:地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段.
堆栈中,各个函数的分布情况是倒序的.即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分.参数在堆栈中的分布情况如下:
最后一个参数
倒数第二个参数
...
第一个参数
函数返回地址
函数代码段[x5] 

5. 代码说明:
int int_size = _INTSIZEOF(int);得到int类型所占字节数
va_start(arg_ptr, i); 得到第一个可变参数地址,[x6] 

根据定义(va_list)&v得到起始参数的地址, 再加上_INTSIZEOF(v) ,就是其实参数下一个参数的地址,即第一个可变参数地址.
j=va_arg(arg_ptr, int); 得到第一个参参数的值,并且arg_ptr指针上移一个_INTSIZEOF(int),即指向下一个可变参数的地址.
va_end(arg_ptr);置空arg_ptr,即arg_ptr=0;
总结:读取可变参数的过程其实就是堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程.
6. 在编程中应该注意的问题和解决办法
虽然可以通过在堆栈中遍历参数列表来读出所有的可变参数,但是由于不知道可变参数有多少个,什么时候应该结束遍历,如果在堆栈中遍历太多,那么很可能读取一些无效的数据.
解决办法:a.可以在第一个起始参数中指定参数个数,那么就可以在循环还中读取所有的可变参数;b.定义一个结束标记,在调用函数的时候,在最后一个参数中传递这个标记,这样在遍历可变参数的时候,可以根据这个标记结束可变参数的遍历;


 [x1]注意,只能在宏中用

 [x2]

注意该用法在宏中才有效,比如:

    #define Q(n)  #n

    printf("%s\n",Q(2));

输出为2

 

但  printf("%s\n",(2));则编译有误

 [x3]同样,这个也只能用在类函数宏中,不能用在函数中

 [x4]转换之后就只能当作字符串使用了,不能被当作变量(##却可以)

 [x5]注意,与《C和C指针》不同的是,这里堆栈是向下的低地址处生长的,两者相反。

 

 从这里可以看出,被调用的函数A在执行的时候,传入参数的存储位置是在堆栈上的,不然va_start就没有意义了。

  而上一个函数调用A时,其传入的形参是保存在临时寄存器上的(这时,A执行的时候,需要拷贝其到堆栈的参数表上);若临时寄存器不够,会保存在堆栈上,而这个位置正好与A执行时的形参存储位置对应,所以也不用有上面的拷贝过程了。 

 

-----这一段有误,错误的地方在于临时寄存器只是起到存储器到存储器的传输中转,上面的拷贝过程是不存在的,取而代之的是直接压栈,参见《C和C指针》P393 函数序部分。由于非中断代码的C语言每条语句的原子性,所以临时寄存器在函数调用时是不需要压栈的

 

 所以临时寄存器是起临时作用,一般用于返回值与形参的传递,在程序调用函数(非中断)时,是不需要把其保存到堆栈上面的。

 

 

 但到了中断中,其保存与恢复必须是全部寄存器(包括临时寄存器),因为其会中断C代码原子。

 [x6]其并不知道有多少个参数

你可能感兴趣的:(2013--2014,嵌入式网络视频开发)