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

Ask*_*aga 253 c pointers dereference

dot(.)运算符用于访问结构的成员,而->C中的箭头运算符()用于访问由有问题的指针引用的结构的成员.

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

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

AnT*_*AnT 346

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

为什么->甚至存在?

在C语言的最初版本之一(我将其称为" C参考手册 "的CRM ,它与1975年5月的第6版Unix一起提供)中,运算符->具有非常独特的含义,而不是同义词*.组合

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

struct S {
  int a;
  int b;
};
Run Code Online (Sandbox Code Playgroud)

并且name a代表偏移量0,而name b代表偏移量2(假设int类型大小为2且没有填充).语言要求翻译单元中所有结构的所有成员都具有唯一的名称或代表相同的偏移值.例如,在同一个翻译单元中,您可以另外声明

struct X {
  int a;
  int x;
};
Run Code Online (Sandbox Code Playgroud)

那没关系,因为这个名字a一直代表偏移0.但是这个额外的声明

struct Y {
  int b;
  int a;
};
Run Code Online (Sandbox Code Playgroud)

将正式无效,因为它试图"重新定义" a为偏移量2和b偏移量0.

这就是->运算符的用武之地.由于每个结构成员名称都有自己的自足全局含义,因此语言支持这些表达式

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
Run Code Online (Sandbox Code Playgroud)

第一分配是由编译器解释为"取地址5,偏移添加2到它并分配42int在所得到的地址值".即上面将分配42int地址的值7.请注意,这种使用->并不关心左侧表达式的类型.左侧被解释为右值数字地址(无论是指针还是整数).

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

(*i).b = 42;
Run Code Online (Sandbox Code Playgroud)

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

正如Keith在评论中指出的那样,->*+ .组合之间的区别是CRM在7.1.8中所指的"放宽要求":除了放宽E1指针类型的要求外,表达式E1?>MOS完全等同于(*E1).MOS

后来,在K&R C中,最初在CRM中描述的许多功能都进行了重大修改.完全删除了"struct member as global offset identifier"的想法.功能,和->运营商成为了的功能完全一致*.组合.

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

同样,在该语言的CRM版本中,.运算符的左操作数必须是左值.这是对该操作数施加的唯一要求(->正如上面所解释的那样,这使得它与之不同).请注意,CRM 要求左操作数.具有结构类型.它只需要它是一个左值,任何左值.这意味着在C的CRM版本中,您可以编写这样的代码

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

struct T c;
c.b = 55;
Run Code Online (Sandbox Code Playgroud)

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

现在请注意,如果你这样做了

S *s;
...
s.b = 42;
Run Code Online (Sandbox Code Playgroud)

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

即在该语言版本中,您提出的关于重载操作符.指针类型的提议不起作用:.当与指针一起使用时,操作符已经具有非常特定的含义(使用左值指针或任何左值).毫无疑问,这是非常奇怪的功能.但它当时就在那里.

当然,这个奇怪的功能并不是一个非常强大的理由来反对.在C-K&R C的返工版本中引入指针的重载操作符(如你所建议的那样)但是它还没有完成.也许当时有一些必须支持的CRM版C编写的遗留代码.

(1975 C参考手册的URL可能不稳定.另一个副本,可能有一些细微差别,在这里.)

  • 呵呵.所以这解释了为什么UNIX中的许多结构(例如,`struct stat`)为它们的字段添加前缀(例如,`st_mode`). (25认同)
  • 引用的C参考手册的第7.1.8节说:"除了放宽E1为指针类型的要求外,表达式''E1-> MOS''完全等同于''(*E1).MOS' "". (10认同)
  • @Leo:嗯,有些人认为C语言是高级汇编程序.在C历史的那个时期,语言确实是一个更高级别的汇编程序. (5认同)
  • @perfectionm1ng:看起来像bell-labs.com已经被阿尔卡特朗讯收购,原来的网页已经消失了.我更新了到另一个网站的链接,虽然我不能说一个人会熬夜多久.无论如何,谷歌搜索"ritchie c参考手册"通常会找到该文件. (5认同)

eff*_*ffe 41

除了历史(好的和已经报告的)原因之外,运算符优先级也有一点问题:点运算符比star运算符具有更高的优先级,所以如果你有包含struct的指针包含指向struct的指针...这两个是等价的:

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

a->b->c->d
Run Code Online (Sandbox Code Playgroud)

但第二个显然更具可读性.箭头运算符具有最高优先级(就像点一样)并且从左到右关联.我认为这比使用点运算符更容易指向struct和struct,因为我们知道表达式中的类型而不必查看声明,甚至可能在另一个文件中.

  • @effeffe,OP说该语言可以很容易地将`abcd`解释为`(*(*(*a).b).c).d`,使` - >`运算符无用.所以OP的版本(`abcd`)同样可读(与`a-> b-> c-> d`相比).这就是为什么你的答案没有回答OP的问题. (15认同)
  • 虽然您所陈述的事实是正确的,但他们并没有以任何方式回答我原来的问题.你解释了a->和*(a)的相等性.符号(在其他问题中已经多次解释)以及对语言设计的模糊陈述有点武断.我没有找到你的答案非常有帮助,因此downvote. (3认同)
  • @Shahbaz对于java程序员来说可能就是这种情况,C/C++程序员会将`abcd`和`a-> b-> c-> d`理解为两个*非常*不同的东西:第一个是单个内存访问嵌套的子对象(在这种情况下只有一个内存对象),第二个是三个内存访问,通过四个可能不同的对象追逐指针.这是内存布局的一个巨大差异,我相信C正确区分这两种情况是正确的. (3认同)
  • 对于包含结构和指向结构的指针的嵌套数据类型,这可能会使事情变得更加困难,因为您必须考虑为每个子成员访问选择正确的运算符.您可能最终得到ab-> c-> d或a-> bc-> d(我在使用freetype库时遇到此问题 - 我需要一直查找它的源代码).这也无法解释为什么在处理指针时让编译器自动取消引用指针的原因. (2认同)
  • @Shahbaz 我并不是说这是对 Java 程序员的侮辱,他们只是习惯了具有完全隐式指针的语言。如果我是作为一名 Java 程序员长大的,我可能会以同样的方式思考......无论如何,我实际上认为我们在 C 中看到的运算符重载不是最佳的。然而,我承认我们都被数学家宠坏了,他们几乎在所有事情上都让他们的运算符超载。我也理解他们的动机,因为可用符号集相当有限。我想,最后这只是你画线的问题...... (2认同)
  • @Shahbaz 您获得了一些安全性,当您取消引用指针时,您需要确保您没有取消引用空指针。只要 a 完全形成(初始化),就保证 abcd 成功。如果 a、b、c 中的任何一个为空,a->b->c->d 将出现段错误。或者 a->bc->d 会告诉你你在哪里进行间接内存访问。 (2认同)

muk*_*nda 19

C也做得很好,没有做出任何模棱两可的事情.

当然,点可以重载以表示两者,但是箭头确保程序员知道他正在操作指针,就像编译器不会让你混合两个不兼容的类型一样.

  • C中的很多东西都是模棱两可和模糊的.有隐式类型转换,数学运算符被重载,链式索引完全不同,这取决于你是否索引多维数组或指针数组,任何东西都可能是一个隐藏任何东西的宏(大写命名约定有帮助,但C没有' T). (8认同)
  • 这是简单而正确的答案.C主要试图避免超载IMO是关于C的最好的事情之一. (4认同)