前置知识:至少需要知道 C 语言中的 (const) qualifier 的修饰对象

读 C23 手册读到了赋值运算符下面的一个例子:

1
2
3
4
5
6
const char **cpp;
char *p;
const char c = ’A’;
cpp = &p; // constraint violation
*cpp = &c; // valid
*p = 0; // valid

假如 line 4 的赋值可以执行,那么最终 line 6 会改变 const char c 的值。因此不被允许。

但是仔细想想其实这件事情有点反直觉。因为 line 4 中,cppconst char **&cchar **。这里 cpp 显然是变量而不是常量,因为 const 修饰的是 char。而且因为右操作数是 char **,因而这个赋值似乎并没有 discard 任何的 qualifier(实际上有)。尽管笔者理解不能使得上面的例子成功执行,这样一种目的上的设计“原因”;但并不明白,是哪几条设计或机制在事实上让这样的程序编译得不到通过。毕竟看起来是这么合理。有多合理呢?看看下面的例子:

1
2
3
4
5
6
7
8
9
// 1. assign char * to const char *
const char *c;
char *b;
c = b; // OK

// 2. assign char ** to const char **
const char **c;
char **b;
c = b; // FAIL

第一个例子看起来无比自然、甚至非常常见,以至于我就算在群友的提示下还是花了很久才弄明白第二个为什么不能通过。

这是为什么呢?我今天下了班再来写…最后那天晚上一回来就一觉睡到早上了。

首先我们来介绍一个概念:Compatible Type(兼容类型)

C 语言标准手册 6.2.7 中说:两种类型是 compatible types 如果它们相同。这一小节里介绍了结构、联合体和枚举类型的 compatible 规则。总结下来就是一一对应。可以认为,它们的每个成员必须保持类型 compatible、qualifier 一致、attribute 一致且成员名一致。另外,对于数组类型,有一套独特的规则:

就不翻译了吧

总之可以理解为全等。

在此基础上,在 6.7.6.1 里介绍了指针类型的 compatible 规则:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

指针本身的 qualifier 必须全等,并且它们指向的类型需要 compatible。

在 6.7.3 里介绍了一般类型的 compatible 规则:

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type; the order of type qualifiers within a list of specifiers or qualifiers does not affect the specified type.

两个类型必须表达(不是拥有的 “have”)相等地 qualified 版本的 compatible 类型。比如 const volatile intvolatile const int。就是说,顺序可以不一样,但是个数和类型必须一样。

原文里给了一个极端的例子:

有 qualifier 就是要用

这里可以看出,const 不一定是常量,而是不应被程序修改的量。

接下来回到今天的例子。

charconst char 并不 compatible。因为它们是 compatible type char 的 differently-qualified version。

因而 char *const char * 并不 compatible。因为它们分别指向 incompatible type charconst char

因而 char **const char ** 并不 compatible。因为它们分别指向 incompatible type char *const char *

……

根据我们的观察,我们不可以把 char 赋值给 const char:

1
2
3
4
5
const char a;
char b;
a = b; // FAIL

const char c = b; // OK

因为 Assignment 表达式的通用约束是:

Assignment operator constraint

左操作数必须是可修改的左值。

注意到 line 5 的写法是 Initialization(6.7.10)。它由 initdeclarator 构成:

image-20250826232839654 image-20250826233012845

initializer 进一步包含了 Assignment-expression(6.5.16)。但是这个 Assignment 表达式并不一定是带等号的式子,它经过语法单元的分解,最终下降到了 primary-expression。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assignment-expression:
conditional-expression:
logical-OR-expression:
logical-AND-expression:
inclusive-OR-expression:
exclusive-OR-expression:
AND-expression:
equality-expression:
relational-expression:
shift-expression:
additive-expression:
multiplicative-expression:
cast-expression:
unary-expression:
postfix-expression:
primary-expression:
identifier
constant
string-literal
( expression )
generic-selection

所以,line 5 中的 assignment-expression 其实是 b

1
2
3
const char c       // declarator
= // =
b; // initializer -> assignment-expression -> -> .... -> identifier

在初始化当中,

image-20250826225221876

规则就和 simple assignment 类似,其中 initializer 取标量类型的 unqualified 版本作为其声明类型。

手册没说聚合类型怎么办,虽然实际上都是推广了的。也有可能是我找错位置了。

我们可以把 char * 赋值给 const char *

1
2
3
const char *a;
char *b;
a = b; // OK

其成立的理由在于,它两侧的类型符合 Assignment 表达式 的要求。

part1 part2

此处适应的条件是第三个:

左操作数有 atomic, qualified or not 的指针类型(也就是任意指针),而两个操作数都指向了 或者是 qualified,或者是 unqualified 版本的 compatible types,其中:左操作数指向的类型拥有右操作数指向的类型拥有的全部 qualifier。

我们依次来看:

  • 左操作数是指针类型。Good
  • 左操作数指向了 const qualified 的 char 类型;右操作数指向了 unqualified 的 char 类型。char-char 之间 compatible。且 {const} 包含空集。Good

因而这个赋值表达式的约束成立。

再来看主角:

1
2
3
const char **a;
char **b;
a = b; // FAIL
  • 左操作数是指针类型。Good
  • 左操作数指向了 unqualified 的 const char * 类型;右操作数指向了 unqualified 的 char * 类型。上文提到,const char *char * 并不 compatible。Bad

即为这段代码编译不过的形式原因。

char *const-qualified 类型是 char *const。所以可以:

1
2
3
char *const *a;
char **b;
a = b; // OK