在C语言中,通常使用#define来定义常数,其后在使用该常数的地方利用定义的宏名来进行常数替换,这样可以避免在程序中到处出现magic numbers的乱象,并且#define是宏定义,不需要为其分配存储空间。但是#define是在预处理器的控制范畴内,只能简单地进行文本替换,不能进行类型安全检查,并且其作用域从#define定义点开始,直到该编译单元结束,无法进行较好的控制。
在C语言中,如果想让编译器进行constant folding,即在编译时,通过计算将复杂的常数表达式化简为一个值,通常只能用#define来进行(C99标准之前)。编译器将始终为const变量分配存储空间。const仅仅意味着定义的变量是只读的,其值不能被修改。C编译器不能将const作为一个编译时的常数来使用,而总是为其分配存储空间。因此如下定义全局数组
const int bufsize = 100;
char buf[bufsize];
尽管看起来符合我们的思维习惯,但编译将出现错误。因为bufsize占有存储空间,C编译器在编译期间不能确定它的值。
因此在C语言中,const并不是非常有用的。如果你想在一个常数表达式中使用一个符号名(也就是说必须在编译时确定其值的话),C几乎限定你只能使用预处理器的#define来进行。
在C中const是外连接的,不能将其定义放在头文件中。如果用const int bufsize;的话,可以表示一个声明(bufsize在别处定义,跟使用extern const int bufsize;是等价的),也可以表示一个定义(如果在连接时,其它地方都没有定义的话,将其初始化为0)。
针对C中const的上述局限性,C++作出了重大的改进。在C++中,可以使用const来定义常数,因为const在编译器的控制范畴内,而非由预处理器控制,所以可以进行类型安全检查,也方便进行作用域控制。
在C++中可以利用const来消除#define定义常数的方式,以提高安全性。通常C++编译器并不为const分配存储空间,而是将其定义保存在符号表中,当使用时进行简单的常数替换(像#define一样不占空间,但拥有类型安全检查和作用域控制)。因此上述不能编译通过的C代码,在C++中可以顺利地通过。当不需要分配空间时(这依赖于数据类型的复杂度和编译器的智能化程度),在类型安全检查之后,像#define一样,为了更高的效率,值将会被折叠进代码中。
因此在C++中,当使用const时并不一定意味着会分配存储空间,是否会分配空间要看是怎么使用的。通常编译器会尽力避免为其分配空间,以通过constant folding来提高效率。常见的C++将会为const变量分配存储空间的情况是:当取了某个const变量的地址时。注意通过引用来进行函数参数传递也是会取变量地址的,尽管从语法上看这并没有取地址,但是应该清楚引用在底层实现上也是通过传递地址的方式来进行的。当不得不为const变量分配空间时,C++仍然能通过一定的手段来尽量进行constant folding。如下代码所示:
const int i = 100; //典型的常数
const int j = i + 10; //从常数表达式得到值
long address = (long)&j; //因为取地址,强迫编译器为j分配存储空间
char buf[j+10]; //j+10仍然是一个常数表达式
当不得不为const分配空间时(与#define相对的别一种用法),const代表该空间在其后是只读的,所以在定义时必须初始化,因为之后已经无法进行赋值以改变其内容。
在聚合数据类型上使用const也是可以的,但是编程者必须明白编译器很可能并没有智能到能将聚合类型保存在符号表中并进行constant folding的程度,所以需要为其分配存储空间。这种情况下const的意义只是告诉编译器,这是一小片不能被改变值的存储区域。因此编译器在编译时不能确保其常量性
const int i[] = { 1, 2 ,3 ,4 };
//float f[i[3]]; //非法
struct S { int i, j; };
const S s[] = { { 1, 2 },{ 3, 4 } };
//double d[s[1].j]; //非法 |