C是什么样的语言

在Donald C. Gause 和Gerald M. Weinberg 合著的《你的灯亮着吗?》一书中,有这样一节。

某计算机制造商开发了一种新型打印机。技术小组在如何保证打印精度的问题上非常苦恼,每次进行新的测试时,工程师都不得不花很长的时间测量打印机的输出结果来追求精确性。
丹(Dan Daring)是这个小组中最年轻但或许是最聪明的工程师。 他发明了一种工具,即每隔8 英寸就在铝条上嵌上小针。使用这个工具,可以很快地找到打印机输出位置的误差。
这个发明显著地提高了生产效率,丹的上司非常高兴,提议给丹颁发一个公司的特别奖赏。他从车间里拿了这个工具,带回办公室,这样他写报告的时候还可以仔细地研究一下。
这个上司显然还用不惯这个工具,当他把这个工具放在桌子上 的时候,将针尖朝上了。更不幸的是,当丹的上司的上司友好地坐 到桌角上,打算谈谈给丹颁发奖励时,部门内的所有人都听到了他 痛苦的尖叫声——他的屁股上被扎了两个相距8 英寸的孔。

C 语言就恰如这个工具。也就是说,它是一门

  • 为了解决眼前问题,由开发现场的人发明的,
  • 虽然使用方便,
  • 但看上去不怎么顺眼,
  • 如果不熟悉的人糊里糊涂地使用了它,难免会带来“悲剧”的语言。

C 的发展历程

众所周知,C 原本是为了开发UNIX 操作系统而设计的语言。

如此说来,好像C 应该比UNIX 更早问世,可惜事实并非如此,最早的 UNIX 是用汇编来写的。因为厌倦了总是苦哈哈地使用汇编语言进行编程,UNIX 的开发者Ken Tompson 开发了一种称为“B”的语言。B 语言是1967 年剑桥大学的Martin Richard 开发的BCPL(Basic CPL)的精简版本。BCPL 的前身是1963 年剑桥大学和伦敦大学共同研究开发的CPL(Combined Programming Lanugage) 语言。

B 语言不直接生成机器码,而是由编译器生成栈式机(Stack Machine)用 的中间代码,中间代码通过解释器(interpreter)执行(类似Java 和早期的 Pascal)。因此,B 语言的执行效率非常低,结果,在后来的UNIX 开发过程中人们放弃了使用B 语言。

在这之后的1971 年,Ken Tompson 的同事Dennis Ritchie 对B 语言做了改良,追加了char 数据类型,并且让B 语言可以直接生成PDP-11的机器代码。曾经在很短的时间内,大家将这门语言称为NB(New B)。

之后,NB 改称为C 语言——这就是C 语言的诞生。后来,主要是为了满足使用UNIX 的程序员的需要,C 语言一边接受来自各方面的建议,一边摸着石头过河般地进行着周而复始的功能扩展。1978 年出版了被称为C 语言宝典的The C Programming Language 一书。此书取了两位作者(Brian Kernighan 和Dennis Ritchie)的姓氏首字母, 简称为K&R。在后面提到的ANSI 标准制定之前,此书一直作为C 语言语法 的参考书被人们广泛使用。听说这本书在最初发行的时候,Prentice-Hall 出版社制订了对于当时存在 的130 个UNIX站点平均每个能卖9 本的销售计划(相比Lift With UNIX[2])。当然了,哪怕是初版K&R的销售量,也以3 位数的数量级超过了 Prentice-Hall 出版社最初的销售计划 。原本只是像“丹的工具”一样为了满足 自用的C 语言,历经坎坷,最终成为全世界广泛使用的开发语言。

顺便提一下,通过http://www.cs.bell-labs.com/cm/cs/cbook/index.html可以看到K&R 的网页,各语种的K&R 封面排列在一起,颇为壮观。

为什么存在奇怪的指针运算

如果试图访问数组的内容,老老实实地使用下标就可以了。为什么存在指针运算这样奇怪的功能呢?

其中的一个原因就是受到了C的祖先B语言的影响。

B是一种“没有类型”的语言。B中可以使用的类型只有word 型(也就是整型),指针也是作为整型来使用的(像浮点型这样高级的事物,你根本见不到)。B 是虚拟机上运行的解释器,这个虚拟机以word 为单位分配内存地址(如今普通的计算机以字节为单位)。由于B以word 为单位,如果指针(仅仅是表现地址的简单的整数)加1,指针就指向数组的下一个元素。为了继承这种特性,C引入了“指针加1,指针前进它所指向类型的长度”这个规则。B 语言中同样存在p[i]是 (p + i)的语法糖这样的规则。可是,这里的(p + i)只不过是单纯的整数之间的加法运算。解引用*、地址运算符&,也以几乎和C相同的形态存在于B语言中。另外还有一个理由就是,早先使用指针运算可以写出高效的程序。通常情况下,我们总是使用循环语句来处理数组,一般都写成下面的形式,

for (i = 0; i < LOOP_MAX; i++) {
/*
* 在这里,使用array[i]进行各种各样的处理。
* array[i]会出现多次。
*/
}

array[i]在循环中会出现多次,每次都要进行相当于 array+i的加法运算,效率自然比较低。
因此,可以使用指针运算重写上面这段循环,

for (p = &array[0]; p != &array[LOOP_MAX],p++) {
/*
* 在这里,使用*p进行各种各样的处理。
* *p会出现多次。
*/
}

尽管*p在循环内部会出现多次,但加法运算只有在循环结束的时候执行一次。

K&R p.119 中叙述了“一般情况下,使用指针的程序比较高效”。上面的说明应该可以作为这段叙述的根据吧。可是,这些无论怎样都是老黄历了。

如今,编译器在不断地被优化,对于循环内部重复出现的表达式的集中处理,是编译器优化的基本内容。对于现在一般的C 编译器,无论你使用数组还是指针,效率上都不会出现明显的差距。基本上都是输出完全相同的机器码。

总的来说,C 的指针运算功能的出现,源自于早期的C自身没有优化手段。这一点并不奇怪,请大家回想一下在前面介绍过的内容,C本来只是为了解决开发现场的人们眼前的问题而出现的一种语言。Unix 之前的OS几乎都是使用汇编写的,即使晦涩难懂,人们也不会大惊小怪。对于当时的环境,追求什么编译器优化实在有点勉为其难。因此,当初开发C 语言的时候,是完全有必要提供指针运算功能的。可是……

不要滥用指针运算

被称为C语言宝典的K&R 指出:“一般情况下,使用指针的程序比较高效。”这完全是“那个时代的错误”。

可是,正如前面所说,对于如今的编译器,无论是使用指针运算还是下标运算,都生成几乎完全相同的执行代码。

事到如今……难道不应该放弃使用指针运算,老老实实地使用下标访问吗?

虽然K&R 被很多人奉为“神书”,可是对于我来说,它连作为菜鸟实习的资料也不够格。为什么这么说?因为在此书中,那些滥用指针的例程完全可以让你崩溃。

莫名其妙地使用像*++args[0]这样的语句,并且乐此不疲,实在让人心烦。

K&R 里面记载了下面这个作为strcpy()实现的例子:

/* 将t拷贝成s;指针版3 */
void strcpy(char *s, char *t)
{
while (*s++ = *t++)
;
}

虽然乍一看不容易理解,但是这种写法其实是非常方便的。因为会在C 程序中经常遇到,所以我们应该掌握这种惯用写法。既然知道“乍一看不容易理解”,那就不应该这样写,难道不是吗?

满大街的C 语言入门书都在教育我们,使用指针运算比使用下标会让程序

  • ☑更有效率
  • ☑更有C 语言范儿

所谓的“更有效率”,只不过是臆想罢了。对于这种“微不足道的”优化工作,与其让人去小心翼翼地做,还不如交给编译器来干。

所谓“更有C语言范儿”好像是有些道理。如果只是为了要让程序“有范儿”,而让代码变得晦涩难懂,那么还是拜托你行行好,扔掉这种恶习吧。

在学校里,我们要完成一些课后作业。好不容易完成了一个使用下标的程序题,不料后面的那道题为“请使用指针将刚才那道题的程序重新完成一遍”。这种事常有吧。

老实说,这种事很无聊。也许你会很“威武”地依然使用下标原封不动
地把程序又写了一遍,然后交给了老师。面对老师的指责,你义正辞严:

咦,下标运算符[]只不过是指针运算的语法糖而已,在本质上这样的写法也是在使用指针啊。

尽管这样,这位可爱的老师可能还是不会放过你,于是你就急了:

行,不就是把像p[i]这样使用下标的地方,机械地一个个替换成* (p+i)嘛。

话说回来,丢了学分,我可不负责哟。在C的世界里,使用指针运算要比使用下标的写法让人感觉更“帅一些”。

但是……与其在这些无聊的地方“耍酷”,倒不如多花点时间学一些有用的知识。你要知道,作为一个程序员,还有堆积如山的知识等着你去掌握呢。

当然,什么样的规则都有例外,比如,在“一个巨大的char 数组中,参杂了各种类型的数据①,并且我们试图读取第多少字节的数据”这样的情况下,还是使用指针运算写的程序比较容易理解。

此外,作为一个C程序员连指针运算的代码也读不懂,多少有点可悲。

尽管如此,让我们至少从现在开始尽量使用下标来写新的程序,这样做对自己,以及对以后有机会阅读你的程序的人,都有好处。

作者前桥和弥(Maebasi Kazuya),1969年出生,著有《彻底掌握C语言》、《Java之谜和陷阱》、《自己设计编程语言》等。其一针见血的“毒舌”文风和对编程语言深刻的见地受到广大读者的欢迎。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值