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