前言
在上一篇博客中,我们了解了代码是如何执行的,简单介绍了编译中预处理步骤,在这篇博客中我们将详细了解预处理。
文章目录
- 一、预定义符号
- 二、#define定义
-
- [2.1 定义常量](#2.1 定义常量)
- [2.2 定义宏](#2.2 定义宏)
- [2.3 创建代码片段](#2.3 创建代码片段)
- 三、#和##运算符
- 四、宏和函数对比
- 五、条件编译
一、预定义符号
预定义符号通常是指编程语言或编译器提供的一组特定的符号或宏,用于在代码中执行某些特定的功能或获取某些信息。
c
__FILE__ //返回当前源文件的文件名
__LINE__ //返回当前代码行的行号。
__DATE__ //返回编译的日期
__TIME__ //返回编译的时间
__func__ //或 __FUNCTION__(C++):返回当前函数的名称
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
一个使用场景
c
// 在错误消息中使用 __FILE__ 和 __LINE__ 打印出源代码文件名和行号
printf("Error in %s, line %d: Division by zero\n", __FILE__, __LINE__);
二、#define定义
#define 是一个预处理指令,用于在C、C++和其他一些编程语言中创建宏或定义符号。它在编译前执行文本替换,将指定的符号替换为其对应的文本。
2.1 定义常量
c
//定义整数常量
#define MAX_VALUE 100
//定义字符串常量
#define WELCOME_MESSAGE "Hello, World!"
2.2 定义宏
宏是一种可以接受参数并展开为一段代码的定义。例如,你可以创建一个用于计算平方的宏。
c
#define SQUARE(x) ((x) * (x))
int main(){
int a = 5;
printf("%d",SQUARE(a))://SQUARE(a)会被替换成((a) * (a)),结果是25
}
需要注意的是 \color{#FF0000}{需要注意的是} 需要注意的是 ,((x) * (x))内部不加括号会计算错误
例如:
c
#define SQUARE(x) (x * x)
int main(){
int a = 5;
printf("%d",SQUARE(a+1))://SQUARE(a)会被替换成(a+1*a+1),结果是11
}
同样的,((x) * (x))外部不加括号可能也会计算错误
举例:
c
#define DOUBLE(x) (x) + (x)
int main(){
int a = 5;
printf("%d",10*DOUBLE(a))://DOUBLE(a)会被替换成 10*(a)+(a),结果是55
}
因此,在定义宏时需格外注意。
此外, 副作用的宏参数 \color{#FF0000}{副作用的宏参数 } 副作用的宏参数 也需要格外注意
副作用的宏参数指的是在宏展开时,宏参数在宏中多次出现,或者在宏中进行了多次计算,特别是当参数包含函数调用、递增/递减操作或其他会改变参数状态的操作时,可能导致意外的行为或错误结果
例如下面这个例子
c
#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);//输出结果是x=6 y=10 z=9
return 0;
}
在( (x++) > (y++) ? (x++) : (y++) )中进行了两次y的自增,一次的x自增,但是是后缀自增,所以在y第二次自增前返回给了z,所以z是9。
2.3 创建代码片段
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
c
#define PRINT_MESSAGE(msg) printf("Message: %s\n", msg)
//如果定义的代码过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
三、#和##运算符
3.1 字符串化操作符#
# 可以用于将宏参数转换为字符串,称为字符串化。
c
#define STRINGIZE(x) #x
int main() {
int a = 5;
printf("将%d转换为字符%s",a,STRINGIZE(5));
return 0;
}
3.2 连接操作符##
可以用于将两个宏参数连接成一个标识符,称为连接操作符(记号粘合)。
例如:
c
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}
这样我们就可以定义根据返回类型来命名不同的函数
c
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main(){
//调⽤函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
四、宏和函数对比
宏和可以实现简单函数的功能,对于实现相对简单的功能,宏更具有优势。但宏并不能取代函数。
以下是宏和函数的对比:
- | #define定义宏 | 函数 |
---|---|---|
编译时 vs. 运行时 | 宏是在编译时执行的文本替换操作。宏展开在编译阶段,不会引入运行时开销 | 函数是在运行时执行的,需要在每次调用时进入和退出函数,具有运行时开销 |
类型安全 | 宏是文本替换,没有类型检查。它们不会执行参数类型检查,可能引入类型错误 | 函数具有明确的参数和返回值类型,编译器会执行类型检查,可以捕获类型错误 |
可读性和维护性 | 宏可以导致代码变得难以阅读,因为它们是文本替换,展开后的代码可能很复杂 | 函数具有明确的名称,参数和返回值,有助于代码的可读性和可维护性 |
代码复用 | 宏可以在多个地方重复使用,但它们通常不能返回值,难以实现复杂的逻辑 | 函数可以在多个地方调用,并且可以返回值,可以实现更复杂的逻辑 |
副作用 | 宏可以包含副作用 | 函数内的副作用通常较少 |
调试 | 调试宏时,你只能查看宏在源代码中的展开,不容易进行调试 | 函数是独立的代码单元,可以更容易进行单步调试和查看函数的堆栈跟踪 |
五、条件编译
在编译⼀个程序的时候我们如果不想编译⼀条语句(⼀组语句),除了将其注释掉,还可以使用条件编译。
条件编译是一种在C和C++中根据条件选择性包含或排除代码的技术。它使用预处理器指令来控制编译过程中哪些代码块应该被编译进最终的可执行程序。
条件编译使用以下预处理器指令来实现:
- #ifdef 和 #ifndef:检查是否已定义了某个符号。
- #if:执行条件判断,通常与预定义符号一起使用。
- #else:在条件不成立时执行一段代码。
- #elif:用于多个条件的情况,类似于 else if。
- #endif:结束条件编译块。
例如:
c
#include <stdio.h>
#define DEBUG
int main() {
int x = 10;
#ifdef DEBUG //检查是否已定义了某个符号
printf("Debugging information: x = %d\n", x);
#else //在条件不成立时执行一段代码
printf("Release information\n");
#endif //结束条件编译块
return 0;
}
//我们已经定义了DEBUG,所以会执行语句printf("Debugging information: x = %d\n", x);
如果你喜欢这篇文章,点赞👍+评论+关注⭐️哦!
欢迎大家提出疑问,以及不同的见解。