预处理指令#define,你真的懂了吗?

学习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。因此,x
x变成了x+2x+2。如果x为5,那么该表达式的值为:
5+2
5+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》一书所写。
如有错误,请指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值