打印

周末看书休闲

[复制链接]
8355|45
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
dld2|  楼主 | 2008-1-19 14:19 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
原文链接:http://book.csdn.net/bookfiles/617/

书名:C专家编程 作者:[美]Peter Van Der Linden 著 徐波 翻译 来源:人民邮电出版社 出版时间:2008年02月 ISBN:9787115171801/TP 定价:45元 


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”)语言,它采用了编译模式而不是解释模式,并引入了类型系统,每个变量在使用前必须先声明。

沙发
dld2|  楼主 | 2008-1-19 14:22 | 只看该作者

1.2

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获得了巨大的成功。

使用特权

评论回复
板凳
dld2|  楼主 | 2008-1-19 14:29 | 只看该作者

1.3

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语言了。

使用特权

评论回复
地板
dld2|  楼主 | 2008-1-19 14:32 | 只看该作者

1.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语言无处不在

使用特权

评论回复
5
dld2|  楼主 | 2008-1-19 14:42 | 只看该作者

1.5

1.5  今日之ANSI C
到了20世纪80年代初,C语言被业界广泛使用,但存在许多不同的实现和差别。PC的实现者发现了C语言优于BASIC的诸多长处,这一发现更是掀起了C语言的高潮。Mirosoft为IBM PC制作了一个C编译器,引入了几个新的关键字(far, near等)帮助指针处理Intel 80x86芯片不规则的架构。随着其他更多并非基于pcc的编译器的兴起,C语言受到了重复BASIC老路的威胁,也就是可能变成一种多个变种松散相关的语言。

形势渐渐明了,一个正式的语言标准是必需的。幸运的是,在这个领域已经有了相当多的先行者——所有成功的编程语言最终都作了标准化。然而,编写标准手册所存在的问题是:只有当你明白它们讲的是什么,那才是可行的。如果人们用日常语言来编写它们,越想把它们写得精确,就越可能使它们变得冗长、乏味且晦涩。如果用数学概念来定义语言,那么标准手册对于大多数人而言不啻于天书。

多年以来,用于定义编程语言标准的手册变得越来越长,但也越来越容易理解。Algol-60就语言复杂性而言,与C语言不相上下,但它的标准手册——Algol-60 Reference Definition只有18页。Pascal用了35页来描述。Kernighan和Ritchie所作的C语言最初报告用了40页,尽管漏掉了一些东西,但对于许多编译器设计者而言,这些已经足够了。定义ANSI C的手册超过了200页。它部分地对C语言的实际应用作了描述,是对标准文档中有些晦涩文字的补充和说明。

1983年,美国国家标准化组织(ANSI)成立了C语言工作小组,开始了C语言的标准化工作。小组所处理的主要事务是确认C语言的常用特性,但对语言本身也作了一些修改,并引入一些有意义的新特性。对于是否要接受near和far关键字,小组内部进行了旷日持久的争论。最终,它们还是没有被纳入以UNIX为中心的相对谨慎的ANSI C标准。尽管当时世界上大约有5000万台PC,而且它是当时应用范围最广的C语言实现平台,但标准仍然认为(我们认为这是对的)不应该通过修改语言来处理某个特定平台所存在的限制。


  
小 启 发
 
该用哪个版本的C语言呢?
就此而论,任何学习或使用C语言的人都应当使用ANSI C,而不是K&R C。

1989年12月,C语言标准草案最终被ANSI委员会接纳。随后,国际标准化组织ISO也接纳了ANSI C标准(令人不快的是,它删除了非常有用的“Rationale”一节,并作了个虽然很小却让人很恼火的修改,就是把文档的格式和段落编码作了改动)。ISO是一个国际性组织,从技术上讲它更权威一些。所以在1990年初,ANSI重新采纳了ISO C(同样删除了Rationale),取代了原先的版本。因此从原则上说,ANSI所采纳的C语言标准是ISO C,我们日常所说的标准C也应该是ISO C。Rationale这一节是非常有用的,能极大地帮助人们理解标准,它后来作为独立的文档出版。[4]


  
小 启 发
 
哪里能得到C语言标准的一份拷贝
C语言标准的官方名称是:ISO/IEC 9899:1990。ISO/IEC是指国际标准化组织和国际电工组织。标准组织定价$130.00出售C语言标准。在美国,你可以通过给下面的地址写信来获取一份标准的拷贝:

American National Standards Institute

11 West 42nd Street

New York, NY 10036

Tel.(212)642-4900

在美国以外的地区,你可以向下面的地址写信求购:

ISO Sales

Case postale 56

CH-1211 Genève 20

Switzerland

要指明自己想要的是英语版本。

另一个办法是购买Herbert Schildt所著的The Anootated ANSI C Standard(纽约,Osborne McGraw-Hill,1993)。这本书包含一个压缩了版面,但内容完整的C语言标准。Herbert Schildt的书有两个优势,首先是价格,$39.95的定价不到标准定价的三分之一。其次,不像ANSI或ISO,它可能在你当地的书店里就有售,你可以利用20世纪的先进手段,通过电话订购和信用*支付。

实际上,在ISO成立第14工作小组(WG14)制定C标准之前,“ANSI C”这个称呼就已被广泛使用。这并没有什么不妥,因为ISO工作小组把最初标准的技术性完善工作留给了ANSI X3J11委员会。在工作接近尾声时,ISO WG14和X3J11一起通力协作,敲定技术细节并确保最终的标准能被两个组织共同接受。事实上,标准的最终形成又推迟了一年,主要是为了修改标准草案以覆盖一些国际化的问题如宽字符和国际区域问题。

这就使得所有几年来一直关心C语言标准的人们将新的标准当成是ANSI C标准。当语言标准最终形成后,所有人都想支持C语言标准。ANSI C同时是一个欧洲标准(CEN 29899)和X/Open标准。ANSI C被采纳为Federal Information Processing Standard(联邦信息处理标准),取名FIPS160,由国家标准和技术局于1991年3月发布,并于1992年8月24日更新。在C语言上的工作仍在继续——据说有可能在C语言中增加复数类型。

使用特权

评论回复
6
dld2|  楼主 | 2008-1-19 14:46 | 只看该作者

1.6

1.6  它很棒,但它符合标准吗
不要添乱—立即解散ISO工作小组。

——匿名人士

ANSI C标准可以说是非常独特的,我们可以从好几个有趣的方面来说明这一点。它定义了下面一些术语,用于描述某种编译器的特点。如果你对这些术语有一个比较好的了解,就有助于你理解什么东西能被语言接受,什么东西不能被语言接受。前两个术语涉及不可移植的代码(unportable code),接下来的两个术语跟坏代码(bad code)有关,而最后两个术语则跟可移植的代码(portable code)有关。

不可移植的代码(unportable code):

由编译器定义的(implementation-defined)——由编译器设计者决定采取何种行动(就是说,在不同的编译器中所采取的行为可能并不相同,但它们都是正确的),并作好文档记录。

例如:当整型数向右移位时,要不要扩展符号位。

未确定的(unspecified)—在某些正确情况下的做法,标准并未明确规定应该怎样做。

例如:参数求值的顺序。

坏代码(bad code):

未定义的(undefined)—在某些不正确情况下的做法,但标准并未规定应该怎样做。你可以采取任何行动,可以什么也不做,也可以发出一条警告信息,或者可以中止程序以及让CPU陷入瘫痪,甚至可以发射核导弹(只要你安装了能发射核弹的硬件系统)。

例如:当一个有符号整数溢出时该采取什么行动。

约束条件(a constraint)—这是一个必须遵守的限制或要求。如果你不遵守,那么你的程序的行为就会变成像上面所说的属于未定义的。这就出现了一种很有意思的情况:分辨某种东西是否是一个约束条件是很容易的,因为标准的每个主题都附有一个“约束(constraint)”小节,列出了所有的约束条件。现在又出现了一个更为有趣的情况:标准规定[5]编译器只有在违反语法规则和约束条件的情况下才能产生错误信息!这意味着所有不属于约束条件的语义规则你都可以不遵循,而且由于这种行为属于未定义行为,编译器可以采取任何行动,甚至不必通知你!

例如:%操作符的操作数必须属于整型。所以,在非整数数据上使用%操作符肯定会引发一条错误信息。

不属于约束条件规则的例子:所有在C语言标准头文件中声明的标识符均保留,所以不能声明一个叫作malloc()的函数,因为在标准头文件里已经有一个函数以此为名。但由于这个规定不是约束条件,因此可以违反它,而且编译器甚至可以不警告你!关于“interpositioning”这一小节的更多内容,参见第5章。


  
软件信条
 
未定义的行为在IBM PC中引起CPU瘫痪!
未定义的软件行为引起CPU瘫痪的说法并不像它乍听上去那样牵强。

IBM PC的显示器以显示控制芯片所提供的水平扫描速率工作。回扫变压器(flyback transformer,一种产生高电压的装置,用于加速电子以点亮显示器上的荧光物质)需要保持一个合理的频率。

然而在软件中,程序员有可能把视频芯片的扫描速率设置成零,这样就会产生一个恒定的电压输出到回归变压器的输入端。这就使它起了电阻器的作用,只是把电能转换成热能,而不是传送到屏幕。这会在数秒之内就把显示器烧毁,那就是未定义的软件行为会导致系统瘫痪的理由。

可移植的代码(portable code):

严格遵循标准的(strictly-conforming)—— 一个严格遵循标准的程序应该是:

· 只使用已确定的特性。

· 不突破任何由编译器实现的限制。

· 不产生任何依赖由编译器定义的或未确定的或未定义的特性的输出。

这样规定的主要目的就是最大限度地保证可移植性。这样,不论你在什么平台上运行严格遵循标准的程序都会产生相同的输出。事实上,在所有遵循标准的程序中,属于这一类的程序并不多。例如,下面这个程序就不是严格遵循标准的:

#include <limits.h>

#include <stdio.h>

int main() { (void)printf("biggest int is %d", INT_MAX); return 0;}

/*并不严格遵循标准:其输出结果是由编译器定义的。*/

在本书的剩余部分,我们通常并不强求例子程序严格遵循标准。因为如果这样做会使文本看上去比较乱,而且不利于理解所讨论的要点。程序的可移植性是非常重要的,所以在你的现实编码中,应该始终要保证加上必要的类型转换、返回值等。

遵循标准的(conforming)— 一个遵循标准的程序可以依赖一些某种编译器特有的不可移植的特性。所以,一个程序有可能在一个特定的编译器里是遵循标准的,但在另一个编译器里却是不遵循标准的。它可以进行扩展,但这些扩展不能修改严格遵循标准的程序的行为。但是,这个规则并不是一个约束条件,所以对于你的程序中不遵循标准之处,你不要指望编译器会给出一条警告信息指出你违反了规定!

上面所举的几个程序实例都是遵循标准的。

使用特权

评论回复
7
dld2|  楼主 | 2008-1-19 14:49 | 只看该作者

1.7

1.7  编译限制
事实上,ANSI C标准对一个能够成功编译的程序的最小长度作了限制,这是在标准第5.2.4.1节规定的。绝大多数语言都有类似的规定,如一个数据名称(dataname)最多可以有多少个字符,一个多维数组的维数最多能够达到多少。但对语言的某种特性的最小值作出规定,如果不是独此一家,至少也是非比寻常的。标准委员会的成员们评论说这是为了指导编译器选择程序最小能够接受的长度。

每一个ANSI C编译器必须能够支持:

· 在函数定义中形参数量的上限至少可以达到31个。

· 在函数调用时实参数量的上限至少可以达到31个。

· 在一条源代码行里至少可以有509个字符。

· 在表达式中至少可以支持32层嵌套的括号。

· long int的最大值不得小于2 147 483 647(就是说,long型整数不得低于32位)等等。进而,一个遵循标准的编译器必须能够编译并执行一个满足上面这些限制的程序。令人惊异的是,上面这些“必须”的限制实际上并不是约束条件,所以当编译器发现违反上述规定的情况时并不一定产生错误信息。

编译器限制通常是一个“编译器质量”的话题。在ANSI C标准中包含它们就是默认如果所有的编译器都设置一些容量上的限制,就会更加有利于代码的移植。当然,一个真正优秀的编译器不应该有预设的限制,而应该只受一些外部因素的限制,如可用的内存或硬盘空间等。这可以通过使用链表或必要时动态扩展表的大小(这个技巧将在第10章解释)来实现。

使用特权

评论回复
8
dld2|  楼主 | 2008-1-19 15:03 | 只看该作者

1.8

1.8  ANSI C标准的结构
如果我们岔开话题,快速浏览一下ANSI C标准的出处和内容,对读者应该是有帮助的。ANSI C标准分成四个主要的部分:

第4节:介绍(共5页)。对术语进行介绍和定义。

第5节:环境(共13页)。描述了围绕和支持C语言的系统,包括在程序启动时发生什么,程序中止时发生什么,以及一些信号和浮点数运算。编译器的最低限制和字符集信息也在这一部分介绍。

第6节:C语言(共78页)。标准的这部分是基于Dennis Ritchie数次出版的经典之作“The C Reference Manual”,包括The C Programming Language的附录A。如果对比标准和附录,就会发现大多数标题都是一样的,顺序也相同。标准中的主题用辞生硬,看上去像表1-1那样(空白的子段落被省略)。

表1-1                                                 ANSIC标准段落形式一览

ANSI C标准中段落的一般形式
 ANSI C标准中段落举例
 
段落号 主题
 6.4 常量表达式
 
语法

    语法图
 语法

    常量表达式:

        条件表达式:
 
描述

    语言特性的一般描述
 描述

    常量表达式可以在编译时而不是运行时计算,因而可以出现在任何常量可以出现的地方
 
约束条件

    这里所列的任何规则如果被破坏,编译器应该给出一条错误信息
 约束条件

    常量表达式不应该包含赋值、增值、减值、函数调用和逗号操作符,除非它们包含在sizeof的操作数内。每个常量表达式应该计算成一个常量,该常量应该在其类型可以表示的范围之内
 
语义

    该特性的意思是什么,起什么作用
 语义

    计算结果是一个常量的常量表达式为一些上下文环境所需要。如果一个浮点表达式在翻译环境中被计算,计算的精度和…
 

续表

ANSI C标准中段落的一般形式
 ANSI C标准中段落举例
 
实例

    一段展示语言特性的代码
 ...
 

最初的附录只有40页,但在ANSI C标准中,足足多了一倍。

第7节:C运行库(共81页)。本节提供了一个遵循标准的编译器必须提供的库函数列表,它们是标准所规定的辅助和实用函数,用于提供基本的或有用的功能。ANSI C标准第7节所描述的C运行库是基于/user/group 1984 年的标准,去除了一些UNIX特有的部分。“/user/group”是一个于1984年成立的UNIX国际用户小组。1989年,它更名为“UniForum”,它现在是一个非盈利性行业协会,其宗旨是完善UNIX操作系统。

UniForum从行为的角度对UNIX进行了成功的定义,这激励了许多有创造性的想法,包括X/Open的可移植性指导方针(第4版,XPG/4出现于1992年12月)、IEEE的POSIX 1003、System Ⅴ Interface Definition(系统5接口定义)以及ANSI C标准函数库。每个人都与ANSI C工作小组协作,确保他们所有的标准草案相互之间保持一致。感谢上帝!

ANSI C标准同时附有一些很有用的附录:

附录F:一般警告信息。在许多常见的情况下,诊断信息并非标准所强制要求,但如果有这方面的信息,肯定对程序员有帮助作用。

附录G:可移植性话题。有一些关于可移植性的一般性建议,把遍布标准各处的所有这方面的建议集中在一个地方。它包括未确定的、未定义的和由编译器定义的行为等方面的信息。


  
软件信条
 
标准设立后轻易不作变动,即使是修改错误
并不能因为标准是由国际标准组织所撰写的就认定它必然完整、一致乃至正确。IEEE POSIX 1003.1-1998标准(它是一个操作系统标准,定义类似UNIX的行为)就存在一个非常有趣的自相矛盾的地方:

“[一个路径名]...最多由PATH_MAX个字节所组成,包括最后面的‘’字符” ——摘自第2.3节。

“PATH_MAX是一个路径名中最多能出现的字节个数(并不是字符串的长度;不包括最后面的‘’字符”——摘自第2.9.5节)。

所以,PATH-MAX个字节既包括最后面的‘’,又不包括最后面的‘’!

看来需要加以解释。答案(IEEE Std 1003.1-1988/INT,1992版,解释编号:15,第36页)认为标准出现了不一致,不过两个结果可以认为都是正确的(这令人很感奇怪,因为一般的观点认为它们不可能两个都是正确的)。

之所以出现这个问题,是由于在修改草案时,所有出现这个词的地方并未得到全部更新。标准化过程非常重视形式,显得僵化。如要更新,只有投票小组批准后才允许对问题进行修改。

这样的错误也曾出现在C标准最早期的脚注里,也就是所附的Rationale文档。事实上,Rationale现在已不属于C标准的一部分,当标准的所有权移交到ISO时,它就被删掉了。


  
小 启 发
 
K&R C和ANSI C之间的区别
阅读本节内容时,我假定你已经完全明白K&R C,对ANSI C也已知道了90%。ANSI C和K&R C的区别分成四大类,按其重要性分列于下:

1. 第一类区别是指一些新的、非常不同的、并且很重要的东西。惟一属于这类区别的特性是原型——把形参的类型作为函数声明的一部分。原型使得编译器很容易根据函数的定义检查函数的用法。

2. 第二类区别是一些新的关键字。ANSIC正式增加了一些关键字:enum代表枚举类型(最初出现于pcc的后期版本),const、volatile、signed、void也有各自相关的语义。另外,原先可能由于疏忽而加入到C中的关键字entry则弃之不用。

3. 第三类区别被称作“安静的改变”——原先的有些语言特性仍然合法,但它的意思有了一些轻微的改变。这方面的例子很多,但都不是很重要,几乎可以被忽略。在你偶尔漫步于它们之上时,可能由于不注意而被其中一个绊了个趔趄。例如,现在的预处理规则定义得更加严格,有一条新规则,就是相邻的字符串字面值会被自动连接在一起。

4. 最后一类区别就是除上面3类之外的所有区别,包括那些在语言的标准化过程中长期争论的东西,这些区别在现实中几乎不可能碰到,如符号粘贴(token-pasting)和三字母词(trigraph)(三字母词就是用3个字符表示一个单独的字符,如果该字符不存在于某种计算机的字符集中,就可以用这3个字符来表示。比如两字母词(digraph) 表示“tab”,而三字母词??< 则表示“开放的花括号”)。

ANSI C中最重要的新特性就是“原型”,这种特性取自C++。原型是函数声明的扩展,这样不仅函数名和返回类型已知,所有的形参类型也是已知的。这就允许编译器在参数的使用和声明之间检查一致性。把“原型”称作是“带有所有参数的函数名”是不够充分的,它应该被称作“函数签名(function signiture)”,或者像Ada那样称作“函数说明(function specification)”。


  
软件信条
 
原型的形成
原型的目的是当我们对函数作前向声明(forward declaration)时,在形参类型中增加一些信息(而不仅仅是函数名和返回类型)。这样,编译器就能够在编译时对函数调用中的实参和函数声明中的形参之间进行一致性检查。在K&R C中,这种检查被推迟到链接时,或者干脆不作检查。使用原型以后,原先的:

char * strcpy();

现在在头文件中的形式如下:

char * strcpy(char *dst, const char *src);

可以省略参数名称,只保留参数类型:

char * strcpy(char *, const char *);

但最好不要省略形参名。尽管编译器并不理睬形参的名称,但它们经常能向程序员们传递一些有用的信息。类似地,函数的定义也从:

char * strcpy(dst, src)

       char *dst, *src;

{ ... }

变成了:

char * strcpy(char *dst, const char *src)  /* 注意没有分号 */

{ ... }

函数头不再以一个分号结尾,而是在后面紧接一个组成函数体的复合语句。

每次编写新函数时都应该使用原型,并确保它在每次调用时都可见。不要回到K&R C老式的函数声明方法,除非需要使用缺省的类型升级(这个话题在第8章详细讨论)。

把同一种东西用几个不同的术语来称呼,确实有点神秘。就好像药品至少有3种名称一样:化学名、商品名和常用名。

使用特权

评论回复
9
dld2|  楼主 | 2008-1-19 15:06 | 只看该作者

1.9

1.9  阅读ANSI C标准,寻找乐趣和裨益
有时候必须非常专注地阅读ANSI C标准才能找到某个问题的答案。一位销售工程师把下面这段代码作为测试例发给Sun的编译器小组。

1 foo(const char **p) { }

2

3 main(int argc, char **argv)

4 {

5            foo(arvg);

6 }

如果编译这段代码,编译器会发出一条警告信息:

line 5: warning: argument is incompatible with prototype 

(第5行:警告:参数与原型不匹配)。

提交代码的工程师想知道为什么会产生这条警告信息,也想知道ANSI C标准的哪一部分讲述了这方面的内容。他认为,实参char* s与形参const char *p应该是相容的,标准库中所有的字符串处理函数都是这样的。那么,为什么实参char **argv与形参const char **p实际上不能相容呢?

答案是肯定的,它们并不相容。要回答这个问题颇费心机,如果研究一下获得这个答案的整个过程,会比仅仅知道结论更有意义。对这个问题的分析是由Sun的其中一位“语言律师”[6]进行的,其过程如下:

在ANSI C标准第6.3.2.2节中讲述约束条件的小节中有这么一句话:

每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)。

这就是说参数传递过程类似于赋值。

所以,除非一个类型为char **的值可以赋值给一个const char **类型的对象,否则肯定会产生一条诊断信息。要想知道这个赋值是否合法,就请回顾标准中有关简单赋值的部分,它位于第6.3.16.1节,描述了下列约束条件:

要使上述的赋值形式合法,必须满足下列条件之一:

两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。

正是这个条件,使得函数调用中实参char*能够与形参const char*匹配(在C标准库中,所有的字符串处理函数就是这样的)。它之所以合法,是因为在下面的代码中:

char *cp;

const char *ccp;

ccp = cp;

· 左操作数是一个指向有const限定符的char的指针。

· 右操作数是一个指向没有限定符的char的指针。

·  char类型与char类型是相容的,左操作数所指向的类型具有右操作数所指向类型的限定符(无),再加上自身的限定符(const)。

注意,反过来就不能进行赋值。如果不信,试试下面的代码:

cp = ccp;     /* 结果产生编译警告 */

标准第6.3.16.1节有没有说char **实参与const char **形参是相容的?没有。

标准第6.1.2.5节中讲述实例的部分声称:

const float *类型并不是一个有限定符的类型——它的类型是“指向一个具有const限定符的float类型的指针”,也就是说const限定符是修饰指针所指向的类型,而不是指针本身。

类似地,const char **也是一个没有限定符的指针类型。它的类型是“指向有const限定符的char类型的指针的指针”。

由于char **和const char **都是没有限定符的指针类型,但它们所指向的类型不一样(前者指向char *,后者指向const char *),因此它们是不相容的。因此,类型为char**的实参与类型为const char**的形参是不相容的,违反了标准第6.3.2.2节所规定的约束条件,编译器必然会产生一条诊断信息。

用这种方式理解这个要点有一定困难。可以用下面这个方法进行理解:

· 左操作数的类型是FOO2,它是一个指向FOO的指针,而FOO是一个没有限定符的指针,它指向一个带有const限定符的char类型,而且……

· 右操作数的类型是BAZ2,它是一个指向BAZ的指针,而BAZ是一个没有限定符的指针,它指向一个没有限定符的字符类型。

FOO和BAZ所指向的类型是相容的,而且它们本身都没有限定符,所以符合标准的约束条件,两者之间进行赋值是合法的。但FOO2和BAZ2之间的关系又有不同,由于相容性是不能传递的,FOO和BAZ所指向的类型相容并不表示FOO2和BAZ2所指向的类型也相容,所以虽然FOO2和BAZ2都没有限定符,但它们之间不能进行赋值。也就是说,它们都是不带限定符的指针,但它们所指向的对象是不同的,所以它们之间不能进行赋值,也就不能分别作为函数的形参和实参。但是,这个约束条件很令人恼火,也很容易让用户混淆。所以,这种赋值方法目前在基于Cfront的C++翻译器中是合法的(虽然这在将来可能会改变)。


  
小 启 发
 
容易混淆的const
关键字const并不能把变量变成常量!在一个符号前加上const限定符只是表示这个符号不能被赋值。也就是它的值对于这个符号来说是只读的,但它并不能防止通过程序的内部(甚至是外部)的方法来修改这个值。const最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据,但其他的函数却可能会修改它。这也许就是C和C++中const最一般的用法。

const可以用在数据上,如:

    const int limit = 10;

这和其他语言差不多,但当你在等式两边加上指针,就有一定难度了:

    const int * limitp = &limit;

    int i = 27;

    limitp = &i;

这段代码表示limitp是一个指向常量整型的指针。这个指针不能用于修改这个整型数,但是在任何时候,这个指针本身的值却可以改变。这样,它就指向了不同的地址,对它进行解除引用(dereference)操作时会得到一个不同的值!

const和*的组合通常只用于在数组形式的参数中模拟传值调用。它声称“我给你一个指向它的指针,但你不能修改它。”这个约定类似于极为常见的void *的用法,尽管在理论上它可以用于任何情形,但通常被限制于把指针从一种类型转换为另一种类型。

类似地,你可以取一个const变量的地址,并且可以...(唔,我最好不要往大家的脑袋里灌输这种思想)。正如Ken Thompson所指出的那样,“const关键字可能引发一些罕见的错误,只会混淆函数库的接口。”回首往事,const关键字原先如果命名为readonly就好多了。

确实,整个标准好像是由一位蹩脚的翻译把它从乌尔都语转译成丹麦语,再转译成英语而来。标准委员会似乎自我感觉良好,所以虽然人们希望语言的规则更简单一些、更清楚一些,但他们觉得这样做会破坏他们的良好感觉,所以拒不采纳。

我感觉,将来还会有许多人产生类似的疑问,而且并不是他们中的每一个人都会仔细揣摩前面详述的推理过程。所以,我们修改了Sun的ANSI C编译器,当它发现不相容的情况时,会打印出更多的警告信息。原先那个例子将会产生的完整信息如下:

Line 6: warning : argument #1 is imcompatible with prototype:

  prototype: pointer to pointer to const char: "barf.c", line 1

  argument: pointer to pointer to char

(第6行:警告:#1实参与原型不相容:

    原型:指向const char的指针的指针。"barf.c", 第1行

    实参:指向char的指针的指针。)

即使程序员不明白为什么会这样,他至少应该明白什么是不相容。

使用特权

评论回复
10
sz_kd| | 2008-1-19 15:08 | 只看该作者

使用特权

评论回复
11
dld2|  楼主 | 2008-1-19 15:17 | 只看该作者

1.10 有内容的一节

1.10  “安静的改变”究竟有多少安静
标准所作的修改并非都如原型那样引人注目。ANSI C作了其他一些修改,目的是使C语言更加可靠。例如,“寻常算术转换(usual arithmetic conversion)”在旧式的K&R C和ANSI C中的意思就有所不同。Kernighan和Ritchie当初是这样写的:

第6.6节:算术转换

许多运算符都会引发转换,以类似的方式产生结果类型。这个模式称为“寻常算术转换”。

首先,任何类型为char或short的操作数被转换为int,任何类型为float的操作数被转换为double。其次,如果其中一个操作数的类型是double,那么另一个操作数被转换成double,计算结果的类型也是double。再次,如果其中一个操作数的类型是long,那么另一个操作数被转换成long,计算结果的类型也是long。或者,如果其中一个操作数的类型是unsigned,那么另一个操作数被转换成unsigned,计算结果的类型也是unsigned。如果不符合上面几种情况,那么两个操作数的类型都作为int,计算结果的类型也是int。

ANSI C手册重新编写了有关内容,填补了其中的漏洞:

第6.2.1.1节 字符和整型(整型升级)

char, short int或者int型位段(bit-field),包括它们的有符号或无符号变型,以及枚举类型,可以使用在需要int或unsigned int的表达式中。如果int可以完整表示源类型的所有值[7],那么该源类型的值就转换为int,否则转换为unsigned int。这称为整型升级。

第6.2.1.5节 寻常算术转换

许多操作数类型为算术类型的双目运算符会引发转换,并以类似的方式产生结果类型。它的目的是产生一个普通类型,同时也是运算结果的类型。这个模式称为“寻常算术转换”。

首先,如果其中一个操作数的类型是long double,那么另一个操作数也被转换为long double。其次,如果其中一个操作数的类型是double,那么另一个操作数也被转换为double。再次,如果其中一个操作数的类型是float,那么另一个操作数也被转换为float。否则,两个操作数进行整型升级(第6.2.1.1节描述整型升级),执行下面的规则:

如果其中一个操作数的类型是unsigned long int,那么另一个操作数也被转换为unsigned long int。其次,如果其中一个操作数的类型是long int,而另一个操作数的类型是unsigned int,如果long int能够完整表示unsigned int的所有值[8],那么unsigned int类型操作数被转换为long int,如果long int不能完整表示unsigned int的所有值[9],那么两个操作数都被转换为unsigned long int。再次,如果其中一个操作数的类型是long int,那么另一个操作数被转换为long int。再再次,如果其中一个操作数的类型是unsigned int,那么另一个操作数被转换为unsigned int。如果所有以上情况都不属于,那么两个操作数都为int。

浮点操作数和浮点表达式的值可以用比类型本身所要求的更大的精度和更广的范围来表示,而它的类型并不因此改变。

采用通俗语言(当然存有漏洞,而且不够精确),ANSI C标准所表示的意思大致如下:

当执行算术运算时,操作数的类型如果不同,就会发生转换。数据类型一般朝着浮点精度更高、长度更长的方向转换,整型数如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned。

K&R C所采用无符号保留(unsigned preserving)原则,就是当一个无符号类型与int或更小的整型混合使用时,结果类型是无符号类型。这是个简单的规则,与硬件无关。但是,正如下面的例子所展示的那样,它有时会使一个负数丢失符号位。

ANSI C标准则采用值保留(value preserving)原则,就是当把几个整型操作数像下面这样混合使用时,结果类型有可能是有符号数,也可能是无符号数,取决于操作数的类型的相对大小。

下面的程序段分别在ANSI C和K&R C编译器中运行时,将打印出不同的信息:

main(){

  if(-1 < (unsigned char)1

     printf("-1 is less than (unsigned char)1: ANSI semantics ");

  else

     printf("-1 NOT less than (unsigned char)1: K&R semantics");

}

程序中的表达式在两种编译器下编译的结果不同。-1的位模式是一样的,但一个编译器(ANSI C)将它解释为负数,另一个编译器(K&R C)却将它解释为无符号数,也就是变成了正数。


  
软件信条
 
一个微妙的Bug
虽然规则作了修改,但微妙的Bug依然存在。在下面这个例子里,变量d比程序所需的下标值小1,这段代码的目的就是处理这种情况。但if表达式的值却不是真。为什么?是不是有Bug:

int array[] = { 23, 34, 12, 17, 204, 99, 16 };

#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))

main( )

{

    int d = -1, x;

    /* ... */

    if(d <= TOTAL_ELEMENTS - 2)

        x = array[d+1];

    /* ... */

}

TOTAL_ELEMENTS所定义的值是unsigned int类型(因为sizeof()的返回类型是无符号数)。if语句在signed int和unsigned int之间测试相等性,所以d被升级为unsigned int类型,-1转换成unsigned int的结果将是一个非常巨大的正整数,致使表达式的值为假。这个bug在ANSI C中存在,而如果K&R C的某种编译器的sizeof()的返回值是无符号数,那么这个Bug也存在。要修正这个问题,只要对TOTAL_ELEMENTS进行强制类型转换即可:

if(d <= (int)TOTAL_ELEMENTS – 2)


  
小 启 发
 
对无符号类型的建议
尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而用它来表示数量。

尽量使用像int那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如-1被翻译为非常大的正数)。

只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型。

这听起来是不是有点诡异,是不是令人吃惊?确实如此!用前面一页所说的规则完成上面这个例子。

最后,为了不让The Elements of Programming Style[10]未来的版本把这段代码作为不良风格的实例,我最好解释一下其中的一些代码。我使用了下面这条语句:

#define TOTAL_ELEMENTS  (sizeof(array) / sizeof(array[0]))

而不是:

#define TOTAL_ELEMENTS  (sizeof(array) / sizeof(int))

因为前者可以在不修改#define语句的情况下改变数组的基本类型(比如,把int变成char)。

Sun公司的ANSI C编译器小组认为从“无符号保留”转到“值保留”对于C语言的语义而言完全没有必要,只会让偶尔遇到这方面问题的人感到吃惊和沮丧。因此,在“尽量不让人误会”的原则下,Sun编译器认可并编译ANSI C的特性,除非该特性在K&R C里另有解释。如果碰到后面这种情况,编译器在缺省情况下使用K&R C的标准,并给出一条警告信息。如果碰到上面这个例子,程序员应该使用强制类型转换告诉编译器最终所希望的类型。在Sun公司运行Solaris 2.x的工作站上只要打开编译器的-Xc开关,就可以使编译器严格遵循ANSI C标准的语义。

在K&R C的许多特性中,有许多在ANSI C中进行了更新,包括许多所谓“安静的转变”。在这种情况下,代码在两种编译器里都能通过编译,但具体含义稍有差别。当程序员发现这种情况时,他们的反应可想而知。因此,这种转变事实上应该称作“讨厌的转变”。总的来说,ANSI委员会试图进行尽可能少的改动,与原先存在的但确实需要改进的特性保持一致。

对于ANSI C族系背景知识的讨论已经够多了。因此,在下面的“轻松一下”一节过后,让我们驶向第2章,进入本书的中心内容。

使用特权

评论回复
12
dld2|  楼主 | 2008-1-19 15:20 | 只看该作者

1.11

1.11  轻松一下——由编译器定义的Pragmas效果
自由软件基金会(Free Software Foundation)是一个独特的组织,它由MIT顶级黑客Richard Stallman所创立。顺便提一下,我们所说的“黑客”,它的原先意思是“天才程序员”。后来这个称呼被媒体所贬损,致使它在局外人眼中成了“邪恶的天才”的代名词。和形容词“bad”一样,“黑客”现在也有两个相反的意思,必须通过上下文才能明白它的确切意思。

Stallman成立自由软件基金会的初衷是:软件应该是免费的,所有人都可以自由使用。FSF的宗旨是“消除在计算机程序拷贝、重发布、理解和修改方面的限制”,它雄心勃勃地想建立一个UNIX的自由软件实现方案,称为GNU(它代表“GNU’s Not UNIX”,对,确实如此)。

许多计算机科学研究生和其他人赞同GNU的哲学,他们设计软件产品,由FSF进行打包并免费发布。通过这些甘心奉献的有天赋的程序员们的辛勤劳动,产生了一些优秀的软件作品。FSF最好的作品之一就是GNU C编译器系列。gcc是一个健壮的、在代码优化方面具有创造性的编译器,可以在很多硬件平台使用,有时甚至比编译器厂商的产品更为优秀。gcc并不适合所有的项目,它在维护性和未来版本连续性方面仍存在一些问题。在现实的开发中,除了编译器之外,还需要很多工具。曾有很长一段时间,GNU的调试器无法在共享库中工作。而且在开发时,GNU C偶尔会让人感到眼花缭乱。

在制订ANSI C标准时,引入了pragma指示符,这个指示符来源于Ada。#pragma用于向编译器提示一些信息,诸如希望把某个特定函数扩展为内联函数,或者取消边界的检查。由于它并非C语言所固有,pragma遭到了一个gcc编译器设计者的积极抵制,他把这个“由编译器定义的”的效果做得很搞笑—在gcc 1.34版,如果使用了pragma,将会导致编译器停止编译,而是运行一个计算机游戏!在gcc手册中有如下说明:

在ANSI C标准中,“#pragma”指令会产生一个由编译器定义的任意效果。在GNU C预处理器中,一旦遇见“#pragma”指令,它首先试图运行“rogue”游戏;如果失败,尝试运行“hack”游戏;如果还是失败,它会尝试运行GNU Emacs,显示汉诺塔(Tower of Hanoi)。如果仍然失败,它就报告一个致命错误。总之,预处理过程不会继续下去。

—— GNU C编译器1.34版手册

GNU C编译器中关于预处理器的那部分源代码如下:

/ *

 * #pragma指示符的行为是由编译器定义的。

 * 在GNU C编译器中,它的定义如下:

 * /

do_pragma()

{

    close(0);

    if(open("/dev/tty", O_RDONLY, 0666) != 0)

                           goto nope;

    close(1);

    if(open("/dev/tty", O_WRONLY, 0666) != 1)

                           goto nope;

    exel("/usr/games/hack", "#pragma", 0);

    exel("/usr/games/rogue", "#pragma", 0);

    exel("/usr/new/emacs", "-f", "hanoi", "9", "-kill", 0);

    exel("/usr/local/emacs", "-f", "hanoi", "9", "-kill", 0);

nope:

  fatal("you are in a maze of twisty compiler features, all different");

}

特别好笑的是,用户手册中的描述是错误的,它把“hack”和“rogue”的次序搞反了。

使用特权

评论回复
13
dld2|  楼主 | 2008-1-19 15:22 | 只看该作者

下面没了。CSDN在连载。贴一下本书目录吧。

第1章 C:穿越时空的迷雾 1
1.1 C语言的史前阶段 1
1.2 C语言的早期体验 4
1.3 标准I/O库和C预处理器 5
1.4 K&R C 8
1.5 今日之ANSI C 10
1.6 它很棒,但它符合标准吗 12
1.7 编译限制 14
1.8 ANSI C标准的结构 15
1.9 阅读ANSI C标准,寻找乐趣和裨益 19
1.10 “安静的改变”究竟有多少安静 22
1.11 轻松一下——由编译器定义的Pragmas效果 25

第2章 这不是Bug,而是语言特性 27
2.1 这关语言特性何事,在Fortran里这就是Bug呀 27
2.2 多做之过 29
2.3 误做之过 36
2.4 少做之过 43
2.5 轻松一下——有些特性确实就是Bug 51
2.6 参考文献 53

第3章 分析C语言的声明 55
3.1 只有编译器才会喜欢的语法 56
3.2 声明是如何形成的 58
3.3 优先级规则 63
3.4 通过图表分析C语言的声明 65
3.5 typedef可以成为你的朋友 67
3.6 typedef int x[10]和#define x int[10]的区别 68
3.7 typedef struct foo{ ... foo; }的含义 69
3.8 理解所有分析过程的代码段 71
3.9 轻松一下——驱动物理实体的软件 73

第4章 令人震惊的事实:数组和指针并不相同 81
4.1 数组并非指针 81
4.2 我的代码为什么无法运行 81
4.3 什么是声明,什么是定义 82
4.4 使声明与定义相匹配 86
4.5 数组和指针的其他区别 86
4.6 轻松一下——回文的乐趣 88

第5章 对链接的思考 91
5.1 函数库、链接和载入 91
5.2 动态链接的优点 94
5.3 函数库链接的5个特殊秘密 98
5.4 警惕Interpositioning 102
5.5 产生链接器报告文件 107
5.6 轻松一下——看看谁在说话:挑战Turing测验 108

第6章 运动的诗章:运行时数据结构 115
6.1 a.out及其传说 116
6.2 段 117
6.3 操作系统在a.out文件里干了些什么 119
6.4 C语言运行时系统在a.out里干了些什么 121
6.5 当函数被调用时发生了什么:过程活动记录 123
6.6 auto和static关键字 126
6.7 控制线程 128
6.8 setjmp和longjmp 128
6.9 UNIX中的堆栈段 130
6.10 MS-DOS中的堆栈段 130
6.11 有用的C语言工具 131
6.12 轻松一下——卡耐基-梅隆大学的编程难题 134
6.13 只适用于高级学员阅读的材料 136

第7章 对内存的思考 137
7.1 Intel 80x86系列 137
7.2 Intel 80x86内存模型以及它的工作原理 141
7.3 虚拟内存 145
7.4 Cache存储器 148
7.5 数据段和堆 152
7.6 内存泄漏 153
7.7 总线错误 157
7.8 轻松一下——“Thing King”和“页面游戏” 163

第8章 为什么程序员无法分清万圣节和圣诞节 169
8.1 Portzebie度量衡系统 169
8.2 根据位模式构筑图形 170
8.3 在等待时类型发生了变化 172
8.4 原型之痛 174
8.5 原型在什么地方会失败 176
8.6 不需要按回车键就能得到一个字符 179
8.7 用C语言实现有限状态机 183
8.8 软件比硬件更困难 185
8.9 如何进行强制类型转换,为何要进行类型强制转换 187
8.10 轻松一下——国际C语言混乱代码大赛 189

第9章 再论数组 199
9.1 什么时候数组与指针相同 199
9.2 为什么会发生混淆 200
9.3 为什么C语言把数组形参当作指针 205
9.4 数组片段的下标 208
9.5 数组和指针可交换性的总结 209
9.6 C语言的多维数组 209
9.7 轻松一下——软件/硬件平衡 215

第10章 再论指针 219
10.1 多维数组的内存布局 219
10.2 指针数组就是Iliffe向量 220
10.3 在锯齿状数组上使用指针 223
10.4 向函数传递一个一维数组 226
10.5 使用指针向函数传递一个多维数组 227
10.6 使用指针从函数返回一个数组 230
10.7 使用指针创建和使用动态数组 232
10.8 轻松一下——程序检验的限制 237

第11章 你懂得C,所以C++不在话下 241
11.1 初识OOP 241
11.2 抽象——取事物的本质特性 243
11.3 封装——把相关的类型、数据和函数组合在一起 245
11.4 展示一些类——用户定义类型享有和预定义类型一样的权限 246
11.5 访问控制 247
11.6 声明 247
11.7 如何调用成员函数 249
11.8 继承——复用已经定义的操作 251
11.9 多重继承——从两个或更多的基类派生 255
11.10 重载——作用于不同类型的同一操作具有相同的名字 256
11.11 C++如何进行操作符重载 257
11.12 C++的输入/输出(I/O) 258
11.13 多态——运行时绑定 258
11.14 解释 260
11.15 C++如何表现多态 261
11.16 新奇玩意——多态 262
11.17 C++的其他要点 263
11.18 如果我的目标是那里,我不会从这里起步 264
11.19 它或许过于复杂,但却是惟一可行的方案 266
11.20 轻松一下——死亡计算机协会 270
11.21 更多阅读材料 271

附录A 程序员工作面试的秘密 273
附录B 术语表 285

使用特权

评论回复
14
hq_y| | 2008-1-19 15:40 | 只看该作者

有没有电子文档可以下载?

使用特权

评论回复
15
dld2|  楼主 | 2008-1-19 15:43 | 只看该作者

不知道,请到这个地址看下吧

http://book.csdn.net/bookfiles/617/

使用特权

评论回复
16
happystar| | 2008-1-19 17:33 | 只看该作者

LZ辛苦啦,帮你顶顶

感觉自己很浮躁,真是静不下心看资料呀。

使用特权

评论回复
17
phoenixmy| | 2008-1-19 17:43 | 只看该作者

不错,楼主连载下去算了

使用特权

评论回复
18
flanker| | 2008-1-19 23:18 | 只看该作者

嗯哪,具经典的书

在网上定了一本,摆在实验室书架上,天天上厕所,无论大小,都夹着书去看。

使用特权

评论回复
19
phoenixmy| | 2008-1-20 00:21 | 只看该作者

不会吧,大小都看??哈哈哈

使用特权

评论回复
20
gyt| | 2008-1-20 09:34 | 只看该作者

辛苦辛苦

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

74

主题

2442

帖子

3

粉丝