- 在ANSI C的标准中,任何一种实现都存在两个不同的环境:翻译环境和执行环境;
而预处理就是在翻译环境进行的,预处理也称为预编译
接下来就来详细讲讲预处理环节的细节 (为了方便观看代码,下面的对代码的解释和板书会采用全角输出的模式)
一、预定义符号
c语言会内置预定义符号,这种标识符在c语言中没有特定的含义,可以用printf函数进行格式化输出。可作为用户标识符使用,让我们了解到当前文件更多的编译信息,但是使用不当也会使程序报错。
他们的功能如下:
二、#define
#define也是预处理指令的一种 ,c语言中可以用#define来定义标识符号和宏;
1、定义标识符号:
可以直接将对应的用户关键字替换为#define所定义的内容,遵循替换原则
(这个相比都不陌生)在写链表或者其他的程序的时候我们经常使用#define来定义字母,来让程序后期的维护更加的安全快捷。
首先我们可以想到的就是 用 #define去定义一个符号,例如:
#define Max 100 //用#define定义整形
#define exm "abcdefg" //用#define定义字符串
我们将其打印,结果如下:
(原则上,只要#define定义的内容符合代码语法规则,就可以定义,例如整形,字符串,甚至是一段代码)
2、定义宏
#define允许把参数替换到文本当中,这种实现通常称为宏:
下面是宏申明的格式
#difne name ( paramen_list ) stuff 其中parament_lsit 是用逗号隔开的参数列表
(需要注意的是,参数列表左边的括号必须和name紧邻,如果留有空白的话就变成了前面所讲的情况)
举个例子:例如我想求一下 一个int a 和 int b 两个整形的较大值
#include<stdio.h> #define Max(x,y) (x>y?x:y) int main() { int a=1; int b=2; int m=Max(a,b); printf("%d\n",m); return 0; }
最后运行结果为 : 2.
上面提到define的替换性,可见,我们所定义的整形或者字符串在define看来是没有数据类型的;所以在定义宏的时候,不会直接将某些表达式的结果传入宏当中,而是将这个表达式整体传过去进行计算。如果是这种情况的话,就难免会产生表达式计算优先级的问题;
举个简单的例子:
#include<stdio.h> #define CALC(x) x*x int main() { int m = CALC( 6); printf("%d\n", m); return 0; }
我们写一个这样的代码,我们传入6,那么用宏来计算之后的值必然是36;
---->但是如果,我们将b改成 3+3 ,结果又会是什么呢?结果如下:
虽然3+3的值也为6,但是传入的时候由于是整体代入,所以最终CALC(3+3)就被替换成了
: int m = 3 + 3 *3 + 3。这种优先级的问题也就体现出来了。
但是居然遇到这种问题,那必然就有解决的方法:
针对于优先级的问题,我们可以想到用括号来解决,例如将上述的 #define后面的内容改为
#define CALC(x) (x)*(x)
这样一来问题就解决了,但是如果我们经常使用#define定义宏之后,其实还会发现参数列表右括号右边的表达式,最好在最外层也带上括号:
#define CALC(x) ((x)*(x))
目前这个平方的宏暂时看不出来什么猫腻,因为这些代码一般都是内层替换的时候出现的问题;我们换个代码来看看:
#include<stdio.h> #define JIA(x) (x)+(x) int main() { printf("%d\n", 2 * JIA(2)); return 0; }
按理来说JIA(2)的结果应该为4,但是打印后的结果确为》》6 ,不难分析,是#define将整体替换为了
printf ("%d\n", 2 * 2 + 2);
这就体现了在宏的外层加上括号的重要性(不管是外层还是内层!):
#include<stdio.h> #define JIA(x) ((x)+(x)) int main() { printf("%d\n", 2 * JIA(2)); return 0; }
3、#define替换规则
接下来详细了解一下#define的替换规则
1、在调用宏时,首先要对参数进行检查,查看是否包含#define定义的字符,如果是,它们首先被替换
2、替换文本随后会被插入到程序中原来的文本位置,对于宏,参数名被他们的值所替换。
3、最后再次对文件结果进行扫描,查看是否还包含#define定义的标识符,如果存在,就重复上述操作。
注意:
1、宏参数和#define定义中可以出现其他#define定义的字符,但是对于宏,不能出现递归
2、当预处理器搜索#define定义的符号的时候,字符串常量里的内容不被搜索,例如:
#define MAX 100
char arr[ 10 ] = "abcdefgxxxMAX"; //其中的MAX不被#define搜索和替换
4、预处理符号#与##
1、#
简单直入,在二、3的替换规则末尾,也就是上面两行内容中提到,#define不能搜索字符串常量里面的内容,那么问题来了,如果某一天需要去替换字符串里面的内容的时候该怎么办?
这就要说说#和##的作用了————将参数插入到字符串当中。
我们先来看看这样的一串代码:
1. #include<stdio.h> 2. int main() 3. { 4. printf("hello world\n"); 5. printf("hello"" world"); 6. return 0; 7. }
它的运行结果都为:hello world
我们可以看出来:两个紧邻的字符串最后合成了一个字符串。
其实在敲代码的时候,不免遇到这样的场景:有一些整形变量如下
int a = 1; int b = 3 ; int c=4.......
我们是否能在一个for循环内使用一个printf函数将如下内容打印下来:
the value of a is 1;
the value of b is 3;
the value of c is 4;
...........以此类推
如果不用for循环,用一个一个用printf函数打印的时候,过程不免冗杂:
int a =1; float b=2.3f; int c =3;
printf("the value of a is %d",a);
printf("the value of a is %f",b);
printf("the value of a is %d",c);
对于一个for循环,我们发现,一个函数时解决不了问题的,所以宏的重要性就体现出来了:
#define print(x,y) printf("the value of "#x" is "format"\n",x);
对上述内容进行一个解释:由于#define不能搜索字符串里面的内容,所以我们的printf里面的内容用多个字符串("the value of" #x " is "" format" "\n")替代,但是由前面的内容可以看到,如果x为我们上述的a,b,c等变量的时候,他就会把x替换为相应的值,而不是直接替换成用户变量(如果传入a,那么#x就会变成"a"),这个时候就要用到#了。
在这里#的作用就是直接将x对应的字符传过去,而不是值的替换,例如我们传入print(a,"%d"):
结果为: printf("the value of " "a" " is " "%d" "\n",x);
即: printf("the value of a is %d\n",x);
我们依次传入 print(变量名,数据类型);就能将其打印出来,这样就方便了很多。
2、##
##可以将位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符:
有个变量的名为 int jinitaimei =6;
#define cat(x,y) x##y
printf("%d\n",cat( ji , nitaimei ));
输出结果为6;
(标识符必须合法,否则会显示标识符未定义)
5、#define定义宏和函数对比
1、#define和函数的简单对比
从上面的例子不免看出来,越是复杂运算,宏所带来副作用越大,所以宏在非必要时一般都用来执行简单的计算,宏被用来实现量大但是计算过程简单的代码时非常适合不过的;
那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点 型等可以用于>来比较的类型。
3.宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
除此之外,在命名上,宏一般采用全大写的形式,而函数则采用一般的驼峰式,或者是下划线加驼峰式的组合式(多为小写,便于区分宏和函数)。
2、表格对比
#define和函数对比
属性 | #define | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 |
函数代码只出现于一个地方;每次使用这个函数时,都调用那个 地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
副作用参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 |
函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 |
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是 相同的 |
调试 | 不方便调试 | 可以逐语句调试 |
递归 | 不能递归 | 可以递归 |
程序预处理:全解版-2