Skip to content

Latest commit

 

History

History
245 lines (135 loc) · 20.2 KB

Think-C_01.md

File metadata and controls

245 lines (135 loc) · 20.2 KB

#第一章 编程方法

这本书以及这个系列的目的,是教你像计算机科学家一样去思考。我喜欢计算机科学家的思考方式,因为他们集合了数学、工程学和自然科学的一些最好的特性。与数学家一样,计算机科学家也使用形式语言表达想法(主要是计算机方面的)。与工程师一样,他们都设计事物,集成系统并权衡选择。与科学家一样,他们都研究复杂系统的行为,设定假设并测试。

计算机科学家最重要的一项技能是 解决问题的能力 。我对这个能力的理解是:确切地阐述问题,创新地思考解决方案,并且准确清晰地表达解决方案。这样的话,学习编程的过程就是一个很好的练习解决问题能力的机会。这就是本章命名为“编程方法”的原因。

首先,您将学习编程,这本身就是一个有用的技能。随着能力的提升,最终可以将编程作为一个解决问题的手段。让我们一起努力,结果会更好。

##1.1 什么是编程语言

现在您要学的编程语言是 C ,在 1970 年代早期,由贝尔实验室的 Dennis M. Ritchie 开发。C 是一种 高级语言 ;您可能还听说过其他高级语言 —— Pascal 、C++ 和 Java 。

根据”高级语言“这个名称您可能已经猜到,还有 低级语言 ,有时叫做机器语言或汇编语言。粗略的讲,计算机只可以执行用低级语言编写的程序。因此,高级语言编写的程序在运行之前必须被转换。转换的过程需要一点时间,这是高级语言的一个小缺点。

但是高级语言的优势很明显。首先,使用高级语言更容易编程;这里的“更容易”意味着花更少的时间编写,程序更短并且可读性更强,也更容易修改。其次,高级语言是可移植的,这意味着程序无需修改,或只做很小的修改就可以运行不同的计算机上。低级语言编写的程序只能运行在一种计算机上,要想在其他计算机上运行只能重写。

因为这些优势,几乎所有程序都是用高级语言编写的。低级语言只用在一些特殊的场合。

有两种方法对程序进行转换:解释编译 。解释器本身是一个程序,它读取高级语言程序并按照程序内容执行。实际上,解释器对程序逐行进行转换,读取一行就执行一行命令。

1-1

编译器也是一个程序,它会读取一个高级语言程序,然后一次性将其全部转换,并且不会执行。通常将编译程序作为一个单独的步骤,然后再执行编译后的程序代码。这种情况下,高级语言程序被称作 源代码,转换后的程序被称作 目标代码可执行程序

举个例子,假设你要用 C 语言写一个程序。你需要用一个文本编辑器编写程序(文本编辑器就是一个简单的文字处理器),写好后,要把它保存为 program.c 的文件,“program”是你自定义的名称,后缀 .c 是一个惯例,表示这个文件的内容是 C 源代码。

然后,根据你的编程环境,离开文本编辑器并且运行编译器。编译器会读取源代码,转换,并创建一个包含目标代码的名为 program.o 的文件,或者是包含可执行程序的 program.exe 文件。

1-2

下一步就是运行程序,这需要某种执行器。执行器的作用是加载程序(将程序从磁盘复制到内存)并让计算机开始运行这个程序。

虽然整个过程看起来有些复杂,但是在大多数编程环境中(有时也叫开发环境),这些步骤都是自动完成的。通常只需写一个程序,然后按一个按钮或输入一条命令来编译和执行之。另一方面,了解这些在后台执行的步骤是有用的,如果在这个过程中发生了错误,你就可以快速的定位问题。

##1.2 什么是程序

程序就是一组指令序列,用于指定怎样执行一种运算。这个运算可以使数学方面的,例如解方程式或找出多项式的根,也可以是其他意义的运算,例如在文档中搜索和替换文本或编译一个程序(很不可思议)。

上面提到的指令——以后我们把它称作 语句 (statement) ——看起来与编程语言不太一样,但是有些基本的操作是大部分语言都可以执行的:

  • input :从键盘、文件、或其他设备获取数据。
  • output :在屏幕上显示数据,或将数据发送到一个文件或其他设备。
  • math :运行基本的数学操作,例如加法和乘法。
  • testing :检测某些条件,执行合适的语句序列。
  • repetition :重复执行某些动作,通常每次执行时会有一些变化。

几乎所有的程序都是这样的。你用过的每个程序,无论多么复杂,它的语句组合都是执行这些操作。因此,描述程序的一种方法就是将大的任务分解为越来越小的子任务的过程,直到子任务简单得只需几条基础操作即可完成。

##1.3 什么是调试

编程是一个复制的过程,又因为由人工操作,难免发生一些错误。由于一些奇怪的原因(wikipedia),程序错误被称作 bugs,定位错误和纠正错误的过程叫做 调试(debugging) 。有下面几种错误会出现在程序中,分辨它们之间的不同之处可以帮助你快速找出它们。

###1.3.1 编译时错误

编译器只能对语法正确的程序进行转换;另外,编译失败的话就无法运行程序了。语法 是指程序的结构以及这些结构应遵循的规则。

例如,在英语中,一个句子必须以一个大写字母开始并以句号结尾。

对于大多数读者,文章中出现一点语法错误不是什么严重的问题,就像 E. E. Cummings 的诗,并不会因为语法错误而影响我们的阅读。

编译器可不会这么宽容。如果程序的任意一个地方有语法错误,编译器都会打印一条错误信息并停止编译,你也就不能运行程序了。

更严重的是,C 的语法规则比英语多得多,而且从编译器得到的错误信息并不是很有用。在你的编程生涯的最最初几周,可能要花大量的时间解决语法错误。但是随着经验的增长,你犯的错误会愈来愈少,找错误也会越来越快。

###1.3.2 运行时错误

第二种错误类型是运行时错误,因为这种错误直到你运行程序时才会发生。

C 不是一种 安全 的语言,例如 Java 就很少会出现运行时错误。C 语言允许你非常直接的操作计算机硬件。大量的运行时错误的出现是因为 C 语言提供了未经保护的内存读写功能。

对于我们未来几周写的简单程序,运行时错误是很少的,也许会在你不经意间出现。

###1.3.3 逻辑错误和语义

第三种错误类型是 逻辑语义 错误。如果你的程序里有逻辑错误,它会成功的编译并运行,这意味着计算机不会产生任何错误信息,但是它并不会做正确的事。确切的说,它会安装你所写的去做。

问题是,你写的程序未必符合你的意愿。这意味着程序(程序的语义)是错误的。寻找逻辑错误是很头疼的,因为要逆向工作,查看程序的输出,从中找出程序的问题。

###1.3.4 实验调试

本节你将获得一个最重要的技能就是调试 。虽然会让人感到挫折,但是调试是编程过程中最需要智商,最具挑战,并且最有趣的一部分。

某种程度上调试类似侦查工作。你要面对各种线索,然后推断出产生目前结果的过程和事件。

调试也是一门实验科学。一旦你有了一个想法,就要修改程序然后再试一次。如果你的假设是正确的,就能够预测修改后的结果,离一个可工作的程序就更近了一步。如果假设是错误的,就要想新的办法了。就像夏洛克.福尔摩斯所说的,“排除了所有的可能性之后,无论剩下的多么不可思议,那就是真相。”(出自阿瑟.柯南.道儿的《四签名》)

有些人认为,编程和调试没有区别。因为,编程是逐步将程序调试到你满意为止的过程。你总是用一个运行的程序来验证你的想法,然后做些小的修改,再根据你的想法调试,所有你一直有一个可运行程序。

例如,Linux 是一个操作系统,它包含了上千行代码,但是最初只是 Linus Torvalds 用于研究 Intel 80386 芯片的小程序。根据 Larry Greenfield ,“Linus 早期的一个项目是打印 AAAA 到 BBBB 的程序。之后就进化成了 Linux”(出自 Linux Users' Guide Beta Version 1)。

在以后的章节,我会给出更多的调试建议和编程实践。

##1.4 形式语言和自然语言

自然语言 是人类讲的语言,例如英语,西班牙语和法语。它们不是被特定的人设计的(虽然人们试图对它们施加影响);而是自然进化的。

形式语言 是人们为特定的应用而设计的。例如,数学家使用的标记就是一种形式语言,非常适用于表达数字和符号之间的关系。化学家用一种形式语言表示分子间的化学结构。最重要的是:

编程语言是一种被设计用于表达运算的形式语言

首先要说的是,形式语言侧重于严格的语法规则。例如,3+3=6 是一个语法正确的数学表达式,但是 3=+6$ 就不正确。还有,H2O 是一个语法正确的化学名称,2Zz 就不是。

语法规则涉及到两个部分,符号和结构。符号是语言基本的元素,例如单词、数字和化学元素。3=+6$ 的问题就是 $ 不是一个合法的数学符号(至少据我所知是这样)。类似的,2Zz 也不合法,因为没有 Zz 这个元素。

另一种语法规则涉及到语句结构;就是符号的安排方式。3=+6$ 在结构上是不合法的,因为不能在等号之后直接跟加号。与之类似,分子式中的下标必须在元素名称之后,而不是之前。

当你读一个英语句子或一条形式语言的语句时,你必须明白这个句子的结构(虽然在自然语言中这个行为是无意识的)。这个过程叫做 解析 (Parsing)。

例如,当你听到一个句子,“The other shoe fell,” 你清楚的知道“the other shoe”是主语,而“fell”是动词。一旦你解析了一个句子,就可以明白它的意思,也就是句子的语义。假设你知道 shoe 是什么,以及 fall 的意义,你就会明白这句换的含义。

虽然形式语言和自然语言有很多共同点——符号、结构、语法和语义——但是还有很多不同点。

  • 多义: 自然语言充满了多义词,读者要通过上下文语境和其他信息来理解它的意思。形势语言被设计成几乎没有多义,这意味着任何语句都只有一个意思,无需联系上下文。
  • 冗余: 为了弄清多义和减少无法理解的词语,自然语言充满了冗余。造成的结果就是很罗嗦。形式语言很少冗余并且很简洁。
  • 字面意义: 自然语言后很多成语和隐喻。如果我说,“The other shoe fell,”可能是 no shoe 或 nothing falling。形式语言就完全可以按字面意义理解。

讲自然语言长大的人往往很难适应形式语言。从某些方面讲,形式语言和自然语言的区别就像诗和散文,但更重要的是:

  • 诗: 在词语使用方面不仅注重意思更关心是否押韵,整首诗会产生情绪上的影响。诗中的多义很常见而且通常是故意的。
  • 散文: 词语的字面意思更重要,句子的结构也会表达很多意思。散文比诗更易解析,但是依然会有多义。
  • 程序: 计算机程序的意思很直白,完全没有多义,可以很方便地通过对符号和结构的解析来理解。

下面有几点关于阅读程序(以及其他形式语言)的建议。首先要牢记,形式语言比自然语言要紧凑的多,所以要花更多的时间来阅读。还有就是结构很重要,所以直接从上到下、从左到右地读可不是好主意。要学着在脑海中解析程序,分辨符号和结构。最后要记住,细节很重要。在自然语言中常出现的小错误,例如拼写错误和标点错误,在形式语言中会引起大问题。

##1.5 第一个程序

按照惯例,用新语言写的第一个程序叫做 “Hello,World”。因为它仅仅是打印一行 “Hello,World”。用 C 语言写是这样的:

#include <stdio.h>
#include <stdlib.h>

/* main: generate some simple output */

int main(void)
{
	printf("Hello,World.\n");
	return(EXIT_SUCCESS);
}

有些人会通过 “Hello,World” 程序的简洁程度来鉴定一种编程语言的品质。根据这个标准,C 的表现很不错。即使如此,这个简单的程序还是有几个地方难以想初学者解释。我们暂时先忽略它们,例如开头两行。

第三行以 /* 开头,以 */ 结束,这表示它是 注释 (comment)。注释就是可以在程序中插入的英文文本,通常用来解释程序的行为。当编译器看到一个 /* ,就会忽略与之匹配的 */ 之前的所有内容。

第四行,注意单词 main。 main 是一个特定的名称,表示程序执行的起始位置。当程序运行时,会从 main 中的第一行语句开始执行,依次向后,直到完成最后一条语句,然后退出。

main 中的语句数目是没有限制的,例子中只有一句。是一条 output 语句,意思是在屏幕上显示或打印一条信息。

在屏幕上打印信息的语句是 printf ,其中双引号之间的字符将被打印。注意最后一个字符 \n 。这个特殊的字符叫做 换行(newline),跟在一行文本的末尾,使得光标移动到显示文本的下一行。下次输出的时候,新的文本就会出现在下一行。语句的结尾是一个分号(;),每条语句必须以分号结尾。

这个程序中还有一些语法需要注意。首先,C 语言使用花括号( { 和 } )聚合语句。本例中,输出语句处在花括号中,表示该语句在 main 中。还要注意语句是有缩进的,这样有助于显示各行所处的位置。

现在就可以做到计算机前编译和运行这个程序了。至于具体怎样操作取决于你的编程环境,这里假设你已经搞定了。

正如我所言,C 编译在语法方面非常严格。如果你编程时犯了任何错误,都不会编译成功。例如,如果把 stdio.h 写错了,就会得到下面的错误信息:

hello_world.c:1:19: error: stdio.h: No such file or directory

这行包含了很多信息,当时这种密集的格式却让人难以解读。如果是一个更友好的编译器会这样说:

”On line 1 of the source code file named hello_world.c,you tried to include a header file named stdio.h. I didn't find anything with that name, but I did find something named stdio.h. Is that what you meant, by any chance?“

不幸的是,很少有编译器会这么宽容。编译器不会真的这么智能,大部分情况下,你得到的错误信息只是一些关于这个错误的暗示。你要花点时间来理解不同的编译器信息。

尽管如此,编译器还是可以成为一个学习语法规则的好工具。先写一个可以工作的程序(例如 hello_world.c),然后想方设法的修改它,看看会发生什么。如果得到了一个错误信息,尽量记住它的内容和原因,以后再遇到同样的信息你就知道它的意思了。

##1.6 词汇

解决问题(problem-solving): 发现问题,找到并阐述解决方案的过程。

高级语言(high-level language): 一类编程语言,例如 C ,设计时侧重于方便人类读写。

低级语言(low-level language): 一类编程语言,设计时侧重于方便计算机执行。也叫做机器语言(machine language)或汇编语言(assembly language)。

形式语言(formal language): 人们为特定目的设计的语言,比如表达数学思想或计算机程序。所有的编程语言都是形式语言。

自然语言(natural language): 自然发展的人们所讲的语言。

可移植(portability): 程序的一种属性,指程序可以在一种以上的计算机上运行。

编译(compile): 将高级语言转换成低级语言,同时,为稍后的执行做好准备。

源代码(source code): 用高级语言写的一个程序,并且没有被编译。

目标代码(object code): 编译器将一个程序转换后输出的文件。

可执行文件(executable): 目标代码的另一个名称,可以被执行。

字节码(byte code): C 语言程序的一种特殊的目标代码。与低级语言类似,但是想高级语言一样可移植。

语句(statement): 一个程序的一部分,指定了一个在程序运行时执行的行为。一条打印语句会把输出显示在屏幕上。

注释(comment): 一个程序的一部分,包含了关于程序的信息,但是对程序的运行没有任何影响。

演算法(algorithm): 用于解决一类问题的一个普通进程。

漏洞(bug): 程序中的错误。

语法(syntax): 程序的结构。

语义(semantics): 程序的意思。

解析(parse): 检查一个程序并分析语法结构。

语法错误(syntax error): 一种程序错误,会导致程序无法被解析(也无法编译)

异常(exception): 一种程序错误,导致程序运行时失败。与叫做运行时错误。

逻辑错误(logical error): 一种程序错误,导致程序做一些超出程序员预期的事情。

调试(debugging): 寻找并解决上述三种错误的过程。

##1.7 练习

Exercise 1.1

计算机科学家有一种令人讨厌的习惯,他们用常用的英文单词来表示一些不常用的意思。例如,在英语中,statement 和 comment 基本是一样的,但是当我们讨论一个程序时,它们的意思就完全不同了。

每章结尾处的词汇表的目的是列出重要的词汇并解释它们在计算机科学中的特殊含义。当你看到熟悉的单词时,不要想当然的认为你知道它们的意思!

  • a. 在计算机术语中,statement 和 comment 有什么区别?
  • b. ”一个程序是可移植的“意味着什么?
  • c. 异常是什么?

Exercise 1.2

继续学习之前,先解决怎样在你的环境里编译和运行 C 语言程序。有些环境提供给了像 1.5 节那样的简单程序。

  • a. 编写 ”Hello World“程序,然后编译并运行它。
  • b. 添加第二条打印语句,在 ”Hello World“ 之后打印第二条信息。比如说 ”Hew are you?” 再次编译并运行。
  • c. 在任意位置添加一行注释并重新编译。再次运行。新的注释不会对程序的执行产生任何影响。

这个练习可能看起来很弱,但这是编程的开始。为了能够调试程序,你必须熟悉你的编程环境。有些环境中,容易与运行中的程序失去联系,你还可能发现在你试图调试一个程序时却意外执行了另一个。添加打印语句是一种简单的在检查程序与运行程序之间建立联系的方法。

Exercise 1.3

想办法制造一些错误,然后看看会产生什么错误信息。有时编译器会明确的告诉你错误是什么,你要做的只是修复它。但是有时编译器会产生有误导的信息。你要培养一种意识,知道什么时候能够信任编译器,什么时候必须自己解决问题。

  • a. 删除一个右花括号(})。
  • b. 删除一个左花括号({)。
  • c. 删除 main 前面的 int 。
  • d. 用 mian 替换 main 。
  • e. 删除注释后面的 */ 。
  • f. 用 pintf 替换 printf 。
  • g. 删除一个括号:( 或 ) 。额外添加一个。
  • h. 删除 return 语句的分号。