学习C语言也有了一段时间,开始接触到了C预处理器和C库,之后学习到了了明示常量 #define,才发现自己之前知道的太少,对C的理解还远远不够,作此总结:
和其他预处理指令一样,明示常量#define也以#号作为一行的开始。ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和指令的其余部分之间有空格。
明示常量:#define
例1:
#define KE printf("E is %d\n",E)
如上述代码来说,每行#define都有3部分组成,第一部分(#define)是指令本身,也称作宏;第二部分(KE)是选定的缩写;第三部分(printf(“E is %d\n”,E))替换列表或替换体。
需要注意的是宏的定义:宏的名称中不允许有空格,需遵循C变量的命名规则,只能使用字符、数字和下划线的_的字符,首字符不能是数字。
一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外)。从宏变成最终替换文本的过程称为宏展开。可以在#define行使用标准C注释,每条注释都会被一个空格代替。
例1中同样进行了替换,但是个新用法,大多数我们只用宏来表示明示常量。从这句语句可以看出宏也可以表示任何字符串,甚至表达整个C表达式。
注意:虽然KE是一个字符串常量,但他只打印一个名为E的变量。由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理不会进行实际的计算,只进行替换,这一过程在编译时进行。
例2:
#define Y 4
#define KE Y*Y
//这里KE = 4*4,而不计算,只进行替换
宏定义还可以包含其他宏(有些编译器不支持这种嵌套功能),如例3
例3:
#define Y "X is %d.\n"
int x = 2;
printf(Y,x);
//这里变成了:printf("X is %d.\n",x);相应的字符串替换了Y。
//当然也可以使用:const char* y = "X is %d.\n";然后可以把y作为printf()的格式字符串。
双引号使替换的字符串成为字符串常量,编译器把该字符串储存在以空字符结尾的数组中。
例4:
#define Y "z"//这里定义了一个字符串(z\0)
#define H "I am a failure, I think it is now, I hope as a friend o\ f you must refueling"
//在宏定义的一行结尾加一个反斜杠字符可以将该行扩展至下一行,但如果下一行与第1//行不对齐的时候,第2行到语句的空格也算字符串的一部分。
还有一点儿需要注意的是预处理器发现程序中的宏后,会用宏等价的替换文本替换,如果替换的字符串中还包含宏,则继续替换宏。唯一例外的就是双引号中的宏。
另外从技术角度来看,可以把宏的替换体看作是记号型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。
例如5:
例5:
#define FRR 2*2 //该宏定义有1个记号:2*2序列。
#define SIC 3 * 3 //该宏定义有3个记号:3、*、3。
替换体中有多个空格时,字符型字符串和记号型字符串处理方式不同。如例5的 #define SIC 3 * 3 ,若预处理器把该替换体解释为字符型字符串, 3 * 3替换SIC。额外的空格是替换体的一部分。若预处理器把该替换体解释为记号型字符串, 3 * 3(分别用单个空格分隔)替换SIC。
总而言之,解释为字符型字符串会把空格视为替换体的一部分,解释为记号型字符串,把空格视为替换体中各记号的分隔符。
#define FRR 2*2 // 有1个记号
#define FRR 2 * 2 // 有3个记号
以上两句的宏定义很显然看出在重定义常量时是不同。
在#define中使用参数:
在#define中使用参数可以创建外形和作用与函数类似的类函数宏。
其实看起来很像函数,因为这样的宏也使用圆括号,而且圆括号中可以有一个或多个参数,随后这些参数出现在替换体中。
例6:
#define MEAN(X,Y) (((X)+(Y))/2) //MEAN(X,Y)为宏,括号里面的为宏参数,(((X)+(Y))/2)为替换体
int Z = 0;
Z = MEAN(2,3); //这看上和函数调用很相似
上述代码替换行为和函数调用看似相同,实则不然,他两个的用法完全不同。
接下来阐述有哪些不同之处:
说明之前,先看部分代码:
例7:
#define SQ(X) X*X
#define PR(X) printf("The result is %d.\n",X)
int main()
{
int x = 5;
int z;
printf("x = %d\n",X);
z = SQ(X);
printf("SQ(X):");
PR(z);
z = SQ(2);
printf("SQ(2):");
PR(z);
printf("SQ(x+2):");
PR(SQ(x+2));
printf("100/SQ(2):");
PR(100/SQ(2));
printf("x is %d.\n",x);
printf("SQ(++x):");
PR(SQ(++x));
printf("after incrementing,x is %x.\n",x);
return 0;
}
当然上述程序只是在阐述宏参数和函数参数不完全相等,输出的值如下(根据不同的编译器输出的值可能完全不同):
上述代码输出结果为:
x = 5;
SQ(X):The result is 25.
SQ(2):The result is 4.
SQ(X+2):The result is 17.
100/SQ(2):The result is 100.
x is 5.
SQ(++x):The result is 42.
after incrementing,x is 7.
前两行与预期值相符,但接下来的结果有些奇怪,程序设置x是5,你可能会认为SQ(x+2)应该是77,即49.但是,输出结果是17,这不是一个平方值!
其实原因很简单了,就是因为预处理器不做计算、不求值,只替换字符序列。
预处理器把x出现的地方都替换成x+2。因此,xx变成了x+2x+2。如果x为5,那么该表达式的值为:
5+25+5 = 5+10+2 = 17!
函数调用在程序运行时把参数的值传递给函数。宏调用在编译器之前把参数记号传递给程序!
当然,你可以加几个括号,但这并不能解决所有的问题。
必要时要使用足够多的圆括号来确保运算和结合的正确顺序。
即使这样也无法避免程序最后一种情况的问题。SQ(++x)变成了++x*++x,递增了两次x,一次在乘法运算之前,一次在乘法运算之后。
所以,要尽量避免用x++作为宏参数。尽量不要在宏定义中使用递增和递减运算符。
还有一种使用:用宏参数创建字符串#运算符
C允许在字符串中含宏参数,#号作为一个预处理运算符,可以把记号转化为字符串。例如:若x是一个宏参数,那么#x就是转换字符串“x”的形参名。这个过程叫做字符串化。
例8:
#define FRR(x) printf("The result "#x" is %d.\n",((x)*(x)))
int main()
{
int y = 5;
FRR(y);
FRR(2+4);
return 0;
}
改程序输出结果为:
The result y is 25.
The result 2+4 is 25.
调用第一个宏,用“y”替换“#x”,调用第二个宏,用“2+4”替换“#x”。
最后不得不提的还有预处理器粘黏剂:##运算符和变参宏:…和_ VA_ARGS _
与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可以用于对象宏的替换部分。##运算符把两个记号组成一个记号。
例9:
#define KE(x) n##x
宏KE(4)将展开为n4。需要注意的是##运算符把记号组合成了一个新的标识符。
至于变参宏:…和_ VA_ARGS 其实也比较简单。
通常一些函数接受数量可变的参数。通过把宏参数列表中最后的参数写成省略号(即:3个点…)来实现这一功能。这样的话 VA_ARGS _就可以用到替换部分中,表明省略号代表什么。
例10:
#define PR(...) printf(_ _VA_ARGS_ _)
//假设稍后调用宏
PR("hollo");
PR("The result "#x" is %d.\n",w,z);
对于第一次调用,_ VA_ARGS 展开为1个参数"hollo"。
对于第二次调用, VA_ARGS _展开为3个参数"The result “#x” is %d.\n"、w、z。
省略号只能代替最后的宏参数,不能放在其他位置。
总结:
1:记住宏名中不允许有空格,但是在替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。
2:用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分正确地展开。
3:用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。
4:如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效举。
该文章是我学习C语言时大部分借鉴《C primer puls》一书所写。
如有错误,请指正。