文档下载地址
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
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_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 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
例如 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]其并不知道有多少个参数