在介绍C++的常前,先看下下面的代码。
for (int i = 0; i < 512; ++i) { …; }
512是什么,它具有什么含义?在代码中若直接使用类似512这些“魔数”(magic number),这些数字的具体含义就不能体现出来。另外,假如程序中多处包含512这个数,随着时间的推移,发现有些512需要更改为1024,有些512需要保持不变。这种情况下,程序员必须小心谨慎地去一个一个地查找程序中所有的512,并保证更改的数字不会出错——使用“魔数”会导致难以维护的问题。
为了解决以上问题,可以使用#define预处理器指令来定义一个常量。例如,
#define MAX_NUM_SIZE 512 for (i = 0; i < MAX_NUM_SIZE; ++i) { …; }
通过使用#define定义MAX_NUM_SIZE这个宏,可以清晰看到512的具体含义,另外,假如需要更改这个数字,只须直接修改#define一处就可以了。
然而,在C++中,并不提倡使用#define定义一个常量。#define本质上是一个预处理器指令,它仅仅表示使用一个串代替别一个串而已。也就是说,#define定义的常量从未被编译器看到——它们在编译器开始处理源码之前就被移走了。
具体来说,避免使用#define来定义常量,原因如下所述。
1. 没有指定类型
#define不涉及为定义的常量做类型检查,为了显式地指定常量类型,需要在常量后加上后缀。比如,对于float类型的常量,在数字后面加上f后缀。
2. 没有指定作用域
#define定义的常量是全局的。
3. 没有访问控制
不能把#define定义的常量标记为公有的,受保护的,或者私有的,它本质上是公有的。
因为宏一旦被定义,它就在其后的编译过程中有效(除非在某处被#undef)。
4. 没有符号
前面的例子中,宏MAX_NUM_SIZE可能会被预处理器从代码中剥离,这样,编译器就无法看见这个名字。这样,程序员在调试时只能看到一些没有任何描述性的常量值。
与使用#define定义常量相比,更可取的办法是使用C++的const限定修饰符来定义常量。例如,对于MAX_NUM_SIZE,可进行如下定义:
const int MAX_NUM_SIZE = 512;
作为一个整型常量,const修饰的MAX_NUM_SIZE肯定被编译器看到,当然会进入符号表。假如将MAX_NUM_SIZE定义在某个类中,还可以对其实施访问控制器和为其指定作用域。例如:
class GamePlayer { private: static const int MAX_NUM_SIZE; // static修饰符是为了确保此常量只有一份实体 }; const int GamePlayer:: MAX_NUM_SIZE = 512;
在上述的代码中,我们定义了一个int型的常量,该常量的作用域为GamePlayer类在,并标记为私有访问,并且会进入符号表。
值得指出的是,要注意const定义常量在某种程序上可能会使代码更臃肿。例如,在某个头文件中定义的了以下的一些常量:
const int MAX_NAME_LENGTH = 128; const float LOG_2E = log2(2.71828183f); const std::string LOG_FILE_NAME = “filename.log”;
在默认情况下,以这种方式定义的变量会促使编译器为每个包含此头文件的模块分配变量存储空间。如果定义了很多常量,并且该头文件被很多.cpp文件包含,那么会导致.o目标文件和最终的二进制文件膨胀。
解决办法是在头文件中使用extern声明常量:
// myconst.h extern const int MAX_NAME_LENGTH; extern const float LOG_2E; extern const std::string LOG_FILE_NAME;
然后在相应的.cpp文件中定义每个常量的值:
// myconst.cpp const int MAX_NAME_LENGTH = 128; const float LOG_2E = log2(2.71828183f); const std::string LOG_FILE_NAME = “filename.log”;
通过这种方式,变量的空间就只会分配一次。
使用extern来声明的常量是全局的,若要将常量的作用域限制在类中,则须在类中声明常量,并将其声明为static(这样它们就不会计入每个对象的内存大小中)。
// myclass.h class MyClass { public: static const int MAX_NAME_LENGTH; static const float LOG_2E; static const std::string LOG_FILE_NAME; };
然后在相应的.cpp文件中定义这些常量:
// myclass.cpp const int MAX_NAME_LENGTH = 128; const float LOG_2E = log2(2.71828183f); const std::string LOG_FILE_NAME = “filename.log”;
当然,在某些情况下,使用枚举类型代替常量也可以避免文件空间膨胀的问题。
参考资料:
1. C++ Primer 中文版,第三版
2. C++ API 设计
3. Effective C++ 中文版,第三版