我将您的问题解释为两个问题:1)为什么.甚至存在,以及2)为什么.不会自动取消引用指针。 这两个问题的答案都有历史根源。
为什么.甚至存在?
在C语言的最初版本之一(我将其称为“C参考手册”的CRM,与1975年5月的第6版Unix一起提供)中,运算符.具有非常独特的含义,而不是.和c组合的同义词
CRM描述的C语言在很多方面与现代C语言截然不同。 在CRM结构中,成员实现了字节偏移的全局概念,可以将其添加到任何地址值,而不受类型限制。即 所有结构成员的所有名称都具有独立的全局含义(因此,必须是唯一的)。 例如,您可以声明
struct S {
int a;
int b;
};
并且名称.将代表偏移0,而名称.代表偏移2(假设c类型的大小为2且没有填充)。 语言要求翻译单元中所有结构的所有成员都具有唯一的名称或代表相同的偏移值。 例如。 在同一个翻译单元中,您还可以声明
struct X {
int a;
int x;
};
那没关系,因为名称.一直代表偏移0.但是这个额外的声明
struct Y {
int b;
int a;
};
将正式无效,因为它试图将.“重新定义”为偏移量2和.作为偏移量0。
这就是.运算符的用武之地。由于每个结构成员名称都有自己的自足全局含义,语言支持这些表达式
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
编译器将第一个赋值解释为“获取地址.,向其添加偏移量.并将c分配给结果地址处的struct T值”。即 以上将在地址c分配b至c值。请注意,->的这种使用并不关心左侧的表达式类型。 左侧被解释为右值数字地址(无论是指针还是整数)。
.和.组合无法实现这种技巧。 你做不到
(*i).b = 42;
因为.已经是一个无效的表达式。 .运算符由于与c分开,因此对其操作数施加了更严格的类型要求。 为了提供解决此限制的能力,CRM引入了struct T运算符,该运算符独立于左侧操作数的类型。
正如Keith在评论中指出的那样,.与. + c组合之间的差异是CRM在7.1.8中所称的“放宽要求”:除了放宽.指针类型的要求外,表达式 .完全相同于c
后来,在K& R C中,最初在CRM中描述的许多特征被显着地重新设计。 完全删除了“struct member as global offset identifier”的想法。 并且.运营商的功能与.和c组合的功能完全相同。
为什么.无法自动取消引用指针?
同样,在该语言的CRM版本中,.运算符的左操作数必须是左值。 这是对该操作数施加的唯一要求(这就是它与.不同的原因,如上所述)。 请注意,CRM不要求c的左操作数具有结构类型。 它只需要它是一个左值,任何左值。 这意味着在C的CRM版本中,您可以编写这样的代码
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
在这种情况下,编译器会将.写入.值,该值位于连续内存块(位于c)的字节偏移量2处,即使类型struct T没有名为b的字段。编译器根本不关心c的实际类型。 所有它关心的是c是一个左值:某种可写的内存块。
现在请注意,如果你这样做了
S *s;
...
s.b = 42;
代码被认为是有效的(因为.也是一个左值),编译器只是尝试将数据写入指针.本身,在字节偏移量2.不用说,这样的事情很容易导致内存溢出,但是 这种语言并不涉及这种问题。
即 在该语言版本中,您提出的关于为指针类型重载运算符.的想法不起作用:运算符.在与指针一起使用时已经具有非常具体的含义(使用左值指针或任何左值)。 毫无疑问,这是非常奇怪的功能。 但它当时就在那里。
当然,这种奇怪的功能并不是在C-K& R C的重新设计版本中引入重载.运算符(如你所建议的)的一个非常强大的原因。但它还没有完成。 也许当时有一些必须支持的CRM版C编写的遗留代码。
(1975 C参考手册的URL可能不稳定。另一个副本,可能有一些细微差别,在这里。)