“这个条款或许应该改为, 尽量使用编译器代替预编译器, 比较好, 因为define或许不被视为语言的一部分.”
为什么这么讲呢? 因为#define的变量几乎不能叫做变量. #define只是一种出现在预处理阶段非常简单粗暴的替换, 对于处理器来讲, 可能根本不知道define存在过.
如:
#define SPACE " "
SPACE 有可能根本没进入记号表(变量记号表), 而是在预处理阶段就已经被移走了. 这时会出现几个问题, 一是如果是对于" "
的使用出现了问题, 报的编译错误可能很难理解, 因为并不是对 SPACE 进行报错, 而是对 " "
进行报错, 如果程序规模较大, 那么追踪该错误是非常困难的. 二是, 你没法对 SPACE 进行 DEBUG, 因为它根本就没有进入记号表.
解决的方法就是用一个常量去替换宏.
const char* Space = " "; // 全大写一般用于宏 这里是变量所以采用大驼峰
作为一个语言上的常量, Space 会理所应当地进入记号表, 就肯定可以被编译器看到, 此时如果报错也是报关于 Space 的错误, 同时 DEBUG 也会更加方便. 并且, 由于预处理器处理 define 是简单粗暴的替换, 所以可能会出现多份 " "
, 而使用变量则不会出现这种问题.
用常量替换 #define 有两种比较特殊的情况需要说明.
-
对于常量指针, 一般有必要对指针和指针的解引用进行 const 修饰, 比如:
const char* const Space = " ";
第一个 const 修饰的是,
*Space
, 表示*Space
不可被改变, 即指针指向的内容不能被改变.第二个 const 修饰的是,
Space
, 表示Space
自身不可被改变, 即该指针不允许再指向别的空间. -
对类内的 const 成员, 推荐加上 static 修饰, 比如:
class test
{
public:
static const int x = 6;
private:
static const int num = 5;
};
因为 const 本身表示不可被修改, 如果不加上 static , 那么每个类创建实体时, 都需要不必要地去为一个常量开辟空间, 这是不可取的, 而 static 会保证, 不管有多少实体, 这个常量都只会有一份.
这里还有一些问题, 你是否认为, x 和 num 在类内都是被定义式定义的? 但是其实这些都是声明式, 通常 C++ 会要求你给所有东西都提供一个定义式, 但是对于类内的 static 成员例外, 你可以值提供声明式, 不提供定义式, 但是, 你不可以进行一些特殊操作, 如取地址. 但是如果你坚持要取地址, 或者你的编译器非要你提供一个定义式, 那么你就需要提供定义式. 如下:
const int test::x;
你可能会感到好奇, 为什么没有值, 这是因为你在声明的时候已经提供了值, 所以在定义的时候, 你不可以提供值.
顺带说一句, define 是无法为类创建专属常量的, 因为 define 不重视作用域(scope), 除非在某处进行了条件编译. 也就是说, 没有 private #define 这种东西, #define 也不能提供任何封装性.
老的编译器或许不支持上面的在声明时给值, 这时你可以把值放到定义式上.
但是, 我们仍旧会碰到问题, 比如开辟数组这个问题上, 我们写出如下代码:
#define NUM 30
int main()
{
int num[NUM] = {0};
return 0;
}
这样写当然是没有问题, 但是如果我们换成常量呢,
const int Num = 30;
int main()
{
int num[Num] = {0};
return 0;
}
// 或者以下代码
class test
{
private:
static const int Total = 50;
int num[Total];
};
此时, 有些编译器就会(错误地)报出编译错误, 认为必须要知道初始值, 也就是必须要知道数组开了多大. 我们来分析一下为什么会这样, #define 的处理在预处理阶段, 编译器工作在预处理后, 代表如果粗暴替换了 30, 那么编译器会很明显地知道需要开多大, 而如果是一个变量, 那么就认为不知道要开多大了. 我认为, 这样做不正确, 这样就导致了, 我们没办法去让程序自己根据情况去开多大, 而是必须要提前设置好.
并不是所以的编译器都会这样, 有的编译器是可以这样做的. 对于不支持的编译器, 我们可以用 enum (枚举)的方法达到效果, 如:
private:
enum
{
Total = 50
};
int num[Total];
};
因为 enum 更像 #define 而不是 const, 所以可以达到目的, 而且取一个 enum 值的地址是不合法的, 就像取 #define 的值的地址不合法一样, 而取 const 的是合法的. 这表明, 如果你并不想别人拿到这个常量的地址, 你可以使用 enum. 而且, enum 其实是模板编程的基础技术.
接着看预处理阶段, 如果你用 #define 写了一个函数, 如:
#define FUNC(a) a * 3
如果你踩过 #define 的坑, 你看到这个就会感到头痛, 因为它让你想起了那些不好的回忆, 它给你收获就是让你知道, 这样一定会出错的. 如写出以下代码:
int main()
{
std::cout << FUNC(1 + 5) << std::endl; // 16
return 0;
}
你或许认为结果非常简单, 就是 18, 但是这是错误, 因为#define的简单粗暴替换, 导致 a = 1 + 5, 而替换后, 就是 1 + 5 * 3, 由于 * 的优先级更高, 导致, 最后结果不能达到预期, 这就是坑. 我们可以用括号解决这个坑:
#define FUNC(a) ((a) * 3)
但是接下来这个, 就是没法用括号解决的了:
#define MAX(a, b) ((a) > (b) ? (b) : (b))
写出如下代码:
int main()
{
int a = 5;
int b = 0;
MAX(++a, b);
std::cout << a << " " << b << std::endl; // a 累加两次
MAX(++a, b + 10);
std::cout << a << " " << b << std::endl; // a 累加一次
return 0;
}
你发现, a 累加多少次竟然和与谁比较有关, 这是由于单纯的替换导致表达式执行了多次. 而单独为其携程一个函数又非常浪费, 幸运的是, C++ 为我们提供了方法, 让我们既能享受宏的快, 又有函数的可预料行为和类型安全性. 那就是 inline.
inline 会为函数提供优化, 使其变得类似于宏, 你不需要加上一堆括号, 也不需要担心参数被计算多次:
inline Max(a, b)
{
return a > b ? a : b;
}
而且类内的函数, 如果编译器认为需要优化成 inline, 那么他会自己帮你加上 inline.
有了 const ,enum, inline, 你就可以给 #define 放个假了, 但是预编译阶段的 #include , #ifdef/#ifndef 仍被需要.
提醒:
- 对于单纯常量, 最好使用 const, enum 代替 #define.
- 对于类似函数的宏, 最好用 inline 代替 #define.