第一章 程序的基本概念
- 程序和编程语言
程序是告诉计算机如何完成一个计算任务,这里的计算可以是数学运算,比如解方程,也可以是符号运算,比如查找和替换文档中的某个单词。
从根本上说,计算机是由数字电路组成的运算机器,只能对数字做运算,程序之所以能做符号运算,是因为符号在计算机内部也是用数字表示的;
此外,程序还可以处理声音和图像,声音和图像在计算机内部必然也是用数字表示的,这些数字经过专门的硬件设备转换成人可以听到、看到的声音和图像。
程序是由一系列指令(Instruction)组成,指令是知识计算机做某种动作的命令,通常包括以下几类:
输入(Input):从键盘、文件或者其他设备 获取数据;
输出(Output):把数据显示到屏幕或者存入一个文件,或者发送到其他设备;
基本运算:执行最基本的数学运算(加减乘除)和数据存储;
测试和分支:测试某个条件,然后根据不同的测试结果执行不同的后续命令;
循环:重复执行一系列操作。
编写程序可以说是这样一个过程:把复杂的任务分解成子任务,把子任务再分解成更简单的任务,层层分解,直到最后简单的可以用以上指令来完成。
编程语言(Programming Language)分为低级语言(Low-level Language)和高级程序语言(High-level Language)。机器语言(Machine Language)和汇编语言(Assembly Language)属于低级语言,直接用计算机指令编写程序。而C、C++、Java、Python等属于高级语言,用语句(Statement)编写程序,语句是计算机指令的抽象表示。
编程语言 |
表达形式 |
C语言 |
a = b + 1; |
汇编语言
|
mov 0x804a01c,%eax add $0x1,%eax mov %eax,0x804a018 |
机器语言
|
a1 1c a0 04 08 83 c0 01 a3 18 a0 04 08 |
汇编语言把机器语言中一组一组的数字用助记符(Mnemonic)表示,直接用这些助记符写出汇编程序,然后让汇编器(Assembler)去查表把助记符替换成数字,也就把汇编语言翻译成了机器语言。
C语言的语句要翻译成三条汇编或机器指令,这个过程称为编译(Compile),由编译器(Compiler)来完成,显然编译器的功能比汇编器要复杂的多。
C语言是可移植的(Portable)或者称为平台无关的(Platform Independent)。平台这个词由很多解释,可以指计算机体系结构(Architecture),也可以指操作系统(Operating System),也可以指两者的组合。不同的计算机体系结构由不同的指令集(Instruction Set),可以识别的机器指令格式是不同的,直接用某种体系结构的汇编或机器指令写出来的程序只能在这种体系结构的计算机上执行,然而各种体系结构的计算机都有各自的C编译器,可以把C程序编译成各种不同体系结构中的机器指令,这意味着C语言写出来的程序只需稍加修改甚至不用修改就可以在不同的计算机上编译运行。
各种高级语言都具有C语言的这些优点,所以绝大部分程序是用高级语言编写的,只有和硬件关系密切的的少数程序(例如驱动程序)才会用到低级语言。
编译执行的过程,首先你用文本编辑器写一个C程序,谭厚保存成一个文件,例如program.c(通常C程序的文件名后缀是.c),这称为源代码(Source Code)或源文件,然后运行编译器对他进行编译,编译的过程并不执行程序,而是把源代码全部翻译成机器指令,再加上一些描述信息,生成一个新的 文件,默认a.out,这称为可执行文件,可执行文件可以被操作系统加载运行计算机执行该文件中由编译器生成的指令。
解释执行的过程,有些高级语言以解释的方式(Interpret)执行,解释执行的过程和C语言执行的过程很不一样。例如编写一个Shell脚本script.sh,用Shell程序/bin/sh解释执行这个脚本:/bin/sh script.sh.这里的/bin/sh称为解释器(Interpreter),它把脚本中的每一行命令解释执行,而不需要生成包含机器指令的可执行文件再执行。
- 自然语言和形式语言
自然语言(Natural Language)就是人类讲的语言,比如汉语、英语和法语。这类语言不是人为设计(虽然有人试图加强一些规则)而是自然进化的。形式语言(Formal Languag)是为了特定应用而人为设计的语言。例如数学家用的数字和运算符号、化学家用的分子式等。编程语言也是一种形式语言,是专门设计用来表达计算过程的形式语言。
- 程序的调试
编程是一件复杂的工作,因为是人做的事情,所以难免经常出错。有时候调试是一件非常复杂的工作,要求程序员概念明确、逻辑清晰、性格沉稳,还需要一点运气。调试的技能我们在后续的学习中慢慢培养,但首先我们要区分清楚程序中的Bug分为哪几类。
编译时错误
编译时只能翻译语法正确的程序,否则将导致编译失败,无法生成可执行文件。对于自然语言来说,一点语法错误不是很严重的问题,因为我们仍然可以读懂句子。而编译器就没那么宽容了,只要有哪怕一个很小的语法错误,编译器就会输出一条错误提示信息然后罢工,你就得不到你想要的结果。虽然大部分情况下编译器给出的错误提示信息就是你出错的代码行,但也有个别时候编译器给出的错误提示信息帮助不大,甚至会误导你。在开始学习编程的前几周,你可能会花大量的事件来纠正语法错误。等到经验更丰富之后你就会觉得,语法错误是最简单最低级的错误吗编译器的错误提示也就那么几种,即使错误提示是有误导的也能够立刻找出真正的错误原因是什么。相比下面两种错误,语法错误解决起来要容易得多。
运行时错误
编译器检查不出这类错误,仍然可以生成可执行文件,但在运行时会出错而导致程序崩溃。我们在调试或者学习C语言的很多语法时,要区分编译时和运行时(Run-time)这两个概念,有些事在编译时做,有些事在运行做。
逻辑错误和语义错误
第三类错误是逻辑错误和语义错误。如果程序里有逻辑错误,编译和运行都会很顺利,看上去也不产生任何错误信息,但是程序没有干它该干的事情,而是干了别的事情。当然不管怎么样,计算机只会按你写的程序去做,问题在于你写的程序不是你真正想要的,这意味着成簇的意思(即语义)是错的。找到逻辑错误在哪里需要十分清醒的头脑,要通过观察小恒旭的输出回过头来判断它到底在做什么。
- 第一个程序
例:Hello World
将这个程序保存成main.c,然后编译执行:
gcc是Linux平台的C编译器,编译后在当前目录下生成可执行文件a.out,直接在命令行输入这个可执行文件的路径就可以执行它。如果不想把文件名叫,可以用gcc的-o参数自己指定文件名:
注意:main是一个特殊的名字,C程序总是从main里面的第一条语句开始执行的,在这个程序中是指printf这条语句。
注释用/* . . . */结构表示,编译器会忽略从/*到*/的所有字符。
语句的末尾以;(Semicolon)结束,下一条语句return 0也是如此。
C语言中用{}括号(Brance或Curly Brance)把语法结构分成组,用若干个(Blank)和Tab字符来缩进,漂亮的程序必须由整齐的缩进。
编译器对于错误是毫不留情的,如果你的程序有一点拼写错误,例如第一行写成了stdoi.h,在编译时会得到错误提示:
由些时候编译器的提示信息不是error而是warning,例如上例中的printf(“Hello,World.\\n”);改成printf(1);然后编译运行:
这个警告信息是说类型不匹配,但勉强还能配得上。警告信息不是致命错误,编译仍然可以继续,如果整个编译过程只有警告信息而没有错误信息,仍然可以生成可执行文件。
出警告信息说明你写的不够规范,可能有Bug,虽然能编译生成可执行文件,但程序的运行结果往往是不正确的,例如上面的程序运行时除了一个段错误,这属于运行时错误。各种警告信息的严重程度不同,像上面这种警告几乎一定表明程序中有Bug,而另外一些警告只表明程序写的不够规范,一般还是能正确运行的,**有些不重要的警告信息gcc默认是不提示的,但这些警告信息也有可能表明程序中有Bug。一个好的习惯是打开gcc的-Wall选项,也就是让gcc提示所有的警告信息,不管是严重的还是不严重的,然后把这些问题从代码中全部消灭。比如把上例中的printf(“Hello,World.\\n”);改成printf(0);然后编译运行:
编译既不报错也不报警告,一切正常,但是运行程序什么也不打印。如果打开-Wall选项编译就会报警告了:
如果printf中的0是你不小心写上去的(例如错误地使用了编辑器的查找替换功能),这个警告就能帮助你发现错误。虽然通常省略-Wall选项,但是强烈建议你写每一个编译命令时,都加上-Wall选项。