点(.
)运算符用于访问 struct 的成员,而C中的箭头运算符(->
)用于访问相关指针引用的 struct 的成员.
指针本身没有任何可以通过点运算符访问的成员(它实际上只是一个描述虚拟内存中某个位置的数字,因此它没有任何成员).因此,如果我们只定义点运算符,以便在指针上使用指针时自动取消对指针的引用(编译器在编译时afaik知道这一信息),那么就不会有歧义.
那么,为什么语言创造者决定通过添加这个看似不必要的操作符来让事情变得更复杂呢?什么是重大的设计决策?
点(.
)运算符用于访问 struct 的成员,而C中的箭头运算符(->
)用于访问相关指针引用的 struct 的成员.
指针本身没有任何可以通过点运算符访问的成员(它实际上只是一个描述虚拟内存中某个位置的数字,因此它没有任何成员).因此,如果我们只定义点运算符,以便在指针上使用指针时自动取消对指针的引用(编译器在编译时afaik知道这一信息),那么就不会有歧义.
那么,为什么语言创造者决定通过添加这个看似不必要的操作符来让事情变得更复杂呢?什么是重大的设计决策?
我会将您的问题解释为两个问题:1)为什么->
甚至存在,2)为什么.
不会自动取消对指针的引用.这两个问题的答案都有历史根源.
Why does 100 even exist?
在C语言的最早版本之一(我将其称为CRM for"C Reference Manual",它于1975年5月随第6版Unix一起发布)中,运算符->
具有非常独特的含义,而不是*
和.
组合的同义词
CRM所描述的C语言在许多方面与现代C语言有很大的不同.在CRM struct 中,成员实现了byte offset的全局概念,它可以添加到任何地址值,没有类型限制.即所有 struct 成员的所有名称都具有独立的全局意义(因此必须是唯一的).例如,您可以声明
struct S {
int a;
int b;
};
名称a
代表偏移量0,而名称b
代表偏移量2(假设int
类型的大小为2且没有填充).该语言要求翻译单元中所有 struct 的所有成员具有唯一的名称或代表相同的偏移量值.例如,在同一翻译单元中,您可以另外声明
struct X {
int a;
int x;
};
这也没关系,因为名称a
始终代表偏移量0.但是这个附加声明
struct Y {
int b;
int a;
};
将在形式上无效,因为它试图将a
重新定义为偏移量2,将b
重新定义为偏移量0.
这就是->
操作员的作用.由于每个struct成员名称都有自己的自给自足的全局含义,因此该语言支持以下表达式
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
值".即,上述将在地址7
处分配42
到int
的值.请注意,使用->
并不关心左侧表达式的类型.左侧被解释为右值数字地址(无论是指针还是整数).
这种诡计在*
和.
的组合中是不可能的.你做不到
(*i).b = 42;
因为*i
已经是一个无效的表达式.*
运算符与.
运算符分开,因此对其操作数的类型要求更严格.为了提供绕过此限制的能力,CRM引入了->
运算符,它独立于左侧操作数的类型.
正如Keith在 comments 中指出的,->
和*
+.
组合之间的这种差异就是CRM在7.1.8:Except for the relaxation of the requirement that 103 be of pointer type, the expression 104 is exactly equivalent to 105中提到的"放松要求"
后来,在K&;R C CRM中最初描述的许多功能都经过了重大修改." struct 成员作为全局偏移标识符"的 idea 被完全删除.->
运算符的功能与*
和.
组合的功能完全相同.
Why can't 100 dereference the pointer automatically?
同样,在该语言的CRM版本中,要求.
运算符的左操作数为lvalue.这就是对该操作数施加的only要求(如上所述,这就是它与->
的不同之处).请注意,CRM not要求左操作数.
具有 struct 类型.它只要求它是一个左值,any个左值.这意味着在C的CRM版本中,您可以编写如下代码
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
在这种情况下,编译器会将55
值写入连续存储器挡路中位于字节偏移量2处的int
值(称为c
),即使TYPE struct T
没有名为b
的字段也是如此.编译器根本不会关心c
的实际类型.它所关心的是c
是一个左值:某种可写存储器挡路.
现在请注意,如果你这样做了
S *s;
...
s.b = 42;
代码将被认为是有效的(因为s
也是一个左值),编译器只需try 在字节偏移量2处写入数据into the pointer 100 itself.不用说,这样的事情很容易导致内存溢出,但语言本身并不关心这些问题.
也就是说,在该语言版本中,你提出的为指针类型重载运算符.
的 idea 是行不通的:当运算符.
与指针(左值指针或任何左值)一起使用时,它已经有了非常具体的含义.毫无疑问,这是非常奇怪的功能.但当时它就在那里.
当然,这种奇怪的功能并不是反对在C-K&;的修改版本中为指针引入重载.
运算符(正如您所建议的)的一个非常有力的理由;R C.但还没有完成.也许在那个时候,有一些用C的CRM版本编写的遗留代码需要支持.
(1975年C版参考手册的URL可能不稳定.另一份可能有细微差别的是here.)