读《C专家编程》关注到的趣事

关于const的混淆

代码

1
2
3
4
5
foo(const char **p) { }
main(int argc, char **argv)
{
foo(argv);
}

编译会报错:

1
line 5: warning: argument is incompatible with protorype

我在自己电脑上编译运行报错为:

1
2
3
4
5
6
7
8
$ gcc consterr.c
consterr.c: In function 'main':
consterr.c:6:6: warning: passing argument 1 of 'foo' from incompatible pointer type [-Wincompatible-pointer-types]
foo(argv);
^~~~
consterr.c:1:6: note: expected 'const char **' but argument is of type 'char **'
void foo(const char **p)
^~~

可以看到是类似的(passing argument 1 of ‘foo’ from incompatible pointer type [-Wincompatible-pointer-types])

也就是说,char **argvconst char **p并不相容。我尝试了其他测试用例:

对于整型是可以通过编译的(很显然)

1
2
3
4
5
6
7
8
9
void foo(const int p)
{
}
int main(int argc, char **argv)
{
int a = 10;
foo(a);
return 0;
}

指向整型的指针也可以通过编译

1
2
3
4
5
6
7
8
9
10
void foo(const int *p)
{
}
int main(int argc, char **argv)
{
int a = 10;
int *p = &a;
foo(p);
return 0;
}

接下来测试二级指针,果然和前面一样报错了:

1
2
3
4
5
6
7
8
9
10
11
void foo(const int **p)
{
}
int main(int argc, char **argv)
{
int a = 10;
int *p = &a;
int **ptr = &p;
foo(ptr);
return 0;
}
1
2
3
4
5
6
7
8
$ gcc consterr.c
consterr.c: In function 'main':
consterr.c:9:6: warning: passing argument 1 of 'foo' from incompatible pointer type [-Wincompatible-pointer-types]
foo(ptr);
^~~
consterr.c:1:6: note: expected 'const int **' but argument is of type 'int **'
void foo(const int **p)
^~~

书中给出的ANSI C标准中描述:

每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)

说明传参过程类似于赋值,接下来我们可以使用赋值语句使问题的情境更清晰,并且从标准中对赋值操作的约束寻找线索。标准中关于赋值的描述有如下约束条件

两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符

因此上述问题的产生来源于const究竟是哪个变量的限定符的混淆

测试以下代码是合法的

1
2
3
char *cp;
const char *ccp;
ccp = cp;
  • 左操作数是一个指向有const限定符的char类型的指针
  • 右操作数是一个指向没有限定符的char的指针

由于这两个指针都指向相同的类型char,自然是相容的,并且左边指针所指向的类型具有右边指针所指向类型的全部限定符,因此是合法的。

这个关于const的结论来自标准中

const float *类型并不是一个有限定符的类型——它的类型是”指向一个具有const限定符的float类型的指针”,也就是说const限定符是修饰指针所指向的类型,而不是指针本身。

回到最初的问题,原问题可以写作:

1
2
3
char **cp2;
const char **ccp2;
ccp2 = cp2;

左操作数ccp2是一个指向char类型的二级指针,其本身和指向的指针(假设为ccp)本身都没有限定符,而它指向的指针则指向一个含有const限定符的char类型

右操作数cp2是一个指向char类型的二级指针,其本身和指向的指针(假设为cp)本身都没有限定符,而它指向的指针则指向一个没有限定符的字符类型

由于ccp2cp2指向的对象内容不同(尽管这两位都没有限定符),因此不能赋值。

尽管ccpcp所指向的类型是相容的,但相容性并不是可以传递的,故不能表示ccp2cp2所指向的类型也相容。

书中对标准的这一点是这么吐槽的(乐):

确实,整个标准好像由一位蹩脚的翻译把它从乌尔都语转译成丹麦语,再转译成英语而来,标准委员会似乎自我感觉良好,所以虽然人们希望语言的规则更简单一些、更清楚一些,但他们觉得这样做会破坏他们的良好感觉,所以拒不采纳。

作者补充道他修改了Sun的ANSI C编译器对此问题打印更多警告信息:

1
2
3
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

对比了一下我的gcc输出的结果,好像还真差不多是这样,谢谢你泰罗。

(作者是Peter Van Der Linden)

另外作者表示const关键字或许命名为readonly更为合适,因为关键字const并不能把变量变成常量,只是表示其限定的符号不能被赋值,也就是说它的值对于这个符号来说是只读的,但它不能防止通过程序的内部(甚至是外部 )的方法来修改这个值。

所以const最常见的用处是限定函数的形参,这样该函数不会修改实参指针所指的数据,但其他函数可能会修改它(这就是标准库的代码中到处都是const的原因吗)。