预处理指令#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、付费专栏及课程。

余额充值