浅谈宏定义的弊端
浅谈宏定义的弊端
前言
代码规范告诉我们,代码中不应出现魔数,于是我们用大量的宏去定义常量。宏定义作为一种预处理方式,提供了一种方便的替换功能。但是如果使用不当,却会导致一些非意料中的结果!
因此使用宏定义时,我们应当了解其可能带来的一些陷阱,如类型安全、边际效应等问题。一旦出现问题,这些错误往往是隐晦的,难以排查。
1. 宏定义的作用域
在源文件定义时,其作用域为定义之后到源文件结束,所以通常会写在文件开头,以扩大其作用域。
在头文件中定义时,作用域是从包含该头文件的位置到文件结尾,同时 #undef 可终止宏定义的作用域。
**但是要注意的是,宏定义的作用域不受命名空间或者说 { } 的限制。**如下面这个例子:
namespace abc
{
define PATH_MAX 256;
}
char path[PATH_MAX];
以上代码可以通过编译,但这不是我们想要的,可以说这是C++的一个糟糕设计吧。
2. 优点
可大量替换代码中的字面常量,利于维护。
如果是用宏定义替代一些简单函数,则可以提高效率,因为函数要保存现场,有参数传递和返回
3. 陷阱
所谓的边际效应,就是因为简单替换而引起的,如
//优先级问题示例1
#define N 10
#define M 100+25
此时计算 M*N 的结果会是 (100 + 25 * 10),而我们期待的结果是(100 + 25) * 10
//优先级问题示例2
#define DIV(x,y) (x+y-1)/y
那么
a = DIV( b & c, 5);
将被转化为:
a = ( b & c + 5 - 1) / 5;
而 +/- 运算符优先级高于 & ,那么上面式子等同于:
a = ( b & (c + 5 - 1) ) / 5;
这不是我们要的结果,为保证优先级正确,所有替换要加括号()
//定义伪函数时,参数不是传递的
#dedfine sqrt(a) ((a)*(a))
int main()
{
int a = 10;
int r = 0;
r = sqrt(a++);
prntf("%d,%d", a, r);
}
输出结果: 12,100
因为a++被执行了两次, (a++)* (a++)
所以这限制了我们的写法,只能写为:
a++; r = sqrt(a);
当然还有更多其它形式的边际效应问题,所以使用宏定义一些复杂表达式 或者伪函数时,一定要慎重
3.1 一个宏定义重名导致的错误
看下面的一个案例,两个头文件中定义了两个重名的宏
//filename1.h
#define PATH_MAX (256)
//filename2.h
#define PATH_MAX (1024)
//xxxx.cpp
#include "system.h"
struct node
{
char path[PATH_MAX];
...
int a;
};
这里假定filename1.h 中的宏是我们自己定义的,我们只想使用filename1.h中的定义。 而filename2.h是别人定义的或一些系统库定义的。
正常情况下,如果一个同时包含了这两个头文件, 会出现重定义编译错误,那么只包含其中一个头文件的话是可以正常编译的。
当我们忘记包含了filename1.h,而恰好因为系统头文件 “system.h” 的包含层级很深,在我们不知道的情况下将 filename2.h包含了进来,那么代码正常编译,但代码运行时却会出现很隐晦的错误并难以排查,如越界等。
那么使用const定义能解决这种问题么?不能!但是const 定义的变量是可以有命名空间的,我们可以使用命名空间避免这种人为错误:
namespace my_namespace
{
const int pATH_MAX = 256;
}
char path[my_namespace::pATH_MAX];
4. 建议
综上,定义字面常量时,建议使用const 代替 #define
const int PATH_MAX = 256;
使用const 定义的优点有:
拥有类型,可帮助编译器侦测出错误用法,并解决边际效应(因为它是个变量而不是简单替换)
如果定义的常量是一个表达式的结果,const 定义只执行一次表达式,而#define定义则会执行多次,从这个角度讲用const定义效率更高。
可以在命名空间内定义,从而避免重名导致的错误