本文整理自个人学习笔记由于本人能力有限如果有不足的地方欢迎各位进行指正发泄情绪请关闭本帖预处理预处理主要任务头文件展开宏替换条件编译编译时可以用gcc -E只进行到预处理头文件展开#includeVS#include:包含系统定义的头文件(/usr/include)搜索路径/usr/include系统的头文件目录:包含自定义的头文件搜索路径先在当前目录查找再到系统的头文件目录查找编译时使用指令gcc -I指定头文件第三方搜索路径宏替换宏替换语法#define NAME Value替换逻辑“傻瓜式“替换注意事项使用解决运算符优先级的问题宏定义宏定义的作用定义常量可以用来杜绝幻数定义函数称为宏函数用编译时间换内存空间inline关键字修饰函数建议编译器在调用点展开用空间换运行时间消除函数调用开销inline函数应当避免包含可修改的static局部变量。不是任何函数加inline都会内嵌函数最好不要太大如果函数太大编译器就不会把这个函数内嵌。为什么叫傻瓜式替换可以看下面的例子#includestdio.h #define SUM(a,b) ab-a*b //宏函数 #define NEWSUM(a,b) (a)(b)-(a)*(b) int main() { int result SUM(1,2); printf(%d\n,result);//这个是正常的得到结果1 //注意这个结果反直觉 result SUM(56,6*5); //它不会把561,6*530算出来代入而是直接进行“傻瓜式”替换 //566*5-56*6*5 216 printf(%d\n,result);//所以这个结果是216 //而不是1130-11*30 -289 //但是可以使用括号进行修正例如上面的NEWSUM这个宏定义 result NEWSUM(56,6*5); //这时候再进行傻瓜式替换得到(56)(6*5)-(56)*(6*5)得到结果-289 }对于避免幻数可以看这个例子#includestdio.h int log(char* username,char* password) { if(strcmp(username,admin)0strcmp(password,admin)0) { return 0; } return 1; } int main(){}这时候就有问题了有时候在实际编程的时候我们经常会突然脑子卡壳忘了0和1哪个是登陆成功哪个是登录失败呢这个情景是程序员经常遇到的情景尤其是在项目庞大的时候需要在许多代码里去翻找代码来确定0和1对应的意义。这个0和1在这里就叫幻数为了避免幻数宏定义的作用就体现出来了我们可以这样做#includestdio.h #define LOG_SUCCESS 0 #define LOG_FAILED 1 int log(char* username,char* password) { if(strcmp(username,admin)0strcmp(password,admin)0) { return LOG_SUCCESS; } return LOG_FAILED; }这样幻数的问题就得到了解决代码的可读性大大提升宏函数又是什么呢#includestdio.h #define MAX(a,b) ((a)(b))?(a):(b) //宏函数看上去像函数但是并没有按照函数的机制去处理 int max(int a,int b) { return ab?a:b; } int main() { int a 5; int b 6; //开销大给形参分配空间要做一系列操作函数调用、函数返回、分配空间、释放空间 int max_num1 max(a,b); //但是我们可以使用宏函数 //只做傻瓜式替换不再占用空间在预处理阶段就替换完成了占用编译时间不占用运行时间不再进行函数调用那些繁琐的操作 //频繁调用且短小的函数最好优化成宏函数 //缺点不安全因为傻瓜式替换稍写不慎就会出错 int max_num2 MAX(a,b); }但是当宏函数复杂的时候又会遇到别的问题#includestdio.h #define DOSOMETHING()\ func1();\ func2();这个宏的本意是当调用函数DOSOMETHING时调用函数func1与函数func2但是当我们这样进行的时候//x为bool类型的变量 if(x) DOSOMETHING();这时候DOSOMETHING就会“傻瓜式展开”为下面的形式//x为bool类型的变量 if(x) func1(); func2();这就出现问题了无论x是否为true都会执行函数func2()如果我们使用{}括起来呢#includestdio.h #define DOSOMETHING(){\ func1();\ func2();} int main() { if(a0) DOSOMETHING(); else{} return 0; }宏展开后代码如下#includestdio.h #define DOSOMETHING(){\ func1();\ func2();} int main() { int a 1; if(a0) { func1(); func2(); }; else{} return 0; }由于在调用DOSOMETHING函数的时候后面有一个分号宏展开之后就会在if的{}后面多一个分号展开之后这个分号会残留在{}的尾部这就会导致后面的else变成了游离的else进而编译错误。但是如果调用DOSOMETHING时不加分号也会导致编译报错。这也是一个问题。那有什么解决办法呢我们可以使用do{...}while(0)#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0)这时候展开之后的就是#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0) int main() { int a 1; if(a0) { do{ func1(); func2(); }while(0); } else{} return 0; }等价于#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0) int main() { int a 1; if(a0) { func1(); func2(); } else{} return 0; }这时候就真的达到了我们想要的目的了关于#与###-字符串化在C语言的宏中#的功能是将其后面的宏参数进行字符串化操作Stringfication简单的说如果你写#param预处理器会把它直接“傻瓜式”替换成param参数的内容用双引号包起来可以看看这个例子就显而易见了#define STR(x) #x int main() { printf(%s\n,STR(hello)); //展开后变成printf(%s\n,hello); //输出hello }哪怕参数里有表达式或空格也会被当成一个整体字符串化#define STR(x) #x printf(%s\n, STR(1 2)); // 展开后变成printf(%s\n, 1 2); // 输出1 2 注意不是 3因为它变成了字符串不会进行计算这也就是为什么我要用“傻瓜式替换”来进行形容再说下面的之前我先提一个知识点printf(hellohello); printf(hello hello); printf(hello hello);这三个表达式完全等价我相信聪明的你们一下子就能懂我意思了所以有如下的使用技巧#define PRINT_VAR(x) printf(#x %d\n, x) int a 10; PRINT_VAR(a); // 展开为 printf(a %d\n, a); // 相邻字符串会自动连接变成 a %d\n // 输出a 10##-连接简单的例子#define CONCAT(a,b) a##b int xy 10; printf(%d\n,CONCAT(x,y)); //展开成xy输出10CONCAT(x, y)→x与y连接成xy然后作为变量名访问。注意拼接的不是字符串xy而是标识符xy。内置宏C语言编译器提供了很多内置宏例如__LINE__-得到当前所在的行数__func__-得到当前所在的函数名称__DATE__-得到当前日期__TIME__-得到当前时间这两个宏可以用在printf中输出调试信息。printf(%s:%s %s:%d,__DATE__,__TIME__,__func__,__LINE__);内置宏大部分都是以__开头所以我们自定义宏的时候一定要尽量避免__开头防止和内置宏冲突typedef与#define的区别typedef的主要作用是给数据类型重命名能够提高移植性、可读性、编写效率typedef重命名函数指针例子#includestdio.h //typedef int(*)(int,int) P_FUNC;错误不可以这样写 //必须要这样写 typedef int (*P_FUNC)(int,int); int add(int a,int b) { return ab; } int main(int argc,char *argv[]) { P_FUNC p_func add; }对于初学者来说这个写法就是这样规定的没有为什么初学者只需要记住即可。typedef与#define的对比#define不能定义复杂的类型如函数指针变量但是typedef可以#define预处理阶段做傻瓜式替换存在风险预处理阶段不做语法检查不安全定义常量时#define可以和const进行对比#define定义的是无类型的宏常量在预处理阶段进行文本替换不分配内存没有作用域和类型检查。const变量是带类型的只读变量分配内存有正常的作用域和链接属性可以是局部、全局也可以是static。它并不是单纯的“局部变量”。
C 语言预处理全面总结(1):宏、#、##、do...while(0) 一网打尽
发布时间:2026/7/16 10:32:57
本文整理自个人学习笔记由于本人能力有限如果有不足的地方欢迎各位进行指正发泄情绪请关闭本帖预处理预处理主要任务头文件展开宏替换条件编译编译时可以用gcc -E只进行到预处理头文件展开#includeVS#include:包含系统定义的头文件(/usr/include)搜索路径/usr/include系统的头文件目录:包含自定义的头文件搜索路径先在当前目录查找再到系统的头文件目录查找编译时使用指令gcc -I指定头文件第三方搜索路径宏替换宏替换语法#define NAME Value替换逻辑“傻瓜式“替换注意事项使用解决运算符优先级的问题宏定义宏定义的作用定义常量可以用来杜绝幻数定义函数称为宏函数用编译时间换内存空间inline关键字修饰函数建议编译器在调用点展开用空间换运行时间消除函数调用开销inline函数应当避免包含可修改的static局部变量。不是任何函数加inline都会内嵌函数最好不要太大如果函数太大编译器就不会把这个函数内嵌。为什么叫傻瓜式替换可以看下面的例子#includestdio.h #define SUM(a,b) ab-a*b //宏函数 #define NEWSUM(a,b) (a)(b)-(a)*(b) int main() { int result SUM(1,2); printf(%d\n,result);//这个是正常的得到结果1 //注意这个结果反直觉 result SUM(56,6*5); //它不会把561,6*530算出来代入而是直接进行“傻瓜式”替换 //566*5-56*6*5 216 printf(%d\n,result);//所以这个结果是216 //而不是1130-11*30 -289 //但是可以使用括号进行修正例如上面的NEWSUM这个宏定义 result NEWSUM(56,6*5); //这时候再进行傻瓜式替换得到(56)(6*5)-(56)*(6*5)得到结果-289 }对于避免幻数可以看这个例子#includestdio.h int log(char* username,char* password) { if(strcmp(username,admin)0strcmp(password,admin)0) { return 0; } return 1; } int main(){}这时候就有问题了有时候在实际编程的时候我们经常会突然脑子卡壳忘了0和1哪个是登陆成功哪个是登录失败呢这个情景是程序员经常遇到的情景尤其是在项目庞大的时候需要在许多代码里去翻找代码来确定0和1对应的意义。这个0和1在这里就叫幻数为了避免幻数宏定义的作用就体现出来了我们可以这样做#includestdio.h #define LOG_SUCCESS 0 #define LOG_FAILED 1 int log(char* username,char* password) { if(strcmp(username,admin)0strcmp(password,admin)0) { return LOG_SUCCESS; } return LOG_FAILED; }这样幻数的问题就得到了解决代码的可读性大大提升宏函数又是什么呢#includestdio.h #define MAX(a,b) ((a)(b))?(a):(b) //宏函数看上去像函数但是并没有按照函数的机制去处理 int max(int a,int b) { return ab?a:b; } int main() { int a 5; int b 6; //开销大给形参分配空间要做一系列操作函数调用、函数返回、分配空间、释放空间 int max_num1 max(a,b); //但是我们可以使用宏函数 //只做傻瓜式替换不再占用空间在预处理阶段就替换完成了占用编译时间不占用运行时间不再进行函数调用那些繁琐的操作 //频繁调用且短小的函数最好优化成宏函数 //缺点不安全因为傻瓜式替换稍写不慎就会出错 int max_num2 MAX(a,b); }但是当宏函数复杂的时候又会遇到别的问题#includestdio.h #define DOSOMETHING()\ func1();\ func2();这个宏的本意是当调用函数DOSOMETHING时调用函数func1与函数func2但是当我们这样进行的时候//x为bool类型的变量 if(x) DOSOMETHING();这时候DOSOMETHING就会“傻瓜式展开”为下面的形式//x为bool类型的变量 if(x) func1(); func2();这就出现问题了无论x是否为true都会执行函数func2()如果我们使用{}括起来呢#includestdio.h #define DOSOMETHING(){\ func1();\ func2();} int main() { if(a0) DOSOMETHING(); else{} return 0; }宏展开后代码如下#includestdio.h #define DOSOMETHING(){\ func1();\ func2();} int main() { int a 1; if(a0) { func1(); func2(); }; else{} return 0; }由于在调用DOSOMETHING函数的时候后面有一个分号宏展开之后就会在if的{}后面多一个分号展开之后这个分号会残留在{}的尾部这就会导致后面的else变成了游离的else进而编译错误。但是如果调用DOSOMETHING时不加分号也会导致编译报错。这也是一个问题。那有什么解决办法呢我们可以使用do{...}while(0)#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0)这时候展开之后的就是#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0) int main() { int a 1; if(a0) { do{ func1(); func2(); }while(0); } else{} return 0; }等价于#includestdio.h #define DOSOMETHING()do{\ func1();\ func2();\ }while(0) int main() { int a 1; if(a0) { func1(); func2(); } else{} return 0; }这时候就真的达到了我们想要的目的了关于#与###-字符串化在C语言的宏中#的功能是将其后面的宏参数进行字符串化操作Stringfication简单的说如果你写#param预处理器会把它直接“傻瓜式”替换成param参数的内容用双引号包起来可以看看这个例子就显而易见了#define STR(x) #x int main() { printf(%s\n,STR(hello)); //展开后变成printf(%s\n,hello); //输出hello }哪怕参数里有表达式或空格也会被当成一个整体字符串化#define STR(x) #x printf(%s\n, STR(1 2)); // 展开后变成printf(%s\n, 1 2); // 输出1 2 注意不是 3因为它变成了字符串不会进行计算这也就是为什么我要用“傻瓜式替换”来进行形容再说下面的之前我先提一个知识点printf(hellohello); printf(hello hello); printf(hello hello);这三个表达式完全等价我相信聪明的你们一下子就能懂我意思了所以有如下的使用技巧#define PRINT_VAR(x) printf(#x %d\n, x) int a 10; PRINT_VAR(a); // 展开为 printf(a %d\n, a); // 相邻字符串会自动连接变成 a %d\n // 输出a 10##-连接简单的例子#define CONCAT(a,b) a##b int xy 10; printf(%d\n,CONCAT(x,y)); //展开成xy输出10CONCAT(x, y)→x与y连接成xy然后作为变量名访问。注意拼接的不是字符串xy而是标识符xy。内置宏C语言编译器提供了很多内置宏例如__LINE__-得到当前所在的行数__func__-得到当前所在的函数名称__DATE__-得到当前日期__TIME__-得到当前时间这两个宏可以用在printf中输出调试信息。printf(%s:%s %s:%d,__DATE__,__TIME__,__func__,__LINE__);内置宏大部分都是以__开头所以我们自定义宏的时候一定要尽量避免__开头防止和内置宏冲突typedef与#define的区别typedef的主要作用是给数据类型重命名能够提高移植性、可读性、编写效率typedef重命名函数指针例子#includestdio.h //typedef int(*)(int,int) P_FUNC;错误不可以这样写 //必须要这样写 typedef int (*P_FUNC)(int,int); int add(int a,int b) { return ab; } int main(int argc,char *argv[]) { P_FUNC p_func add; }对于初学者来说这个写法就是这样规定的没有为什么初学者只需要记住即可。typedef与#define的对比#define不能定义复杂的类型如函数指针变量但是typedef可以#define预处理阶段做傻瓜式替换存在风险预处理阶段不做语法检查不安全定义常量时#define可以和const进行对比#define定义的是无类型的宏常量在预处理阶段进行文本替换不分配内存没有作用域和类型检查。const变量是带类型的只读变量分配内存有正常的作用域和链接属性可以是局部、全局也可以是static。它并不是单纯的“局部变量”。