1.9 阅读ANSI C标准,寻找乐趣和裨益
有时候必须非常专注地阅读ANSI C标准才能找到某个问题的答案。一位销售工程师把下面这段代码作为测试例发给Sun的编译器小组。
1 foo(const char **p) { }
2
3 main(int argc, char **argv)
4 {
5 foo(arvg);
6 }
如果编译这段代码,编译器会发出一条警告信息:
line 5: warning: argument is incompatible with prototype
(第5行:警告:参数与原型不匹配)。
提交代码的工程师想知道为什么会产生这条警告信息,也想知道ANSI C标准的哪一部分讲述了这方面的内容。他认为,实参char s与形参const char p应该是相容的,标准库中所有的字符串处理函数都是这样的。那么,为什么实参char argv与形参const char p实际上不能相容呢?
答案是肯定的,它们并不相容。要回答这个问题颇费心机,如果研究一下获得这个答案的整个过程,会比仅仅知道结论更有意义。对这个问题的分析是由Sun的其中一位“语言律师”[6]进行的,其过程如下:
在ANSI C标准第6.3.2.2节中讲述约束条件的小节中有这么一句话:
每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)。
这就是说参数传递过程类似于赋值。
所以,除非一个类型为char 的值可以赋值给一个const char 类型的对象,否则肯定会产生一条诊断信息。要想知道这个赋值是否合法,就请回顾标准中有关简单赋值的部分,它位于第6.3.16.1节,描述了下列约束条件:
要使上述的赋值形式合法,必须满足下列条件之一:
两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。
正是这个条件,使得函数调用中实参char能够与形参const char匹配(在C标准库中,所有的字符串处理函数就是这样的)。它之所以合法,是因为在下面的代码中:
char *cp;
const char *ccp;
ccp = cp;
左操作数是一个指向有const限定符的char的指针。
右操作数是一个指向没有限定符的char的指针。
char类型与char类型是相容的,左操作数所指向的类型具有右操作数所指向类型的限定符(无),再加上自身的限定符(const)。
注意,反过来就不能进行赋值。如果不信,试试下面的代码:
cp = ccp; /* 结果产生编译警告 */
标准第6.3.16.1节有没有说char 实参与const char 形参是相容的?没有。
标准第6.1.2.5节中讲述实例的部分声称:
const float *类型并不是一个有限定符的类型——它的类型是“指向一个具有const限定符的float类型的指针”,也就是说const限定符是修饰指针所指向的类型,而不是指针本身。
类似地,const char **也是一个没有限定符的指针类型。它的类型是“指向有const限定符的char类型的指针的指针”。
由于char 和const char 都是没有限定符的指针类型,但它们所指向的类型不一样(前者指向char ,后者指向const char ),因此它们是不相容的。因此,类型为char的实参与类型为const char的形参是不相容的,违反了标准第6.3.2.2节所规定的约束条件,编译器必然会产生一条诊断信息。
用这种方式理解这个要点有一定困难。可以用下面这个方法进行理解:
左操作数的类型是FOO2,它是一个指向FOO的指针,而FOO是一个没有限定符的指针,它指向一个带有const限定符的char类型,而且……
右操作数的类型是BAZ2,它是一个指向BAZ的指针,而BAZ是一个没有限定符的指针,它指向一个没有限定符的字符类型。
FOO和BAZ所指向的类型是相容的,而且它们本身都没有限定符,所以符合标准的约束条件,两者之间进行赋值是合法的。但FOO2和BAZ2之间的关系又有不同,由于相容性是不能传递的,FOO和BAZ所指向的类型相容并不表示FOO2和BAZ2所指向的类型也相容,所以虽然FOO2和BAZ2都没有限定符,但它们之间不能进行赋值。也就是说,它们都是不带限定符的指针,但它们所指向的对象是不同的,所以它们之间不能进行赋值,也就不能分别作为函数的形参和实参。但是,这个约束条件很令人恼火,也很容易让用户混淆。所以,这种赋值方法目前在基于Cfront的C++翻译器中是合法的(虽然这在将来可能会改变)。
容易混淆的const
关键字const并不能把变量变成常量!在一个符号前加上const限定符只是表示这个符号不能被赋值。也就是它的值对于这个符号来说是只读的,但它并不能防止通过程序的内部(甚至是外部)的方法来修改这个值。const最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据,但其他的函数却可能会修改它。这也许就是C和C++中const最一般的用法。
const可以用在数据上,如:
const int limit = 10;
这和其他语言差不多,但当你在等式两边加上指针,就有一定难度了:
const int * limitp = &limit;
int i = 27;
limitp = &i;
这段代码表示limitp是一个指向常量整型的指针。这个指针不能用于修改这个整型数,但是在任何时候,这个指针本身的值却可以改变。这样,它就指向了不同的地址,对它进行解除引用(dereference)操作时会得到一个不同的值!
const和的组合通常只用于在数组形式的参数中模拟传值调用。它声称“我给你一个指向它的指针,但你不能修改它。”这个约定类似于极为常见的void 的用法,尽管在理论上它可以用于任何情形,但通常被限制于把指针从一种类型转换为另一种类型。
类似地,你可以取一个const变量的地址,并且可以...(唔,我最好不要往大家的脑袋里灌输这种思想)。正如Ken Thompson所指出的那样,“const关键字可能引发一些罕见的错误,只会混淆函数库的接口。”回首往事,const关键字原先如果命名为readonly就好多了。
确实,整个标准好像是由一位蹩脚的翻译把它从乌尔都语转译成丹麦语,再转译成英语而来。标准委员会似乎自我感觉良好,所以虽然人们希望语言的规则更简单一些、更清楚一些,但他们觉得这样做会破坏他们的良好感觉,所以拒不采纳。
我感觉,将来还会有许多人产生类似的疑问,而且并不是他们中的每一个人都会仔细揣摩前面详述的推理过程。所以,我们修改了Sun的ANSI C编译器,当它发现不相容的情况时,会打印出更多的警告信息。原先那个例子将会产生的完整信息如下:
Line 6: warning : argument #1 is imcompatible with prototype:
prototype: pointer to pointer to const char: "barf.c", line 1
argument: pointer to pointer to char
(第6行:警告:#1实参与原型不相容:
原型:指向const char的指针的指针。"barf.c", 第1行
实参:指向char的指针的指针。)
即使程序员不明白为什么会这样,他至少应该明白什么是不相容。