汉扬编程 C语言入门 C语言以语法简洁著称,那为什么还要额外提供“->”运算符?

C语言以语法简洁著称,那为什么还要额外提供“->”运算符?

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

”运算符?\”>

struct test *x;x.member = 1; // 非法x->member = 1; // 合法C语言为何要有“->”运算符?

”运算符?\”>

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

”运算符?\”>

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

”运算符?\”>

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

”运算符?\”>

CRM 所描述的C语言在许多方面都与现代C语言有很大的不同

”运算符?\”>

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).da->b->c->d 它们的功能是一致的,但是第二种写法无论是书写,还是阅读,都要简洁的多。

欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

未经许可,禁止转载。

本文来自网络,不代表汉扬编程立场,转载请注明出处:http://www.hyzlch.com/cjia/6592.html

C语言main函数必不可少,一起来了解C语言main函数

C语言中什么是变量?如何定义变量?

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

返回顶部