为什么C中的箭头(->)运算符存在?


264

点(.)运算符用于访问结构的成员,而->C语言中的箭头运算符()用于访问所讨论的指针引用的结构的成员。

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

那么,为什么语言创建者决定通过添加此看似不必要的运算符来使事情变得更复杂?重大的设计决定是什么?


1
相关:stackoverflow.com/questions/221346/…- 另外,您可以覆盖->
Krease 2012年

16
@Chris那是关于C ++的,这当然有很大的不同。但是,由于我们正在谈论C 为什么如此设计,因此,我们假装我们回到1970年代-C ++出现之前。
Mysticial

5
我最好的猜测是,存在箭头运算符以可视方式表示“观看它!您在这里处理指针”
克里斯

4
乍一看,我觉得这个问题很奇怪。并非所有事物都经过精心设计。如果您一生都保持这种风格,那么您的世界将充满疑问。得票最多的答案确实是翔实而明确的。但这并没有解决您问题的关键。按照您的提问方式,我可以问太多问题。例如,关键字“ int”是“ integer”的缩写;为什么关键字“ double”也不短?
junwanghe

1
@junwanghe这个问题实际上代表有效的关注点-为什么.运算符的优先级比*运算符高?如果没有,我们可以拥有* ptr.member和var.member。
milleniumbug

Answers:


358

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

为什么->甚至存在?

在C语言的第一个版本(1975年5月第6版Unix附带的C语言,我称为CRM 参考手册)中,运算符->具有非常排他的含义,不是同义词*.组合

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

struct S {
  int a;
  int b;
};

并且name a代表偏移量0,而name 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并分配42int结果地址中的值”。即上述内容将分配42intaddress的值7。注意这种用法->并不关心左侧表达式的类型。左侧被解释为右值数字地址(可以是指针或整数)。

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

(*i).b = 42;

以来 *i已经是一个无效的表达式。由于*运算符与分开.,因此对其操作数施加了更严格的类型要求。为了提供解决此限制的功能,CRM引入了->运算符,该运算符与左侧操作数的类型无关。

正如Keith在评论中所指出的,+ ->*+ .组合之间的区别就是CRM在7.1.8中称为“放松要求”:除了放宽E1指针类型的需求外,该表达式E1−>MOS完全等于(*E1).MOS

后来,在K&R C中,对CRM中最初描述的许多功能进行了重新设计。完全删除了“结构成员作为全局偏移量标识符”的想法。和功能->运营商成为了的功能完全一致*.组合。

为什么不能.自动取消引用指针?

同样,在该语言的CRM版本中,.要求运算符的左操作数为lvalue。那是对该操作数施加的唯一要求(这就是使它不同于->,如上所述)的原因。需要注意的是CRM并没有要求的左操作数.有一个结构类型。它只是要求它是一个左值,任何左值。这意味着在C的CRM版本中,您可以编写如下代码

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

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

现在请注意,如果您这样做

S *s;
...
s.b = 42;

该代码将被视为有效(因为s它也是一个左值),并且编译器将仅尝试将数据写入指针s本身字节偏移量2。不用说,类似的事情很容易导致内存溢出,但是这种语言并不关心这类事情。

即在该语言的该版本中,您对重载运算符的建议想法 .指针类型将不起作用:.与指针(与左值指针或任何左值一起使用)时,运算符已经具有非常特殊的含义。毫无疑问,这是非常奇怪的功能。但是当时在那里。

当然,这种怪异的功能并不是引入超载的强烈理由 .在重新设计的C-K&R C版本中指针操作符的(如您所建议的那样),但尚未完成。也许那时是必须要支持一些用CRM版本C编写的遗留代码。

(《 1975 C参考手册》的URL可能不稳定。这里可能有一些细微差别,这是另一份副本。)


10
引用的C参考手册的第7.1.8节说:“除了放松对E1为指针类型的要求外,表达式“ E1-> MOS”与“(* E1).MOS”完全等效。 '。”
基思·汤普森

1
为什么*i在地址5处没有默认类型的左值(int?)?然后(* i).b会以相同的方式工作。
Random832

5
@Leo:好吧,有些人喜欢C语言作为高级汇编程序。在C历史的那个时期,该语言实际上确实是高级汇编程序。
AnT 2012年

29
嗯 因此,这解释了为什么UNIX中的许多结构(例如struct stat)会为其字段加上前缀(例如st_mode)。
icktoofay

5
@ perfectionm1ng:看起来bell-labs.com已被阿尔卡特朗讯接管,原始页面已消失。我更新了指向另一个站点的链接,尽管我不能说该站点将保持多长时间。无论如何,谷歌搜索“ ritchie c参考手册”通常会找到该文档。
AnT

46

除了历史原因(良好且已报告的原因)之外,运算符优先级还存在一个小问题:点运算符的优先级高于星形运算符,因此,如果您的结构包含指向结构的指针,则包含指向结构的指针...这两个等效:

(*(*(*a).b).c).d

a->b->c->d

但是第二点显然更具可读性。箭头运算符具有最高优先级(仅作为点),并且从左到右关联。我认为这比将点运算符都用于指向struct和struct的指针更清楚,因为我们从表达式中知道类型,而不必查看声明,该声明甚至可以位于另一个文件中。


2
使用同时包含结构和结构指针的嵌套数据类型,这会使事情变得更加困难,因为您必须考虑为每个子成员访问选择正确的运算符。您可能最终会得到ab-> c-> d或a-> bc-> d(我在使用freetype库时遇到了这个问题-我需要一直查看它的源代码)。这也不能解释为什么在处理指针时不能让编译器自动取消引用指针的原因。
Askaga 2012年

3
尽管您所陈述的事实是正确的,但它们并不能以任何方式回答我的原始问题。您解释了a->和*(a)的相等性。符号(在其他问题中已经多次解释),以及对语言设计的含糊说法有些随意。我认为您的回答不是很有帮助,因此请投反对票。
Askaga 2012年

16
@ effeffe,OP表示该语言很容易解释a.b.c.d(*(*(*a).b).c).d,从而使->操作员无用。因此,OP的版本(a.b.c.d)与相比具有同等可读性a->b->c->d。这就是为什么您的答案没有回答OP的问题的原因。
Shahbaz 2013年

4
@Shahbaz这可以是用于Java程序员的情况下,C / C ++程序员将理解a.b.c.da->b->c->d作为两个非常不同的事情:首先是单个存储器访问到嵌套子对象(只有在这种情况下,单个存储对象),第二个是三个内存访问,通过四个可能不同的对象追逐指针。那是内存布局上的巨大差异,我相信C非常明显地区分这两种情况是正确的。
cmaster-恢复莫妮卡

2
@Shahbaz我并不是说作为Java程序员的侮辱,他们只是习惯于具有完全隐式指针的语言。如果我是一名Java程序员长大的,我可能会以同样的方式思考...无论如何,实际上,我认为我们在C语言中看到的运算符重载并不是最优的。但是,我承认,我们所有人都被数学家宠坏了,他们为几乎所有事情自由地重载了运算符。我也了解它们的动机,因为可用符号的集合非常有限。我想,最终这只是问题所在……
cmaster-恢复莫妮卡17-10-17

19

C在不使任何歧义方面也做得很好。

当然,点可以重载以表示两者的含义,但是箭头可确保程序员知道他正在对指针进行操作,就像编译器不允许您混合使用两种不兼容的类型一样。


4
这是简单正确的答案。Ç大多尝试避免过载,其IMO是约C.最好的事情之一
jforberg

10
C语言中的很多东西都是模棱两可和模糊的。存在隐式类型转换,数学运算符被重载,链式索引的作用完全不同,这取决于您是在对多维数组还是对指针数组进行索引,而任何东西都可能是隐藏任何东西的宏(大写命名约定对此有所帮助,但是C可以做到) t)。
PSkocik
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.