ADA 95教程 高级特性 高级子程序主题
在本教程的第1部分中,我们详细介绍了子程序的主题,但是还有许多其他的事情需要讨论,因此我们将返回到子程序中来了解更高级的主题。
默认参数
Example program ------> e_c18_p1.ada
-- Chapter 18 - Program 1 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Defaults is Index : INTEGER; Animal_Sum : INTEGER; procedure Animals(Total : in out INTEGER; Cows : in INTEGER := 0; Pigs : in INTEGER := 0; Dogs : in INTEGER := 0) is begin Total := Cows + Pigs + Dogs; Put("Cows ="); Put(Cows, 3); Put(" Pigs ="); Put(Pigs, 3); Put(" Dogs ="); Put(Dogs, 3); Put(" and they total"); Put(Total, 4); New_Line; end Animals; begin Index := 3; Animals(Animal_Sum, 2, 3, 4); Animals(Animal_Sum, 3, Index, 4); Animals(Dogs => 4, Total => Animal_Sum); Animals(Total => Animal_Sum, Pigs => 2 * Index + 1, Cows => 5); Animals(Dogs => Index + 4, Total => Animal_Sum); Animals(Animal_Sum, Dogs => 4, Pigs => Index, Cows => 2); Animals(Animal_Sum); end Defaults; -- Result of Execution -- Cows = 2 Pigs = 3 Dogs = 4 and they total 9 -- Cows = 3 Pigs = 3 Dogs = 4 and they total 10 -- Cows = 0 Pigs = 0 Dogs = 4 and they total 4 -- Cows = 5 Pigs = 7 Dogs = 0 and they total 12 -- Cows = 0 Pigs = 0 Dogs = 7 and they total 7 -- Cows = 2 Pigs = 3 Dogs = 4 and they total 9 -- Cows = 0 Pigs = 0 Dogs = 0 and they total 0
检查名为e_c18_p1.ada的程序,以获取过程定义中使用的默认参数的一些示例。该过程有四个形式参数,其中第一个参数为mode in out,另外三个参数为 mode in。三个in参数的默认值均为零。当我们调用这个过程时,我们不需要为每个变量提供一个值,那些我们不提供值的变量在执行时将默认为零。当然,列表中的第一个变量Total必须提供一个变量名,这样才能返回一个值。因此,不能默认。
过程本身以及对它的前两个调用(第29行和第30行)应该不会对您的理解造成任何问题。然而,当我们到达31号线时,我们有几件事要指出。
实际参数的命名符号
我们对第31行中的实际参数使用命名聚合表示法,因此它们可以按任何顺序列出,但由于过程头中定义了默认值,因此我们不必指定每个参数,从而允许默认值在调用过程时生效。当您编译并运行这个程序时,您将看到Cows和Pigs的默认值为零。第32行和第33行也使用了命名聚合表示法,应该清楚地说明它们的操作。第34行使用混合聚合表示法,正如我们前面讨论的,位置聚合表示法最初可以使用,但是在切换到命名表示法之后,所有剩余的条目都必须命名,除非允许它们默认。第35行说明了退化情况,其中只使用结果,所有三个输入变量默认为零。
带out模式的参数不能为默认值
由于mode in out或mode out的参数必须能够返回值,因此必须将变量定义为其实际参数,因此不能默认。
默认参数对您来说并不新鲜,因为您以前在过程调用中实际使用过它们。当您调用程序New_Line时,后面有一个可选号码,如New_Line(2)所示。要在监视器上留出空间的行数默认为包中的一行Ada.Text_IO,这是随编译器提供给您的,但您可以通过插入行数的值来覆盖默认值。Ada 95参考手册(ARM)附录A.10.1对其进行了定义,其中名为Spacing的形式参数默认值为1。请参阅您的文档或ARM,并查看它的使用情况。
动态默认参数
Example program ------> e_c18_p2.ada
-- Chapter 18 - Program 2 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Default2 is Index : INTEGER; Animal_Sum : INTEGER; function Cow_Constant return INTEGER is begin return 7; end Cow_Constant; function Pig_Constant return INTEGER is Animals : INTEGER := Cow_Constant - 3; begin return 2 * Animals + 5; end Pig_Constant; procedure Animals(Total : in out INTEGER; Cows : in INTEGER := 2 * Cow_Constant; Pigs : in INTEGER := Cow_Constant + Pig_Constant; Dogs : in INTEGER := 0) is begin Total := Cows + Pigs + Dogs; Put("Cows ="); Put(Cows, 3); Put(" Pigs ="); Put(Pigs, 3); Put(" Dogs ="); Put(Dogs, 3); Put(" and they total"); Put(Total, 4); New_Line; end Animals; begin Index := 3; Animals(Animal_Sum, 2, 3, 4); Animals(Animal_Sum, 2, Index, 4); Animals(Dogs => 4, Total => Animal_Sum); Animals(Total => Animal_Sum, Pigs => 2 * Index + 1, Cows => 5); Animals(Dogs => Index + 4, Total => Animal_Sum); Animals(Animal_Sum, Dogs => 4, Pigs => Index, Cows => 2); Animals(Animal_Sum); end Default2; -- Result of Execution -- Cows = 2 Pigs = 3 Dogs = 4 and they total 9 -- Cows = 2 Pigs = 3 Dogs = 4 and they total 9 -- Cows = 14 Pigs = 20 Dogs = 4 and they total 38 -- Cows = 5 Pigs = 7 Dogs = 0 and they total 12 -- Cows = 14 Pigs = 20 Dogs = 7 and they total 41 -- Cows = 2 Pigs = 3 Dogs = 4 and they total 9 -- Cows = 14 Pigs = 20 Dogs = 0 and they total 34
名为e_c18_p2.ada的程序与上一个示例程序相同,只是有一个细节。形式参数的默认值的定义在这里以不同的方式声明。您将注意到,默认值不仅是算术组合,而且要组合的值是函数调用的结果,每次调用过程时都会动态地计算这些值。默认值是过程每次调用的常量,但每次调用都会对其求值,因此每次调用和执行过程Animals 时都可能不同。如前所述,这称为精化。例如,如果从第12行的Cow_Constant返回的值返回全局变量的值,则主程序可以修改全局变量的值,从而在每次调用过程之前修改默认变量的值。因此,可以根据当前读取的时间、环境温度或系统中可以读取的任何其他可变条件,在几个不同的程序中的每个程序中设置不同的默认值。
一定要编译并运行这个程序并观察结果,将它们与您期望程序输出的结果进行比较。
递归之谜
Example program ------> e_c18_p3.ada
-- Chapter 18 - Program 3 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Recurson is Index : INTEGER; procedure Print_And_Decrement(Value : in INTEGER) is New_Value : INTEGER; begin Put("The value of the index is now"); Put(Value, 3); New_Line; New_Value := Value - 1; if New_Value > 0 then Print_And_Decrement(New_Value); end if; end Print_And_Decrement; begin Index := 7; Print_And_Decrement(Index); end Recurson; -- Result of execution -- The value of the index is now 7 -- The value of the index is now 6 -- The value of the index is now 5 -- The value of the index is now 4 -- The value of the index is now 3 -- The value of the index is now 2 -- The value of the index is now 1
对于有经验的Pascal程序员来说,这个主题是没有问题的,但是对于FORTRAN程序员来说,它可能是一个全新的、有点复杂的主题。继续使用它,您将看到什么是递归以及如何有效地使用它。检查名为e_c18_p3.ada的示例程序,它是可能的最简单的递归程序,但是它非常适合描述递归是什么以及它是如何工作的。
从主程序开始,我们给名为Index的变量赋值7,然后调用名为Print_And_Decrement的过程,将Index的值作为实际参数。到达过程本身时,我们使用形式参数的name值,并在监视器上用适当的文本行显示该值。继续到第15行,我们减少传递变量的值,然后将结果比较为零。如果该值大于零,则调用名为Print_And_Decrement的过程,并将新递减的值称为New_Value。这里是FORTRAN程序员注意到一些新东西的地方。我们从内部调用过程,这就是递归。假设调用过程的另一个完整副本,再次递减该值,如果它仍然不是零,则调用过程的另一个副本。最终,传递的变量的值将减少到零,所有过程调用都将完成,每个过程都将返回到调用它的过程,直到我们再次到达主程序。
您应该编译并运行这个程序,看看它是否真的执行了我们所说的操作,然后返回来进一步讨论这个程序以及它正在执行的操作。这是一种非常愚蠢的从7到1的计数方法,但是它是一种非常简单的方法来说明递归的用法。在本教程的后面部分,我们将为您的教学提供递归的优秀用法的插图。
到底发生了什么?
当我们调用Print_And_Decrement过程时,它首先详细说明它的形式变量,并将调用程序传递的值赋给它们。它们存储在堆栈中,这是Ada系统留出的内存的内部部分,用于存储动态变量和常量,并可供以后使用。然后生成局部变量并对其进行详细说明,尽管在本例中只有一个局部变量,并使用引用它们的方法存储在堆栈上。最后,程序代码本身被实际执行,当它完成时,局部变量和形式变量被从堆栈中删除,不再存在。在递归调用中,堆栈随着每个新调用而增长,当控件返回到代码的前一个调用时,该调用的变量仍在堆栈中并可供使用。在这个程序中,我们实际上只存储了可执行代码的一个副本,但是在堆栈中存储了形式变量和局部变量的多个副本。
所有ADA子程序都是可重入的
如果你在系统编程方面有经验,并且理解程序的可重入性意味着什么,你就会理解这种可变存储方式是如何允许所有Ada子程序可重入的。ARM要求所有Ada子程序都是可重入的,但是如果您不知道这意味着什么,不要担心。
在递归调用之后插入一些代码来显示形式参数的值,以便在每次递归调用返回时看到形式变量的值仍然可用,这对您来说是一个很好的练习。该代码将立即插入第18行之后。
在继续下一个示例程序之前,您应该花一些必要的时间完全理解这个程序。
递归函数
Example program ------> e_c18_p4.ada
-- Chapter 18 - Program 4 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure FuncRecr is START : constant := -2; STOP : constant := 5; Result : INTEGER; Data_Value : INTEGER; function Factorial_Possible(Number : INTEGER) return BOOLEAN; function Factorial(Number : INTEGER) return INTEGER is begin if not Factorial_Possible(Number) then Put("Factorial not possible for"); Put(Number, 4); New_Line; return 0; end if; if Number = 0 then return 1; elsif Number = 1 then return 1; else return Factorial(Number - 1) * Number; end if; end Factorial; function Factorial_Possible(Number : INTEGER) return BOOLEAN is begin if Number >= 0 then return TRUE; else return FALSE; end if; end Factorial_Possible; begin Put("Factorial program"); New_Line(2); for Number_To_Try in START..STOP loop Put(Number_To_Try, 3); if Factorial_Possible(Number_To_Try) then Result := Factorial(Number_To_Try); Put(" is legal to factorialize and the result is"); Put(Result, 6); else Put(" is not legal to factorialize."); end if; New_Line; end loop; New_Line; Data_Value := 4; Result := Factorial(2 - Data_Value); -- Factorial(-2) Result := Factorial(Data_Value + 3); -- Factorial(7) Result := Factorial(2 * Data_Value - 3); -- Factorial(5) Result := Factorial(Factorial(3)); -- Factorial(6) Result := Factorial(4); -- Factorial(4) Result := Factorial(0); -- Factorial(0) end FuncRecr; -- Result of Execution -- Factorial program -- -- -2 is not legal to factorialize. -- -1 is not legal to factorialize. -- 0 is legal to factorialize and the result is 1 -- 1 is legal to factorialize and the result is 1 -- 2 is legal to factorialize and the result is 2 -- 3 is legal to factorialize and the result is 6 -- 4 is legal to factorialize and the result is 24 -- 5 is legal to factorialize and the result is 120 -- -- Factorial not possible for -2
检查名为e_c18_p4.ada的程序以获得递归函数的示例,该函数实际上与递归过程没有什么不同。这个程序用来说明另外两个Ada概念,这两个概念对您来说都不是新概念,但都包含有价值的见解。第一种是将在一开始讨论的部分声明,另一种是将推迟几段讨论的例外情况。
部分声明
Ada要求您在使用它之前定义所有内容,而且这个规则永远不会被打破。假设您希望有一个包含两个过程、两个函数的程序,甚至一个过程和一个函数,彼此递归调用。如果首先声明了A,那么B可以调用它,但是A不能调用B,因为在A被详细阐述的时候它不会被定义,并且我们不能打破在使用它之前定义一切的规则。这更恰当地称为线性声明。Ada通过允许类型、过程、函数或包的部分声明来解决这个问题。如果您还记得的话,我们在研究访问类型变量时使用了关于记录的部分声明,因此这个概念对您来说并不完全是新概念。将部分声明称为函数规范也是合适的。
在本示例程序中,我们希望将函数按所示顺序放置在程序中,除了说明可以这样做之外,没有其他好的理由,但是我们有线性声明的问题,因为Factorial调用 Factorial_Possible,并且它们的顺序是错误的。第12行中的部分声明告诉系统,函数 Factorial_Possible将在稍后定义,它的特征是什么,因为形式参数是与其返回类型一起定义的。然后完全定义Factorial函数,它可以调用另一个函数,因为它有接口的定义。然后我们就有了可能的函数阶乘的完整定义,并且在满足线性声明的要求的同时,我们已经完成了我们期望的目标。
现在是递归函数
假设您熟悉高等数学中的阶乘函数,我们将继续进行程序描述。由于阶乘(N)与N次阶乘(N-1)相同,我们可以使用递归技术计算任意数字的阶乘。我们继续递归,直到我们到达需要阶乘(1)的值的点,我们将其定义为1,然后开始返回递归链,每次乘以我们在特定递归调用中输入的值。通过将阶乘(0)的值定义为1,并将取任何负数的阶乘设置为非法,我们现在可以编写完整的程序。大多数程序都涉及检查限制和输出消息,但实际工作由第27行完成,这是本段前面定义的递归调用。
额外的检查已经完成
课程对你来说一点也不难理解,所以你只能自己学习了。然而,关于风格的一些评论应该被提及。函数Factorial调用另一个函数来验证值是否可分解,然后再尝试这样做,主程序在第44行到第54行中,在调用函数来尝试分解数字之前检查值。像这样的冗余检查不一定是坏的,因为这个过程被编写成包含所有内容的,并且主程序可能希望执行与这个过程所指示的完全不同的操作。然而,在第58到63行中,主程序通过调用过程而不首先检查数据来接受过程提供的默认错误处理。
编译并运行这个程序,根据程序的哪个部分处理错误来观察各种消息输出。请注意,该程序并不意味着是一个良好的编程风格的说明,只是作为一个使用Ada可以做的一些事情的说明。
再来看看例外情况
最后一个程序是不寻常的,因为它能够说明Ada系统定义的四个标准异常中的三个。它们可以说明如下:;
1.Program_Error-删除第27行的返回,您将退出函数的底部。(实际上,经过验证的编译器将生成编译错误。)
2.Constraint_Error-将第14行中形式参数的类型更改为正数,它涵盖了所有合法计算阶乘的数字的范围。然后以-1作为参数调用函数。
3.Storage_Error-调用阶乘(100000)。在完成所需数量的递归之前,堆栈应该溢出。
返回数组的函数
Example program ------> e_c18_p5.ada
-- Chapter 18 - Program 5 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Revers is type MY_ARRAY is array(3..10) of INTEGER; Store_Here : MY_ARRAY := (3, 16, -5, 6, 12, 66, -13, 57); Another : MY_ARRAY; function Reverse_Array(Data : MY_ARRAY) return MY_ARRAY is Temp : MY_ARRAY; begin for Index in Data'RANGE loop Temp(Index) := Data(Data'FIRST + Data'LAST - Index); end loop; return Temp; end Reverse_Array; begin for Index in Store_Here'Range loop Put(Store_Here(Index), 5); end loop; New_Line; Another := Reverse_Array(Store_Here); for Index in Another'Range loop Put(Another(Index), 5); end loop; New_Line; Another := Reverse_Array(Another); for Index in Another'Range loop Put(Another(Index), 5); end loop; New_Line; end Revers; -- Result of Execution -- 3 16 -5 6 12 66 -13 57 -- 57 -13 66 12 6 -5 16 3 -- 3 16 -5 6 12 66 -13 57
名为e_c18_p5.ada的示例程序将为您提供一个函数示例,该函数返回多个标量变量,实际上,它返回整型变量的整个数组。程序本身非常简单,唯一不同于大多数函数的是第12行中列出的返回类型,以及第18行中的实际返回。当然,它们必须在类型上一致,否则编译过程中会出现类型不匹配错误。
使用这里给出的技术,没有理由说函数不能返回一个记录,甚至是一个记录数组,只要类型被正确定义并且在使用时它们是一致的。
内联pragma
pragma是一个编译器指令,正如我们前面提到的,内联pragma告诉编译器展开被调用的子程序体,并为每个调用将其插入到调用程序中。由于代码是内联插入的,因此没有调用序列,因此在设置子程序链接时也不会浪费时间,但是每个子程序调用都有单独的代码段。结果通常是一个更大但更快的程序。这个pragma的用法说明如下;
pragma INLINE(subprogram1, subprogram2, ... );
这一行插入到编译单元的声明性部分,遵循子程序规范。根据定义,子程序的含义不受pragma的影响。
运算符重载和“use”子句
Example program ------> e_c18_p6.ada
-- Chapter 18 - Program 6 package Shape is type BOX is record Length : INTEGER; Width : INTEGER; Height : INTEGER; end record; function Make_A_Box(In_Length, In_Width, In_Height : INTEGER) return BOX; function "+"(Left, Right : BOX) return BOX; function "+"(Left : INTEGER; Right : BOX) return BOX; function "*"(Left : INTEGER; Right : BOX) return BOX; procedure Print_Box(Input_Box : BOX); end Shape; with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; package body Shape is function Make_A_Box(In_Length, In_Width, In_Height : INTEGER) return BOX is Temp_Box : BOX; begin Temp_Box.Length := In_Length; Temp_Box.Width := In_Width; Temp_Box.Height := In_Height; return Temp_Box; end Make_A_Box; function "+"(Left, Right : BOX) return BOX is Temp_Box : BOX; begin Temp_Box.Length := Left.Length + Right.Length; Temp_Box.Width := Left.Width + Right.Width; Temp_Box.Height := Left.Height + Right.Height; return Temp_Box; end "+"; function "+"(Left : INTEGER; Right : BOX) return BOX is Temp_Box : BOX; begin Temp_Box.Length := Left + Right.Length; Temp_Box.Width := Left + Right.Width; Temp_Box.Height := Left + Right.Height; return Temp_Box; end "+"; function "*"(Left : INTEGER; Right : BOX) return BOX is Temp_Box : BOX; begin Temp_Box.Length := Left * Right.Length; Temp_Box.Width := Left * Right.Width; Temp_Box.Height := Left * Right.Height; return Temp_Box; end "*"; procedure Print_Box(Input_Box : BOX) is begin Put("Length = "); Put(Input_Box.Length, 3); Put(" Width = "); Put(Input_Box.Width, 3); Put(" Height = "); Put(Input_Box.Height, 3); New_Line; end Print_Box; end Shape; with Shape; use type Shape.BOX; procedure OperOver is Small_Box : Shape.BOX; Big_Box : Shape.BOX; begin Small_Box := Shape.Make_A_Box(2, 3, 2); Big_Box := Shape.Make_A_Box(4, 5, 3); Shape.Print_Box(Small_Box); Shape.Print_Box(Big_Box); Shape.Print_Box(Small_Box + Big_Box); Shape.Print_Box(4 + Small_Box); Shape.Print_Box(10 * Big_Box); Shape.Print_Box(Small_Box + 5 * Big_Box); Shape.Print_Box(Shape."+"(Small_Box, Shape."*"(5, Big_Box))); end OperOver; -- Result of execution -- Length = 2 Width = 3 Height = 2 -- Length = 4 Width = 5 Height = 3 -- Length = 6 Width = 8 Height = 5 -- Length = 6 Width = 7 Height = 6 -- Length = 40 Width = 50 Height = 30 -- Length = 22 Width = 28 Height = 17 -- Length = 22 Width = 28 Height = 17
本教程之前已经提到过操作员过载,但是有必要在这个非常重要的主题上多讲一点内容,我们将使用e_c18_p6.ada作为讨论的工具。
我们在第2行到第18行定义了一个名为Shape的包规范,包中定义了一个名为BOX的记录类型。子程序 Make_A_Box and Print_Box 为我们提供了生成和显示Box类型的任何变量的能力,并且我们提供了三个重载操作符用于这种类型。其中两个子程序重载+运算符以用于此类变量,另一个为我们提供乘法功能。如果需要的话,可以定义更多的重载,但是这三个说明了该技术。
包主体在第22行到第75行中给出。重载操作在现实世界中没有什么意义,因为添加两个框通常不会通过简单地添加所有三维的大小来完成。然而,我们对重载操作符的技术更感兴趣,因此接口才是真正重要的。实现应该很简单,你可以自己学习。
呼叫程序
调用程序无疑是这个例子中最有趣的部分,从第80行开始,我们有一个新的use-type构造。在这一点上,一个简短的离题是为了解释这是怎么回事。
许多有经验的Ada程序员认为use语句不应该在Ada程序中使用,因为它隐藏了任何子程序调用或类型定义的源代码。因此,许多项目禁止使用它。在作者看来Ada.Text_IO应该是一个允许的违反这一规则,但这是另一个离题,所以我们将不进一步考虑它。为了使重载运算符可用作中缀运算符,如本示例程序的第94行到第97行所示,必须包含use子句。如果不包括第97行,则需要按照第99行的说明编写,这是一种非常难看且难以阅读的符号。在第80行中使用use type构造使重载运算符可用作中缀运算符,但不允许使用Shape包中没有包前缀的任何其他子程序。这使得可读性更容易,并且一些项目负责人希望获得额外的安全性。
在第84行和第85行定义变量时,以及在调用非运算符重载的子程序时(如第89行到第99行所示),都需要包名。注意,第97行计算我们期望的结果,因为乘法是在加法之前完成的。重载运算符时不可能更改优先级顺序。
编程练习
1.如前所述,在e_c18_p3.ada 中包含一个output语句,以显示递归调用后的value值,从而看到递归通过递归调用链返回(Solution)
2.用名为Increment和Display的两个过程编写一个递归程序,这两个过程互相调用并递增一个变量,初始化为1,直到它达到8的计数(Solution)
---------------------------------------------------------------------------------------------------------------------------
原英文版出处:https://perso.telecom-paristech.fr/pautet/Ada95/a95list.htm
翻译(百度):博客园 一个默默的 *** 的人