const一词是英文constant的缩写,设立这个关键字的本意,是希望让它所修饰的对象成为一个常量。记得在国家间的外交中,有一个经常用到的术语:“从事与身份不符的活动”,这个const恰恰也正从事着这样的活动,呵呵。C语言可以有三种方法定义一个常量:#define、const和枚举,但只有枚举才是真正的常量,什么是真正的常量?真正的常量是没有存储空间的,是一个右值,这意味着通过任何合法的手段也不会被修改,但被const修饰的对象依然是一个左值,尽管这个对象被const限定,笔者仍然至少可以找到三种合法的手段去修改它,而#define所做的只不过是编译期替换而已,只有枚举常量才能真正做到这一点。const实在不应该被命名为const,这会让人们产生误解,它应该命名为readonly或类似的字眼,意即不能通过被const修饰的对象修改它所指向的对象或者它所代表的对象。但在C的世界里把const称为常量早已是普遍的现象,那我们就只好随大流咯,也称之为常量吧,只要知道它实际上不是真正的常量就行了。
第七章曾经讨论过const int *p;与int * const p的区别,这两个声明的中文名称常常搞得混乱不堪。第一个声明的const是声明说明符,它修饰p所指向的对象,但p仍然是可变的,这意味着p是一个指向常量的指针,简称常量指针。第二个声明的const是声明符的一部分,它修饰的对象是p,这意味着p是一个常量,而且是一个指针类型的常量,简称指针常量。指针常量又常常被人称为“常指针”或“常指针变量”,常指针变量这个名称有点蹩脚,又常又变的,容易让人摸不着头脑,最好还是不要这样称呼。这里还得再强调一次指针常量与地址常量是不同的,不能把数组名称为指针常量,也不能把指针常量称为地址常量,因为指针常量依然是一个左值,而数组名是一个右值,这里肯定有人会问:“什么?指针常量是一个左值?我没听错吧?”你的确没有听错,C89对于左值是这样定义的:
对象是一个命名的存储区域,左值(lvalue)是引用某个对象的表达式。
换言之,如果一个表达式引用的是一个具有具体存储空间的对象,它就是一个左值!那么既然指针常量是一个左值,为什么却不能给它赋值呢?是因为它受限于赋值表达式的一条规则:赋值表达式的左值不能含有限定词!
为了防止指针指向的常量被修改,C标准对于指针间赋值有一个规定,就是左值必须包含右值的所有限定词。 这就限定了一个指向const对象的指针不能赋值给指向非const对象的指针,但反过来就允许。这个规定初看上去非常合理,但其效用其实只限于一级指针,二级指针间的赋值即使满足规定也不再安全,下面举个例子:
const int i=10;
const int **p1;
int *p2;
p1 = &p2;
*p1 = &i;
*p2 = 20;
现在你会发现,作为常量的i的值被修改了。i的值被修改的关键原因在*p1=&i;这一句,&i是一个指向常量的一级地址,如果没有二级指针p1,受限于上述规定,作为左值接受这个一级地址的指针就必须也是一个指向常量的一级指针,于是就不能进行下一步赋值20的操作。因此,正由于指向const对象的二级指针p1的出现,使得*p1也是一个指向const的指针,于是*p1=&i能够合法地运行,常量i的值被修改也就成了一个预想中的结果了。有鉴于此,某些编译器也会限定非const二级指针之间的赋值,规定上面的p1=&p2也是非法的。
第七章介绍声明符的指针部分有一种形式:
* 类型限定符表opt 指针
这种形式产生了一种比较复杂的带const的指针,例如:
const int * const *** const ** const p;
这是一个会让人头晕目眩的表达式,声明符部分嵌套了九次,如何辨认谁是const,谁不是const呢?一旦明白了其中的原则,其实是非常简单的。第一和最后一个const大家都已经很熟悉的了。对于藏在一堆*号中的const,有一个非常简单的原则:const与左边最后一个声明说明符之间有多少个*号,那么就是多少级指针是const的。例如从右数起第二个const,它与int之间有4个*号,那么p的四级部分就是const的,下面的赋值表达式是非法的:
**p = (int *const***)10;
但下面的赋值是允许的:
***p=(int*const**)10;
从左边数起第二个const,它与int之间有1个*,那么p的一级部分是const的,也就是*****p = (int*const***const*)10;是非法的。