首先功能是不一样的,结构体用点,结构体指针用箭头。
我们约定一下符号和名称:用 s, structure 表示一个结构体,p, pointer 表示结构体的指针。又假设有一个成员叫做 member。
为何又要构造两个功能一样的运算符? 为何要构造出这两个功能类似的运算符呢?为什么不都用一个.,指针就写成(*p).member ?
简单的说,就是一个快捷方式,一个语法糖。
但是这两个操作在 C 语言历史上的出现,没有先后(反对 @farseerfc 的说法)。
下面,就要开始考古了。
C 语言最初只是一个名为 UNIX 的操作系统的工具。UNIX 是当今包括 Linux,Android,iOS,macOS 等这些你听过的或者没听过的操作系统的设计原型。
C 语言的发展可以分为:
- 早期 C 语言(1972~1973),出现在了 UNIX 操作系统里,UNIX 此时是第二个版本
- 早期 C 语言(1973~1978),设计出了struct结构体,UNIX 此时是第三个版本
- K&R 一书出现后(1978~1989),有了非正式的规范
- ANSI C 和 ISO C 后(1989~1999),国家级乃至国际规范的出现
- C99(1999~2011)
- C11(2011~现在)
本文主要聚焦在 1 和 2 的交界上。
C 语言在诞生之初只是为 UNIX 操作系统设计的一个工具而已,这时 UNIX 也才是刚出来。刚被创造出来 C 语言,没有 struct 结构体,但是对于现代的程序员来说,这个风格已经非常熟悉了。这是来自 UNIX v2 的,复制文件的cp命令的源代码(许可证见文末,下同)(来自 此处):
main(argc,argv)
char **argv;
{
char buf[512];
int fold, fnew, n;
char *p1, *p2, *bp;
int mode;
if(argc != 3) {
write(1,"Usage: cp oldfile newfile\n",26);
exit();
}
if((fold = open(argv[1],0)) < 0){
write(1,&#34;Cannot open old file.\n&#34;,22);
exit();
}
fstat(fold,buf);
mode = buf[2] & 037;
if((fnew = creat(argv[2],mode)) < 0){
stat(argv[2], buf);
if((buf[3] & 0100) != 0){
p1 = argv[1] - 1;
p2 = argv[2] - 1;
bp = buf - 1;
while(*++bp = *++p2);
*bp = &#39;/&#39;;
p2 = bp;
while(*++bp = *++p1)
if(*bp == &#39;/&#39;)
bp = p2;
if((fnew = creat(buf,mode)) < 0){
write(1,&#34;Cannot creat new file.\n&#34;,23);
exit();
}
}else{
write(1,&#34;Cannot creat new file.\n&#34;,23);
exit();
}
}
while(n = read(fold, buf, 512))
if(n < 0){
write(1,&#34;Read error\n&#34;,11);
exit();
}else
if(write(fnew,buf,n) != n){
write(1,&#34;Write error.\n&#34;,13);
exit();
}
fstat(fnew,buf);
exit();
}不能用结构体的代码,遇到了数组,就各类信息都开一个数组。上面那样复制一下文件还好说,一旦写起算法,代码看起来就很难受。
过了一年,1973 年前后,UNIX v3 的 C 语言编译器中第一次开始出现和处理 struct 的语法,但编译器自身此时还没有用 struct。那么 C 编译器此时是怎么处理和理解新添加的类型 struct 的.和->(仅有的)两种操作呢? 见下面的编译器代码片段(来自 此处):
build(op) {
...
switch (op) {
...
case 39: /* . (structure ref) */
case 50: /* -> (indirect structure ref) */
if (p2[0]!=20 | p2[3]!=4) /* not mos */
error(&#34;Illegal structure ref&#34;);
*cp++ = p1;
t = t2;
if ((t&030) == 030) /* array */
t = decref(t);
setype(p1, t);
if (op==39) /* is &#34;.&#34; */
build(35); /* unary & */
*cp++ = block(1,21,7,0,p2[5]);
build(40); /* + */
if ((t2&030) != 030) /* not array */
build(36); /* unary * */
return;
}
...
}(注:mos 是指 member of structure。)
首先值的注意的是, 39 号操作.和 50 号操作->都是共用这一段处理代码。
build(40);这一行的 40 号操作是加法。往上三行 if(op==39) build(35);这里的 35 号操作是 &。
换句话说:
- 编译器会将p->member变成访问p+offset_member这个内存地址的变量
- 编译器会将s.member变成访问&s+offset_member这个内存地址的变量
这么看来,p->member反而更加直接?
没错,所以,s.member等价于(&s)->member。反之:(*p).member则是(&(*p))->member,即是p->member,解了指针又取指针,绕回来了。
说点题外话
为什么地址是编译器最后需要的?
这反映了 CPU 里寻找一个结构体的成员的地址的本质:一个结构体是一个内存区域,但是真实的情况是程序需要记住这个内存区域的首地址。有了开头的地址,我们就可以加上位移去访问它的第二个成员,第三个成员,……
这和普通的变量是相同的概念:对于一个内存中的变量,比如整数i,运行的时候 CPU 需要知道&i是多少,才能存取i。
设计出 struct 之后的世界……
UNIX v3 开发出了具有 struct 功能的编译器,UNIX 随后的版本里就立刻用上了。1973 年出现的这个语法后来在 1975 年的 UNIX v6 里的文档 C Reference Manual 第七页里出现了:
7.1.7 primary‐lvalue . member‐of‐structure
An lvalue expression followed by a dot followed by the name of a
member of a structure is a primary expression. The object re‐
ferred to by the lvalue is assumed to have the same form as the
structure containing the structure member. The result of the ex‐
pression is an lvalue appropriately offset from the origin of the
given lvalue whose type is that of the named structure member.
The given lvalue is not required to have any particular type.
Structures are discussed in §8.5.
7.1.8 primary‐expression −> member‐of‐structure
The primary‐expression is assumed to be a pointer which points
to an object of the same form as the structure of which the mem‐
ber‐of‐structure is a part. The result is an lvalue appropriate‐
ly offset from the origin of the pointed‐to structure whose type
is that of the named structure member. The type of the primary‐
expression need not in fact be pointer; it is sufficient that it
be a pointer, character, or integer.
Except for the relaxation of the requirement that E1 be of
pointer type, the expression “E1−>MOS” is exactly equivalent to
“(*E1).MOS”.(可见此处)
最后一段说:
primary-expression 对类型不做过多的限定,除此之外 E1->MOS 完全等价于 (*E1).MOS。 这里的“不做过多限定”是指,(当时的)C 语言里p->member的p甚至可以是一个整数类型等。
工程师就喜欢层层封装,结构体正是最典型的封装之一。对于 UNIX 这样一个操作系统工程来说,结构体提供了非常丰富的语义。UNIX v3 之后,UNIX 的几乎各个部分迅速改用 C 语言编写,UNIX 代码仓库的内容也开始爆炸式地丰富起来,这其中或许 struct 的出现也有着不可磨灭的功劳。
(正文完)
附:适用于本文的 UNIX 操作系统源码许可证 |