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