计算机系统概论十六章.doc
《计算机系统概论十六章.doc》由会员分享,可在线阅读,更多相关《计算机系统概论十六章.doc(12页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、第十六章 指针和数组16.1 介绍在这一章,我们介绍(实际上是重新介绍)两个简单但是功能强大的程序设计概念:指针和数组。在我们写LC-3的汇编代码时,我们已经使用了指针和数组。现在,我们使用C语言来研究它们。指针就是一个如变量之类的存储对象的地址。使用指针,我们可以间接地访问这些提供了非常有用的功能的对象。例如,使用指针,我们可以构建一个修改被调用者传递的参数的函数。使用指针,我们可以构建一些复杂的在程序执行过程中可以增长和缩短的数据组织。数组是在存储器中被连续排列的一列数据。例如,在本书前半部分的LC-3的几个例子中,我们把一个字符文档表示成在存储器中连续排列的字符序列。这些连续排列的字符被
2、称为字符数组。为了访问数组中的一个特定项,我们需要指明我们要访问的是哪个元素。正如我们即将看到的,一个像a4这样的表达式将访问名为a的数组中的第五个元素因为我们从元素0开始为数组计数,所以它是第五个元素。数组很有用,因为它们允许我们方便地处理一组数据,诸如向量、矩阵、列表和字符串,这些都自然的表示了现实世界的某些特定的对象。16.2 指针我们以一个经典的例子来讨论指针的用途。在图16.1所示的C程序中,函数Swap被设计用来交换它的两个参数的值。函数Swap被main函数调用,它的变元ValueA在这个程序中的值是3, 变元ValueB的值为4。一旦Swap将控制返回给main,我们期望Val
3、ueA和ValueB的值被交换。但是,编译并执行这段代码,你会发现传给Swap的两个变元仍然保留着原来的值。我们来检查一下在Swap函数执行时的运行时栈,分析一下这到底是为什么。图16.2显示的是函数完成之前,就在第25行的语句执行之后,但是控制还没有返回到main函数之前的运行时栈的状态。注意,Swap函数已经在它自己的活动记录中修改了参数firstValue和secondValue的局部副本的值。当Swap完成并将控制返回给main时,当Swap的活动记录从栈中弹出时,这些被修改的值就被丢掉了。main中的两个值没有被交换。我们的程序出现了错误。在C中,变元总是以值的形式从调用函数传递到被
4、调用函数。C将出现在函数调用中的每个变元都作为表达式来计算,然后将表达式的值压入运行时栈中,以便将它们传递给被调用函数。Swap要修改调用者传递给它的变元,它就必须访问调用函数的活动记录为了修改变元的值,它必须访问它们存储的单元。Swap函数需要main中的ValueA和ValueB的地址,以便改变它们的值。正如我们将在下面几节看到的一样,指针及其相关运算使这些成为可能。16.2.1 声明指针变量一个指针变量包含了一个存储对象的地址,比如一个变量的地址。一个指针指向了它包含的地址中的变量。与指针变量有关的是它所指的对象的类型。所以,比如,一个整数指针变量指向一个整数变量。为了用C声明一个指针变
5、量,我们使用下面的语法:int *ptr;这里,我们声明了一个名为ptr的变量,它指向一个整数。星号(*)表明后面的标识符是一个指针变量。C程序员经常说ptr是int星号类型。类似的,我们可以声明char *cp;double *dp;变量cp指向一个字符,dp指向一个双精度浮点数。指针变量的初始化方式与所有其他变量类似。如果一个指针变量被声明为一个局部变量,它就不会被自动初始化。使用*来声明指针变量的语法可能起初看起来有点奇怪,但是一旦我们学习了指针运算符,这种语法后面的基本原理就会变得更清楚。16.2.2 指针运算符C有两种与指针操作有关的运算符,地址运算符“&”和间接运算符“*”。地址运
6、算符&地址运算符,它的符号是&,生成了它的操作数的存储地址,操作数必须是像一个变量那样的存储对象。在下面的代码序列中,指针变量ptr将指向整数变量object。第二个赋值语句的右手边的表达式生成了object的存储地址。int object;int *ptr;object = 4;ptr = &object;让我们查看一下这个序列的LC-3代码。两个声明的变量都是局部的,被分配到栈中。回忆栈底指针R5指向第一个被声明的局部变量,也就是这个例子中的object。ANDR0,R0,#0; 清空R0 ADDR0,R0,#4; R0 = 4STRR0,R5,#0; object = 4;ADDR0,R
7、5,#0; 生成object的存储地址STRR0,R5,#-1; ptr = &object;图16.3显示了在语句ptr = &object;被执行以后,包含这段代码的函数的活动记录。为了让事情更具体,每个存储单元都标记了一个地址,这些地址是我们任意在xEFF0范围内选择的。栈底指针R5当前指向xEFF2。注意,object中包含了整数值4,prt中包含了object的存储地址。间接运算符*第二种指针运算符称为间接或者解除引用运算符,它的符号是星号,*(在这里发音为star)。这个运算符允许我们间接操作一个存储对象里的值。例如,表达式*ptr指的是被指针变量ptr指向的值。回忆前面的例子:*
8、ptr指的是存储在变量object中的值。这里,*ptr和object可以互换使用。添加到前面的C代码的例子中,*ptr=*ptr+1;本质上,*ptr=*ptr+1;是object=object+1;的另一种方式。就像我们所见过的其他类型的变量,*ptr根据它出现在赋值运算符的哪一边来表示不同的意思。在赋值运算符的右边,它指的是出现在那个单元中的值(这个例子中是数值4)。在赋值运算符的左边,它指明了要作修改的单元(这个例子中是object的地址)。让我们查看一下前面代码中的最后一条语句的LC-3代码。注意,这段代码不同于假如最后一条C语句是object=object+1;而生成的代码。使用指
9、针的间接引用,编译器为右边的间接运算符生成2条LDR指令,一个用来加载包含在ptr中的存储地址,另一个用来取出存在那个地址中的值。对左边的间接引用,编译器生成一条STR R1,R0,#0。如果那条语句变为object=*ptr+1;,编译器将会生成STR R1,R5,#0。16.2.3 使用指针传递引用使用地址和间接运算符,我们可以修正图16.1中的不能实现两个参数交换的Swap函数。图16.4列出了一个被称为NewSwap的Swap的修正版本的程序。我们做的第一处修改是NewSwap的参数不再是整数,而是整数指针(int *)。这两个参数是要交换的两个变量的存储地址。在NewSwap的函数体
10、的内部,我们使用间接运算符*获得这些指针所指的值。现在,当我们从main中调用NewSwap时,我们需要为我们想交换的两个变量提供存储地址,而不是像我们在前面的代码版本中提供变量的值。为了提供地址,&运算符实现这个目的。图16.5显示了当NewSwap函数中的不同语句被执行时的运行时栈。三个子图(A-C)对应于23、24和25行被执行后的运行时栈。通过设计,C按数值把信息从调用函数传递到被调用函数:也就是说,计算调用语句中的变元表达式的值,其结果通过运行时栈被传给被调用函数。然而,在NewSwap中,我们通过使用地址运算符&构建了一个对于变元的按引用的调用。当按引用传递变元时,其地址被传给被调
11、用函数为了使其有效,变元必须是变量或其他存储对象(即,必须有个地址)。被调用函数就能使用间接运算符*来访问(以及修改)其对象的原来的值。16.2.4 空指针有时我们说指针不指向任何东西很有用。当我们讨论诸如在19章中的链表的动态数据结构时,你将会对为什么这个概念很有用的原因更加清楚。从现在开始,我们称那个不指向任何东西的指针叫做空指针。在C语言中,我们用下面的赋值语句指明这一点:int *ptr;ptr=NULL;这里,我们将NULL的值赋值给指针变量ptr。在C中,NULL是一个特别定义的预处理宏,它包含一个没有任何指针可以包含的值,除非指针包含空值。例如,在一个特定的系统中,NULL可能等
12、于0,因为没有一个有效的存储对象可以存储在单元0中。16.2.5 阐明语法现在我们回顾在11章介绍的一些符号。既然我们知道如何传递一个引用,让我们重新查看I/O库函数scanf:scanf (%d, &input );因为函数scanf需要使用从键盘读入的十进制数值更新变量input,所以scanf需要input的地址而不是它的值。这样,就需要地址运算符&。如果我们省略地址运算符,这个程序就会以一个错误结束。你是否能为发生这个的原因提出一个可能的理由?为什么没有引用,scanf就不可能正确的工作?在我们完成对于指针的介绍之前,让我们尝试搞清楚指针声明的语法。为了声明一个指针变量,我们使用下面的
13、形式声明:type *ptr;在这里,type可以是任何预定义(程序定义的)的类型,诸如int、char、double等。名字ptr可以为任何合法的变量标识符。使用这个声明,当应用*(解除引用)运算符时,我们便能声明一个type类型的变量。也就是说,*ptr是type类型的。我们也能够声明一个返回指针类型的函数(我们为什么这么做将在后面的章节中变的更明显)。例如,我们能使用形如int *MaxSwap()的声明,声明一个函数。与所有其他的运算符一样,地址和间接运算符根据C语言中的优先级与结合性规则进行计算。这些运算符和其他所有运算符的优先级和结合性规则都被列于表12.15中。注意,两种指针运算
14、符都有很高的优先级。16.2.6 一个包含指针的例子让我们查看一个包含了指针的例子。假设我们要编一个程序,给定一个整数除数与一个整数被除数,计算商与余数。也就是说,程序将计算都是整数的divident/divisor和divident%divisor。这个程序的结构非常简单,只需要顺序结构也就是说,不需要重复。然而,难点是我们想通过一个C函数计算商和除数。我们可以容易的构建一个函数产生单个输出结果(比如说,商),我们可以使用返回值机制返回调用者。一个只能计算商的函数由一条语句组成,如return divident/divisor;。然而,为了给调用函数提供多个值,我们将使用指针变量,通过引用机
15、制进行调用。图16.6的代码中包含了实现这个工作的函数。函数IntDivide接受4个参数,两个是整数类型,另外两个是指向整数的指针。函数IntDivide用第二个参数y去除第一个参数x。结果的整数部分被赋值给指针quoPtr 所指向的存储单元,而余数部分被赋值给指针remPtr所指向的存储单元。请注意,函数IntDivide还返回了一个值来指明它的状态:如果除数为0,它返回一个-1,告诉调用者有错误产生。否则就返回一个0,告诉调用者计算已无故障完成。在返回之后,main函数检查返回值来判断商和余数中的值是否正确。利用返回值来标记在调用者和被调用者之间调用函数时发生的问题,对于传达调用时的错误
16、情况是一种很好的保护性程序设计实践。16.3 数组 考虑一个程序,它保存在一次计算机工程课程考试中50个学生的期末成绩。最方便的存储这些数据的方法是声明一个单独的对象,比如examScore,我们把50个不同的整数值存储在其中。我们可以使用一个下标访问这个对象中的某个特定的考试成绩,下标是从这个对象顶部开始的偏移量。例如,examScore32提供了第33个学生的考试成绩(第一个学生的考试成绩存储在examScore0中)。这个例子中的对象examScore就是一个整数数组。一个数组就是一个类似的数据项的集合,顺序存储在存储器中。特别的,数组中所有的元素都必须是同一类型的(例如,int,cha
17、r等等)。当程序运算的数据被自然的表达为一个连续的数值序列时,数组是最有用的。因为许多现实世界的数据属于这个类别(比如某门课程的学生考试成绩),数组是有用得令人不可思议的数据结构。例如,当我们想写一个程序使得接受从键盘输入的100个数字序列,并对它们按升序排列,这时,数组就会是对于在存储器中存储这些数字的自然的选择。如果我们使用在这之前用过的简单变量来写这个程序几乎是不可能的。16.3.1 数组的声明和使用 首先,让我们看看在C程序中怎样声明一个数组。和其他变量一样,数组必须有一个相关的类型。这个类型表明了存储在数组里的数值的属性。下面是对一个包含10个整数的数组的声明:int grid10;
18、 关键词int表明我们声明的是整数类型的事物。这个数组的名称是grid。中括号表明我们声明的是一个数组,10则表示这个数组包含10个将被按顺序放在存储器中的整数。图16.7显示了grid如何被分配空间的图形表示。第一个元素,grid0被分配在最低的存储地址,而最后一个元素,grid9在最高的地址。如果数组grid是一个局域变量,那么它的存储空间将被分配于运行时栈上。 让我们来看看怎样访问数组中的不同的值。注意在图16.7中数组的第一个元素实际上编号为0,这表示最后一个元素编号为9。为了访问某一特定元素,我们在中括号中提供了一个下标,例如:grid6=grid3+1; 这条语句读出存储在grid
19、的第四个(记住,我们从0开始编号)元素中的值,把它加上1,并把结果存储进grid的第七个元素中。让我们看看这个例子的LC-3代码。我们假设grid是分配在运行时栈中的唯一局域变量。这就意味着栈底指针R5将指向grid9。ADDR0,R5,# -9; 将grid的基址给R0 LDRR1,R0,#3; R1grid3 ADDR1,R1,#1; R1grid3+1STRR1,R0,#6; grid6 = grid3+1;注意到第一条指令计算出数组的基址,也就是grid0的地址,然后将其传给R0。数组的基址通常是数组的第一个元素的地址。我们可以通过将要访问的元素的下标加上基址,访问数组中的任意元素。数
20、组的强大功能来自于数组的下标可以是任意的合法的C语言整数表达式这一事实。下面的例子显示了这点:gridx+1=gridx+2; 让我们看看这条语句的LC-3代码。假设x是被直接分配到运行时栈中的数组grid顶上的局部变量。 LDRR0,R5,# -10; 加载x的值 ADDR1,R5,# -9; 将grid的基址给R1 ADDR1,R0,R1; 计算gridx的地址 LDRR2,R1,#0; R2gridx ADDR2,R2,#2; R2gridx+2 LDRR0,R5,# -10; 加载x的值 ADDR0,R0,#1; R0x+1 ADDR1,R5,# -9; 将grid的基址给R1 ADD
21、R1,R0,R1; 计算gridx+1的地址 STRR2,R1,#0; gridx+1=gridx+2;16.3.2 使用数组的例子我们从一个简单的C程序开始,此程序将两个数组的每个对应元素相加,得到两个数组的和。每个数组表示某门课程中的学生考试成绩列表。每个数组元素都包含一个学生的成绩。为了生成每个学生的累积点,我们想执行Totali=Exam1i+Exam2i。图16.8包含了读出两个10个元素的整数数组,把它们加为另一个10个元素的数组,并打印出这个和的C代码。一个风格上的注意:注意表示输入集合大小的常数值的预处理宏NUM_STUDENT的使用。这是预处理宏的通常的用途,通常它位于源文件
22、的开头(或位于C的头文件中)。现在,如果我们想增加数组的大小,例如学生注册人数发生变化,我们只需改变宏的定义(一个改变),并重新编译该程序。如果我们不使用宏,那么改变数组的大小就需要在很多处修改代码。这些改变可能很难跟踪,忘记一处就可能导致程序无法正确运行。对于数组的大小,使用预处理宏是好的程序设计实践。现在来看一个包含数组的更复杂一些的例子。图16.9列出了一个从键盘输入一个十进制数的序列(总个数为MAX_NUMS),然后判断这个序列中的每个输入的数字重复出现的次数的C程序。然后,程序输出每个数字,以及它重复的次数。在这个程序里,我们使用了两个数组,numbers和repeats。它们被声明
23、为包含MAX_NUMS个整数值。数组numbers存储输入的序列。数组repeats在程序中用来计算numbers中的元素在输入序列中重复出现的次数。例如,如果number3等于115,并且在输入序列中共有4个115(即,在数组numbers中有4个115),那么repeats3将等于4。这个程序由3个循环组成,中间的循环实际上是一个由两个循环组成的嵌套循环。第一个和第三个for循环是从键盘输入和产生程序输出的简单循环。中间的for循环包含嵌套循环。代码体判断每个元素在整个输入数组内重复的次数。外面的循环将变量index从0重复至MAX_NUMS;我们使用index从第一个元素numbers0
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 计算机系统 概论 十六
限制150内