关于指针:为什么C中的箭头( – >)运算符存在?

Why does the arrow (->) operator in C exist?

dot(.运算符用于访问结构的成员,而c中的箭头运算符(->用于访问由相关指针引用的结构的成员。

指针本身没有任何可以用点运算符访问的成员(它实际上只是一个描述虚拟内存中某个位置的数字,因此没有任何成员)。因此,如果我们只是定义了点运算符,以便在指针上使用指针时自动取消对指针的引用(编译器在编译时已知的信息)。

那么,为什么语言创建者决定通过添加这个看似不必要的操作符使事情变得更复杂呢?什么是重大设计决策?


我将把你的问题解释为两个问题:1)为什么->甚至存在;2)为什么.不会自动取消指针的引用。这两个问题的答案都有历史渊源。好的。

为什么->甚至存在?好的。

在C语言的第一个版本(我将其称为"C参考手册"的CRM,1975年5月随第6版Unix提供)中,操作人员->具有非常专有的含义,而不是*.组合的同义词。好的。

CRM所描述的C语言在许多方面与现代C语言有很大的不同。在CRM结构中,成员实现了字节偏移量的全局概念,可以将其添加到任何地址值,而不受类型限制。即,所有结构成员的所有名称都具有独立的全局含义(因此必须是唯一的)。例如,您可以声明好的。

1
2
3
4
struct S {
  int a;
  int b;
};

名称a代表偏移量0,而名称b代表偏移量2(假设int类型的大小为2,没有填充)。语言要求翻译单元中所有结构的所有成员都具有唯一的名称或代表相同的偏移值。例如,在同一翻译单元中,您可以另外声明好的。

1
2
3
4
struct X {
  int a;
  int x;
};

这样就可以了,因为名称a始终代表偏移量0。但是这个附加声明好的。

1
2
3
4
struct Y {
  int b;
  int a;
};

因为它试图将a重新定义为偏移量2,将b重新定义为偏移量0,所以在形式上是无效的。好的。

这就是->操作符的切入点。由于每个结构成员名称都有自己的自足全局含义,因此语言支持如下表达式好的。

1
2
3
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转让给地址7int价值。注意,使用->并不关心左侧的表达类型。左侧被解释为右值数字地址(无论是指针还是整数)。好的。

这种欺骗是不可能与*.的组合。你做不到好的。

1
(*i).b = 42;

因为*i已经是一个无效的表达式。由于*运算符与.独立,因此对其操作数施加了更严格的类型要求。为了解决这个限制,CRM引入了->运算符,它独立于左侧操作数的类型。好的。

正如基思在评论中指出的,->*.组合之间的差异,正是CRM在7.1.8中所指的"放宽要求":除了放宽E1为指针类型的要求外,表达式E1?>MOS(*E1).MOS完全相同。好的。

后来,在K&R C中,许多最初在CRM中描述的功能都被显著地修改了。"结构成员作为全局偏移标识符"的想法被完全删除。->操作符的功能与*.组合的功能完全相同。好的。

为什么.不能自动取消对指针的引用?好的。

同样,在语言的CRM版本中,.运算符的左操作数必须是左值。这是对该操作数施加的唯一要求(这也使得它不同于上述的->)。注意,CRM不要求.的左操作数具有结构类型。它只要求它是一个左值,任何左值。这意味着在CRM版本的C中,您可以编写这样的代码好的。

1
2
3
4
5
struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

在这种情况下,编译器会将55写入一个int值,该值位于称为c的连续内存块中的字节偏移量2处,即使类型struct T没有名为b的字段。编译器根本不关心c的实际类型。它只关心c是一个左值:某种可写内存块。好的。

现在注意如果你这样做了好的。

1
2
3
S *s;
...
s.b = 42;

代码被认为是有效的(因为s也是一个左值),编译器只会试图在字节偏移量2处将数据写入指针s本身。不用说,像这样的事情很容易导致记忆溢出,但是语言本身并不关心这些事情。好的。

也就是说,在该语言版本中,对于指针类型重载操作符.的建议不起作用:操作符.在与指针一起使用时(与lvalue指针或任何lvalue一起使用时)已经有了非常具体的含义。毫无疑问,这是非常奇怪的功能。但当时它就在那里。好的。

当然,这种奇怪的功能并不是在C-K&R C的修订版本中引入指针(如您所建议的)的重载.运算符的一个很好的理由,但它还没有完成。也许当时有一些用CRM版本C编写的遗留代码必须得到支持。好的。

(1975 C参考手册的URL可能不稳定。另一份可能有细微差别的副本在这里。)好的。好啊。


除了历史原因(好的和已经报告的)之外,还有一个与运算符优先级有关的小问题:点运算符的优先级高于星运算符,因此,如果您的结构包含指向结构的指针,并且包含指向结构的指针…这两个相当:

1
2
3
(*(*(*a).b).c).d

a->b->c->d

但第二个更清晰易读。箭头运算符具有最高优先级(与点一样),并从左向右关联。我认为对于指向struct和struct的指针,这比使用点运算符更清楚,因为我们从表达式中知道类型,而不必查看声明,甚至可以在另一个文件中。


C也很好地避免了任何模棱两可的事情。

当然,点可以被重载来表示这两种情况,但是箭头确保程序员知道他在指针上操作,就像编译器不会让您混合两种不兼容的类型一样。