宏——基础
编译4个过程:预处理,编译,汇编,连接。宏就是在预处理阶段发挥作用。
宏结尾没有;,因为凡是;结尾的东西,都是由第二阶段“编译”来处理的(a.i->a.s),而所有预编译的代码都是在预编译阶段处理的,为了以示区分,所以所有预编译的代码都不需要分号结尾。
宏有两种,一种是有宏体宏,另一种是无宏体宏。
无宏体宏
什么是无宏体宏
只有宏名、没有宏体。
定义形式
#define 宏名
举例
#define X86
预编译完后,由于这个宏没有宏体,所以宏直接替换为空,空就是啥也没有。
有宏体宏
有宏体宏分为两种,一种是无参宏,另一种是有参宏。
无参宏
定义形式
#define 宏名 宏体
举例
#define YES 1 #define NO 0 #define PI 3.1415926 #define OUT printf(“Hello,World”);
预处理后,宏名会被替换为宏体。
带参宏
定义形式
#define 宏名(参数表) 宏体
举例
#define S(a,b) a*b*10 int main(void) { int va; va = S(3,2); //3对应a,2对应b printf("va = %d\n", va); return 0; }
预编译处理时,将宏体中的a和b,使用参数中的3和2来替换。va = S(a, b) —> va = 3*2*10
带参宏需要注意之处
①宏名和参数列表之间不能有空格
#define S (a,b) a*b*10
由于S和(a, b)之间有空格,宏名变为了S,宏体变为了(a,b) a*b*10,含义发生了变化。
②写带参宏的时,不要吝啬括号
#define S(a,b) a*b*10
其实这个带参宏是有缺点的,如果参数写成如下形式的话,替换后结果可能就完全背离了你的本意。
S(x+1, y+2) —> x+1*y+2*10
对于预编译器来说,它再处理宏定义时,它并不知道你的使用意图是什么,它只会忠实的进行替换工作,但是替换之后是否能够达到你要的效果,这个就不一定了。怎么解决?
为了避免这种情况,大家在定义带参宏时不要吝啬括号。
#define S(a,b) ((a)*(b)*10) //为了保险起见,对整个宏体最好也加一个()。
S(x+1, y+2) ——> ((x+1)*(y+2)*10)
带参宏 与 函数
这两个玩意儿长得很像,但实际上是两个完全不同的东西。
例子
#include <stdio.h> #define S(a,b) a*b*10 void s(int a, int b) { return a*b*10; } int main(void) { int va1, va2; va1 = S(3, 2); //引用带参宏 va2 = s(3, 2); //调用函数 printf("va1 = %d, va2 = %d\n", va1, va2); return 0; }
仅仅从调用来看,这两个东西确实长得很像,如果将宏也定义为小写的话,仅看调用的话,很难看出这个到底谁是函数谁是宏定义。为了能够让大家快速的区分带参宏和函数,大家在定义宏的时候,宏名一定要大写,否则在阅读代码时,很容易与函数搞混,非常不利于代码的阅读和理解。
二者的区别
二者是有着本质区别的:
带参宏
处理阶段:预编译
宏只是一个供我们程序员识别的一个符号,一旦预编译之后带参宏就会消失了,取而代之的是宏体。
参数列表
带参宏的形参是没有类型的,我们使用int 、float等类型只有一个目的,就是使用类型来开辟一个变量空间,变量空间的字节数和存储格式是由类型来决定的,所以定义变量时必须要有类型说明。而带参宏的参数仅仅只起到替换说明的作用,不需要开辟空间来存放实参的值,既然不需要开辟空间,那就不需要类型的说明。
函数
处理阶段:由编译、汇编、链接阶段处理
在“预处理阶段”是不会处理函数这个东西的,在预处理前后,函数没有任何变化。
函数是一个独立体,有调用的过程
运行函数时涉及调用的过程:
调用时:从当前函数跳转到被调用的函数,开辟形参和自动局部变量时,涉及压栈操作。
调用结束:返回到调用函数,释放函数的形参和自动局部变量的空间时,涉及弹栈操作
函数的参数列表
函数的形参是需要被开辟空间的,所以必须要要有类型说明。
宏的一些值得强调的地方
预处理完之后,宏定义和宏引用都会消失
#define NUM 100 //宏定义,预处理后消失 int main { int a; a = NUM; //宏引用,预处理后被替换为宏体,宏引用消失 return 0; }
宏名的字母一般习惯大写,以便与变量名、函数名相区别
如果宏名小写的话,会很容易和正常的变量和函数混淆。
疑问:难道真的没有小写的宏吗?
其实也不是,在少数某些特殊情况下,还真有定义为小写的,但是这种情况比较少见。标准IO函数,有三个宏(stdio.h):
stdin:标准输入(从键盘输入数据)
stdout:标准输出
stderr:标注出错输出
这三个宏其实就是小写的,之所以写成小写,应该是历史遗留问题。
所有预编译的代码都是独占一行的(不能多行)
#define STUDENT struct student{int a; int b;};
为了独占一行,我把结构体写在了一行中,但是这样子不方便理解,我们往往会把它改成如下形式
#define STUDENT struct student{\ int a; \ int b;\ };
加了\(连行符)后,其实这几行在同一行中。
宏的作用域 与 #undef
正常情况下的宏作用域为从定义为位置开始,一直到文件的末尾。如果你希望结束宏的作用域的话,可以使用#undef这个预编译关键字。
#define NUM 100 int fun(); int main(void) { int a = NUM; return 0; } #undef NUM int fun() { int a = NUM;//这里将无法使用这个宏 }
定义宏时可以嵌套引用其它的宏,但是不能嵌套引用自己
嵌套其它宏
#define WIDTH 80 #define LENGTH (WIDTH)+40 #define AREA WIDTH*(LENGTH) int main(void) { int a = AREA; return 0; }
这种形式很显然是正确的。如下写法也是正确的
#define AREA WIDTH*(LENGTH) #define WIDTH 80 #define LENGTH (WIDTH)+40 int main(void) { int a = AREA; //WIDTH*(LENGTH) —>80*(LENGTH) —>>80*40 return 0; }
这个写法是正确的,只要宏引用的位置在定义位置的作用域范围内就行。显然AREA的引用都在AREA、WIDTH、LENGTH作用域内,所以AREA的引用在替换时,完全不存在任何问题。如下代码AREA的引用不再LENGTH作用域内,预处理没问题,但是编译时回报未定义符号
#define AREA WIDTH*(LENGTH) #define WIDTH 80 int main(void) { int a = AREA; WIDTH*(LENGTH) —>80*(LENGTH) —>>80*40 return 0; } #define LENGTH (WIDTH)+40
为什么不能嵌套自己
#define AREA AREA*10 int main(void) { int a = AREA; return 0; }
嵌套自己时在预编译器做完替换后,最后还剩一个宏名,这个宏名无法再被替换,最后留给第二阶段编译时,将变成一个无法识别的符号,从而报错。所以宏不能嵌套自己,这个和函数不一样,函数嵌套调用自己是递归,宏嵌套引用自己就是犯错。
只作字符替换,不做正确性检查
预编译器处理宏时,预编译器只关心替换的事情,至于替换的宏体的写法是否正确,预编译器本身并不做检查,因为判断写法是否正确的这件事情是由第二阶段的编译来做的。
#define NUM 100WEE int main(void) { int a = NUM; return 0; }
整形数100WEE的写法完全是错的,但是在预编译时根本不会提示任何错误,预编译器会忠实的将NUM换为100WEE,但是后续编译时就会报无法识别100WEE的错误。
预定义宏
什么是预定义宏
预定义宏,也可以称为编译器内置宏,这个宏并没有定义在哪个.h文件中,所以不能再哪个.h中找到这些玩意。进行预编译时,当预编译器看到这些玩意时,会自动处理这些预定义宏。其实将这些预定义宏称为预编译关键字,可能更好些。
作用
__DATE__:代表预处理的日期
当预处理器检测到__DATE__后,会将其替换为"月 日 年"的字符串形式的时间,时间格式是西方人习惯的格式。
__FILE__:代表当前预编译正在处理的那个源文件的文件名
当预处理器检测到__FILE__后,会将其替换为"***.c"的文件名。
__LINE__:代表__LINE__当前所在行的行号
当预处理器检测到__LINE__后,会将其替换为__LINE__当前所在行的行号(整形)。
__TIME__:代表对源文件进行预编译时的时间
当预处理器检测到__TIME__后,会将其替换为“hh:mm:ss”格式的时间。
__func__:当前__func__所在函数的函数名
不过这个在预编译阶段不会被处理,而是留到编译阶段处理。
预定义宏的意义 与 调试宏
意义
常常用于调试打印、跟踪代码用。当一个程序写大了后,在调试程序的功能性错误时,往往需要打印信息来跟踪代码,看看程序是运行到什么位置时才出现了功能性错误,以方便我们调试。
printf("%s %d %s\n", __FILE__, __LINE__, __func__);
调试宏
在每个需要打印的地方都写printf会非常的麻烦,因此我们可以把它写成调试宏。
#include <stdio.h> //调试宏,DEBUG的名字可以自己随便起 #define DEBUG printf("%s %d %s\n", __FILE__, __LINE__, __func__); void exchange(int *p1, int *p2) { DEBUG int tmp = 0; DEBUG tmp = *p1; DEBUG *p1 = *p2; DEBUG *p2 = tmp; DEBUG } int main(void) { int a = 10; int b = 30; DEBUG exchange(&a, &b); DEBUG printf("a=%d, b=%d\n", a, b); DEBUG return 0; }
通过打印信息来跟踪程序,其实有些时候比“单步运行调试”更好用,因为单步运行调试在某些情况其实很麻烦,不如打印信息来的好使。
如果你想打印自定义信息的话,我们还可以将调试宏定义为带参宏
#define DEBUG(s1, s2) printf(s1, s2);
疑问:感觉这么写,也不比直接写printf("%d\n", va)方便多少呀?
不直接使用printf,而是写成DEBUG(s1, s2)带参宏的形式,可以方便我们使用“条件编译”来快速打开和关闭调试宏,后面将再介绍这个问题。