基本上,每一个C语言程序员都明白点运算符“.”和箭头运算符“-”可以用于访问结构体的成员,只不过箭头运算符“-”需要与结构体指针结合使用。事实上按照现在流行的C语言语法,通过结构体指针直接访问成员,也只能通过箭头运算符。
structtest*x
;x.member=1;//非法
x-member=1;//合法
C语言为何要有“-gt;”运算符?C语言为何要有“-”运算符?
抛开结构体不谈,C语言中的指针本身并无需要用到点运算符“.”的地方,因此结构体指针与点运算符“.”结合时,编译器把这种结合解释为访问结构体成员,按理说并不会产生歧义,C语言以语法简洁闻名,那为什么还要提供“多余”的“-”运算符呢?或者说,C语言中的箭头运算符“-”有什么历史渊源吗?
上述问题其实可以简化成两个子问题,一是为什么C语言要有“-”运算符,再就是为什么C语言中的“.”运算符不能与结构体指针结合访问成员。
C语言“-”运算符的历史
其实,在C语言的第一个版本(相关C参考手册(CReferenceManual,CRM)在年5月随第6版Unix一起发布)中,“-”运算符并不像今天一样与“.”运算符同义,而是另有一种特有的含义。
CRM所描述的C语言在许多方面都与现代CCRM所描述的C语言在许多方面都与现代C语言有很大的不同,例如CRM的结构体成员实现了全局字节偏移的概念,没有类型限制,可以访问任意地址。也就是说,当时的C语言中,所有的结构体成员的名字都具有独立的全局含义,因此所有结构体的成员名都不能一样。
structS{inta;intb;};上面这几行C语言代码定义了结构体S,成员a代表0偏移,而成员b则代表2字节偏移(这里假设int变量占用2字节内存,也不考虑内存对齐)。
当时C语言做了这样的限制:所有结构体的所有成员,要么有唯一的名字,要么代表唯一的字节偏移量,例如:
structX{
inta;
intx;
};
上述代码定义了结构体X,它也包含成员a,它的名字与结构体S中的成员a重复了,但是没有问题,因为它们都代表0偏移。下面这种定义就属于非法了:
structY{
intb;
inta;
};
因为结构体Y中的成员a与结构体S中的成员a重名,并且代表的字节偏移量也不相等。
早期箭头运算符“-gt;”是用于确定偏移量在当时的C语言语法中,箭头运算符“-”就是用于确定偏移量的。既然每个结构体的成员代表的字节偏移量都是全局的,那么下面这样的语句也是合法的:
inti=5;i-b=42;-a=0;上述几行C语言代码的意义很明确:i-b表示以5为基准的2字节偏移处,因此i-b=42;的意思是将地址7处的int值设置为42。同样的道理,-a=0;则表示将地址处的int值设置为0。
读者应注意,在当时版本的C语言中,箭头运算符“-”并不关心它的左表达式,因此哪怕-a也是合法的。
箭头运算符“-gt;”并不关心它的左表达式这样利用结构体成员偏移量的做法对于“*”和“.”运算符的组合是不可用的,例如
inti=5;
(*i).b=42;
*i本身就是一个无效的表达式,“*”是一个独立的运算符,因此对其操作数施加了更加严格的类型要求。当时CRM引入箭头运算符“-”就是用于解决这种限制带来的不便的。
后来,在KR设计的C语言中,许多CRM中的功能被重新设计,“结构体成员作为全局偏移标识符”的设计被完全推翻,此后箭头运算符“-”的功能与“*”和“.”运算符结合的功能完全相同。
为什么C语言不支持“.”运算符与结构体指针结合访问成员?
同样,在CRM描述的C语言中,“.”运算符的左操作数被要求必须是一个左值,这也是它与“-”运算符不同的原因,如上所述。请注意,CRM不需要“.”运算符的左操作数是结构体类型的,它只要求左操作数是左值。
这里读者应该区分“左操作数”和“左值”。
应该区分“左操作数”和“左值”这意味着在CRM版本的C语言中,程序员可以编写下面这样的代码:
structS{
inta,b;
};
structT{
floatx,y,z;
};
structTc;
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语言中,对“.”运算符重载(使其支持通过结构体指针访问成员)根本就行不通,因为“.”运算符与指针结合时,已经具备自己的含义了(与左值结合,访问指定偏移量的内存)。虽然以今天的眼光来看,这样的设计很古怪,但是当时的确就是这样设计的。
当然了,这样的奇怪设计并不是“.”运算符不能与结构体指针结合使用访问成员的充足理由,但是后来KR在重新设计C语言时没有考虑重载“.”运算符,应该是需要兼容之前版本的C语言,毕竟历史遗留下来的C语言代码也是需要得到支持的。
最后
可能也有读者认为,即使是今天的C语言,似乎“-”运算符也不是必须的,因为“*”和“.”运算符结合就能轻易的代替它:
structS*p;
p-b=3;//完全可以使用下面这样的语句替换
(*p).b=3;
既然简洁是C语言的特点,就应该做到极致,何必提供“多余的”箭头运算符“-”呢?的确如此,就功能性而言,“-”完全可以不要,但是在C语言程序开发中,我们还需要考虑程序员的感受,请看下面这两种写法:
(*(*(*a).b).c).d
a-b-c-d
它们的功能是一致的,但是第二种写法无论是书写,还是阅读,都要简洁的多。
欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就