先看一段包含警告的代码:
1 | void foo(const char **p) { } |
我们可能认为,由于实参 char *s
与形参 const char *p
应该是相容的(标准库中所有的字符串处理函数都是这样写的),那么,为什么实参 char **argv
与形参 const char **p
却不能相容呢?
容易混淆的 const
回答上述问题前先看一下关键字 const 的用法和实际使用的含义。注意,它不能把变量变成常量!在一个符号前加上 const 限定符只是表示这个符号不能被赋值。
这意味着,我们可以取一个 const 变量的地址,并且可以改变它的值(但最好还是不要这样)。回首往事,const 关键字原先如果命名为 readonly 就好多了,因为 readonly 没有不能改变的意思(外部可以去改变),而 const 只表示自身不会变。const 的最简单用法:
1 | const int num1 = 10; |
当在赋值号两边加上指针,就要注意了:
1 | const int *num2 = &num1; |
const 修饰符修饰的不是 int 指针,而是 int 本身,意味着 int 不能被改变,而 int 指针的值(地址)可以被改写。const 最有用的地方就是用来修饰函数的形参,这样表示该函数不会去修改实参指针所指向的数据内容。它声称「我给你一个指向它的指针,但你不能修改它」。这个约定类似于 void *
的用法。尽管它可以用于任何类型,但通常被限定于把指针从一种类型转换为另一种类型。
总结:
- int const *p(int 只读)
- const int *p(int 只读)
- int * const p(指针只读)
- int const * const p(都只读)
指针赋值的约束条件
回到问题本身,ANSI C 标准中有这么一句话(约束条件):
每个实参都应该具有自己的类型,这样它的值就可以赋值给它所对应的形参类型的对象(该对象的类型不能含有限定符)。
这就是说参数传递过程类似于赋值。所以,这样的代码也是非法的:
1 | char **cp; |
要使指针的赋值合法,必须满足下列约束条件:
- 两个操作数都是指向有限定符或无限定符的相容类型的指针。
- 左边指针所指向的类型必须具有右边指针所指向类型的全部限定符(「无限定符」也被看作是一种限定符)。
先来看一下为什么实参 char *
和 const char *
匹配,即:
1 | char *cp; |
它之所以合法,是因为它满足:
- 左操作数是一个指向有 const 限定符修饰的 char 的指针。
- 右操作数是一个指向没有限定符修饰的 char 的指针。(相当于空集可以包含于其它集合)
- char 类型与 char 类型是相容的,左操作数所指向的类型具有右操作数所指向的类型的限定符(无 + const)。
注意,反过来就不能赋值了,比如:
1 | cp = ccp; // 产生警告 |
类似地,const char **
是一个指向没有限定符修饰的指针类型。注意两个指针本身 const char **
和 char **
都没有被 const 限定符修饰。然而,它们指向的类型不一样,const char **
指向 **const char ***,char **
指向 **char ***。
const char
和char
→ 相容const char *
和char *
→ 不相容(注意注意!有点奇葩!)- 我猜
char * const
和char *
是相容的。
相容性是不能传递的。
换言之,
- 左操作数的类型是 FOO_2,它是一个指向 FOO 的指针,而 FOO 是一个没有限定符的指针,它指向一个带有 const 限定符的 char 类型。
- 右操作符的类型是 BAZ_2,它是一个指向 BAZ 的指针,而 BAZ 是一个没有限定符的指针,它指向一个没有限定符的字符类型。
FOO
和 BAZ
所指向的类型是相容的,而且它们本身都没有限定符,所以两者之间可以赋值(注意只是单向的)。但是 FOO
和 BAZ
所指向的类型相容并不意味着 FOO_2
和 BAZ_2
所指向的类型也相容,所以即使 FOO_2
和 BAZ_2
都没有限定符,但它们之间不能进行赋值(双向都不行,因为所指向的对象本身不相容)。
参考
《C 专家编程》(Expert C Programming)p19-21