计算机系统概论第十一章.docx
- 文档编号:7624233
- 上传时间:2023-01-25
- 格式:DOCX
- 页数:13
- 大小:29.53KB
计算机系统概论第十一章.docx
《计算机系统概论第十一章.docx》由会员分享,可在线阅读,更多相关《计算机系统概论第十一章.docx(13页珍藏版)》请在冰豆网上搜索。
计算机系统概论第十一章
第十一章C程序设计简介
11.1我们的目标
恭喜你,欢迎来到本书的第二部分!
你刚刚完成了有关现代计算机系统的基础底层结构的介绍。
有了坚固的基础知识,现在,你已经做好学习基本的高级语言程序设计的准备了。
在本书的第二部分,我们将根据C语言来讨论高级语言程序设计的概念。
在每一步中,对于新的高级语言的概念,我们将能够与底层的计算机系统建立联系。
在这个观点上,一切都将不再神秘。
我们自底向上的研究计算机系统,是为了揭示出,当计算机在执行你编写的程序时确实毫无神奇可言。
我们相信消除了神秘感,你将更快、更深入地理解程序设计的概念,进而成为更好的程序员。
让我们开始快速的回顾第一部分。
在开头的10章里,我们描述了LC-3,一个拥有复杂的、真实的计算机的所有重要特点的简单计算机。
在LC-3的设计的背后(事实上,在所有现代计算机的背后),都存在着这样一个基本的思想:
简单元件被系统地相互连接,从而形成更复杂的设备。
MOS晶体管被连接起来,构建逻辑门。
逻辑门被用来构建存储器和数据通路元件。
存储器和数据通路元件相互连接,构建了LC-3。
这种系统的连接简单元件进而创造出更复杂的设备的重要概念将贯穿计算的始终,不仅在硬件设计中,也在软件设计之中。
就是这种简单的设计思想,使我们能够创造出总体上非常复杂的计算系统。
在描述了LC-3的硬件之后,我们描述了怎样使用它本身的机器语言,即0和1进行程序设计。
在尝到用机器语言程序设计时的易出错和不自然的处理过程后,我们很快转移到更加用户友好的LC-3汇编语言。
我们描述了怎样将一个程序设计问题系统地分解成可以容易的在LC-3上编码的片段。
我们检查了低级的自陷子程序如何代表程序员执行普遍需要的任务,例如输入和输出。
系统分解和子程序的概念不仅仅对汇编级的程序设计是重要的,对高级语言程序设计也是重要的。
在这本书结束之前,你将继续多次看到这些概念的例子。
在本书的这一半中,我们的主要目标是介绍基本的高级程序设计概念——变量,控制结构,函数,数组,指针,递归,简单数据结构——并且为了解决程序设计问题教授一种好的解决问题的方法。
我们做到这些的主要传达手段就是C语言。
我们的目标不是提供对C语言的全面介绍,而仅仅是提供一部分基本要点让程序设计初学者了解程序设计的基础并且能够写出相当复杂的程序。
对于在正文中没有介绍的C语言的一些方面感兴趣的读者,我们在附录D中提供了对这种语言更加全面的一个描述。
在这一章中,我们将从低级汇编语言程序设计过渡到高级语言C语言程序设计。
我们将解释高级语言为什么产生,它们为什么重要,以及它们如何和计算系统的底层相互作用。
我们将通过一个简单的示例程序开始学习C语言。
我们用这个例子指出一些你需要知道的重要的细节,以便你开始编写自己的C代码。
11.2填补空白
随着计算机硬件变得更快、处理能力更强,软件应用也变得更加复杂。
新一代的计算机系统产生出比前几代更强大的新一代软件。
随着软件变得更加复杂,开发工作也变得更加困难。
为了避免程序员被迅速压垮,保证程序设计过程尽可能的简单就十分急迫。
对此过程中的任何部分的自动化(即,让计算机做一部分该工作)都是一个受欢迎的进步。
当我们从第五、六章的LC-3的机器语言转换到第七章的LC-3的汇编语言时,无疑你会发现并欣赏汇编语言是如何简化LC-3的程序设计。
0和1变成了助记符,存储器地址变成了符号标记。
指令和存储器地址都被以一种更适用于人使用而不是机器的形式展现出来。
在层次转换时,汇编器填补了算法层和ISA层之间的空白(见图1.6)。
对于语言层来说,填补更多空白将更令人期待。
高级语言正是这么做的,它们有助于使程序设计变得更容易。
让我们看看它们是以哪些方式提供帮助的。
●高级语言允许我们给数值一个符号名
当使用机器语言进行程序设计时,如果我们想记录一个循环的重复次数,我们需要留出一个存储单元或一个寄存器来存储计数器的值。
为了读取这个计数器,我们需要记住我们刚刚把它存储在哪一点。
而使用汇编语言,由于我们可以给计数器的存储单元分配一个有意义的标记,所以这个过程就更容易了。
使用像C那样的高级语言,程序员仅需分配一个名字给该数值(并且,我们之后会看到,还要提供一个类型),程序设计语言负责为它分配存储单元,当程序员引用它时,程序设计语言执行适当的数据传送操作。
既然大部分程序都包含了许多数值,使用这样一种便利的处理数值的方式确实是极其有用的进步。
●高级语言提供了可表达性
大多数人在描述现实世界中的对象的交互时,比描述数字世界中像整数、字符和浮点数这类对象的交互更轻松。
因为高级语言的用户友好的倾向,它们使得程序员更富于表现力。
在高级语言中,程序员可以用更少的代码表达更复杂的任务,这些代码本身看起来更像人类语言。
例如,如果我们想计算一个三角形的面积,我们可以简单的写为:
area=0.5*base*height;
另一个例子:
我们经常写代码来测试一个条件,如果条件为真做某件事,如果条件为假做另外一件事。
在高级语言中,像那样普便的任务可以用类似英语的形式简单的被表达出来。
例如,如果条件isItCloudy为真时,我们想要get(Umbrella),如果为假,则是get(Sunglasses),在C语言中,我们可以使用如下C控制结构:
if (isItCloudy)
get(Umbrella);
else
get(Sunglasses);
●高级语言提供了对底层硬件的抽象
换句话说,高级语言提供了独立于底层ISA或硬件的一个统一接口。
例如,程序员经常会做一些指令集不能自然支持的运算。
在LC-3中,没有执行做整数乘法的指令。
而是LC-3汇编语言程序员必须写一小段代码来执行乘法运算。
高级语言所支持的运算集通常比ISA所支持的大。
无论程序员何时使用它,高级语言都将生成必要的代码去执行操作运算。
知道这些高级运算将被正确地执行而不必去处理低级的实现过程,程序员就能专注于实际的程序设计任务。
●高级语言增强了代码的可读性
由于普遍的控制结构使用简单的,类似于英语的语句被表达出来,程序本身变得更加易读。
一个人查看高级语言编写的程序,与用汇编语言写的程序相比,可以注意到循环和判定结构,可以用更少的力气理解代码。
无庸置疑,你会发现代码的可读性在程序设计时是相当重要的。
作为程序员,我们经常会被分配给调试或基于其他人的代码进行程序设计的任务。
如果语言的组织是用户友好的,理解其中的代码会是一项比较简单的任务。
●许多高级程序提供了防止出错的安全措施
通过让程序员去遵守一套严格的规则,高级语言能在程序被翻译或运行时进行检查。
如果某些规则或条件被违反,一条错误信息将指示程序员到错误可能存在的代码中的那点。
用这种方式,高级语言帮助程序员更快地进行他(她)的程序设计工作。
11.3翻译高级语言程序
正如LC-3汇编语言程序需要被翻译(或者更专业地说,被汇编)成机器语言一样,所有高级语言写的程序也是如此。
毕竟,底层的硬件只能执行机器代码。
这些翻译如何被完成取决于特定的高级语言。
一种翻译技术被称为解释。
使用解释技术,一种叫做解释器的翻译程序读入高级语言程序,执行程序员指示的操作。
高级语言程序不直接执行而是被解释程序执行。
另一个技术叫做编译,叫做编译器的翻译程序完成将高级语言程序翻译为机器语言的工作。
编译器的输出被称为可执行映像,它可以直接在硬件上执行。
紧记一点,解释器和编译器本身都是运行于计算机系统上的程序。
11.3.1解释
使用解释,一个高级语言程序就是一组提供给解释器程序的命令。
解释器读命令,然后根据语言的定义进行执行。
高级语言程序并不是直接被硬件执行,事实上只是解释器的输入数据。
解释器是一个执行程序的虚拟机。
许多解释器一次翻译高级语言程序的一段,一行,一条命令或一个子程序。
例如,解释器可能读入高级语言程序的一行,直接在底层硬件上执行那一行。
如果这行为:
“取B的平方根,把结果存入C”,解释器将会通过执行计算机的ISA中的正确的指令流来计算平方根。
一旦当前的行被处理完,解释器就会移到下一行并执行它。
这个过程直到整个高级语言程序被执行完才会结束。
通常被解释的高级语言包括LISP,BASIC和PERL,有特殊目的的语言倾向于使用解释技术,比如被称为Matlab的数学语言。
LC-3的模拟器也是一个解释器。
其它的例子包括UNIX的命令解释程序。
11.3.2编译
另一方面,使用编译,高级语言程序被翻译成可以在硬件上直接执行的机器代码。
为了有效的做这件事,编译器必须在翻译之前将源程序当作大的单元(通常是整个源文件)来分析。
一个程序只需要被编译一次就可以多次执行。
许多程序设计语言,包括C,C++,FORTRAN都是典型的编译。
LC-3的汇编器就是一个基本的编译器的例子。
编译器处理包括高级语言程序的文件,然后生成一个可执行映像。
编译器并不执行程序(虽然有些复杂的编译器为了更好的优化它的性能而执行程序),它只是把高级语言翻译成计算机本身的机器语言。
11.3.3从正反两方面辩论
任何一种翻译技术都既有优点,又有缺点。
使用解释,开发和调试程序通常更容易。
解释器经常允许一次执行程序的一段(例如,一行)。
这就允许程序员查看中间的结果,并且在执行时修改代码。
通常使用解释,调试也更容易。
解释的代码更易于在不同的计算系统间移植。
然而,使用解释,程序执行起来需要花更多的时间,因为有一个中间媒介——解释器实际在做工作。
在编译器的帮助下,程序员可以制造执行得更快的代码,并且更有效的使用存储器。
因为编译产生更高效的代码,绝大多数商业化生产的软件趋于使用编译技术。
11.4C程序设计语言
C程序设计语言是在1972年被DennisRitchie在贝尔实验室开发出来的。
C语言被开发用作编写编译器和操作系统,正因为这个原因,这门语言有一个低级的倾向。
它允许程序员以一个非常低的层次来处理数据,然而它也提供了高级语言的可表达性和便利。
正是由于这些原因,C语言直到今天都被广泛使用,而不只是用于开发编译器和系统软件的语言。
C程序设计语言在程序设计语言的进展中有其特殊的地位。
图11.1提供了一些比较重要的程序设计语言的发展的时间线。
从1954年第一个高级程序设计语言FORTRAN的出现开始,每个后期的语言都尝试着改正它的前期语言的问题。
然而要完全的跟踪一个语言的母体是有点困难的(实际上,一个人可以肯定的说所有先前的语言对某种特定的语言都有些影响),有一点很清楚就是C语言对C++和Java都有着直接的影响,而这两者都是当今比较重要的语言。
C++和Java也被Simula语言和它的早期语言所影响。
C++和Java的面向对象的特性来自于这些语言。
如果我们用C++和Java编程,几乎我们在本书中讨论的有关C语言的所有方面都会是相同的。
一旦你理解了本书中这一部分的概念,C++和Java都会更容易掌握,因为它们都类似于C。
因为它的那些低级的方法和它对其他当前重要的语言的根本影响,C是我们自底向上探索计算系统所选择的语言。
C语言允许我们把底层与基本的高级程序设计语言的概念的讨论清晰的联系起来。
一旦这些更基本的概念被理解后,学习更多的高级概念,如面向对象程序设计,就更容易向上飞跃。
本书中出现的所有C语言的例子和特殊细节都是基于一个叫做ANSIC的C标准版本上的。
就像许多程序设计语言一样,C语言在这些年出现了几个不同的版本。
在1989年,美国国家标准协会(ANSI)提出了一个“明确的与机器无关的C语言的定义”来标准化这一流行的语言。
这个版本被称为ANSIC。
ANSIC被大多数C编译器所支持。
为了编译和验证本书的样例代码,使用一个遵从ANSIC的C编译器是必要的。
11.4.1C编译器
C编译器是从C语言源程序翻译到可执行映像的典型模式。
回忆7.4.1节中,可执行映像是准备加载到存储器并被执行的程序的机器语言表示。
整个编译过程包含预处理器,编译器本身和链接器。
通常,整个机制被碰巧称为编译器,因为当我们使用C编译器时,预处理器和链接器通常被自动调用。
图11.2显示了编译过程是如何被这些组件处理的。
预处理器
正如它的名字所暗示的,C预处理器在把C程序传入编译器之前先对它进行预处理。
C预处理器从头至尾扫描源文件(源文件包含真正的C程序),寻找预处理指令,并根据预处理指令来执行。
这些指令与LC-3汇编语言中的伪操作相似。
它们以某些控制方式指示预处理器转换C源文件。
例如:
我们可以指示预处理器用字符串30代替字符串DAYS_THIS_MONTH,或指示它在当前的行内向源文件里插入文件stdio.h里的内容。
我们将在随后的章节中讨论这些操作为什么有用。
所有的预处理器指令都以磅字符#开头。
一个有用的C程序以某种方式依赖于预处理器。
编译器
在预处理器对输入的源文件做了转换之后,程序已做好传入编译器的准备。
编译器把预处理过的程序转换为一个目标模块。
回忆7.4.2节,一个目标模块是整个程序的其中一段的机器码。
编译有两个主要阶段:
分析,源程序被分解或者分析为其组成部分;和合成,生成程序的机器代码版本。
分析阶段的工作是读入、分析和构造原始程序的内部表示。
合成阶段生成机器码,并且如果需要的话,试图优化代码,使得在即将运行的计算机上执行得更快更高效。
典型的,这两个阶段都被分成执行特定任务,如分析、寄存器分配,或指令表的子阶段来完成。
一些编译器生成汇编代码,并使用汇编器去完成到机器代码的翻译。
编译器在翻译程序时使用的最重要的内部簿记机制是符号表。
符号表是编译器的内部簿记方法,用来记录程序员在程序中使用的所有符号名。
C编译器的符号表非常类似于LC-3所维护的符号表(见7.3.3节)。
我们将在下一章中详细查看C编译器的符号表。
链接器
在编译器把源文件翻译成目标代码后,由链接器接管工作。
链接器的工作是把所有的目标模块链接形成程序的可执行映像。
可执行映像是一个能够被加载到存储器中,并被底层硬件所执行的程序版本。
例如,当你在你的PC上单击浏览器图标时,你就是在命令操作系统从你的硬盘驱动器中读取浏览器的可执行映像,加载进存储器中,并开始执行。
通常,C程序依赖于库程序。
库程序执行普遍而有用的任务(如I/O),为系统软件(例如,操作系统和编译器)的开发者的通用目的而准备。
如果一个程序用到了库程序,那么链接器会查找与程序相对应的目标代码,并把它链接进最终的可执行映像中。
你对链接库目标的过程不应该感到陌生,我们在9.2.5节,在有关LC-3的内容里描述过这个过程。
通常,库目标根据计算机系统被保存在一个特定的地方。
例如,在UNIX中,许多通用的库目标可以在/usr/lib目录中找到。
11.5一个简单的例子
我们现在准备开始讨论C语言程序设计的概念。
我们呈现的许多新的C的概念都结合了被一个“假想的”LC-3的C编译器生成LC-3代码。
在一些情况下,我们将描述当这些代码被执行时究竟发生了什么。
记住一点,你不可能使用一台基于LC-3的计算机,而只能是基于真正的ISA,如x86。
例如,如果你使用一个基于Windows的PC机,很可能是你的编译器会生成x86代码,而不是LC-3代码。
我们提供的许多例子都是可以编译和执行的完整的程序。
为了更清楚地说明,我们提供的一些例子并不是十分完整的程序,在编译前需要补充完整。
为了达到目的,我们把这些部分代码称为代码片断。
让我们从一个简单的C的例子开始。
图11.3显示了它的源代码。
我们将利用这个例子,通过指出一个典型的C程序的一些重要特点,从而开始C语言的学习过程。
这是一个简单的例子:
它提示用户输入一个数字,然后它会从该数字开始向下计数直到零。
鼓励你编译和执行这段程序。
此时,彻底理解每一行的目的并不重要。
然而这个例子的某些方面有助于你写出自己的C代码,以及理解本书中的接下来的例子。
我们将注意到如下四个方面:
main函数,代码注释和编程风格,预处理指令,以及I/O函数调用。
11.5.1main函数
main函数从包含intmain()的那行(17行)开始,到这段代码的最后一行的大括号结束。
源代码中的这些行组成了名为main函数的函数定义。
在LC-3汇编语言程序中被称为子程序的部分(在第9章中讨论的),在C语言中被称作函数。
函数是C语言中十分重要的一部分,我们将要用整个第14章去研究它们。
在C语言中,main函数起一个很特殊的作用:
它是程序开始执行的地方。
因此,每一个C程序都需要一个main函数。
注意,在ANSIC中,main必须被声明返回一个整数值。
也就是说,main必须是int类型的,因而代码的第17行为intmain()。
1/*
2*
3*程序名称:
倒序计数,我们的第一个C程序
4*
5*描述:
这个程序提示用户输入一个正整数,
6*然后从这个数往下计数到0,
7*并且把每个数字显示出来。
8*
9*/
10
11/*下两行是预处理指令*/
12#include
13#defineSTOP0
14
15/*函数:
main*/
16/*描述:
提示输入,然后倒序显示出来*/
17intmain()
18{
19/*变量声明*/
20intcounter;/*保存中间计数值*/
21intstartPoint;/*向下计数的起始值*/
22
23/*提示用户输入*/
24printf("=====CountdownProgram=====\n");
25printf("Enterapositiveinteger:
");
26scanf("%d\n",&StartPoint);
27
28/*从输入的数向下计数到0*/
29for(counter=StartPoint;counter>=STOP;counter--)
30printf("%d\n",counter);
31}
图11.3提示用户输入一个十进制整数,并从该数向下计数到0的程序
在这个例子中,main函数的代码(即大括号之间的代码)可以被分解成两部分。
第一部分包含了函数中变量的声明。
两个变量,一个称为counter,另一个称为startPoint,被创造以用于main函数中。
变量是高级程序设计语言提供的一种十分有用的特征。
它们提供给我们一种在程序中用符号命名数值的方法。
第二部分包含了函数的语句。
这些语句表达了当函数被执行时会发生的行为。
对于所有的C程序,执行都从main开始,一条语句接着一条语句地处理,直到main中的最后一条语句被完成。
在这个例子中,第一组语句(第24—26行)显示一条信息,提示用户输入一个整数。
一旦用户输入一个整数,程序就进入到最后一句,那是一个for循环(我们将要在第13章讨论的一种重复结构)。
这个循环从用户输入的数字往下计数直到0为止。
例如,如果用户输入的数字是5,程序输出如下所示:
=====CountdownProgram=====
Enterapositiveinteger:
5
5
4
3
2
1
0
注意,在例子中有许多行源代码以分号结束。
在C中,分号被用来结束声明和语句,它们对于编译器把程序明确的分解为其组件是必须的。
11.5.2格式,注释和风格
C语言是一种自由格式的语言。
那就是说,程序中单词之间和行之间的空格数量不会改变程序的意图。
在遵守C的语法规则的前提下,程序员可以以他/她看上去适当的任意方式自由地构造程序。
程序员可以自由的采用使程序更易读的方式来格式化代码。
在示例程序中,注意到for循环以这种方式缩进,使得被循环的语句更容易识别。
在该例子中,还要注意在main函数中使用空行来分隔开不同的代码区。
这些空行不是必须的,只是被用来提供代码的可视化分割。
通常,在一起完成一个更大的任务的语句被组合成视觉上可识别的单元。
在本书中的C代码的例子都是采用按照惯例的典型的C的缩进风格。
风格是可变的。
程序员有时把风格用作一种表达的方式。
自由地定义你自己的风格,记住目的是通过格式来帮助传达程序的意图。
C语言里的注释和LC-3中的不同。
C的注释以/*开始以*/结束。
它们可以跨越多行。
注意,这个示例程序包含了许多行注释,一些是单行的,一些跨越多行。
在不同语言中,注释以不同方式表示。
例如,C++中的注释还可以以序列//开始,延伸至该行末尾。
不管注释如何表示,其目的总是相同的:
它们为程序员提供了一种以人类语言来描述代码含义的方法。
恰当的代码注释是编程过程中非常重要的一部分。
好的注释增强了代码的可读性,允许对代码不熟悉的人理解得更快。
因为编程任务经常是团队工作,代码通常在程序员间被共享或借用。
为了使编程团体的工作更高效,或者是为了写一些值得共享的代码,你必须先采用一种好的注释风格。
好的注释风格的一个方面,就是在每个源文件的开头提供信息,描述包含在其中的代码,最后一次修改的日期,以及由谁修改。
而且,每个函数(见示例中的main函数)都应该有一个简洁的描述,关于这个函数能完成什么,以及关于它的输入和输出的描述。
而且,注释通常被点缀在代码中以解释代码不同片段的目的。
但是过多的注释是有害的,因为它会搞乱代码,使它更难读。
特别要当心那些不能提供除了代码中明显的信息之外的额外信息的注释。
11.5.3C预处理器
在11.4.1节中我们简明地提及了C预处理程序。
回忆原始C程序被传入编译器之前,它转换了原始C程序。
我们的简单例子包含了两个常用的预处理指令:
#define和#nclude。
本书中的C示例只依赖于这两个指令。
#define指令是一个简单而又有用的指令,能命令C预处理器用文本Y代替匹配X的任意出现的文本。
也就是说,宏X被Y替代。
在例子中,#define使STOP被0替代。
所以下面的源文件行
for(counte=startPoint;counter>=STOP;counter--)
被转换(内部地,只在预处理器和编译器之间)为
for(counte=startPoint;counter>=0;counter--)
它为什么有帮助?
通常,在一个程序内,#define指令被用于创造固定的数值。
下面是几个例子:
#defineNUMBER_OF_STUDENTS25
#defineMAX_LENGTH 80
#defineLENGTH_OF_GAME 300
#definePRICE_OF_FUEL 1.49
#defineCOLOR_OF_EYES brown
所以,例如,我们用符号PRICE_OF_FUEL指燃料的价格。
如果燃料价格改变,我们只需简单地修改宏PRICE_OF_FUEL的定义,然后预处理器为我们完成实际的替换。
这会是非常方便的——如果燃料价格在一个程序中被经常使用,我们只需修改源代码中的一行,就能改变所有代码中的价格。
注意,最后一个例子与其它例子有稍许不同。
在这个例子中,一个字符串COLOUR_OF_EYES被替代为另一个字符串brown。
常用的编程风格使用大写字母用作宏的名字。
#include指令指示预处理器插入另一个文件到源文件中。
本质上,#include指令本身被那个文件的内容所代替。
在此,这条命令的使用也许你还不能完全明白,当我们更深入地研究C语言,你就会逐渐明白C语言的头文件是怎样被用来支持#defines和那些在多个源文件中使用的声明。
例如,对于C语言,所有使用C的I/O函数的程序必须包含I/O库的头文件stdio.h,该文件定义了C库中一些与I/O函数相关的信息,预处理器指令,#include
有两种不同形式的#include指令:
#include
#include
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 计算机系统 概论 第十一
![提示](https://static.bdocx.com/images/bang_tan.gif)