《计算机系统概论第十四章.doc》由会员分享,可在线阅读,更多相关《计算机系统概论第十四章.doc(11页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、第十四章 函数14.1 简介函数就是子程序,而子程序是现代程序设计语言的灵魂。函数为程序员提供了一个扩充写程序的基本程序块的集合的方式。也就是说,它们能使程序员扩充被语言自然支持的包括新的基本要素的运算和结构的集合。函数是如此重要的一个概念,以至它们很多年前就成为语言的一部分,所有的指令集结构包括LC-3,都直接支持它们。函数为什么如此重要?函数(或过程,或子程序,或方法它们都是相同主题的变种)提供了抽象的能力。也就是说,它们提高了我们把一个组件的功能与其实现的细节分隔开来的能力。一旦组件被构建,并且我们理解了其结构,不考虑其实现的细节,我们就能够把该组件作为一个程序块使用。如果没有抽象,我们
2、在构建复杂的系统诸如计算机和运行于其上的软件的能力就会严重被削弱。函数对我们而言并不是什么新事物。自从我们使用LC-3的汇编语言编程时我们就已经使用过函数的变种,虽然LC-3的汇编语言与C语言在语法上有区别,但是它们后面的思想在很大程度上是相同的。C语言主要是面向函数的。C程序本质上是函数的集合。每条语句属于一个(并仅属于一个)函数。所有的C程序总是从main函数开始和结束执行。main函数可能会调用其他的函数,并且这些函数也可能依次调用更多的函数。控制最终将会返回main函数,然后当main结束的时候,程序结束(假如没有任何事件使这个程序预先结束)。在这一章,我们提供了一个关于C语言中函数的
3、介绍。为了得到包含函数的C的语法的感觉,我们从查看几个短小的程序开始。然后,我们查看函数是如何实现的,查看对于函数在高级语言中运行所必须的低级操作。在这一章的最后一部分,我们把问题解决的思想应用于受益于使用函数的编程问题。14.2 C语言中的函数让我们从一个简单的包括函数的C程序入手。图14.1是一个使用一个名为PrintBanner的函数来打印一条消息的程序。这个程序首先从main函数开始执行,然后调用函数PrintBanner。这个函数打印一行由字符“=”组成的文本到输出设备。PrintBanner是一个函数的最简单形式:它不需要来自调用者的输入,就可以实现,它也不会给调用者提供输出数据(
4、不考虑打印到屏幕上的标记)。换句话说,main没有将任何变元传给PrintBanner,Printbanne也没有返回值传给main。我们把main函数称为调用者,把PrintBanner称为被调用者。14.2.1 有一个参数的函数PrintBanner和main不需要任何的信息交换的事实简化了它们的接口。然而,一般说来,我们想在调用者和被调用者之间传递一些信息。下一个例子示范了这一点是如何在C中实现的。图14.2中的代码包含了一个基于输入的参数执行一个运算的Factorial函数。Factorial执行从1到n之间的所有整数的乘法运算,这里的n是由调用函数(此处为main)提供的值。由这个函
5、数执行的计算可以被表示为代数式:factorial(n)=n!=123n在图14.2的C代码中,这个函数计算出的结果被命名为result。它的值被返回(使用return语句)给调用者。我们说Factorial函数需要一个来自于它的调用者的一个整数变元,它将一个整数值返回给它的调用者。在这个特殊的例子里面,调用者中的变量answer被赋值为来自Factorial的返回值。让我们更进一步看看C里面包含函数的语法。在图4.12的代码中,有4行是我们特别感兴趣的。对Factorial的声明在第3行。它的定义从第19行开始。对Factorial的调用是在第14行;这条语句调用了这个函数。在第27行,Fa
6、ctorial返回到它的调用者。声明在前面的例子中,对Factorial的函数声明出现在第3行。函数的声明有什么目的呢?与变量的声明是告诉编译器关于变量的属性的方式相同,函数声明是告诉编译器关于函数的一些相关属性。函数声明有时被称为函数的原型,它包括函数的名称,它的返回值的类型,以及它所期望的输入值的列表。函数声明以分号结束。出现在函数声明中的第一项是函数返回值的类型。这个类型可以是任何一种C的数据类型(例如:int、char、double)。这个类型描述了函数产生的单个输出值的类型。不是所有的函数都有返回值。例如,先前的一个例子中的PrintBanner函数就没有返回值。如果一个函数没有返回
7、值,那么它的返回值类型就被声明为void,告诉编译器该函数没有任何返回值。接着出现在声明中的是函数的名称。一个函数的名称可以是任何一个合法的C的标识符。经常地,程序员仔细的选择函数名称,以反映函数执行的行为。例如,Factorial对我们所举例子中的函数是个很好的选择,因为它所执行的运算的数学术语就是factorial。另外,使用函数名和变量名能够轻易地被区分的命名规范是一个好的风格。在本书的例子中,我们通过把函数名的第一个字母大写,例如Factorial,来实现这一点。最后,一个函数的声明也描述了函数中所需要的输入参数的类型和顺序。这些是函数期望从它的调用者那里获得的值的类型以及接受它们的顺
8、序。我们可以有选择地(而且是经常地)在声明指定每个参数的名称。例如,Factorial函数把一个整数作为输入参数,并且在内部把这个值称作n。有些函数可能不需要输入值。PrintBanner函数不需要任何的输入参数,因此它的参数列表为空。调用我们例子中的第14行是调用Factorial的函数调用。在这个语句中,main函数调用了Factorial。然而在Factorial能够开始前,main必须传一个整数值给Factorial。在调用者内部被传给被调用者的值被称为变元。变元可以是任意的合法表达式,但是,它们必须与被调用者所期望的类型相匹配。这些变元位于被调用者的名称后面的圆括号里。在这个例子中,
9、main函数传递变量number的值作为变元。从Factorial返回的值接着被赋值给整数变量answer。定义从19行开始的代码是对Factorial的函数定义。注意定义的第一行与函数声明相匹配(然而,没了分号)。在函数名后面的圆括号里的是函数的形式参数列表。这个形式参数列表是一个变量声明的列表,其中每一个变量都被初始化为调用者提供的对应的变元。在这个例子中,当Factorial在14行被调用,参数n会被初始化为main中的number的值。不管函数在程序中的哪个地方被调用,出现在调用中的实际变元都应当与形式参数列表的类型和顺序相匹配。函数体出现在参数列表后面的大括号中。函数体包含了定义函数
10、所要执行的计算的语句和声明。任何在大括号中声明的变量都是该函数的局部变量。在C语言中,有一个关于认识函数的非常重要的概念是任何调用者的局部变量对于被调用函数都是不可见的。特别地,Factorial不能修改number变量。在C语言中,调用者的变元作为值被传给被调用者。返回值在27行中,控制从Factorial传回到调用者main。既然Factorial要返回一个值,一个表达式必须跟在return关键词之后,而且这个表达式的类型应当与函数声明的返回值类型相匹配。在Factorial这个例子中,语句return result; 把存储在result中的计算的数值传回调用者。一般说来,返回一个数值的
11、函数必须在函数体中包含至少一个return语句。不返回数值的函数声明为void类型的函数不需要return语句,return是可选的。对于这些函数,在最后一条语句执行之后,控制就传回调用者。那么main函数呢?它的类型是int(ANSI标准要求的),然而它不包含return。严格地说,在我们至今已看过的例子中,main的结尾处应当包含一个return 0。在C语言中,如果非void函数没有一个明确的返回值的话,最后一条语句的值就会返回给调用者。既然main的返回值会被大多数调用者所忽略(main的调用者是谁?),我们就在本书中将它们省略,使我们的例子更紧凑。让我们总结一下这些各种语法成分:函数
12、声明(或者原型)告诉编译器关于函数,指明它的名字,调用者期望函数所包含的参数的数目和类型,以及函数的返回值的类型。函数定义是关于函数的实际源代码。定义包含了一个形式参数列表,参数列表指明函数的参数名和调用者期望的参数顺序。函数是被函数调用所调用。函数的输入值,或变元,被列在函数调用的圆括号之内。从字面上看,列于函数调用中的每一个变元的值都被赋值到参数列表中的对应的参数中,第一个变元被赋值到第一个参数上,第二个变元被赋值到第二个参数上,等等。返回值是函数的输出,并被传回调用函数。14.2.2 例子:环的面积我们在图14.3中用一个短小的例子更深入地示范C语言的函数语法。这个C程序将计算一个被移出
13、一个小圆的圆的面积。换句话说,它使用一个指定的外部和内部的半径来计算这个环的面积。在这个程序里,使用一个函数计算一个给定半径的圆的面积。函数AreaOfCircle接受一个double类型的参数,然后返回一个double类型的数值给调用者。下面这点对于我们很重要,要反复说明:当函数AreaOfCircle执行的时候,它可以“看到”并且修改其局部变量pi和参数radius。然而,它不能修改任何main函数中的变量,除了那些通过它返回的值。本例中的函数AreaOfCircle与我们在这一章的前面的例子中看到的函数在用法上有一点不同。注意在main函数中多次调用函数AreaOfCircle。在这种情
14、况下,AreaOfCircle执行了一个有用的、简单的计算,所以将其封装成一个函数是有益的。真正的程序会以更大的规模在成千上万个不同的地方包含函数的调用。通过构建AreaOfCircle,把相同的基本运算封装到函数中,我们就可能节省程序中的代码数量,这对于维护代码是有益的。此程序还采取了一个更好的结构。使用AreaOfCircle,与直接嵌入公式相比,代码的意图更明显。你也许还记得我们在12.6.2节对常量的讨论,那时我们指出,变量pi应该通过使用代码中25行的const标识符被声明为常量。在这里我们将这些省去是为了让那些跳过12章的“附加的主题”这一节的人更容易接受这个例子。14.3 C语言
15、中函数的实现让我们现在仔细地看一下C语言中的函数是怎样在机器中执行的。函数在C语言中和LC-3汇编语言中的子程序(我们在第9章讨论的)是相当的,并且它们操作的核心是一样的。在C语言中,调用一个函数需要3个步骤:(1)调用者的参数传给被调用者,并且控制被传给被调用者;(2)被调用者执行它的任务;(3)返回值被传回给调用者,并且控制权返回调用者。我们放在调用机制中的一个重要约束条件是函数必须是与调用者无关的。那就是说,一个函数应该能够被任何一个函数调用。在这一节,我们将通过LC-3来查看这个是怎样完成的。14.3.1 运行时栈在我们开始之前,我们首先需要讨论C以及其他现代高级语言中函数的重要的组成
16、部分。在一个函数被调用之前,我们需要一个激活它的办法,那就是,当一个函数开始执行时,它的局部变量必须被分配存储单元。让我们解释:每一个函数都有一个能够存储它的局部变量的存储器模板。回忆我们在12.5.2节的讨论,函数的一个活动记录就是它的局部变量在存储器的相关位置的模板。函数中每一个被声明的变量在活动记录中都将被给予一个位置。回忆框架指针指明了活动记录的开始。问题:活动记录放在存储器的什么位置上?让我们考虑一些选择。选择1:编译器可以系统地为每一个函数在存储器中分配空间以放置其活动记录。函数A可能被分配到存储单元X,以放置其活动记录,当然,假设活动记录不重叠的话,函数B可能被分配到单元Y,等等
17、。这看起来像是以一种最直接的方式去管理其分配,然而这个选择有一个严重的限制。如果函数A调用它本身将会发生什么呢?我们称其为“递归”,它是一个很重要的编程概念,我们将在第17章中讨论。如果函数A调用它自己,函数A的被调用版本将会覆盖函数A的调用版本的局部变量,程序就将不能像我们所期望的那样运行。对于允许递归函数的C程序设计语言,选择1不能工作。选择2:每当一个函数被调用时,在存储器中为其分配一个活动记录。当函数返回调用者时,它的活动记录将被回收,以便被分配给后面的函数。这个选择从概念上看起来要比第一个选择困难得多,然而它允许函数可以递归。每一次函数调用都会在存储器中为其局部数值获得它自己的空间。
18、例如,如果函数A调用函数A,被调用版本将被分配一个它自己的活动记录,用来存储其局部数值,这个记录与调用版本不同。有一个因素降低了选择2工作的复杂性:函数的调用模式(即函数A调用B,B调用C等)可以容易的通过一个栈数据结构(第十章)被跟踪。让我们通过一个例子来示范。图14.4中的代码包含了3个函数,main、Watt和Volta。每个函数做了什么对于这个例子并不重要,所以我们省略了它们的一些细节,但是对于明确它们之间的调用模式已经足够了。main函数调用Watt,Watt调用Volta。然后,main调用Volta,最后,控制返回到main。每一个函数都有一个由局部变量、一些薄记信息以及由调用者
19、输入的参数组成的活动记录(我们将在下面的段落中再讨论参数和薄记信息)。不管函数什么时候被调用,它的活动记录都将被分配到存储器的某个空间,正如我们在前面的段落所说的,以类似于栈的方式。图14.5中的图表明了这一点。每个阴影部分表示某个特定的函数的活动记录。这一系列图表明,当不同的函数被调用以及从其调用者返回时,运行时栈是如何增长和缩小的。紧记一点,当我们向栈中压入数值时,栈顶是向着低数字的存储单元方向移动,或“增长”的。图14.5(a)是当程序开始执行时运行时栈的图。由于程序都是由main开始执行,因此main的活动记录在栈中首先得到分配。图14.5(b)显示了当Watt被main调用之后的运行
20、时栈。注意,活动记录是以类似栈的方式被分配的。也就是说,无论函数什么时候被调用,它的活动记录就会被压入栈中。无论什么时候从函数返回,它的活动记录就会从栈中被弹出。图14.5的(c)到(f)部分显示了在执行这段代码的过程中,在不同点时的运行时栈的状态。值得注意的是5指向活动记录中的某个内部地址(它指向局部变量的最底部)。还要注意6如何总是指向栈的最顶部它被称作栈指针。一般说来,这两个寄存器在运行时栈和语言的函数的实现中都起着关键的作用。14.3.2 全部工作很清楚,当一个函数被调用的时候,在机器层要进行很多工作。参数必须被传递,活动记录被压入、弹出,控制从一个函数转移到另一个。其中某些工作由调用
21、函数完成,某些由被调用函数完成。要完成所有的这些,需要下面几步:第一,调用函数的代码把它的变元复制进被调用函数可以访问的存储区域。第二,被调用函数开头的代码把它的活动记录压入栈中,并且保存一些薄记信息使得当控制返回调用函数时,调用者的局部变量和寄存器看起来好像都没被动过。第三,被调用函数执行它的任务。第四,当被调用函数完成它的工作时,它的活动记录被从栈中弹出,并且控制返回到调用函数。最后,一旦控制返回到调用函数,执行代码取回被调用函数的返回值。现在我们将查看一下执行这些操作的实际的LC-3代码。我们通过检查与下面的函数调用:wVolta(w,10); ,即图14.4中的18行的代码,相关的LC
22、-3代码,来实现这一点。调用在语句w=Volta(w,10); 中,函数Volta通过使用两个变元被调用。然后,由Volta返回的数值被赋值给局部整数变量w。在翻译这个函数调用时,编译器会生成实现下面操作的LC-3代码:1、通过把两个变元直接压入运行时栈的顶部,把数值传递给函数Volta。回想一下R6指向运行时栈的顶部。也就是说,它包含了当前在运行时栈顶部的数据项的地址。为了将一个数据项压入栈中,我们首先递减R6,然后把R6用作基本地址来存储该数据值。在LC-3中,C的函数调用中的变元按照它们出现在函数调用中的顺序,从右向左被压入栈中。在Watt的情况下,我们首先压入数值10(最右边的变元),
23、然后是w的数值。2、通过JSR指令将控制传给Volta。执行这个函数调用的LC-3代码如下所示:ANDR0,R0,#0; R00 ADDR0,R0,#10; R010ADDR6,R6,#-1STRR0,R6,#0; 压入10LDRR0,R5,#0; 加载wADDR6,R6,#-1STRR0,R6,#0; 压入wJSRVolta图14.6描述了由这些指令造成的运行时栈的改变。注意这些变元的数值被立即压入调用者(Watt)的活动记录的顶部。被调用者的活动记录将会被直接构建于调用者的活动记录的顶部。开始被调用函数在函数Watt中的JSR之后立即执行的指令,是被调用函数Volta中的第一条指令。被调用
24、者开头的代码处理一些与调用有关的重要的薄记。第一件事就是为返回值分配存储空间。被调用者通过递减栈指针,把一个存储单元压入栈中。并且,这个单元会在返回调用者之前,被写入一个返回值。接下来,被调用函数保存了足够的关于调用者的信息,以便当最终调用完成的时候,调用者可以正确的重新获得程序的控制。特别是,我们需要保存调用者的返回地址,它在R7中(为什么在R7中?回忆JSR指令是如何工作的。),和调用者的框架指针,它在R5中。制作一个调用者的框架指针的副本是很重要的,我们把这个叫做动态链接,以便当控制返回至调用者时,它能够再次访问它的局部变量。如果返回地址或动态链接被破坏了,那么当被调用者结束时,我们重新
25、正确的开始调用者就会有麻烦。因此我们为这两个在存储器中制作副本,是很重要的。最后,当这些都完成后,被调用者将通过调整R6,在栈上为它的局部变量分配足够的空间,并且设置R5指向它的局部变量的最底部。为了扼要重述,下面是在函数开始时需要发生的行为列表:1、被调用者为返回值在栈上留下空间。返回值被分配到调用者的参数的顶上。2、被调用者把R7中的返回地址的一份副本压入栈中。3、被调用者把R5中的动态链接(调用者的框架指针)的一份副本压入栈中。4、被调用者为它的局部变量在栈上分配足够的空间,并且调整R5指向局部变量的最底部,R6指向栈顶。为Volta完成的代码如下:VoltaADDR6,R6,#-1;
26、为返回值分配空间 ADDR6,R6,#-1STRR7,R6,#0; 压入R7(返回地址)ADDR6,R6,#-1STRR5,R6,#0; 压入R5(框架指针,动态链接)ADDR5,R6,#-1; 设置新的框架指针ADDR6,R6,#-2; 为Volta的局部变量分配空间图14.7总结了到目前为止被我们遇到的代码完成的存储器的改变。这两个活动记录一个是Watt,一个是Volta在存储器中的安排是显而易见的。注意Volta的活动记录的一些记录是被Watt写的。特别是Volta的活动记录的参数区域。Watt把它的局部变量w的值作为第一个参数,把数值10作为第二个参数写入。记住这些数值是根据它们在函数
27、调用中的位置从右向左被压入栈中。因此,w的值出现在数值10的上面。一旦被调用,Volta将使用q和r引用这些数值。问题:Volta的局部变量的初值是什么?回忆第11章中像这样的局部变量是未被初始化的。关于局部变量的初值的一个习题见习题14.10。注意栈上的每个活动记录都有相同的结构。每个活动记录都包含为函数的局部变量、簿记信息(由调用程序的返回地址和动态链接组成)、返回值,和函数的参数分配的空间。结束被调用函数一旦被调用函数完成了它的工作,它在将控制权返还给调用函数之前,必须先执行几个任务。首先,返回一个数值的函数需要一个能将返回值正确的传给调用函数的机制。其次,被调用函数必须弹出当前的活动记
28、录。列举如下:1、如果存在返回值,它会被写入活动记录中的返回值记录中;2、局部变量会被从栈中弹处;3、动态链接被恢复;4、返回地址被恢复;5、RET指令将控制权返回给调用程序。与Volta对应的LC-3指令如下:LDRR0, R5, #0; 加载局部变量kSTRR0, R5, #3; 把它写入返回值的位置ADDR6, R5, #1; 局部变量出栈LDRR5, R6, #0; 动态链接出栈ADDR6, R6, #1LDRR7, R6, #0; 返回地址出栈ADDR6, R6, #1RET前两个指令将返回值写入Volta的活动记录的返回值记录中,在这个例子中,返回值是局部变量k。接着,通过把栈指针
29、移动到框架指针之下,弹出局部变量。动态链接被恢复,然后,返回地址被恢复,最后,我们返回到给调用者。你需要记住的是:即使Volta的活动记录从栈中被弹出,数值仍留在存储器中。从调用函数返回当被调用函数执行了RET指令之后,控制被传回调用函数。在一些情况下,没有返回值(如果被调用函数被声明为void),而且在一些情况下,调用函数忽略返回值。另一方面,在我们前面的例子中,返回值被赋值给Watt中的变量w。特别的,有两个行为必须被执行:1、返回值(如果有一个)从栈中弹出。2、变元从栈中弹出。JSR后面的代码看起来如下:JSRVolta LDRR0, R6, #0; 加载栈顶的返回值STRR0, R5,
30、 #0; w = Volta(w,10);ADDR6, R6, #1; 返回值出栈ADDR6, R6, #2; 变元出栈一旦代码完成,调用就完成了,调用函数就能恢复它正常的操作。注意,在返回到调用者之前,被调用者恢复调用者的环境。对于调用者,看起来好象除了一个新值(返回值)被压入到栈中之外什么事情也没改变。调用者保存/被调用者保存在我们结束关于函数实现的讨论之前,我们需要讨论一个到现在还没有明确的问题。在一个函数的执行过程中,R0到R3可以包含作为正在执行的计算的一部分的临时数据。寄存器R4到R7则被用作其它的用途:R4是全局数据段的指针,R5是框架指针,R6是栈指针,R7则被用来存放返回地址
31、。如果我们调用一个函数,根据我们描述的调用规则,R4到R7不会改变,或者按预先决定的方式改变。但是寄存器R0、R1、R2和R3将发生什么事情呢?在一般情况下,我们希望能保证被调用函数不会改写它们。为了实现这一点,调用规则典型的采取两种策略之一:(1)调用者通过把这些寄存器压入活动记录中,保存它们。这叫做调用者保存规则。(我们在第9章也讨论过。)当控制返回到调用者的时候,调用者通过把这些寄存器从栈中弹出,恢复它们。(2)另一选择,被调用者通过在活动记录的薄记区域中增加四个区域来保存寄存器。这叫做被调用者保存。当被调用者开始运行时,它把R0到R3,和R5到R7都保存到薄记区域中,在返回调用者之前再
32、恢复它们。14.3.3 全部结合在一起在Watt中的函数调用和Volta的开始和结束的代码被列于图14.8中。在前面几节中出现过的LC-3的代码片断被全部结合在一起,显示了代码的总体结构。这些代码比起原先的代码优化了很多。我们已经把对栈指针R6的操作和返回值的压入和弹出结合成一个指令。总之,当一个函数调用另一个函数的时候,我们的LC-3的C的调用规则包括一系列被执行的步骤。调用函数把每个参数的值压入栈中,并且执行一个JSR(跳转到子程序)到被调用函数。被调用者为返回值分配一个空间,保存关于调用者的一些薄记信息,然后为它的局部变量分配空间。然后被调用者执行它的任务。当任务被完成的时候,被调用者把
33、返回值写入为它保留的空间,弹出和恢复薄记信息,并返回到调用者。然后调用者弹出放在栈中的返回值和参数,再继续执行。你可能是感到惊讶,为什么仅仅执行一个函数调用,我们就要进行所有的这些步骤。也就是说,所有这些代码真的是必需的吗?调用规则不能被做得更简单吗? 真正的调用规则的一个特性是,一般情况下,任何函数都应该能够调用其他任何函数。为了使这点成为可能,调用规则应该被组织为调用者除了知道被调用者的接口(也就是说,被调用者返回值的类型和它期望的参数值的类型)之外,不需要知道任何其它事情。同样地,被调用者应该被写为与调用它的函数无关。因为这个一般性特征,C函数的调用规则需要我们在这里概述的这些步骤。14
34、.4 使用函数解决问题因为函数对我们是有用的,我们必须把它们整合到我们的解决问题的编程方法中。在这一节中,我们将会通过两个示例问题示范函数的使用,每一个例子示范了一个不同的函数应用。从概念上说,在问题的算法的自顶向下的设计过程中,函数对于分解问题是好的思想。当我们分解一个问题的时候,自然的“组件” 出现在被算法执行的任务中。这些组件自然的选择使用函数。我们的第一个例子包括把本文从小写字母转换为大写字母,它呈现了在自顶向下的设计过程中自然出现的组件函数的一个例子。函数对于封装程序所需的基本操作是有用的。通过构建那样的一个函数,我们在某种意义上扩充了程序设计语言的运算集合,对于手头的特定问题再对它
35、们进行裁减。在第二个问题的情况中,判定毕达哥拉斯三角形,我们将会开发一个简单的计算 x2的函数,用来协助计算。14.4.1 问题1:大小写转换在这一节中,我们开发一个从键盘上读取输入并把它们回显到屏幕上的程序。我们已经在13章见到一个这样做的程序的例子(见图13.8)。然而,这次,我们更改一个微小的地方:我们想要这个程序在屏幕上回显之前把小写字母转换成大写字母。我们的解决这个问题的方法是把图13.8的回显程序用作一个起始点。前面的代码使用一个while循环读取来自键盘的一个输入字符,然后把它打印到输出设备。对于这个基本的结构,我们想要增加一个组件,检查该字符是否是一个小写字母,如果是的话,把它
36、转换成大写字母。有一个输入和一个输出。我们可以直接把代码添加到while循环中,但是由于这个组件的独立性,我们将构建一个函数来做这个工作。 当每个字符从键盘上被扫描之后,并在它被显示于显示器之前,调用此转换函数。函数需要一个字符作为参数,并返回同一字符(如果该字符已经是大写,或不是字母字符)或该字符的大写版本。图14.9显示了此程序的流程图。阴影部分是原始的回显程序的流程图。对于这个原始的流程图,我们添加一个执行转换的组件函数。图14.10是这个完整的C程序。它从键盘接受输入数据,把每个输入字符转换成大写,再打印出结果。如果输入的字符是一个换行符,则程序结束。从小写到大写的转换是由函数ToUp
37、per完成的。注意,在执行转换的函数体中使用了ASCII码字面常量。记住:单引号中的单个字符(如A)的值为该字符的ASCII码的值。因此,表达式a-A,就是a的ASCII码值减去A的ASCII码值。14.4.2 问题2:毕达哥拉斯定理现在我们尝试写一个程序,找出小于某个特定的输入值的所有毕达哥拉斯三角形。毕达哥拉斯三角形就是满足a2+b2=c2的特点的三个整数值a、b、c的集合。换句话说,a和b是三角形的直角边长,而c是斜边长。例如,3,4和5就是一个毕达哥拉斯三角形。现在的问题就是:计算出所有小于用户提供的一个上限的三角形a、b和c。对于这个问题,我们将试图通过检查所有组合的办法来找出所有的
38、三角形。那就是说,如果用户指定的上限是max,我们将检查所有小于max的整数的组合,以确定它们是否满足毕达哥拉斯三角形的属性。为了检查所有的组合,我们让sideA、sideB和sideC中的每一个都从1变化到max。这意味着使用计数器控制的循环。更确切的说,我们要用一个for循环来改变sideC,用另一个来改变sideB,再用一个改变sideA,每一个嵌套在另一个中。在这些循环的核心,我们将查看这三个值是否支持该属性,如果是,就将其打印出来。现在,在执行这个三角形的检查中,我们需要计算以下表达式:(sideC*sideC=(sideA*sideA+sideB*sideB)因为平方运算对于这个问
39、题来说是一个基本操作意味着它在很多地方都会被需要我们将把它封装进一个能返回整数参数的平方的Squared函数中。前面的表达式将重新被写为下面的表达式。注意这些代码给出了将计算什么的一个更清晰的说明。(Squared(sideC)= = Squared(sideA)+ Squared(sideB)这个问题的C程序在图14.11中提供。还有比通过检查所有组合的技术更好的方法来计算它(你能修改下面的代码使它运行效率更高吗?);检查所有组合的技术适合于我们演示函数用途的目的。14.5 总结在这一章中,我们介绍了C 语言中函数的概念。从最早的语言起,诸如函数的子程序的一般概念就已经是程序设计语言的一部分
40、了。函数是很有用的,因为它们使我们能够构建或许对某个特定的程序任务(或者是各种任务)很有用的新的基本程序块。从某种意义上来说,函数使得我们扩展了语言提供的原始的运算和结构。从这一章中你应该掌握的关键概念为: C语言中函数的语法为了在C语言中使用一个函数,我们必须使用函数声明来声明这个函数(典型的,我们在代码的开头进行),指定函数的名字,函数返回值的类型,以及函数期望输入的数值的类型和顺序。一个函数的定义包含了函数实际的代码。当要求一个函数被执行时,函数就被调用。函数调用包含变元将被作为参数传到函数中的值。 C函数在底层的实现在C中与执行函数有关的复杂性的一部分,就是在源文件中,一个函数可以被任
41、意其他函数调用(甚至是其它目标文件中的函数)。为了帮助处理这个复杂性,我们为一个函数调用另一个函数制定了一个一般的调用规则。为了有助于一些函数可能调用它本身的事实,我们让这个调用规则以运行时栈为基础。这个调用规则包括调用者通过把变元的值压入栈中来传递它们,然后调用被调用函数。调用者所写的变元变成了被调用者的活动记录的参数。被调用者执行其任务,然后把返回值留给调用者后,把活动记录从栈中弹出。 使用函数进行程序设计不使用函数写程序是可以的,结果是你的代码会难以读懂、维护、扩充,而且会比使用函数的代码更容易出错。函数提供了抽象的能力:我们可以写一个函数来执行一个特定的任务,对它进行调试、测试,然后再
42、在程序内任何需要它的地方使用它。习题14.5 如下程序的输出是什么?14.6 如下程序的输出是什么?14.7 如下代码是名为Bump的C函数:a 画出Bump的活动记录。b 将活动记录中的每条记录标记为如下5种标识之一:(1) 局部变量(2) 变元(3) 指令的地址(4) 数据的地址(5) 其他c 这些记录有些是由调用Bump的函数写的,有些是由Bump函数本身写的,请标出由Bump写的记录。14.8 如下代码的输出是什么?并解释Swap函数的功能。14.10 一个包含函数food的C程序已经被编译为LC-3汇编语言,部分代码如下:a 此函数有多少个局部变量?b 假设此函数有两个整数参数x和y
43、,请写出计算表达式x+y的LC-3代码。 14.11 对于如下C代码:a 程序的输出是什么?b 当函数Unit开始执行时,局部变量z的值是什么?14.13 写一个函数,输出一个整数的以4为基的值(只使用0、1、2和3)。并使用这个函数写一个程序,程序从键盘读入两个整数,显示它们以4为基的数值以及二者之和。14.14 写一个函数,如果第一个整数参数能被第二个整除,返回1。并使用这个函数写一个程序,程序找出能被所有小于10的整数整除的最小的数。14.15 如下C程序被编译为LC-3机器语言,在开始执行之前被加载进地址x3000。不考虑I/O使用的库函数的JSR指令,目标代码包括3条JSR指令(分别是到函数f、g和h)。假设3条JSR指令的地址分别位于x3102、x3301和x3304。并假设用户输入的值为“4 5 6”。请画出当程序从函数f返回时的运行时栈,并标出各单元的内容。假设运行时栈的基址位于xEFFF。14.17 a写一个C函数,模拟4选1的多路选择器的行为。b写一个C函数,模拟LC-3的ALU的行为。14.18 在电话的键盘上,标有2、3、4.9的键也与字母与其对应。例如,标有2的键与字母A、B和C相对应。写一个程序,将一个7位数的电话号码映射为所有可能的字符序列。要求使用对数字和字母进行映射的函数。数字1和0没有对应的字母与其映射。
限制150内