点 ( .
) 运算符用于访问结构的成员,而->
C 中的箭头运算符 () 用于访问由相关指针引用的结构的成员。
指针本身没有任何可以使用点运算符访问的成员(它实际上只是一个描述虚拟内存中位置的数字,因此它没有任何成员)。因此,如果我们只定义点运算符以在指针上使用它时自动取消引用指针(编译器在编译时知道的信息 afaik),则不会有歧义。
那么为什么语言创建者决定通过添加这个看似不必要的运算符来使事情变得更复杂呢?什么是重大设计决策?
点 ( .
) 运算符用于访问结构的成员,而->
C 中的箭头运算符 () 用于访问由相关指针引用的结构的成员。
指针本身没有任何可以使用点运算符访问的成员(它实际上只是一个描述虚拟内存中位置的数字,因此它没有任何成员)。因此,如果我们只定义点运算符以在指针上使用它时自动取消引用指针(编译器在编译时知道的信息 afaik),则不会有歧义。
那么为什么语言创建者决定通过添加这个看似不必要的运算符来使事情变得更复杂呢?什么是重大设计决策?
我会将您的问题解释为两个问题:1)为什么->
甚至存在,以及 2)为什么.
不自动取消引用指针。这两个问题的答案都有历史根源。
为什么->
甚至存在?
在 C 语言的最早版本之一(我将其称为“ C 参考手册”的 CRM,它于 1975 年 5 月随第 6 版 Unix 一起提供)中,运算符->
具有非常独特的含义,而不是同义词*
和.
组合
CRM 描述的 C 语言在许多方面与现代 C 语言有很大不同。在 CRM 结构成员中实现了字节偏移的全局概念,它可以添加到任何地址值,没有类型限制。即所有结构成员的所有名称都具有独立的全局含义(因此,必须是唯一的)。例如,您可以声明
struct S {
int a;
int b;
};
namea
代表偏移量 0,而 nameb
代表偏移量 2(假设int
类型为大小 2 且没有填充)。该语言要求翻译单元中所有结构的所有成员要么具有唯一的名称,要么代表相同的偏移值。例如,在同一个翻译单元中,您可以另外声明
struct X {
int a;
int x;
};
没关系,因为名称a
始终代表偏移量 0。但是这个附加声明
struct Y {
int b;
int a;
};
将正式无效,因为它试图“重新定义”a
为偏移量 2 和b
偏移量 0。
这就是->
运算符的用武之地。由于每个结构成员名称都有自己自给自足的全局含义,因此语言支持这样的表达式
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
编译器将第一个赋值解释为“获取地址5
,为其添加偏移量2
并分配42
给int
结果地址处的值”。即上面将分配42
给int
地址的值7
。请注意,这种使用->
不关心左侧表达式的类型。左侧被解释为右值数字地址(无论是指针还是整数)。
这种诡计是不可能*
和.
组合在一起的。你做不到
(*i).b = 42;
因为*i
已经是一个无效的表达式。*
运算符,因为它与 分开,所以对其.
操作数施加了更严格的类型要求。为了提供解决此限制的能力,CRM 引入了->
运算符,它独立于左侧操作数的类型。
正如 Keith 在评论中指出的那样,->
and *
+.
组合之间的这种差异是 CRM 在 7.1.8 中所说的“放宽要求”:除了放宽E1
指针类型的要求外,表达式E1−>MOS
完全等同于(*E1).MOS
后来,在 K&R C 中,最初在 CRM 中描述的许多功能都进行了重大修改。“结构成员作为全局偏移标识符”的想法被完全删除。并且操作符的功能与组合->
的功能完全一致。*
.
为什么不能.
自动取消引用指针?
同样,在语言的 CRM 版本中,运算符的左操作数.
必须是左值。这是对该操作数施加的唯一要求(这就是使它与 不同的原因->
,如上所述)。请注意,CRM不要求 的左操作数.
具有结构类型。它只要求它是一个左值,任何左值。这意味着在 C 的 CRM 版本中,您可以编写这样的代码
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
在这种情况下,编译器将写入位于称为 的连续内存块中位于字节偏移 2 处55
的值,即使 type没有名为 的字段。编译器根本不关心 的实际类型。它所关心的只是一个左值:某种可写的内存块。int
c
struct T
b
c
c
现在请注意,如果您这样做了
S *s;
...
s.b = 42;
代码将被认为是有效的(因为s
也是左值),编译器会简单地尝试将数据写入指针s
本身,字节偏移量为 2。不用说,这样的事情很容易导致内存溢出,但是语言不关心这些问题。
即在该语言版本中,您提出的关于.
为指针类型重载运算符的想法将不起作用:运算符.
在与指针一起使用时已经具有非常特定的含义(使用左值指针或根本没有任何左值)。毫无疑问,这是非常奇怪的功能。但它当时就在那里。
当然,这种奇怪的功能并不是反对.
在 C - K&R C 的重新设计版本中引入指针重载运算符(如您所建议的那样)的一个强有力的理由。但它还没有完成。也许当时有一些用 C 的 CRM 版本编写的遗留代码必须得到支持。
(1975 C 参考手册的 URL 可能不稳定。另一个副本,可能有一些细微的差别,在这里。)
除了历史(好的和已经报道的)原因之外,运算符优先级也存在一个小问题:点运算符的优先级高于星号运算符,因此如果您的结构包含指向结构的指针,则包含指向结构的指针......这两个是等价的:
(*(*(*a).b).c).d
a->b->c->d
但第二个显然更具可读性。箭头运算符具有最高优先级(就像点一样)并从左到右关联。我认为这比对结构和结构的指针都使用点运算符更清楚,因为我们可以从表达式中知道类型,而不必查看声明,甚至可以在另一个文件中。
C 在不使任何含糊不清方面也做得很好。
当然,点可能被重载以表示这两种情况,但箭头确保程序员知道他正在对指针进行操作,就像编译器不允许您混合两种不兼容的类型一样。