一、前言
二、预处理器的操作
三、宏扩展
四、符号:# 与 ##
五、可变参数的处理
六、奇思妙想的宏
七、总结
一、前言
一直以来,我都有这样一种感觉:当我学习一个新领域的知识时,如果其中的某个知识点在刚开始接触时,我感觉比较难懂、不好理解,那么以后不论我花多长时间去研究这个知识点,心里会一直认为该知识点比较难,也就是说第一印象特别的重要。
就比如 C 语言中的宏定义,好像跟我犯冲一样,我一直觉得宏定义是 C 语言中最难的部分,就好比有有些小伙伴一直觉得指针是 C 语言中最难的部分一样。
宏的本质就是代码生成器,在预处理器的支持下实现代码的动态生成,具体的操作通过条件编译和宏扩展来实现。我们先在心中建立这么一个基本的概念,然后通过实际的描述和代码来深入的体会:如何驾驭宏定义。
所以,今天我们就来把宏定义所有的知识点进行汇总、深挖,希望经过这篇文章,我能够摆脱心理的这个魔障。看完这篇总结文章后,我相信你也一定能够对宏定义有一个总体、全局的把握。
二、预处理器的操作
1. 宏的生效环节:预处理
一个 C 程序在编译的时候,从源文件开始到最后生成二进制可执行文件,一共经历 4 个阶段:
我们今天讨论的内容就是在第一个环节:预处理,由预处理器来完成这个阶段的工作,包括下面这 4 项工作:
文件引入(#include);条件编译(#if..#elif..#endif);宏扩展(macro expansions);行控制(line control)。2. 条件编译
一般情况下,C 语言文件中的每一行代码都是要被编译的,但是有时候出于对程序代码优化的考虑,希望只对其中的一部分代码进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。
简单的说:就是预处理器根据我们设置的条件,对代码进行动态的处理,把有效的代码输出到一个中间文件,然后送给编译器进行编译。
条件编译基本上在所有的项目代码中都被使用到,例如:当你需要考虑下面的几种情况时,就一定会使用条件编译:
需要把程序编译成不同平台下的可执行程序;同一套代码需要运行在同一平台上的不同功能产品上;在程序中存在着一些测试目的的代码,不想污染产品级的代码,需要屏蔽掉。
这里举 3 个例子,在代码中经常看到的关于条件编译:
示例1:用来区分 C 和 C++ 代码
#ifdef __cplusplus extern "C" { #endif void hello(); #ifdef __cplusplus } #endif
这样的代码几乎在每个开源库中都可能见到,主要的目的就是 C 和 C++ 混合编程,具体来说就是:
如果使用 gcc 来编译,那么宏 __cplusplus 将不存在,其中的 extern "C" 将会被忽略;如果使用 g++ 来编译,那么宏 __cplusplus 就存在,其中的 extern "C" 就发生作用,编译出来的函数名 hello 就不会被 g++ 编译器改写,因此就可以被 C 代码来调用;
示例2:用来区分不同的平台
#if defined(linux) || defined(__linux) || defined(__linux__) sleep(1000 * 1000); // 调用 Linux 平台下的库函数#elif defined(WIN32) || defined(_WIN32) Sleep(1000 * 1000); // 调用 Windows 平台下的库函数(第一个字母是大写)#endif
那么,这些 linux, __linux, __linux__, WIN32, _WIN32 是从哪里来的呢?我们可以认为是编译目标平台(操作系统)为我们预先准备好的。
示例3:在编写 Windows 平台下的动态库时,声明导出和导入函数
#if defined(linux) || defined(__linux) || defined(__linux__) #define LIBA_API #else #ifdef LIBA_STATIC #define LIBA_API #else #ifdef LIBA_API_EXPORTS #define LIBA_API __declspec(dllexport) #else #define LIBA_API __declspec(dllimport) #endif #endif#endif
// 函数声明LIBA_API void hello();
这段代码是直接从我之前在 B 站录制的一个小视频里的示例拿过来的,当时主要是演示如何如何在 Linux 平台下使用 make 和 cmake 构建工具来编译,后来又小伙伴让我在 Windows 平台下也用 make 和 cmake 来构建,所以就写了上面这段宏定义。
在使用 MSVC 编译动态库时,需要在编译选项(Makefle 或者 CMakeLists.txt)中定义宏 LIBA_API_EXPORTS,那么导出函数 hello 的最前面的宏 LIBA_API 就会被替换成:__declspec(dllexport),表示导出操作;在编译应用程序的时候,使用动态库,需要 include 动态库的头文件,此时在编译选项中不需要定义宏 LIBA_API_EXPORTS,那么 hello 函数最前面的 LIBA_API 就会被替换成 __declspec(dllimport),表示导入操作;补充一点:如果使用静态库,编译选项中不需要任何宏定义,那么宏 LIBA_API 就为空。3. 平台预定义的宏
上面已经看到了,目标平台会为我们预先定义好一些宏,方便我们在程序中使用。除了上面的操作系统相关宏,还有另一类宏定义,在日志系统中被广泛的使用:
FILE:当前源代码文件名;
LINE:当前源代码的行号;
FUNCTION:当前执行的函数名;
DATE:编译日期;
TIME:编译时间;
例如:
printf("file name: %s, function name = %s, current line:%d ", __FILE__, __FUNCTION__, __LINE__);