2

C语言为什么要有->运算符,有.运算符不就够了吗?

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89-%E8%BF%90%E7%AE%97%E7%AC%A6-%E6%9C%89-%E8%BF%90%E7%AE%97%E7%AC%A6%E4%B8%8D%E5%B0%B1%E5%A4%9F%E4%BA%86%E5%90%97/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

基本上,每一个C语言程序员都明白点运算符“.”和箭头运算符“->”可以用于访问结构体的成员,只不过箭头运算符“->”需要与结构体指针结合使用。事实上按照现在流行的C语言语法,通过结构体指针直接访问成员,也只能通过箭头运算符。

struct test *x;
x.member = 1;   // 非法
x->member = 1; // 合法

C语言为何要有“->”运算符?

抛开结构体不谈,C语言中的指针本身并无需要用到点运算符“.”的地方,因此结构体指针与点运算符“.”结合时,编译器把这种结合解释为访问结构体成员,按理说并不会产生歧义,C语言以语法简洁闻名,那为什么还要提供看似“多余”的“->”运算符呢?或者说,C语言中的箭头运算符“->”有什么历史渊源吗?

上述问题其实可以简化成两个子问题,一是为什么C语言要有“->”运算符,再就是为什么C语言中的“.”运算符不能与结构体指针结合访问成员。

C语言“->”运算符的历史

其实,在C语言的第一个版本(相关C参考手册(C Reference Manual,CRM)在1975年5月随第6版Unix一起发布)中,“->”运算符并不像今天一样与“.”运算符同义,而是有其一种特有的含义。

CRM 所描述的C语言在许多方面都与现代C语言有很大的不同,例如 CRM 的结构体成员实现了全局字节偏移的概念,没有类型限制,可以访问任意地址。也就是说,当时的C语言中,所有的结构体成员的名字都具有独立的全局含义,因此所有结构体的成员名都不能一样。

struct S {
  int a;
  int b;
};

上面这几行C语言代码定义了结构体 S,成员 a 代表 0 偏移,而成员 b 则代表 2 字节偏移(这里假设 int 变量占用 2 字节内存,也不考虑内存对齐)。当时C语言做了这样的限制:所有结构体的所有成员,要么有唯一的名字,要么代表唯一的字节偏移量,例如:

struct X {
  int a;
  int x;
};

上述代码定义了结构体 X,它也包含成员 a,它的名字与结构体 S 中的成员 a 重复了,但是没有问题,因为它们都代表 0 偏移。但是下面这种定义就属于非法了:

struct Y {
  int b;
  int a;
};

因为结构体 Y 中的成员 a 与结构体 S 中的成员 a 重名,并且代表的字节偏移量也不相等。

在当时的C语言语法中,箭头运算符“->”就是用于确定偏移量的。既然每个结构体的成员代表的字节偏移量都是全局的,那么下面这样的语句也是合法的:

int i = 5;
i->b = 42;
100->a = 0;

上述几行C语言代码的意义也很明确:i->b 表示以 5 为基准的 2 字节偏移处,因此 i->b=42; 的意思是将地址 7 处的 int 值设置为 42。同样的到里,100->a=0; 则表示将地址 100 处的 int 值设置为 0。

读者应注意,在当时版本的C语言中,箭头运算符“->”并不关心它的左表达式,因此哪怕 100->a 也是合法的。

这样利用结构体成员偏移量的做法对于“* ”和“.”运算符的组合是不可用的,例如

int i = 5;
(*i).b = 42;

*i 本身就是一个无效的表达式,“* ”是一个独立的运算符,因此对其操作数施加了更加严格的类型要求。当时 CRM 引入箭头运算符“->”就是用于解决这种限制带来的不便的。

后来,在 K&R 设计的C语言中,许多 CRM 中的功能被重新设计,“结构体成员作为全局偏移标识符”的设计被完全推翻,此后箭头运算符“->”的功能与“* ”和“.”运算符结合的功能完全相同。

为什么C语言不支持“.”运算符与结构体指针结合访问成员?

同样,在 CRM 描述的C语言中,“.”运算符的左操作数被要求必须是一个左值,这也是它与“->”运算符不同的原因,如上所述。请注意,CRM 不需要“.”运算符的左操作数是结构体类型的,它只要求左操作数是左值。

这里读者应该区分“左操作数”和“左值”的区别。

这意味着在 CRM 版本的C语言中,程序员可以编写下面这样的代码:

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

struct T c;
c.b = 55;

读者应该注意到结构体 T 并没有成员 b,但是 c.b=55; 却仍然是合法的,编译器不关心变量 c 的类型,它只关心 c 是一个左值:某种可写的内存块。因此 c.b=55; 的意义是将 55 写入名为 c 的连续内存块中字节偏移量 2 处的 int 值中。

因此,如果我们写了下面这样的C语言代码:

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

编译器将认为这样是有效的,因为 s 也是一个左值。最终得到的C语言程序将尝试将 42 写到指针变量 s 本身(而不是它索引的结构体)所在连续内存字节偏移量 2 处。不用说,这样的结果必定会产生预料之外的结果,很可能带来内存溢出,但是编程语言本身并不关心这些事情。

也就是说,在那个版本的C语言中,对“.”运算符重载(使其支持通过结构体指针访问成员)根本就行不通,因为“.”运算符与指针结合时,已经具备自己的含义了(与左值结合,访问指定偏移量的内存)。虽然以今天的眼光来看,当时的设计很古怪,但是当时的确就是那样设计的。

当然了,这样的奇怪设计并不是“.”运算符不能与结构体指针结合使用访问成员的充足理由,但是后来 K&R 在重新设计C语言时没有考虑重载“.”运算符,应该是需要兼容之前版本的C语言,毕竟历史遗留下来的C语言代码也是需要得到支持的。

可能也有读者认为,即使是今天的C语言,似乎“->”运算符也不是必须的,因为“* ”和“.”运算符结合就能轻易的代替它:

struct S *p;
p->b = 3;
// 完全可以使用下面这样的语句替换
(*p).b = 3;

既然简洁是C语言的特点,就应该做到极致,何必提供“多余的”箭头运算符“->”呢?的确如此,就功能性而言,“->”完全可以不要,但是在C语言程序开发中,我们还需要考虑程序员的感受,例如下面两种写法:

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

它们的功能是一致的,但是第二种写法无论是书写,还是阅读,都要简洁的多。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK