1.前言2.预处理器运行3.宏展开4.符号:#和##5.可变参数处理6.奇斯的宏奇思妙想7.总结1.前言我一直有这样的感受:当我在学习一个新领域的知识时,如果其中的某个知识点在刚接触的时候很难理解,很难理解,所以以后无论我花多长时间学习这个知识点,我都会一直认为这个知识点比较难,也就是说第一印象特别重要。比如C语言中的宏定义,就好像和我作对。我一直认为宏定义是C语言最难的部分,就像有些朋友总认为指针是C语言最难的部分一样。宏的本质是一个代码生成器,在预处理器的支持下实现代码的动态生成,具体的操作是通过条件编译和宏展开来实现的。我们首先在脑海中建立这样一个基本概念,然后通过实际的描述和代码来深入理解:如何控制宏定义。所以,今天我们就把宏定义的所有知识点进行总结和挖掘。我希望通过这篇文章,我可以摆脱这种心理障碍。看完这篇总结文章,相信你也能对宏定义有一个整体、整体的把握。二、预处理器的运行1、宏的作用环节:预处理编译C程序时,从源文件到最终的二进制可执行文件,要经过4个阶段:我们今天要讨论的是在第一个环节:预处理,由预处理器完成,包括以下四个任务:文件引入(#include);条件编译(#if..#elif..#endif);宏展开(macroexpansions);线路控制(linecontrol)。2.条件编译一般情况下,C语言文件中的每一行代码都需要编译,但有时为了程序代码的优化,希望只编译其中的一部分代码。在程序中加入条件,使编译器只编译满足条件的代码,丢弃不满足条件的代码。这是条件编译。简单来说:预处理器根据我们设置的条件动态地处理代码,将有效的代码输出到一个中间文件中,然后送到编译器进行编译。条件编译基本上在所有的项目代码中都会用到。例如,当需要考虑以下情况时,必须使用条件编译:需要将程序编译成不同平台的可执行程序;代码需要运行在同一平台的不同功能产品上;程序中有一些测试用的代码,如果不想污染产品级代码就需要屏蔽。这里举3个例子,在有关条件编译的代码中经常看到:例子一:用于区分C和C++代码在开源库中看到,主要目的是混合C和C++编程,具体来说:如果使用gcc编译,那么宏__cplusplus将不存在,extern"C"将被忽略;如果使用g++编译,那么宏__cplusplus存在,其中的extern"C"就会生效,编译后的函数名hello不会被g++编译器改写,所以可以被C代码调用;例2:使用区分不同平台#ifdefined(linux)||defined(__linux)||defined(__linux__)sleep(1000*1000);//调用linux平台下的库函数#elifdefined(WIN32)||defined(_WIN32)Sleep(1000*1000);//调用Windows平台下的库函数(首字母大写)#endif那么,这些linux,__linux,__linux__,WIN32,_WIN32是从哪里来的呢?我们可以认为编译目标平台(操作系统)已经为我们预先准备好了。例3:Windows平台下写动态库时,声明导出和导入函数dllexport)#else#defineLIBA_API__declspec(dllimport)#endif#endif#endif//函数声明LIBA_APIvoidhello();这段代码直接取自我之前在B站录制的一个小视频中的例子,当时主要是为了演示如何在Linux平台下使用make和cmake构建工具进行编译。后来朋友让我在Windows平台下使用make和cmake进行构建,于是写了上面的宏定义。使用MSVC编译动态库时,需要在编译选项(Makefle或CMakeLists.txt)中定义宏LIBA_API_EXPORTS,那么导出函数hello的第一个宏LIBA_API会被替换为:__declspec(dllexport),意思是出口业务;在编译应用程序时,如果使用了动态库,则需要包含动态库的头文件。这时候不需要在编译选项中定义宏LIBA_API_EXPORTS,那么hello函数开头的LIBA_API就会被__declspec(dllimport)代替,也就是import操作;还有一点:如果你用的是静态库,编译选项里不需要任何宏定义,那么宏LIBA_API就是空的。3.平台预定义的宏正如我们上面看到的,目标平台会为我们预定义一些宏,方便我们在程序中使用。除了上面操作系统相关的宏之外,还有一类在日志系统中被广泛使用的宏定义:FILE:当前源代码文件名;LINE:当前源代码行号;FUNCTION:当前执行的函数名;DATE:编译日期;TIME:编译时间;例如:printf("filename:%s,functionname=%s,currentline:%d\n",__FILE__,__FUNCTION__,__LINE__);3.宏展开所谓宏展开就是代码替换,这部分内容也是我要表达的主要内容。宏扩展的最大好处如下:减少重复代码;完成一些无法通过C语法实现的功能(字符串拼接);动态定义数据类型,实现类似于C++中模板的功能;程序更容易理解和修改(例如:数字、字符串常开);我们在写代码的时候,凡是用到宏名的地方都可以理解为占位符。在编译器的预处理环节,这些宏名会被宏定义中的那些代码段替换。注意:这只是一个简单的文本替换。1.最常用的宏为了下面的描述方便,先来看一些常用的宏定义:(1)数据类型定义#ifndefBOOLtypedefcharBOOL;#endif#ifndefTRUE#defineTRUE#endif#ifndefFALSE#defineFALSE#endif定义在数据类型,需要注意的一点是:如果你的程序需要用不同平台的编译器编译,那么你需要检查这些宏定义所控制的数据类型是否已经被你使用的编译器定义。例如:在gcc中没有BOOL类型,但是在MSVC中,BOOL类型定义为int类型。(2)获取最大值和最小值#defineMAX(a,b)(((a)>(b))?(a):(b))#defineMIN(a,b)(((a)<(b))?(a):(b))(3)计算数组的元素个数#defineARRAY_SIZE(x)(sizeof(x)/sizeof((x)[0]))(4)位操作#defineBIT_MASK(x)(1<<(x))#defineBIT_GET(x,y)(((x)>>(y))&0x01u)#defineBIT_SET(x,y)((x)|(1<<(y)))#defineBIT_CLR(x,y)((x)&(~(1<<(y))))#defineBIT_INVERT(x,y)((x)^(1<<(y)))2。区别从上面的宏来看,这些操作都可以用函数来实现,那么它们的优缺点是什么?函数实现:需要判断形参的类型,调用时检查参数;调用Functions需要额外的开销:操作函数栈中的形参、返回值等;通过宏实现:无需校验参数,参数传递更灵活;直接展开宏的代码,执行时不需要调用函数;如果在多个地方调用同一个宏,会增加代码量;最好举个例子来说明,我们以上面的比较大小为例:(1)用宏实现#defineMAX(a,b)(((a)>(b))?(a):(b))intmain(){printf("max:%d\n",MAX(1,2));}(2)用函数实现intmax(inta,intb){if(a>b)returna;returnb;}intmain(){printf("max:%d\n",max(1,2));}除了函数调用的开销外,似乎没有什么区别。这里比较的是2个整型数据,如果需要比较2个浮点型数据怎么办?使用宏调用:MAX(1.1,2.2);一切都好;使用函数调用:max(1.1,2.2);编译错误:类型不匹配。这时候使用宏的优势就体现出来了:因为宏中没有类型的概念,调用者可以传入任何数据类型,然后在后面的比较操作中,大于或者小于操作都是使用C语言用自己的语法来执行。如果用函数实现,那么就需要定义一个操作浮点类型的函数,以后可以比较:char类型,long类型数据等,在C++中,可以实现这样的操作通过参数模板,这也是一种动态代码生成机制。当定义一个函数模板时,会根据调用者的实际参数动态生成多个函数。例如定义如下函数模板:template
