《程序员的自我修养》是有学习价值的

2009/05/23 | 14:20 | 分类:计算机科学与编程 | 标签: | 1,656次阅读

  不久前收到了博文视点寄来的新书——《程序员的自我修养——链接、装载与库》。比较巧的是,我这段时间正在从事一个涉及多语言互操作的项目,那些天还在看一些有关C++语言设计和实现原理的文献,这本《程序员的自我修养》和我最近的工作还是多多少少有点联系的。
  有的人可能认为读博士的人不应该再去关注这些面向程序员、纠结于技术细节的书了,而要专心于自己的研究领域,做偏科学、偏理论的事。这是有道理的,博士教育和硕士教育的差异即在此,漂泊在茫无涯际的技术海洋很可能让本应该专注于一个方向的人不知所向。但是我们也要反思,自己的基础是否足够扎实?对计算机原理的理解是否超越了那些具体技术的层面?自己是否真的具备了从事科研的素质?我的导师在他的科普作品《电脑启示录》(中篇 硅谷的秘密)中曾提到作为计算机科研人员应该具备的基本素质,并例举了几个用来考察这些素质的问题,诸如:从计算机开机到操作系统等待用户输入,经历的一系列流程是什么?在浏览器里敲入一个网址到网页呈现给用户,期间又有哪些工作细节?这些过程看似稀疏平常,其中的大道理在本科计算机课程中也都或多或少地介绍过,然而要精确地表达每个步骤,更重要地是说明每个步骤为什么要这样设计、为什么会这样实现、其中的科学依据是什么,往往不是每个计算机专业毕业生都能说清楚的。这类问题常常能从侧面反映一个从业人员的理论功底及实践经验。《程序员的自我修养》所阐述的也正是同一类的问题:一个程序由硬盘上目标文件、可执行文件变成内存中的进程体、CPU中的指令流,整个过程的来龙去脉是什么,有什么原理、诀窍、讲究和因果联系。
  在计算机领域从事不同具体工作的科研、技术人员,静下心来分析一下这些平时被各种层面的接口掩盖了的机制是有好处的。初用VC++编程的时候,也许你会奇怪,监视窗口为什么要输出一串“烫”字;干Linux工程时,仅仅链接了几个标准库文件就把程序搞崩溃了,你会认为这是链接器或标准库的bug,还是自己没有弄明白它们的机理?如果你在计算机学科的其它方面有过一些细致的了解,你又会发现,像自举引导、延迟绑定等共性的方法,时间与空间转换、策略与机制分离等共性的原则在编译、链接、装载过程中也都有生动的体现。而即使你是做研究的,方向与程序原理相隔甚远,看看这些计算机领域内实现相对成熟、应用相对普遍的、原理相对通用的问题,对自己的工作也很有启发意义。
  尽管这本书有待市场的考验,但它研究的问题确属计算机学科中的经典。把理解链接、装载与库作为程序员的自我修养是否合适?我想,它算不上充分条件,不过确实是一个必要条件。不仅对程序员来说必要,对任何一个从事计算机研究与开发的人来说都是有价值的。

编译器与标准——严格还是智能?

2009/03/21 | 14:21 | 分类:计算机科学与编程 | 标签: | 768次阅读

  最近做的C/C++工程需要在多种平台下编译(下面所说的编译泛指编译、连接等过程)并运行通过,多种不同版本的编译器考验着代码正确性和严格性。经测试,我们的历史代码中存在着这样几类问题,使得它们在VS2008和低版本gcc下编译通过,但在高版本的gcc下编译就会或多或少地报错:
  1、使用了外部的函数,但没有在本文件中声明或#include对应的头文件。VS2008和gcc3.2在很多情况下可以智能地从一同编译的其它文件或标准库中找到对应的函数并连接,而gcc4.3要求必须声明。
  2、使用模板时,T::name表示T::name类型还是T类的name成员?即使不使用typename关键字,VS2008和gcc3.2在很多情况下可以智能推断T::name a;中的T::name表示表示一个类型。但在gcc4.1/4.3下必须使用typename T::name来表示T::name类型。
  3、隐式类型转换。对于函数Func(string&),在VS2008中可以采用Func(string("new"));来调用,但gcc不能隐式地转换构造函数的返回,需要使用string str("new"); Func(str);来调用。
  4、一些copy-paste粗心小问题,比如声明类的成员函数时不应该加“类名::”限定。如果加了,VS2008会忽略,而gcc4.1/4.3会报错。
  5、非标准的库函数,比如VS2008的itoa(_itoa)。如果说上述几条是“严格性”的问题,这个应该算是“正确性”的问题了———个编译器扩展的内容在另一个编译器看来自然是不知所云。当然,为方便起见,使用编译器提供的非标准扩展也是可以的(比如在不完全支持C99的gcc和VC版本下,分别使用atoq和_atoi64来代替C99的atoll),但这时候别忘了使用条件编译,针对不同的编译器提供不同的代码。
  等等。
  总体来看,我们用到的几个编译器的严格程度如下。注意仅仅是从一些小的侧面反映的结果,不完全准确,说明大致情况而已。

  ─────── 更严格 ──────→
   VS2008  gcc3.2  gcc4.1  gcc4.3
  ←────── 更智能 ───────

  编译器的严格性也许是一把双刃剑。但我个人认为严格一些还是利大于弊的。拿上面的问题1来说,我就遇到过gcc3.2“智能”连接错误的问题,而如果使用gcc4.3,这个bug就可以轻易地被定位,节约大量的调试时间。
  智能的底线是正确性。如果代码中出现了不规范的写法,编译器给出智能判断并忽略问题的同时,应该给程序员一定的警告信息,让他们核实代码并判断是否需要向标准的方向修改。不过可悲的是编译器的警告往往被程序员们一略而过了。也许正是因为这一点,gcc才变得越发严格,试图和程序员划清责任域,减少扯皮现象?
  当然,仅仅写出语法上严格的代码,解决了不同编译器下编译通过的问题,我们还不能保证程序运行时语义的正确性。例如程序中存在未初始化的变量,一种编译器的实现是以0填充,恰好与程序员的原意相符;而切换到另一编译器下,未初始化的变量成了随机值,程序运行就会出问题。所以,对C/C++这样高度自由的语言来说,严格的编译器也不是万能的,人的因素始终是第一位的。
  说了这么些严格和智能,评判它们的依据是什么?自然是语言的标准了(C99、ISO C++之类)。有关编译器对语言(包含库)标准的支持,我们大约可以按以下维度划分:

      标准要求实现的
         ↑
         │
  标准未定义的 │ 标准定义的
     ←───┼───→
         │
         │
         ↓
      编译器扩展的

  标准定义的语法在任何编译器下都应该有相同、无歧义的语义。尽管其内部实现可能不同,或是依赖于具体的机器架构,但在标准中总有明确的说明。而标准中也存在大量未定义的语义,这通常是为了让编译器作者放开限制,做更高效、更简单的实现。这类语法在不同的编译器或不同的运行环境下可能产生不同的结果,经典的例子就是n=m+++m++这种副作用语句的叠加,再如L'abcd'表示什么的问题。
  工程中通常应该严格禁用标准未定义的语法,因为机器架构、编译器种类和版本、优化选项、运行时环境等的变化都有可能对程序的运行结果产生不可预知的影响。但有些时候标准未定义的的语法也是有用的,比如为了学习理解编译器的原理和优化算法。另外在一些技巧性游戏,如Quine,以及IOCCCTime Limit Exceeded这样的hack竞赛中,使用未定义语法的捷径也是未尝不可的。
  而编译器扩展的语法和库则是一类相对有用的工具。不同于标准未定义的内容,编译器所做的扩展也是有严格定义的,不会产生不可预知的结果。在对代码的迁移性要求不高的场合直接使用并无大碍。如果对代码的迁移性有要求,那就如上文所述使用条件编译来限制其作用范围。毕竟编译器所做的扩展是为了弥补标准库的不足,方便开发者。如果对代码将来的使用范围有明显的预知,就需要权衡是否使用编译器的扩展特性。不过就连Linux内核的代码都在使用gcc的扩展语法
  在C/C++语言教学中,强调标准是十分必要的。然而市面上的C/C++教材鱼目混珠:那天在师兄的桌上看到的一本国内某知名高校出版的C++教材上,Hello World程序竟有3处明显的问题,不用试验就知道在VS2008或gcc下编译有两处是Error,一处至少是Warning(据说在VC6下可以编译通过)。一些高校计算机教学中普遍使用历史遗留的、非标准的编译器也成为滋生不良编程习惯、阻碍标准推广的源泉。究其原因,某些教师的惰性是一方面,而相关资格考试与现行标准、业界规范的严重脱节也成为应试教育体制下技术标准难以为师生重视的重要因素。正面的例子又如何呢?北理工计算机学院有一门程序设计实践与方法的本科课程,这门课的重心本是算法与编程技巧的训练,但由于它使用了基于gcc的Online Judge机制和像ACM-ICPC一样严格、众多的测试用例,使得学生对代码的标准与严格、算法的正确与高效、条件的完全覆盖必须做仔细的考量。我自己上过这门课,也做过这门课的助教,两种角色的体验让我深知师生各自的抱怨与苦衷。错误究竟在谁?我想不在于这门课的教师和学生,而在于这门课之外的计算机教育氛围。改变这个氛围并不可以一蹴而就,但至少有两点是可以尽己所能的:主观方面,建立标准意识,培养良好的编程习惯;客观方面,使用实现了符合现行标准的编译器。尽管这对计算机教学的整体环境影响甚微,但起码对未来从事计算机方面的工作来说是有益的——实际工程对标准的要求非同儿戏。

使用C++实现的Java语言子集词法、语法、语义分析器

2007/12/27 | 10:05 | 分类:Windows开发 | 标签: | 837次阅读
  上学期的一个编译原理大作业,公布出来给大家参考。使用C++实现的Java语言子集词法、语法、语义分析器。参考开源的GJC(Generic Java Compiler,Sun Microsystems,Java语言实现)开发,提供源代码与说明文档(包含详细的FA设计、数据结构与算法说明以及一个完整的编译实例)。
 
  仅供参考,请勿挪做他用。
 
  下载:
http://www.linjian.cn/files/c_cpp/JavaLex_Java2Asm.rar
http://files.linjian.org/c_cpp/JavaLex_Java2Asm.rar



  原作业要求如下:
 
  一、Java语言词法分析器(JavaLex)
 
  用C++作为宿主语言完成Java语言词法分析器的设计和实现。具体要求为:使用DFA实现词法分析器的设计;实现对Java源程序中注释的过滤;词法分析结果属性字流存放在独立文件中;统计源程序每行单词的个数和整个源文件单词个数;具有报告词法错误和出错位置(源程序行号和该行字符)的功能。
 
  二、Java语言子集语法、语义分析器(Java2Asm)
 
  用C++作为宿主语言完成Java语言子集语法、语义分析器。具体要求为:完成下列文法描述的Java源代码的语法分析、语义分析及代码生成。代码生成的目标代码为MASM汇编代码;词法分析使用实验一完成的词法分析器(即与词法分析器有直接接口);通过测试用例的验收。
 

Java语言子集文法

 

<Sw>  while ( Er ) S ;

<Er>  Vc < Vc | Vc > Vc

<S>   标识符 = Ex ;              // 标识符使用词法分析识别结果

<Vc> 标识符 | 整常数            // 标识符、整常数使用词法分析识别结果

<Ex>  Vc P Ex | Vc

<P>   + | - | * | /              // 运算符必须符合优先级和结合性