5650|6

1170

帖子

0

TA的资源

至上芯片

楼主
 

周末看闲书 [复制链接]

C:穿越时空的迷雾 C诡异离奇,缺陷重重,却获得了巨大的成功。 —— Dennis Ritchie 1.1 C语言的史前阶段 听上去有些荒谬,C语言的产生竟然源于一个失败的项目。1969年,通用电气、麻省理工学院和贝尔实验室联合创立了一个庞大的项目——Multics工程。该项目的目的是创建一个操作系统,但显然遇到了麻烦:它不但无法交付原先所承诺的快速而便捷的在线系统,甚至连一点有用的东西都没有弄出来。虽然开发小组最终勉强让Multics开动起来,但他们还是陷入了泥淖,就像IBM在OS/360上面一样。他们试图建立一个非常巨大的操作系统,能够应用于规模很小的硬件系统中。Multics成了总结工程教训的宝库,但它同时也为C语言体现“小即是美”铺平了道路。 当心灰意冷的贝尔实验室的专家们撤离Multics工程后,他们又去寻找其他任务。其中一位名叫Ken Thompson的研究人员对另一个操作系统很感兴趣,他为此好几次向贝尔管理层提议,但均遭否决。在等待官方批准时,Thompson和他的同事Dennis Ritchie自娱自乐,把Thompson的“太空旅行”软件移植到不太常用的PDP-7系统上。太空旅行软件模拟太阳系的主要星体,把它们显示在图形屏幕上,并创建了一架航天飞机,它能够飞行并降落到各个行星上。与此同时,Thompson加紧工作,为PDP-7编写了一个简易的新型操作系统。它比Multics简单得多,也轻便得多。整个系统都是用汇编语言编写的。Brian Kernighan在1970年给它取名为UNIX,自嘲地总结了从Multics中获得的那些不应该做的教训。图1-1描述了早期C、UNIX和相关硬件系统的关系。 图1-1 早期C、UNIX和相关的硬件系统 是先有C语言还是先有UNIX呢?说起这个问题,人们很容易陷入先有鸡还是先有蛋的套套中。确切地说,UNIX比C语言出现得早(这也是为什么UNIX的系统时间是从1970年1月1日起按秒计算的,它就是那时候产生的啊)。然而,我们这里讨论的不是家禽趣闻,而是编程故事。用汇编语言编写UNIX显得很笨拙,在编制数据结构时浪费了大量的时间,而且系统难以调试,理解起来也很困难。Thompson想利用高级语言的一些优点,但又不想像PL/I[1]那样效率低下,也不想碰见在Multics中曾遇到过的复杂问题。在用Fortran进行了一番简短而又不成功的尝试之后,Thompson创建了B语言,他把用于研究的语言BCPL[2]作了简化,使B的解释器能常驻于PDP-7只有8KB大小的内存中。B语言从来不曾真正成功过,因为硬件系统的内存限制,它只允许放置解释器,而不是编译器,由此产生的低效阻碍了使用B语言进行UNIX自身的系统编程。 软件信条 编译器设计者的金科玉律:效率(几乎)就是一切 在编译器中,效率几乎就是一切。当然还有一些其他需要关心的东西,如有意义的错误信息、良好的文档和产品支持。但与用户需要的速度相比,这些因素就黯然失色了。编译器的效率包括两个方面:运行效率(代码的运行速度)和编译效率(产生可执行代码的速度)。除了一些开发和学习环境之外,运行效率起决定性作用。 有很多编译优化措施会延长编译时间,但却能缩短运行时间。还有一些优化措施(如清除无用代码和忽略运行时检查等)既能缩短编译时间,又能减少运行时间,同时还能减少内存的使用量。这些优化措施的不利之处在于可能无法发现程序中无效的运行结果。优化措施本身在转换代码时是非常谨慎的,但如果程序员编写了无效的代码(如:越过数组边界引用对象,因为他们“知道”附近有他们需要的变量)就可能引发错误的结果。 这就是为什么说效率几乎就是一切但也并不是绝对的道理。如果得到的结果是不正确的,那么效率再高又有什么意义呢?编译器设计者通常会提供一些编译器选项。这样,每个程序员可以选择自己想要的优化措施。B语言不算成功,而Dennis Ritchie所创造的注重效率的“New B”却获得了成功,充分证明了编译器设计者的这条金科玉律。 B语言通过省略一些特性(如嵌套过程和一些循环结构),对BCPL语言作了简化,并发扬了“引用数组元素相当于对指针加上偏移量的引用”这个想法。B语言同时保持了BCPL语言无类型这个特点,它仅有的操作数就是机器的字。Thomposon发明了++和--操作符,并把它加入到PDP-7的B编译器中。它们在C语言中依然存在,很多人天真地以为这是由于PDP-11存在对应的自动增/减地址模型,这种想法是错误的!自动增/减机制的出现早于PDP-11硬件系统的出现。尽管在C语言中,拷贝字符串中的一个字符的语句: *p++ = *s++; 可以极其有效地被编译为PDP-11代码: moveb (r0)+, (r1)+ 这使得许多人错误地以为前者的语句形式是根据后者特意设计的。 当1970年开发平台转移到PDP-11以后,无类型语言很快就显得不合时宜了。这种处理器以硬件支持几种不同长度的数据类型为特色,而B语言无法表达不同的数据类型。效率也是一个问题,这也迫使Thompson在PDP-11上重新用汇编语言实现了UNIX。Dennis Ritchie利用PDP-11的强大性能,创立了能够同时解决多种数据类型和效率的“New B”(这个名字很快变成了“C”)语言,它采用了编译模式而不是解释模式,并引入了类型系统,每个变量在使用前必须先声明。
此帖出自单片机论坛

最新回复

可以增长知识  详情 回复 发表于 2008-1-25 15:42
点赞 关注
 

回复
举报

1170

帖子

0

TA的资源

至上芯片

沙发
 

回复: 周末看闲书

1.2 C语言的早期体验 增加类型系统的主要目的是帮助编译器设计者区分新型PDP-11机器所拥有的不同数据类型,如单精度浮点数、双精度浮点数和字符等。这与其他一些语言如Pascal形成了鲜明的对比。在Pascal中,类型系统的目的是保护程序员,防止他们在数据上进行无效的操作。由于设计哲学不同,C语言排斥强类型,它允许程序员需要时可以在不同类型的对象间赋值。类型系统的加入可以说是事后诸葛,从未在可用性方面进行过认真的评估和严格的测试。时至今日,许多C程序员仍然认为“强类型”只不过是增加了敲击键盘的无用功。 除了类型系统之外,C语言的许多其他特性是为了方便编译器设计者而建立的(为什么不呢?开始几年C语言的主要客户就是那些编译器设计者啊)。根据编译器设计者的思路而发展形成的语言特性有: · 数组下标从0而不是1开始。绝大多数人习惯从1而不是0开始计数。编译器设计者则选择从0开始,因为偏移量的概念在他们心中已是根深蒂固。但这种设计让一般人感觉很别扭。尽管我们定义了一个数组a[100],你可千万别往a[100]里存储数据,因为这个数组的合法范围是从a[0]到a[99]。 · C语言的基本数据类型直接与底层硬件相对应。例如,不像Fortran,C语言中不存在内置的复数类型。某种语言要素如果底层硬件没有提供直接的支持,那么编译器设计者就不会在它上面浪费任何精力。C语言一开始并不支持浮点类型,直到硬件系统能够直接支持浮点数之后才增加了对它的支持。 · auto关键字显然是摆设。这个关键字只对创建符号表入口的编译器设计者有意义。它的意思是“在进入程序块时自动进行内存分配”(与全局静态分配或在堆上动态分配相反)。其他程序员不必操心auto这个关键字,它是缺省的变量内存分配模式。 · 表达式中的数组名可以看作是指针。把数组当作指针,简化了很多东西。我们不再需要一种复杂的机制区分它们,把它们传递到一个函数时不必忍受必须复制所有数组内容的低效率。不过,数组和指针并不是在任何情况下都是等效的,更详细的讨论参见第4章。 · float被自动扩展为double。尽管在ANSI C中情况不再如此,但最初浮点数常量的精度都是double型的,所有表达式中float变量总被自动转换成double。这样做的理由从未公诸于众,但它与PDP-11中浮点数的硬件表示方式有关。首先,在PDP-11或VAX中,从float转换到double代价非常小,只要在后面增加一个每个位均为0的字即可。如果要转换回来,去掉第二个字就可以了。其次,要知道在某些PDP-11的浮点数硬件表示形式中有一个运算模式位(mode bit),你可以只进行float的运算,也可以只进行double的运算,但如果想在这两种方式间进行切换,就必须修改这个位来改变运算模式。在早期的UNIX程序中,float用得不是太多,所以把运算模式固定为double 是比较方便的,省得编译器设计者去跟踪它的变化。 · 不允许嵌套函数(函数内部包含另一个函数的定义)。这简化了编译器,并稍微提高了C程序的运行时组织结构。具体的机理在第6章“运动的诗章:运行时数据结构”中详细描述。 · register关键字。这个关键字能给编译器设计者提供线索,就是程序中的哪些变量属于热门(经常被使用),这样就可以把它们存放到寄存器中。这个设计可以说是一个失误,如果让编译器在使用各个变量时自动处理寄存器的分配工作,显然比一经声明就把这类变量在生命期内始终保留在寄存器里要好。使用register关键字,简化了编译器,却把包袱丢给了程序员。 为了C编译器设计者的方便而建立的其他语言特性还有很多。这本身不是一件坏事,它大大简化了C语言本身,而且通过回避一些复杂的语言要素(如Ada中的泛型和任务,PL/I中的字符串处理,C++中的模板和多重继承),C语言更容易学习和实现,而且效率非常高。 和其他大多数语言不同,C语言有一个漫长的进化过程。在目前这个形式之前,它经历了许多中间状态。它历经多年,从一个实用工具进化为一种经过大量试验和测试的语言。第一个C编译器大约出现在1970年,距今20多年了[3]。时光荏苒,作为它的根基的UNIX系统得到了广泛使用,C语言也随之茁壮成长。它对直接由硬件支持的底层操作的强调,带来了极高的效率和移植性,反过来也帮助UNIX获得了巨大的成功。
此帖出自单片机论坛
 
 

回复

1170

帖子

0

TA的资源

至上芯片

板凳
 

回复: 周末看闲书

1.3 标准I/O库和C预处理器 C编译器不曾实现的一些功能必须通过其他途径实现。在C语言中,它们在运行时进行处理,既可以出现在应用程序代码中,也可以出现在运行时函数库(runtime library)中。在许多其他语言中,编译器会植入一些代码,隐式地调用运行时支持工具,这样程序员就无须操心它们了。但在C语言中,绝大多数库函数或辅助程序都需要显式调用。例如,在C语言中(必要时),程序员必须管理动态内存的使用,创建各种大小的数组,测试数组边界,并自己进行范围检测。 与此类似,C语言原先并没有定义I/O,而是由库函数提供。后来,这实际上成了标准机制。可移植的I/O由Mike Lesk编写,最初出现在1972年左右,可在当时存在的3个平台上通用。实践经验表明,它的性能低于预期值。所以,人们对它又进行了优化和裁剪,后来成为标准I/O函数库。 C预处理器大约也是在这个时候被加入的,倡议者是Alan Snyder。它所实现的3个主要功能是: · 字符串替换:形式类似“把所有的foo替换为baz”,通常用于为常量提供一个符号名。 · 头文件包含(这是在BCPL中首创的):一般性的声明可以被分离到头文件中,并且可以被许多源文件使用。虽然约定采用“.h”作为头文件的扩展名,但在头文件和包含实现代码的对象库之间在命名上却没有相应的约定,这多少令人不快。 · 通用代码模板的扩展。与函数不同,宏(marco)在连续几个调用中所接收的参数的类型可以不同(宏的实际参数只是按照原样输出)。这个特性的加入比前两个稍晚,而且多少显得有些笨拙。在宏的扩展中,空格会对扩展的结果造成很大的影响。 #define a(y) a_expanded(y) a(x); 被扩展为: a_expanded(x); 而: #define a (y) a_expanded (y) a(x); 则被扩展为: (y) a_expanded (y)(x) 它们所表示的意思风马牛不相及。你可能会以为在宏里面使用花括号就像在C语言的其他部分一样,能把多条语句组合成一条复合语句,但实际上并非如此。 这里对C语言的预处理器并不作太多的讨论。这反映了这样一个观点:对于宏这样的预处理器,只应该适量使用,所以无须深入讨论。C++在这方面引入了一些新的方法,使得预处理器几乎无用武之地。 软件信条 C并非Algol 70年代后期,Steve Bourne在贝尔实验室编写UNIX第7版的shell(命令解释器)时,决定采用C预处理器使C语言看上去更像Algol-68。早年在英国剑桥大学时,Steve曾编写过一个Algol-68编译器。他发现如果代码中有显式的“结束语句”提示,诸如if ... fi或者case ... esac等,调试起来会更容易。Steve认为仅仅一个“}”是不够的,因此他建立了许多预处理定义: #define STRING char * #define IF if( #define THEN ){ #define ELSE }else( #define FI ;} #define WHILE while( #define DO ){ #define OD ;} #define INT int #define BEGIN { #define END } 这样,他就可以像下面这样编写代码: INT compare(s1, s2) STRING s1; STRING s2; BEGIN WHILE *s1++ == *s2 DO IF *s2++ == 0 THEN return(0); FI OD return(*--s1 - *s2); END 再看一下相应的C代码: int compare(s1, s2) char *s1, *s2; { while(*s1++ == *s2){ if(*s2++ == 0) return(0); } return (*--s1 - *s2); } Bourne shell的影响远远超出了贝尔实验室的范围,这也使得这种类似Algol-68的C语言变型名声大噪。但是,有些C程序员对此感到不满。他们抱怨这种记法使别人难以维护代码。时至今日,BSD 4.3 Bourne shell(保存于/bin/sh)依然是这种记法写的。 我有一个特别的理由反对Bourne Shell,在我的书桌上堆满了针对它的Bug报告!我把它们发给Sam,我们都发现了这样的Bug:这个shell不使用malloc,而是使用sbrk自行负责堆存储的管理。在维护这类软件时,每解决两个问题通常又会引入一个新问题。Steve解释说他之所以采用这种特制的内存分配器,是为了提高字符串处理的效率,他从来不曾想到其他人会阅读他的代码。 Bourne创立的这种C语言变型事实上促成了异想天开的国际C语言混乱代码大赛(The International Obfuscated C Code Competition),比赛要求参赛的程序员尽可能地编写神秘而混乱的程序来压倒对手(关于这个比赛,以后还有更详尽的说明)。 宏最好只用于命名常量,并为一些适当的结构提供简捷的记法。宏名应该大写,这样便很容易与函数调用区分开来。千万不要使用C预处理器来修改语言的基础结构,因为这样一来C语言就不再是C语言了。
此帖出自单片机论坛
 
 
 

回复

1170

帖子

0

TA的资源

至上芯片

4
 

回复: 周末看闲书

1.4 K&R C 到了20世纪70年代中期,C语言已经很接近目前这种我们所知道和喜爱的形式了。更多的改进仍然存在,但大部分都只是一些细节的变化(比如允许函数返回结构值)和一些对基本类型进行扩展以适应新的硬件变化的改进。(比如增加关键字unsigned和long)。1978年,Steve Johnson编写了pcc这个可移植的C编译器。它的源代码对贝尔实验室之外开放,并被广泛移植,形成了整整一代C编译器的基础。C语言的演化之路如图1-2所示。 图1-2 后期的C 软件信条 一个非比寻常的Bug C语言从Algol-68中继承了一个特性,就是复合赋值符。它允许对一个重复出现的操作数只写一次而不是两次,给代码生成器一个提示,即操作数寻址也可以类似地紧凑。这方面的一个例子是用b+=3作为b=b+3的缩写。复合赋值符最初的写法是先写赋值符,再写操作符,就像:b=+3。在B语言的词法分析器里有一个技巧,使实现=op这种形式要比实现目前所使用的op=形式更简单一些。但这种形式会引起混淆,它很容易把 b=-3; /* 从b中减去3 */ 和 b= -3; /* 把-3赋给b */ 搞混淆。 因此,这个特性被修改为目前所使用的这种形式。作为修改的一部分,代码格式器程序indent也作了相应修改,用于确定复合赋值符的过时形式,并交换两者的位置,把它转换为对应的标准形式。这是个非常糟糕的决定,任何格式器都不应该修改程序中除空白之外的任何东西。令人不快的是,这种做法会引入一个Bug,就是几乎任何东西(只要不是变量),如果它出现在赋值符后面,就会与赋值符交换位置。 如果你运气好,这个Bug可能会引起语法错误,如: epsilon=.0001; 会被交换成: epsilon.=0001; 这条语句将无法通过编译器,你马上就能发现错误。但一条源语句也可能是这样的: valve=!open; /*valve被设置为open的逻辑反*/ 会悄无声息地交换成: valve!=open; /*valve与open进行不相等比较*/ 这条语句同样能够通过编译,但它的作用与源语句明显不同,它并不改变valve的值。 在后面这种情况下,这个Bug会潜伏下来,并不会被马上检测到。在赋值后面加个空格是很自然的事,所以随着复合赋值符的过时形式越来越罕见,人们也逐渐忘记了indent程序曾经被用于“改进”这种过时的形式。这个由indent程序引起的 Bug直到20世纪80年代中期才在各种C编译器中销声匿迹。这是一个应被坚决摒弃的东西! 1978年,C语言经典名著The C Programming Language出版了。这本书受到了广泛的赞誉,其作者Brian Kernighan和Dennis Ritchie也因此名声大噪,所以这个版本的C语言就被称为“K&R C”。出版商最初估计这本书将售出1000册左右。截止到1994年,这本书大约售出了150万册(参见图1-3)。C语言成为最近20年最成功的编程语言之一,可能就是最成功的。但随着C语言的广泛流行,许多人试图从C语言中产生其他变种。 图1-3 像猫王艾尔维斯一样,C语言无处不在
此帖出自单片机论坛
 
 
 

回复

3138

帖子

0

TA的资源

裸片初长成(初级)

5
 

油泥克思比马克思还牛!

书好, 闲书更好, 看闲书更更好, 有看闲书的时间更更更好, 可惜,俺已经没了看闲书的功夫, 所以,不好…… :L
此帖出自单片机论坛
 
 
 

回复

10

帖子

0

TA的资源

一粒金砂(初级)

6
 

回复:周末看闲书

精彩精彩!
此帖出自单片机论坛
 
 
 

回复

22

帖子

0

TA的资源

一粒金砂(初级)

7
 

回复:周末看闲书

可以增长知识
此帖出自单片机论坛
 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/6 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表