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

12 浏览
0 Comments

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

点(.)运算符用于访问结构的成员,而在C语言中,箭头(->)运算符用于访问由指针引用的结构的成员。\n指针本身没有任何可以使用点运算符访问的成员(实际上它只是描述虚拟内存中位置的数字,所以没有任何成员)。因此,如果我们只定义点运算符在指针上自动解引用,那么就不会产生歧义(编译器在编译时已经知道这个信息)。\n那么为什么语言创建者决定通过添加这个看似不必要的运算符来使事情变得更加复杂呢?这是一个重大的设计决策。

0
0 Comments

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

C语言在不产生任何歧义的同时也做得很好。当然,点运算符也可以被重载以表示两种含义,但箭头运算符确保程序员知道他们正在操作一个指针,就像编译器不允许您混合两种不兼容类型一样。

这是简单而正确的答案。在我看来,C语言大多数情况下都试图避免重载,这是C语言中最好的事情之一。

C语言中有很多模糊和歧义的地方。存在隐式类型转换,数学运算符被重载,链式索引根据索引的是多维数组还是指针数组而产生完全不同的结果,任何东西都可能是隐藏的宏(大写命名约定在这方面有帮助,但C语言并不是完全如此)。

按照这种推理,为什么还要有箭头运算符呢?如果必须使用(*a).b来访问结构体的内容,它会更加确保程序员知道他们正在操作一个指针。

(*a).b没有同样的意义,因为当我们想获取b的值时,我们不需要/不想解引用a

解决方法:

在C语言中,为了避免歧义和混淆,引入了箭头(->)运算符。箭头运算符明确表示操作的是指针,而不是普通的点运算符可能会产生的多种含义。这样,程序员在操作指针时就不会犯错误。

例如,如果我们有一个指向结构体的指针a,并且想要访问结构体中的成员变量b,我们可以使用箭头运算符:a->b。这样,我们就可以直接访问b,而无需解引用指针。

如果没有箭头运算符,我们将不得不使用间接访问运算符和点运算符的组合:(*a).b。这样的语法不仅更加繁琐,还容易出错。

因此,箭头运算符的存在使得C语言更加清晰和易于理解,同时也提高了代码的可读性和可维护性。

0
0 Comments

为什么C语言中存在箭头(->)操作符?

除了历史原因之外,操作符的优先级也是一个小问题:点操作符的优先级高于星号操作符。因此,如果你有一个包含指向结构体的指针的结构体,这两个表达式是等价的:

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

但是第二个表达式显然更易读。箭头操作符与点操作符具有相同的优先级,并且从左到右关联。我认为这比在指向结构体和结构体的指针上都使用点操作符更清晰,因为我们可以从表达式中知道类型,而不必查看声明,这甚至可能在另一个文件中。

对于包含结构体和指向结构体的嵌套数据类型,这可能会使事情变得更加困难,因为你必须考虑为每个子成员访问选择正确的操作符。你可能最终会得到a.b->c->d或a->b.c->d(我在使用freetype库时遇到了这个问题-我一直需要查看它的源代码)。此外,这并不能解释为什么在处理指针时编译器不能自动解引用指针。

我不认为这比所有的括号更难理解,但可能只是个人口味问题。无论如何,并没有必要有一个原因,几乎语言中的每个东西都可以用另一种方式实现,我只是试图解释为什么这个操作符是有用的。并不是所有的东西都是严格必需的,我们甚至可以在没有switch或for的情况下生活,但它们是有用的。

尽管你陈述的事实是正确的,但它们并没有以任何方式回答我的原始问题。你解释了a->和*(a)表示相等(这已经在其他问题中解释了多次),并模糊地陈述了语言设计有些随意。我觉得你的回答没有帮助,所以我给了你一个负分。

我的观点并不是两种不同形式的相等性,而是箭头操作符的可能优势,而你的问题是关于为什么它被添加到语言中的。但是,如果你正在寻找一个经过验证的历史原因,是的,我的回答不能提供这个。不管怎样,感谢你回来解释你的决定。

回答中的确有一些相关部分,但还是有点主观。我在理解问题时太晚了。

这可能对于Java程序员来说是正确的,但是C/C++程序员会将a.b.c.d和a->b->c->d视为两个非常不同的东西:第一个是对嵌套子对象的单个内存访问(在这种情况下只有一个内存对象),第二个是三个内存访问,通过四个可能不同的对象追踪指针。这在内存布局上有很大的区别,我认为C在非常明显地区分这两种情况时是正确的。

尽管我与你对Java程序员的侮辱相同意见,但你的论点并不是一个很好的论点。例如,对于a + b的情况,如果a和b是int或float,性能上有很大的差别,尤其是在没有FPU的情况下。C应该对int加法和float加法进行语法上的区分吗?关于禁止整数提升,因为它是一个隐藏的mov指令,你怎么看?这里的关键是,a.b可以根据a是否为指针来完成工作。这里没有歧义。

而且,从区分中你并没有获得任何好处。如果你写了a.b,并且编译器给出一个错误,说a是一个指针,你会突然改变主意,因为a->b更昂贵,并重新构造你的代码,也许编写一个宏来避免向函数传递指针或通过值传递结构体?还是你只是将a.b更改为a->b并重新编译?

我并不是说这是对Java程序员的侮辱,他们只是习惯于一种完全隐式指针的语言。如果我被培养成Java程序员,我可能会以相同的方式思考...无论如何,我实际上认为我们在C中看到的操作符重载不太理想。然而,我承认我们都被数学家们给惯坏了,他们几乎对所有东西都自由地重载他们的操作符。我也理解他们的动机,因为可用的符号集相当有限。我猜,最后这只是一个画线的问题...

当你解引用指针时,你会获得一点安全性,你需要确保你不会解引用空指针。只要a完全形成(初始化),a.b.c.d就能保证成功。如果a,b,c中的任何一个为空,a->b->c->d就会导致段错误。或者a->b.c->d将告诉你你正在进行间接内存访问的位置。

(*(*(*a).b).c).d是完全荒谬的。这就是为什么每个避免使用->的人都使用a[0].b[0].c[0].d。至少我是这样听说的。:-D(嗯,他们也可以使用0[a].0[b].0[c].d作为替代)。

对我来说,它们两个看起来都很傻,因为数组语法如果没有数组就会产生误导,并且我不明白为什么任何人都应该避免箭头操作符。此外,0[a]的表示法除了荒谬之外,还由于操作符优先级的原因无法工作。

Rust编程语言允许你在嵌套结构体上执行a.b.c.d.e,甚至不需要解引用操作符*

0
0 Comments

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

我将您的问题理解为两个问题:1) 为什么`->`运算符存在,2) 为什么`.`运算符不会自动解引用指针。对这两个问题的回答都有历史根源。

为什么`->`运算符存在?

在C语言的早期版本(我将其称为CRM,即《C参考手册》),`->`运算符具有独特的含义,并不等同于`*`和`.`的组合。

CRM描述的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。

这就是`->`运算符的用途。由于每个结构体成员名称都有自己独立的全局含义,该语言支持如下表达式:

int i = 5;
i->b = 42;  // 将42写入地址为7的`int`值
100->a = 0; // 将0写入地址为100的`int`值

编译器会将第一个赋值解释为“获取地址为5,添加偏移量为2,并将42分配给结果地址处的`int`值”。也就是说,上述代码会将42分配给地址为7的`int`值。请注意,此处使用的`->`与左侧表达式的类型无关。左侧将解释为rvalue数值地址(无论是指针还是整数)。

而这种技巧在`*`和`.`的组合中是不可能的。您不能这样做:

(*i).b = 42;

因为`*i`已经是无效的表达式。`*`运算符由于与`.`分离,对其操作数施加了更严格的类型要求。为了解决这个限制,CRM引入了`->`运算符,该运算符独立于左操作数的类型。

正如Keith在评论中指出的那样,CRM将`->`和`*`+`.`的组合之间的差异称为“放宽要求”(7.1.8):除了要求`E1`是指针类型之外,表达式`E1->MOS`与`(*E1).MOS`完全等效

后来,在K&R C中,许多在CRM中描述的特性进行了重大改写。"结构体成员作为全局偏移标识符"的概念完全被移除。而`->`运算符的功能与`*`和`.`的组合完全相同。

为什么`.`运算符不能自动解引用指针?

再次强调,在CRM版本的语言中,`.`运算符的左操作数必须是一个lvalue。这是对该操作数的唯一要求(也是它与`->`不同的地方,如上所述)。需要注意的是,CRM并不要求`.`的左操作数具有结构体类型,只要求它是一个lvalue,任何类型的lvalue都可以。这意味着在CRM版本的C语言中,您可以编写以下代码:

struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;

在这种情况下,编译器会将55写入位于连续内存块`c`中偏移量为2的`int`值,即使类型`struct T`没有名为`b`的字段。编译器根本不关心`c`的实际类型,它只关心`c`是一个lvalue:一种可写的内存块。

现在请注意,如果您执行以下操作:

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

代码将被视为有效(因为`s`也是一个lvalue),编译器将简单地尝试将数据写入指针`s`本身,位于偏移量2处。不用说,这样的操作很容易导致内存溢出,但是该语言并不关心这样的问题。

也就是说,在该语言的那个版本中,您提出的关于为指针类型重载`.`运算符的想法不起作用:当与左值指针或任何左值一起使用时,`.`运算符已经有了非常具体的含义。这确实是一种非常奇怪的功能,毫无疑问。但在当时,它确实存在。

当然,这种奇怪的功能并不是不引入针对指针重载`.`运算符的一个很强的理由(正如您所建议的),在重新设计的C语言(K&R C)中也没有这样做。也许在当时,有一些使用CRM版本C编写的遗留代码需要得到支持。

1975年的C参考手册的URL可能不稳定。另一个副本(可能有一些细微的差异)在此处。

0