define本质上是将文本内容在​编译时进行替换​,又因为他可以替换参数所以就出现了宏。


​#define​​替换文本

语法:​​#define name stuff​

举一个例子

#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )

我们C语言的程序员内部有默契,我们把​​#define​​定义的一般做​全大写​函数命名一般不是全大写一般是首字母大写或其他部分大写,当然我们宏定义假做成函数也会没有全大写。

​#define定义宏​


​#define允许把参数替换到文本中,这种实现通常被称为宏,或定义宏​


​define​​name(由逗号隔开的符号) stuff

举例:

#include<stdio.h>
#define SQUARE(x) x*x
int main()
{
printf("%d", SQUARE(3));
return 0;
}

![[202201210943514.png]]

值得注意的是:

上面的宏其实有一个很大的弊病.

我们用下面的代码来说明:

#include<stdio.h>
#define SQUARE(x) x * x
int main()
{
printf("%d", SQUARE(3+2));
return 0;
}

![[202201210945131.png]]

这并不是我们想要的答案我们想得到5*5可是这个宏定义给的式子却给了我们11这是因为我们在定义宏时没有​考虑到运算的优先级​.

首先我们的宏在编译阶段会直接和代码替换本次的宏就将printf函数内容进行了替换使​​SQUARE(3+2)替换成了3+2*3+2这样我们就得到了11的值。​

所以使用宏的时候一定要加括号!加括号!

#include<stdio.h>
#define SQUARE(x) ((x) * (x))
int main()
{
printf("%d", SQUARE(3+2));
return 0;
}

就不会出现以上情况了。

** #define 替换规则 **

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们​首先被替换​。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,​不能出现递归​。
  2. 当预处理器搜索#define定义的符号的时候,​字符串常量的内容并不被搜索​。
  3. 传递给函数宏的参数不能包含看似预处理指令的标记。
#define A 100
printf("A"); //不行
"#define A 100" //不行
字符串宏常量
//第一个宏,字符串没有带双引号,直接报错
#define PATH1 hello!

//第二个宏,字符串带上双引用,有告警,能够编译通过。不过windows中路径分割符需要\\,输出乱码,改过之后,正常
//双斜杠是为了防止转义成别的字符
#define PATH1 "C:\\Users\\Desktop\\C语言"

注:​可以使用反斜杠进行续行。

用宏定义充当注释符号

直接上结论: ** 是先执行去注释,在进行宏替换。**

(可以去linux平台上试一试)

//当前,我们用BSC充当C++风格的注释  
#define BSC //
int main()
{
BSC printf("hello world\n");
return 0;
}

//结果
hello world
//我们发现,并没有报错
//甚至可以运行,但是目标代码并没有被注释掉

//查看预处理过程
int main()
{
printf("hello world\n");
return 0;
}

倘若,先执行宏替换,那么先得到的代码应该是。

int main()  
{
//将BSC替换成为‘//’
// printf("hello world\n");
return 0;
}
//再执行去注释,那么代码最终的样子,应该是
int main()
{
return 0; //printf被注释掉
}

但实际并非如此。 所以​是先执行去注释,在进行宏替换​。

//先去掉宏后面的//,因为是注释  
#define BSC // //最终宏变成了#define BSC
int main()
{
BSC printf("hello world\n"); //因为BSC是空,所以在进行替换之后,就是printf("hello world\n");
return 0;
}

再来一个例子:

#define BSC //
#define BMC /*
#define EMC */

//注意:EMC已经被是先被注释掉,BMC成为空

int main()
{
BSC printf("hello world\n");
BMC printf("hello bit\n"); EMC
return 0;
}

//预处理之后
int main()
{
printf("hello world\n");
printf("hello bit\n"); EMC
return 0;
//在宏定义那里,EMC已经被注释掉了,所以这里无法替换
}

宏定义中的空格

#define INC(a) ((a)++) //定义不能带空格  
int main()
{
int i = 0;
INC (i); //使用可以带空格,但是严重不推荐(这种地方还是不要特立独行的好)
printf("%d\n", i);
}

#和##

介绍这俩之前我们需要先知道

char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s\n", p);

他们的打印结果都是"hello bit"这是因为​字符串有自动连接​的特点.

  • 字符串的自动连接
  • # 把一个宏参数变成​对应的字符串​。
  • ##将两个符号合成为一个符号

介绍一下​​#​​操作直接上例子:

#define PRINT(x) printf("The "#x" value is %d",x)
int main()
{
int a = 10;
PRINT(a);
return 0;
}

![[Pasted image 20220227161626.png]]

这样define里的#x和"x"一样.

#define PRINT(x,y) printf("The "#x" value is" #y,x)

##的作用把​两边的符号合成一个符号​.

#define ADD_TO_SUM(num, value) sum##num += value
int main()
{
int sum1 = 20;
int sum2 = 10;
ADD_TO_SUM(1, 20);//sum1+=20
printf("%d", sum1);
return 0;
}

![[Pasted image 20220227161705.png]]

注: 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

带有副作用的宏参数

x+1;//不带副作用
x++;//带有副作用

不要对宏使用带有副作用的参数.

如下例

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}

![[Pasted image 20220227162001.png]]

不然总会带来我们不想要的结果因为y进行了两次++而x进行了一次++ z得到的是y++的值也就是9当然我们知道宏MAX是什么可以轻松反推但是未来大型项目中这样搞不知会出现什么bug.

undef

移除宏定义

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

![[Pasted image 20220227163330.png]]

2个问题:

  1. 宏只能在main上面定义吗?
  2. 在一个源文件内,宏的有效范围是什么?
#define M 10  
int main()
{
# define N 100
printf("%d, %d\n", M, N);
return 0;
}

![[Pasted image 20220227162302.png]]

这个也是可以的。

结论:宏定义在哪都可以,一般放在最上面。

这样呢?

![[Pasted image 20220227162715.png]]

这样会报错。

![[Pasted image 20220227163129.png]]

结论:宏的有效范围,是从定义处往下有效,之前无效。

一个问题

![[Pasted image 20220227063358.png]]

![[Pasted image 20220227153731.png]]

现在貌似没问题,但是

![[Pasted image 20220227153907.png]]

如果是这样呢?

直接报错。

![[Pasted image 20220227154019.png]]

else悬空的问题,看图就能懂√

结论:

宏定义多条语句在特定场景下可能会有一些问题。

方案1:

if和else带上花括号。

但是不是所有的用户都有带上花括号的习惯,为了代码的可维护性,我们还要进行修改。

方案2:

主动给宏加上花括号。

但是报错了,原因:

![[Pasted image 20220227155349.png]]

多了一个;

我们不能强迫程序员不加分号,所以方案二作废。

方案3:

dowhile结构。

#define INIT_VALUE(a,b) \  
do{a = 0;b = 0; }while(0)

int main()
{
int flag = 0;
scanf("%d", &flag);
int a = 100;
int b = 200;
printf("before: %d, %d\n", a, b);
if(flag){
INIT_VALUE(a,b);
}
else{
printf("error!\n");
}
printf("after: %d, %d\n", a, b);
return 0;
}

结论:​当我们需要宏进行多语句替换的时候, 推荐使用do-while-zero结构。当然,如若可以,宏方面的东西,推荐少用。

宏和函数的对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个​小型计算工作​所需要的时间更多。所以宏比函数在程序的​规模和速度方面​更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。​宏是类型无关的​。

当然和宏相比函数也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则​可能大幅度增加程序的长度​。
  2. 宏是没法调试的​。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type)\  
(type *)malloc(num * sizeof(type))

//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

属 性

#define定义宏

函数

代码长度

每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。

函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码

执行速度

更快

存在函数的调用和返回的额外开销,所以相对慢一些。

操作符优先级

宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号

函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。

带有副作用的参数

参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。

函数参数只在传参的时候求值一次,结果更容易控制。

参数类型

宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型

函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。

调试

宏是不方便调试的

函数是可以逐语句调试的

递归

宏是不能递归的

函数是可以递归的

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:

把宏名全部大写。
函数名不要全部大写。