【C语言】 —— 预处理详解(上)
创始人
2024-11-18 21:36:38
0

【C语言】 —— 预处理详解(上)

  • 一、预定义符号
  • 二、# d e f i n e define define 定义常量(符号)
  • 三、# d e f i n e define define 定义宏
  • 四、带有副作用的宏参数
  • 五、宏替换的规则
  • 六、宏和函数的对比

一、预定义符号

  C语言中设置了一些预定义符号,可以直接使用,预定义符号也就是在预处理期间处理的

__FILE__    //进行编译的源文件 __LINE__	//文件当前的行号 __DATE__	//文件被编译日期 __TIME__	//文件被编译那一瞬的时间 __STDC__	//如果编译器遵循ANSI C(标准C),其值为1,否则未定义(报错) 

  
举例:

#include  int main() { 	printf("%s\n", __FILE__); 	printf("%d\n", __LINE__); 	printf("%s\n", __DATE__); 	printf("%s\n", __TIME__); 	return 0; } 

  
运行结果:

在这里插入图片描述

  这里随便提一下,VS 不完全支持ANSI C(标准C); g c c gcc gcc 支持ANSI C(标准C)
  

二、# d e f i n e define define 定义常量(符号)

基本语法:

# define name stuff 

举个例子:

#define MAX 100 #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__ ) 
  • 第二句就是懒,咋地
  • 第三句 f o r for for 循环的初始化、判断、调整都可以省略,但是判断如果省略,则意味着判断条件恒为真,即死循环
  • 第四局最好别这么搞,很容易出事的
  • 第五句续行符是防止分行后出现问题,其本质是转义后面的回车符,让回车不再是回车。续行符后面什么都不能有,按下 “\” 直接回车,否则续的就不是下面一行的代码了

  
  现在问题来了:用 # d e f i n e define define 定义标识符的时候,要不要在后面加上

比如:

#define MAX 1000 #define MAX 1000;  int main() { 	int n = MAX; 	return 0; } 

在这里插入图片描述

  上述代码加上,好像只是有点多余,但对程序运行并没有什么影响
  好像加 或者不加 都可以?
  真的是这样的吗?
  
  我们看下面的例子:

//例一 int main() { 	printf("%d\n", MAX); 	return 0; }  //例二 int mian() { 	int max = 0; 	if (1) 		max = MAX; 	else 		max = 1; 	return 0 } 

替换后:

printf("%d\n", 1000;); 

打印1000;是什么意思?

if (1) 	max = 1000; 	; else 	max = 1; 

e l s e else else 匹配谁?
  
  你看,这就出问题了吧,所以用 # d e f i n e define define 定义表示符的时候,后面不要加

  

三、# d e f i n e define define 定义宏

  # d e f i n e define define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为 ( m a r c o ) (marco) (marco)或 定义宏 ( d e f i n e m a c r o ) (define macro) (definemacro)
  宏和上面宏定义标识符的区别就是:宏有参数
  
  下面是宏的声明方式:

#define name(parament - list) stuff 

  其中的 p a r a m e n t parament parament - l i s t list list(参数列表)是一个由逗号隔开的符号表,他们可能出现在 s t u f f stuff stuff 中
  注: p a r a m e n t parament parament - l i s t list list(参数列表)的左括号必须与 n a m e name name紧邻,如果之间有任何空白存在,参数列表就会被解释为 s t u f f stuff stuff 的一部分。
  

举例:

//实现一个宏,计算一个数的平方 #define SQUARE(x) x*x  int main() { 	int a = 5; 	int ret = SQUARE(a); 	printf("%d\n", ret); 	return 0; } 

  
运行结果:

在这里插入图片描述

  可以看到,正确运算出 5 的平方

  但其实上述代码是有问题的,请看下面代码段:

int a = 5; printf("%d\n", SQUARE(a + 1)); 

  
运行结果:

在这里插入图片描述

  为什么会是这样呢? 5+1 的结果是 36,而 6 ∗ * ∗ 6 应该是 36 才对,11 是怎么得来的呢?

  问题出现在宏上,我们知道宏是直接替换的,那么上述代码直接替换的结果:

printf("%d\n", a+1*a+1); 

   5 + 6 + 1,结果自然是 11 了

  我们在宏定义两边加上括号,这个问题就轻松解决了

#define SQUARE(x) (x)*(x) 

  那这么定义就毫无问题了吗?我们来看看下面这个宏定义

#define DOUBLE(X) (X)+(X) 

  定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

int a = 5; printf("%d\n", 10 * DOUBLE(a)); 

  
运行结果:

在这里插入图片描述

  输出结果不是100 而是 55,原因和上面类似,依然是 优先级的问题
  
解决方法:

#define DOUBLE(X) ((X)+(X)) 

  综上,在使用宏的时候千万不要吝啬括号,以避免在使用宏是由于参数中的操作符或者邻近操作符之间不可预料的相互作用。

  

四、带有副作用的宏参数

  当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的结果。副作用就是表达式求值的时候出现永久性的效果。
  
例如:

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

  下面代码的 MAX 宏可以证明具有副作用的参数所引起的问题

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

  
运行结果:

在这里插入图片描述

  为什么会这样呢?我们一起来分析分析

z = ((X++) > (y++) ? (x++) : (y++)) 
  • 首先先进行判断: x x x++ 与 y y y++ 判断,因为是 后置++,判断时 x x x 为 5, y y y 为 8,8 > 5
  • 判断完后 x x x 为 6, y y y 为 9
  • 再接着执行 y y y ++,因为是 后置++,返回结果 9
  • 再接着 y y y 进行自增, y y y 最终结果为 10

  我们将 x x x 和 y y y 传入宏中,出来的结果都已经改变了,特别是 y y y,经过了两次改变,你说可不可怕
  当向宏中传递有副作用的参数,而并且参数在宏中出现了不止一次,那么该参数的副作用也不止一次
  

五、宏替换的规则

在程序中扩展 # d e f i n e define define 定义的符号和宏时,需要涉及几个步骤

  • 在调用宏时,先对参数进行检查,看看是否包含 # d e f i n e define define 定义的标识符。如果是,他们首先被替换
  • 替换文本随后被插入到程序中原来的位置,对于宏参数名被他们的值所替换
#define MAX(a,b) ((a) > (b) ? (a):(b)) #define M 10  int main() { 	int x = 5; 	int z = MAX(x, M); 	return 0; } 
  • M A X ( x , M ) MAX(x, M) MAX(x,M) 中的 M M M 首先被替换成 10,10 插入到原来 M M M 所在的位置
  • 最后,再次对结果进行扫描,看看是否包含任何由 # d e f i n e define define 定义的符号,如果是,就重复上述处理了过程

  上述代码中 MAX 也是由 # d e f i n e define define 定义的。上一次检验中,它的参数 M 已经完成了替换,这次该替换它了
   M A X ( x , 10 ) MAX(x, 10) MAX(x,10) 被替换成 ( ( x ) > ( 10 ) ((x) > (10) ((x)>(10)  ? ? ?  ( x ) : ( 10 ) ) (x):(10)) (x):(10))
  
  当然,宏里面嵌套宏也是可以的

MAX(x, MAX(2, 3)) 

  这时,先将参数中的宏进行替换,再对整个宏进行替换
  但需要注意的是,这不是递归,这只是一个宏作为另一个宏的参数。递归是宏内部又调用了宏本身
  同时,当预处理器搜索 # d e f i n e define define 定义的符号时,字符串常量的内容并不被搜索
  什么意思呢?举个栗子就明白了

#define M 10  int main() { 	int x = 5; 	int z = MAX(x, MAX(2, 3)); 	printf("M = %d", x); 	return 0; } 

  上述代码中printf("M = %d", x);中的 M M M 就不被替换成 10

  

六、宏和函数的对比

  上述用宏求两个数的较大值,我们完全可以写成函数

int Max(int x, int y) { 	return x > y ? x : y; } 

  我们发现他们都能完成同样的功能。但就对于 “求两个数的较大值” 这个功能而言,写成宏会更有优势一些
  
原因有二:

  • 用于调用函数函数返回代码可能比实际执行这个小型计算工作所花费的时间更多(调用函数时要建立栈帧)。所以宏比函数在程序的规模速度上更胜一筹
  • 更为重要的是函数的参数必须声明类型,这就导致函数只能在类型合适的表达式上使用。反之,这个宏可以适用多种类型:整形、长整形、浮点型等都可以用
    > 来比较。宏的参数是类型无关

  那是不是以后都用宏呢?其实宏只使用于简单计算,不适合做那些复杂的、大的运算和函数相比宏的 劣势

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

  但有些时候,宏可以做到函数做不到的事情
  
例如:

int* p = (int*)malloc(10 * sizeof * (int)); 

  我们嫌这样写 m a l l o c malloc malloc 函数太麻烦了,我想把大小类型传过去就能开辟空间

Malloc(10, int); 

  函数能不能做到呢?不行,因为函数是不能传递类型的
  而宏可以做到,因为宏压根不检查你参数的类型

#define Malloc(n, type) (type*)malloc(n * sizeof(type))  int main() { 	int* p = Malloc(10, int); 	return 0; } 

  
宏和函数的一个对比:

属性# d e f i n e define define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中,除了非常小的宏外,程序的长度会大幅度增长函数代码值出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销(开辟栈帧),先对慢些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多加括号函数参数只有在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预估的结果函数参数只有在传参时调用一次,结果更容易预测
参数类型宏的参数与类型无关,只要对参数的操作是合法的,他就可以使用于任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的
调试宏是不方便调试的函数是可以逐句调试的
递归宏是不能递归的函数是可以递归的

  那有没有什么办法把他们的优点结合起来呢
  在 C++ 中引入了内联函数 i n l i n e inline inline —— 既又宏优点又有函数的优点
  它的执行速度和宏一样快,但效果又和函数一样
  

相关内容

热门资讯

2024电赛H题参考方案——自... 目录一、题目要求二、参考资源获取三、参考方案1、环境搭建及工程移植 2、移植MPU6050模块 3、...
米侠浏览器怎么保存密码-保存表... 在米侠浏览器中,进入设置,找到“密码与自动填充”选项,开启“保存表单密码”,浏览网页时,会自动保存登...
雷电云手机怎么操作 雷电云手机通过雷电模拟器进行操作。首先下载并安装雷电模拟器,然后登录账号,选择或创建游戏角色,使用键...
osvdb是什么 osvdb是一个用于收集、整理和发布开源软件漏洞信息的数据库。它为安全研究人员、开发人员和企业提供了...
STM32自定义协议串口接收解... 1、在使用串口接收自定义协议指令时,需要串口解析收到的是什么指令,举例通...
米侠浏览器怎么看vip视频 米侠浏览器并不能直接观看VIP视频,因为VIP视频通常需要付费才能观看。如果你想在米侠浏览器上观看V...
荣耀畅玩5c的参数(荣耀畅玩5... 荣耀畅玩5C配备5.2英寸1080p屏,搭载麒麟650处理器,前置800万+后置1300万像素摄像头...
苹果8的截图功能怎么弄 苹果8截图功能很简单,只需同时按住侧面的电源键和音量加键,屏幕会闪一下,表示截图成功。苹果8截图方法...
搭建cool-admin-ja... 为什么选择 Cool Admin?​随着技术不断地发展,特别是最近 Ai...
STM32是使用的内部时钟还是... STM32是使用的内部时钟还是外部时钟,经常会有人问这个问题。1、先了解时钟树...