Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FrankHB/6399410 to your computer and use it in GitHub Desktop.
Save FrankHB/6399410 to your computer and use it in GitHub Desktop.
应作者要求,阅《轻松学习C程序设计——揭开计算机与程序设计的奥秘(修订版)》(资源通过网络找到),评论如下。
版权声明:本文使用 CC-BY-SA 3.0 发表。
  应作者要求,阅《轻松学习C程序设计——揭开计算机与程序设计的奥秘(修订版)》(资源通过网络找到),评论如下。
(此处找到的材料的节标号可能有错乱,按页面顺序评论。因此也不保证分节评论。)
1 计算机的重要性和基本工作原理
·作者使用“理想厨房系统”作为计算机系统的比喻。有以下一些问题:
  难以估计对于初学者来说这个比喻是否得当,有助于真正理解。
  厨房中突兀地出现了“地址传送带”“控制传送带”“IR”“PC”“R0”等和喻体不直接相关的生造抽象。这些抽象是否真容易理解?后面虽然有比较详细的说明,但细节并不十分直观且篇幅不小,可能对读者造成负担。
  语言风格问题。虽然总体比较清晰,但一些名词并不十分浅显易懂(相反容易觉得很“专业”——某种意义上事实的确如此)。如“取指周期”——具体什么意思,并没有在此直接解释。
·《理想厨房系统与计算机系统术语对照表》中“内存(又称为主存,包含很多大小相等的基本存储单元)”——错误。
  虽然通常说的内存往往是指主存,但实际上是两回事。
  在论述存储的体系结构时,习惯上“内存”指的是联机存储,和脱机存储即“外存”对立。
  很容易找到反例,如智能手机等设备常见的 Flash ROM 属于内存,但它不是主存。这些设备的主存使用 RAM 。
  此外,“内存”作为英文 memory 一词的翻译,也可以指一般的存储,没有特别的“内”的意思。只不过通常在 PC 等设备上程序必要的主要存储就是联机 RAM 主存抽象得到的,主存、内存和 RAM 三个概念有时候就会被混用。(后文有区分 RAM 和内存,但并没有完全论述清楚。)
·“一个字节(即8位)”——一个字节即 8 位也只是通常情况,不总是成立。
  下文“字节:一个8位的二进制的位串,就构成了一个字节(Byte)”在这个意义上显然是错误的。
  正式地说,在体系结构中的字节和八个位组成的八元组/八位组(octet) 是两个相对独立的概念。只是通常一个字节是 8 位容易导致一些错觉。
  习惯上,作为存储容量的计量单位,一个字节被默认为 8 位组成, ISO/IEC 80000-13 规范化了字节的这种含义。但是,更严格的场合中,如通信、编码等规范化时,会严格区分两个概念。
  特别地, C 语言明确允许一个字节大于 8 位。考虑到 C 语言是本书讨论的重点,在这里混淆这两个概念是不合适的。
·二进制数介绍比较详尽,但较复杂,初学者可能比较难接受。
·“操作系统又被称为系统软件”——系统软件是个比较笼统的概念,但显然不止包括操作系统。
·“(称为可执行程序,程序文件名的扩展名为.exe或.com)”——显然错误。文件扩展名只是特定环境(如操作系统)下的约定。文件是否表示可执行的程序,取决于文件的内容以及环境是否允许。
·本章内容比较多,可能缺乏一些典型的简单抽象,如冯·诺依曼结构只是一笔带过。对于读者而言,一口气理解大量术语和比喻(是否一定有助于理解还有疑问),很可能比记忆经典的简单抽象更困难,且更难以保证理解内容的准确性。
·“不过,Lisp,Forth这些函数式语言与以上所列这些命令型或面向对象型语言有较大的区别”—— Forth 不是典型的函数式语言,相反,它的主要范型是指令式/命令式(imperative) 的。
2 C语言的基本概念
2.1.2 C语言程序的一级构成成分——函数
·“一级构成”是无稽之谈。
·“参照本书末附录A的上机指导,在Windows操作系统环境下,使用VC++6.0编译并运行以下C语言程序(虽然这是一个C++语言的编译器,但该编译器也很适用于对C语言的源程序进行编译和调试。只不过要记住:C源程序的文件名一定要以“. c” 作为扩展名。注意:此处扩展名用的“c”是小写,不能用大写。如果你忘记了加上这个扩展名,该编译器就会将你的程序当作C++的源程序进行编译。”
  在扩展名上又一次的低级错误。注意 Windows 原生的文件系统( FAT 和 NTFS 等)上虽然可记录文件名中字母的大小写,但是把仅有大小写差异的文件名视为相同文件的文件名。所以 VC++ 区分大小写是完全没有必要的。
  通常类 UNIX 环境的文件系统严格区分文件名中字母的大小写,使用的编译器会 .C 作为 C++ 程序的扩展名。
·“一个C语言源程序类似于某个特殊的公司,main()函数的角色类似于公司的总经理(该公司的特殊性在与:每个员工所负责的工作都是互不相同的)”——尽管下文有表指出大致的对应关系,但缺乏直观性,略牵强。
2.2 C语言程序的二级构成成分——定义、语句、注释和预处理命令
·标题即体现抽象层次混乱。事实上这四个概念中没有任何两个是并列的。而且,从严格规定的翻译阶段(phase of translation) 来看,注释和预处理指令比上文所谓的“一级”成分函数更高。因此这是显然错误的。
·“其中的PI是一个符号常量”——符号常量是个现时不常用的老旧说法,不是 C 的正式概念,且容易引起混淆。(什么是“常量”?)
·“这是一条 本 章 将要重点讲解的赋值语句。”—— C 并没有单独的赋值语句。
·使用了 C99 引入的单行注释“ // ”,但和本书其它部分强调 C89 和 C99 的区别不同,这里没有提到 C99 。(虽然 VC++ 到 2013 年都没完全支持 C99 ,不过这个倒是有扩展支持了。)
  此外,关于标准的版本,正确说法是 ANSI C89 的正文被接受为 ISO C90 ,而 ISO C99 在 2000 年被接受为 ANSI C 标准。之后标准一般直接称为 ISO C 。本书所谓的 ANSI C99 的提法不合适。
·“C语言程序的二级主要构成成分,分为两大类:定义序列和语句序列。在函数体中,定义序列在前,语句序列在后”——显然错误,并且无视了声明的重要性。
·“高级语言源程序中的定义,是用来定义变量和定义(用户自定义的)类型的”——漏了函数,非常不妥。下面“每一条定义都要以分号结束”显然也是错的——考虑函数定义。
·“高级语言源程序中的语句,其实就是用来告诉编译程序:我们想要计算机对于源程序中以变量或常量形式出现的数据,执行什么样的运算(算术运算还是逻辑运算等等)和操作(取数、存数、输入、输出);或者,我们想要计算机根据哪个表达式的计算结果,去选择下一条要执行的语句(这句话,你或许要学了选择结构这一章以后,才能真正懂得)。”
  这一段再次暴露了抽象的混乱。实际上,在 C 这个层次上,运算完全可以归类为操作的一种,并不需要区分存取和算术操作——而这些操作的语义蕴含于表达式(不是语句)的求值中。
  而 ISO C 使用的方法是定义一个抽象机,操作表现为这个抽象机的行为。存取可能是表达式求值包含的副作用(具体地,对外部环境的 I/O 操作和向对象存储值一定是副作用, volatile 左值的读取也是副作用)。
·“C语言源程序的二级次要构成成分是:注释、编译预处理命令和声明(只是对在别处出现的定义,起着辅助说明作用。请参见函数一章)”——错误。直接无视了声明作为定义,相当于篡改了“声明”的概念。
·“命令型(或称为过程型)高级语言源程序的本质”——命令型和过程型不是一回事。关于这点在 http://tieba.baidu.com/p/1912906851 已经有过澄清。
·“与大多数其他高级语言不一样,在编译C源程序之前,都必须事先运行一个编译预处理程序”——错误,不在所有情况下适用。尽管完整的翻译阶段包含预处理,但是预处理后的中间程序可不需要再次预处理,且仍然可以是 C 源程序。
·“对源程序进行一些(通常是少量的)辅助性的插入、替换和编辑工作。”——实际情况通常很不“少量”。可以观察 GCC 等常用实现的预处理后的程序,比较一下和原始代码的大小。
·“但也有一些编译预处理命令——比如条件编译命令——是可以书写在函数体内部的”——这里的说法比较含糊。预处理本身不禁止指令和函数的顺序,事实上通常预处理器的实现无视函数。尽管不是常规做法, #include 等指令写在函数体内部也可行。
2.1.6 C语言源程序的编写、编译、链接和调试过程(参见附录A)
·这里的流程和 VC++6 实际所做的不一样。例如, nmake 的调用被无视了。虽然可以理解这样描述是为了方便新手阅读,但接下来关于编译器的行为也是很含糊的。
  之前本书有提到“编译程序(又称为编译器)”——实际上这个说法是不严格的。典型的实现中,供用户通过命令行接口调用有若干个程序(可执行程序或库),它们可以总称为编译器。其中一个程序称为编译器驱动程序(compiler driver) ,负责调用其它程序完成预处理等,有时候也被称为编译器。
  对于 VC++ 来说, C/C++ 编译器的驱动程序一般是 cl.exe 。 VC++ 中 cl 调用了附带的动态链接库完成翻译的不同阶段,一般笼统地称为编译——而链接则是 link.exe 完成的。
  对于 GCC 来说,由于按 *NIX 传统倾向于直接调用分别调用各个静态链接的可执行程序而不是动态库,这点更加明显:可以很容易观察到占用最多时间的往往是实际执行代码生成工作的程序(例如 MinGW 和 Cygwin 下的可执行文件名是 cc1plus.exe )。
·“不走弯路,通过一本书就能真正掌握编程的基本思路和技巧,就是最短最快的捷径”——这个有 Peter Norvig 吐槽大概够了:http://tieba.baidu.com/p/2563465238 。
2.3 C语言源程序的正文部分
·所谓的“正文部分”仍然是生造的概念。
2.4 C语言的字符集
·“C语言源程序的全部正文部分,都只能够使用如下所列举的字符来构成”——其它地方说的好好的 C99 呢?
  此外,标题党。这里所说的实际上是基本源字符集(basic source character set) 。明明标题是“ C 语言的字符集”,另一个重要的基本执行字符集(basic execution character set) 却一点不提。
  全角和半角也有乱掉的情况(下略)。
2.5 标识符
·“在高级程序设计语言中,我们通常用标识符来命名,我们想要用计算机进行加工的其数值可以变化的数据——变量(→)、不可变化的一部分数据——符号常量”——又来了个“符号常量”的混沌说法。
2.6 关键字
·“关键字”有的教科书又称为“保留字”——这里补充一下,对于 C 来说问题不大,但有些语言后者范围更大:如 Java 的 goto 是保留字但不是有意义的关键字;再如 C++ 的 and 等 alternative token ,尽管不是正式说法,也可被称为保留字。
·“切记:不要将关键字作为普通的标识符来定义和使用”——意思容易理解,但是说法不严谨(考虑到后面提到了“分隔符”之类的词法问题,这里的漏洞更加严重)。
  在 C 语言中,术语“标识符(identifier) ”出现在两个地方:一种是作为预处理记号(preprocessing-token) ,此处只有标识符没有关键字;另一种是记号(token) ,其中包括关键字和标识符等。
  即预处理之前的标识符被预处理后分化为关键字、标识符和其它记号。
2.7 分隔符
·标题的分类是胡扯。无论是 seperator 还是 delimiter 都不像。
·“C语言字符集中的空格,逗号,回车/换行(ASCII码为13)这三个字符在源程序中起着分隔的作用”——词法和语法混起来讲先不论,制表符的存在感呢?
·“在同类项之间作分隔:要用逗号,空格则可加可不加”——什么叫“同类项”?后面有例子,但是比较笼统——似乎只是说了变量名之间算同类项,但又无法让读者知道是不是有其它适用的外延。
2.8 常量
·“A.整型常量 567,-425 ,0 等, 是没有小数分量的数值”——整型常量?是指整数常量(integer-constant) ?负号是什么情况(看来又是谭×流么)?
·“B.实型(浮点型)常量”——“实型”看来又是谭×流的说法。
·浮点数十六进制表示和二进制阶码果然没存在感。
·“而是通过采用编译预处理命令中的宏定义的符号常量(→)来处理此事。即用标识符来命名的常量”——完全胡扯。常量(constant) 的语法里根本就没有这号玩意儿。
2.9 变量
·“比如register int num; 编译器就很有可能将变量num的存储单元安排在CPU的寄存器中)”——现代的编译器比较少理会 register 关键字,因为往往编译器比用户更清楚到底是不是存进寄存器里比较高效( C 还好点, C++11 直接 deprecated 掉了)。
·“给要使用的变量起名字,正式的术语称为定义变量”——胡扯。“extern int i;”显然不是定义,但它就是取了个名字。
·“对于简单类型的变量”——没说清什么是“简单类型”,大概是想说声明符可以放在要声明的标识符一侧,不用顾及函数或数组的声明符。当然 C 在这里是比较坑。
·“变量名一般要用标识符来命名”——难道还可以不用标识符命名?
·“任何一个int 型的整型变量,在VisualC++ 6.0编译环境下都被编译程序分配了4个字节的内存空间作为存储单元;在Turbo C 2.0编译环境下被分配了2个字节的内存空间作为存储单元。”——这种事无巨细的说法是很糊弄的风格。如果整个优化掉了呢?
·“在程序运行中,以实数值形式(即有小数分量)出现的变化着的数值(比如34.1,-678.34等)”——无理数数算不算有小数分量的实数?
·“单精度浮点型变量的有效位数是十进制的7位”——并非刚好 7 位。
·“任何字符型的变量,在内存空间都被编译程序分配了一个字节的内存存储单元,用来存放该变量所对应字符的ASCII码(大多其他高级语言也都是这样”——又是常见错误。
  上面提到过了, C 有基本执行字符集。这决定了运行时存储的整数和所表示的字符的关联。
  更重要的一点是,无论是基本源字符集还是基本执行字符集,都只是说要至少包含哪些字符,没明确具体实现为什么字符集什么编码。
   ASCII 只是最常见的实现。一个不兼容的 ASCII 的例子是 EBCDIC 。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment