本文主要写了预处理指定和#define 宏替换及宏函数,以及为什么会用到 do/while(0); 读者需要那部分知识可以直接点击目录里面的链接
本文参考:https://www.cnblogs.com/flowingwind/p/8304668.html
https://www.cnblogs.com/bytebee/p/8205707.html
https://www.cnblogs.com/wuweierzhi/p/11591999.html
目录
一、预编译指令
命令 | 命令效果 |
#空指令 | 无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
#error | 停止编译并显示错误信息 |
条件编译命令最常见的形式为:
- #ifdef 标识符
- 程序段1
- #else
- 程序段2
- #endif
例:
#ifndef bool
#define ture 1
#define false 0
#endif
在早期vc中bool变量用1,0表示,即可以这么定义,保证程序的兼容性
在头文件中使用#ifdef和#ifndef是非常重要的,可以防止双重定义的错误。
//main.cpp文件
#include "cput.h"
#include "put.h"
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
//cput.h 头文件
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
//put.h头文件
#include "cput.h"
int put()
{
cput();
return 0;
}
编译出错;在main.cpp中两次包含了cput.h
尝试模拟还原编译过程;
当编译器编译main.cpp时
//预编译先将头文件展开加载到main.cpp文件中
//展开#include "cput.h"内容
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
//展开#include "put.h"内容
//put.h包含了cput.h先展开
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
int put()
{
cput();
return 0;
}
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
很明显合并展开后的代码,定义了两次cput()函数;
如果将cput.h改成下面形式:
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
当编译器编译main.cpp时合并后的main.cpp文件将会是这样的:
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
int put()
{
cput();
return 0;
}
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
这次编译通过运行成功;因为在展开put.h中包含的cput.h,会不生效,前面已经定义了宏_CPUT_H_
二、宏定义
#define 宏定义,简单理解就是直接替换。
宏定义可以帮助我们防止出错,提高代码的可移植性和可读性等。
在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为宏定义。那么究竟是用函数好,还是宏定义好?这就要求我们对二者进行合理的取舍。
我们来看一个例子,比较两个数或者表达式大小,首先我们把它写成宏定义:
#define MAX(a, b) ((a)>(b) ? (a):(b))
其次,把它用函数来实现:
int max(int a, int b)
{
return ((a > b) ? a : b)
}
很显然,我们不会选择用函数来完成这个任务,原因有两个:
1.函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;
2.函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。看下面的例子:
/* \ 表示换行符*/
#define MALLOC(n, type) \
((type *) malloc((n) * sizeof(type)) \*强制类型转换*\
利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。我们可以观察一下这个宏确切的工作过程:
int *ptr;
ptr = MALLOC(5, int);
将这宏展开以后的结果:
ptr = (int *)malloc((5) * sizeof(int));
这个例子是宏定义的经典应用之一,完成了函数不能完成的功能,但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。
example:
define的单行定义:
#define maxi(a,b) ((a>b) ? a:b)
define的多行定义
define可以替代多行的代码,例如MFC中的宏定义:
#define MACRO(arg1, arg2) do{\
\
stmt1; \
stmt2; \
\
}while(0)
宏定义写出swap(x,y)交换函数
#define swap(x, y)\
x = x + y; \
y = x - y; \
x = x - y;
zigbee里多行define有如下例子
#define FillAndSendTxOptions(TRANSSEQ, ADDR, ID, LEN, TxO ){ \
afStatus_t stat; \
ZDP_TxOptions = (TxO); \
stat = fillAndSend( (TRANSSEQ), (ADDR), (ID), (LEN) ); \
ZDP_TxOptions = AF_TX_OPTIONS_NONE; \
return stat; \
}
三、do while(0);
1.帮助定义复杂的宏以避免错误
举例来说,假设你需要定义这样一个宏:
#define DOSOMETHING(x) foo1(x); foo2(x)
有如下调用语句
DOSOMETHING(value);
这时将宏展开为:
fool(value); fool(value);
但是如果你在调用的时候这么写:
if(a>0)
DOSOMETHING(value);
因为宏在预处理的时候会直接被展开,你实际上写的代码是这个样子的:
if(a>0)
foo1(value);
foo2(value);
这就出现了问题,因为无论a是否大于0,foo2(value)都会被执行,导致程序出错。
那么仅仅使用{}将foo1(x)和foo2(x)包起来行么?比如:
#define DOSOMETHING(x) { foo1(x); foo2(x); }
我们在写代码的时候都习惯在语句右面加上分号,如果在宏中使用{},代码编译展开后宏就相当于这样写了:“{...};”,展开后就是这个样子:
if(a>0)
{
foo1();foo2();
};
很明显,这是一个语法错误(大括号后多了一个分号)。
现在的编译器会自动检测自动忽略分号,不会报错,但是我们还是希望能跑在老的编译器上。
在没有do/while(0)的情况下,在所有可能情况下,期望我们写的多语句宏总能有正确的表现几乎是不可能的。
如果我们使用do{...}while(0)来定义宏,即:
#define DOSOMETHING(x) do{foo1(x); foo2(x);} while(0) //注意这里没有分号
对于上面的if语句,将会被扩展为:
if(a > 0)
do{foo1(x); foo2(x);} while(0);
这样,宏被展开后,上面的调用语句才会保留初始的语义。do能确保大括号里的逻辑能被执行,而while(0)能确保该逻辑只被执行一次,就像没有循环语句一样。
总结:在Linux和其它代码库里的,很多宏实现都使用do/while(0)来包裹他们的逻辑,这样不管在调用代码中怎么使用分号和大括号,而该宏总能确保其行为是一致的。
cocos2d-x中大量使用了这种宏定义:
#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)
2. 避免使用goto控制程序流(和宏定义无关,属于代码优化)
在一些函数中,我们可能需要在return语句之前做一些清理工作,比如释放在函数开始处由malloc申请的内存空间,使用goto总是一种简单的方法:
int foo()
{
somestruct *ptr = malloc(...);
dosomething...;
if(error)
goto END;
dosomething...;
if(error)
goto END;
dosomething...;
END:
free(ptr);
return 0;
}
但由于goto不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,这个时候我们可以使用do{...}while(0)来做同样的事情:
int foo()
{
somestruct *ptr = malloc(...);
do
{
dosomething...;
if(error)
break;
dosomething...;
if(error)
break;
dosomething...;
}
while(0); //注意这里有;分号
free(ptr);
return 0;
}
这里将函数主体部分使用do{...}while(0)包含起来,使用break来代替goto,后续的清理工作在while之后,现在既能达到同样的效果,而且代码的可读性、可维护性都要比上面的goto代码好的多了。
我经常使用这个种技能在Lua里,Lua不支持do{...}while(0)语法,但是Lua有一种类似的语法repeat...until,伪代码如下:
repeat
dosomething...
if error then
break;
end
dosomething...;
if error then
break;
end
dosomething...;
until (1);
print("break repeat");
这样和do{...}while(0)一样,也保证了只执行一次,可以用break调出循环。
3. 避免由宏引起的警告
内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{...}while(0)来定义空宏:
#define EMPTYMICRO do{}while(0)
这种情况不太常见,因为有很多编译器,已经支持空宏。
4. 定义单一的函数块来完成复杂的操作
如果你有一个复杂的函数,变量很多,而且你不想要增加新的函数,可以使用do{...}while(0),将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。
但是我不建议这样做,尽量声明不同的变量名,以便于后续开发人员阅读。
int key;
string value;
int func()
{
int key = GetKey();
string value = GetValue();
dosomething for key,value;
do{
int key;string value;
dosomething for this key,value;
}while(0);
}