Fork me on GitHub

宏——基础

编译4个过程:预处理,编译,汇编,连接。宏就是在预处理阶段发挥作用。

宏结尾没有;,因为凡是;结尾的东西,都是由第二阶段“编译”来处理的(a.i->a.s),而所有预编译的代码都是在预编译阶段处理的,为了以示区分,所以所有预编译的代码都不需要分号结尾。

宏有两种,一种是有宏体宏,另一种是无宏体宏。

无宏体宏

什么是无宏体宏

只有宏名、没有宏体。

定义形式

#define 宏名 

举例

#define X86

预编译完后,由于这个宏没有宏体,所以宏直接替换为空,空就是啥也没有。

有宏体宏

有宏体宏分为两种,一种是无参宏,另一种是有参宏。

无参宏

定义形式

#define 宏名 宏体

举例

#define YES 1
#define NO 0
#define PI 3.1415926
#define OUT printf(“Hello,World”);
View Code

预处理后,宏名会被替换为宏体。

带参宏

定义形式

#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;
}
View Code

预编译处理时,将宏体中的a和b,使用参数中的3和2来替换。va = S(a, b) —> va = 3*2*10

 带参宏需要注意之处

①宏名和参数列表之间不能有空格

#define  S  (a,b)  a*b*10  
View Code

由于S和(a, b)之间有空格,宏名变为了S,宏体变为了(a,b) a*b*10,含义发生了变化。

②写带参宏的时,不要吝啬括号

#define  S(a,b)  a*b*10
View Code

其实这个带参宏是有缺点的,如果参数写成如下形式的话,替换后结果可能就完全背离了你的本意。

S(x+1, y+2) —> x+1*y+2*10

对于预编译器来说,它再处理宏定义时,它并不知道你的使用意图是什么,它只会忠实的进行替换工作,但是替换之后是否能够达到你要的效果,这个就不一定了。怎么解决?

为了避免这种情况,大家在定义带参宏时不要吝啬括号。

#define  S(a,b)  ((a)*(b)*10)   //为了保险起见,对整个宏体最好也加一个()。
View Code

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;
}
View Code

仅仅从调用来看,这两个东西确实长得很像,如果将宏也定义为小写的话,仅看调用的话,很难看出这个到底谁是函数谁是宏定义。为了能够让大家快速的区分带参宏和函数,大家在定义宏的时候,宏名一定要大写,否则在阅读代码时,很容易与函数搞混,非常不利于代码的阅读和理解。

二者的区别

二者是有着本质区别的:

带参宏

处理阶段:预编译

宏只是一个供我们程序员识别的一个符号,一旦预编译之后带参宏就会消失了,取而代之的是宏体。

参数列表

带参宏的形参是没有类型的,我们使用int 、float等类型只有一个目的,就是使用类型来开辟一个变量空间,变量空间的字节数和存储格式是由类型来决定的,所以定义变量时必须要有类型说明。而带参宏的参数仅仅只起到替换说明的作用,不需要开辟空间来存放实参的值,既然不需要开辟空间,那就不需要类型的说明。

函数

处理阶段:由编译、汇编、链接阶段处理

在“预处理阶段”是不会处理函数这个东西的,在预处理前后,函数没有任何变化。

函数是一个独立体,有调用的过程

运行函数时涉及调用的过程:

调用时:从当前函数跳转到被调用的函数,开辟形参和自动局部变量时,涉及压栈操作。

调用结束:返回到调用函数,释放函数的形参和自动局部变量的空间时,涉及弹栈操作

函数的参数列表

函数的形参是需要被开辟空间的,所以必须要要有类型说明。

宏的一些值得强调的地方

预处理完之后,宏定义和宏引用都会消失

#define NUM  100  //宏定义,预处理后消失
int main
{
    int a;            
    a = NUM;  //宏引用,预处理后被替换为宏体,宏引用消失
    return 0;
}
View Code

宏名的字母一般习惯大写,以便与变量名、函数名相区别

如果宏名小写的话,会很容易和正常的变量和函数混淆。

疑问:难道真的没有小写的宏吗?

其实也不是,在少数某些特殊情况下,还真有定义为小写的,但是这种情况比较少见。标准IO函数,有三个宏(stdio.h):

stdin:标准输入(从键盘输入数据)

stdout:标准输出

stderr:标注出错输出

这三个宏其实就是小写的,之所以写成小写,应该是历史遗留问题。

所有预编译的代码都是独占一行的(不能多行)

#define STUDENT struct student{int a; int b;};

为了独占一行,我把结构体写在了一行中,但是这样子不方便理解,我们往往会把它改成如下形式

#define STUDENT struct student{\
    int a; \
    int b;\
};
View Code

加了\(连行符)后,其实这几行在同一行中。

宏的作用域 与 #undef

正常情况下的宏作用域为从定义为位置开始,一直到文件的末尾。如果你希望结束宏的作用域的话,可以使用#undef这个预编译关键字。

#define NUM 100 
int fun();
                
int main(void)
{
    int a = NUM;
    return 0;
}
                
#undef NUM
                
int fun()
{
    int a = NUM;//这里将无法使用这个宏
}
View Code

定义宏时可以嵌套引用其它的宏,但是不能嵌套引用自己

嵌套其它宏

#define   WIDTH       80
#define   LENGTH       (WIDTH)+40
#define   AREA        WIDTH*(LENGTH)
                    
int main(void)
{
    int a = AREA;
    return 0;
}
View Code

这种形式很显然是正确的。如下写法也是正确的

#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;
}
View Code

这个写法是正确的,只要宏引用的位置在定义位置的作用域范围内就行。显然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
View Code

为什么不能嵌套自己

#define  AREA      AREA*10
int main(void)
{
    int a = AREA;
    return 0;
}
View Code

嵌套自己时在预编译器做完替换后,最后还剩一个宏名,这个宏名无法再被替换,最后留给第二阶段编译时,将变成一个无法识别的符号,从而报错。所以宏不能嵌套自己,这个和函数不一样,函数嵌套调用自己是递归,宏嵌套引用自己就是犯错。

只作字符替换,不做正确性检查

预编译器处理宏时,预编译器只关心替换的事情,至于替换的宏体的写法是否正确,预编译器本身并不做检查,因为判断写法是否正确的这件事情是由第二阶段的编译来做的。

#define NUM 100WEE
int main(void)
{        
    int a = NUM;
    return 0;
}    
View Code

整形数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__);
View Code

调试宏

在每个需要打印的地方都写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;
}
View Code

通过打印信息来跟踪程序,其实有些时候比“单步运行调试”更好用,因为单步运行调试在某些情况其实很麻烦,不如打印信息来的好使。

如果你想打印自定义信息的话,我们还可以将调试宏定义为带参宏

#define DEBUG(s1, s2) printf(s1, s2);

疑问:感觉这么写,也不比直接写printf("%d\n", va)方便多少呀?

不直接使用printf,而是写成DEBUG(s1, s2)带参宏的形式,可以方便我们使用“条件编译”来快速打开和关闭调试宏,后面将再介绍这个问题。

posted @ 2018-08-04 09:41  克拉默与矩阵  阅读(650)  评论(0编辑  收藏  举报