第15课|子程序与参数传递

8 阅读8分钟

本课目标

完成本课后,你将能够:

  • 掌握过程与函数的声明与使用
  • 深入理解参数模式(inoutin out)的本质
  • 熟练使用默认参数与命名参数调用
  • 理解子程序的重载与递归
  • 应用子程序进行模块化设计

一、子程序概述

1.1 过程与函数

Ada 的子程序分为两类:

  • 过程(procedure):执行一系列操作,不返回值
  • 函数(function):计算并返回一个值
-- 过程:无返回值
procedure Greet (Name : in String) is
begin
   Ada.Text_IO.Put_Line ("Hello, " & Name);
end Greet;

-- 函数:返回一个值
function Square (X : Float) return Float is
begin
   return X * X;
end Square;

1.2 子程序的结构

-- 完整结构
procedure Name (参数列表) is
   -- 声明部分(局部常量、变量、类型等)
begin
   -- 执行部分
   -- 可以使用 return; (过程)或 return 表达式; (函数)
end Name;

1.3 子程序的基本调用

-- 过程调用(语句)
Greet ("Ada");

-- 函数调用(表达式)
Area : Float := Square (5.0);

二、参数模式

Ada 使用参数模式定义数据流向,在编译时保证正确性。

2.1 三种参数模式

模式含义数据流向可读可写
in输入参数调用者 → 子程序否(但可初始化)
out输出参数子程序 → 调用者否(未初始化)
in out输入/输出参数双向

2.2 in 模式

默认模式,可省略 in 关键字。参数在子程序内是只读的:

procedure Show (Value : in Integer) is
begin
   Ada.Text_IO.Put_Line (Integer'Image (Value));
   -- Value := 10;   -- 错误!不能修改 in 参数
end Show;

2.3 out 模式

输出参数,子程序内部必须对其进行赋值,调用时实参可以是未初始化的变量:

procedure Get_Value (Result : out Integer) is
begin
   Result := 42;   -- 必须赋值
end Get_Value;

-- 调用
X : Integer;        -- 未初始化
Get_Value (X);      -- X 现在为 42

注意out 参数在子程序开始时是未定义的(即使实参已有值),因此不能读取它(除非先赋值)。

2.4 in out 模式

可读可写,实参必须是变量:

procedure Increment (Value : in out Integer) is
begin
   Value := Value + 1;
end Increment;

-- 调用
X : Integer := 5;
Increment (X);      -- X 变为 6

2.5 参数传递机制

Ada 的参数传递由编译器决定(通常按引用或按值),但程序员不直接控制。对于标量类型,通常按值传递;对于大对象(如数组、记录),通常按引用传递。这不会影响程序语义,因为参数模式保证了正确的数据流向。


三、参数定义与调用

3.1 位置关联与命名关联

调用子程序时,可以按位置传递参数,也可以按名称传递:

procedure Format (Value : Float; Width : Integer; Precision : Integer) is
begin
   Ada.Float_Text_IO.Put (Value, Fore => Width, Aft => Precision, Exp => 0);
end Format;

-- 位置关联
Format (3.14159, 5, 2);

-- 命名关联(可任意顺序)
Format (Precision => 2, Value => 3.14159, Width => 5);

命名关联使代码更清晰,尤其当参数较多或有默认值时。

3.2 默认参数

可以为参数指定默认值,调用时可省略:

procedure Log (Message : String; Level : String := "INFO") is
begin
   Ada.Text_IO.Put_Line (Level & ": " & Message);
end Log;

-- 调用
Log ("System started");               -- Level 默认为 "INFO"
Log ("Error detected", "ERROR");      -- 覆盖默认值

默认参数只能在末尾省略(位置关联时),但使用命名关联可省略任何有默认值的参数。

3.3 参数的类型与约束

参数可以是任何类型,包括有约束类型:

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
subtype Weekday is Day range Mon .. Fri;

procedure Work_Day (D : Weekday) is ...   -- 只接受工作日

3.4 无约束数组作为参数

无约束数组参数可以接受任意大小的实参:

function Sum (A : array (Integer range <>) of Float) return Float is
   S : Float := 0.0;
begin
   for I in A'Range loop
      S := S + A (I);
   end loop;
   return S;
end Sum;

-- 调用
V1 : array (1 .. 5) of Float := (1.0, 2.0, 3.0, 4.0, 5.0);
V2 : array (0 .. 9) of Float := (others => 1.0);
Total1 := Sum (V1);   -- OK
Total2 := Sum (V2);   -- OK

四、函数

4.1 函数的定义与返回

函数必须指定返回类型,且至少有一条 return 语句返回该类型的值:

function Max (A, B : Integer) return Integer is
begin
   if A > B then
      return A;
   else
      return B;
   end if;
end Max;

4.2 函数返回值的使用

X := Max (10, 20);                     -- 赋值给变量
if Max (A, B) > 100 then ...           -- 用在条件中
Ada.Text_IO.Put_Line (Integer'Image (Max (5, 7)));  -- 直接输出

4.3 函数与过程的区别

  • 函数不能有 outin out 参数(Ada 2012 之后允许 in out 参数,但通常不推荐)
  • 函数必须返回一个值,过程不能返回
  • 函数调用是表达式,过程调用是语句

五、子程序的重载

重载允许在同一作用域中使用相同的子程序名,但参数类型或个数不同。

5.1 重载规则

-- 重载过程
procedure Print (Value : Integer) is
begin
   Ada.Integer_Text_IO.Put (Value);
end Print;

procedure Print (Value : Float) is
begin
   Ada.Float_Text_IO.Put (Value);
end Print;

-- 重载函数
function Max (A, B : Integer) return Integer is ...
function Max (A, B : Float) return Float is ...

5.2 重载解析

编译器根据调用时实参的类型和数量选择正确的子程序。如果无法确定(如字面量可匹配多种类型),可以使用类型转换或命名关联来区分。

Print (10);        -- 调用 Integer 版本
Print (3.14);      -- 调用 Float 版本

-- 歧义情况
procedure Test (X : Integer) is ...
procedure Test (X : Float) is ...
Test (10);         -- OK,字面量 10 是 Integer 类型
Test (10.0);       -- OK,字面量 10.0 是 Float 类型
Test (1);          -- 字面量 1 可能被解释为 Integer,没有歧义

六、递归

Ada 支持递归,函数或过程可以调用自身。

6.1 递归示例:阶乘

function Factorial (N : Natural) return Natural is
begin
   if N = 0 then
      return 1;
   else
      return N * Factorial (N - 1);
   end if;
end Factorial;

6.2 递归示例:斐波那契数列

function Fib (N : Natural) return Natural is
begin
   if N <= 1 then
      return N;
   else
      return Fib (N - 1) + Fib (N - 2);
   end if;
end Fib;

注意:递归深度受系统栈限制,深层递归可能导致栈溢出。对于性能敏感场景,考虑迭代实现。


七、子程序的声明与体分离

7.1 前向声明

子程序的体可以放在声明部分之后,使用前向声明(forward declaration)让子程序互相调用:

procedure A (X : in Integer);  -- 前向声明

procedure B (Y : in Integer) is
begin
   A (Y);   -- 现在可以调用 A
end B;

procedure A (X : in Integer) is
begin
   B (X);
end A;

7.2 包规范与包体

在实际项目中,子程序通常声明在包规范(.ads)中,实现在包体(.adb)中。这将在后续“包”课程中详细讲解。


八、完整示例:数学工具库

-- 文件名: math_utils.adb
-- 功能:演示子程序的各种特性

with Ada.Text_IO;
with Ada.Float_Text_IO;

procedure Math_Utils is

   -- 过程:输出一个整数的二进制表示
   procedure Print_Binary (N : in Natural; Width : in Positive := 8) is
      Temp : Natural := N;
      Bits : array (1 .. Width) of Character := (others => '0');
   begin
      for I in reverse 1 .. Width loop
         if Temp mod 2 = 1 then
            Bits (I) := '1';
         end if;
         Temp := Temp / 2;
         exit when Temp = 0;
      end loop;
      Ada.Text_IO.Put (Bits);
   end Print_Binary;

   -- 函数:幂运算(递归)
   function Power (Base : Float; Exponent : Natural) return Float is
   begin
      if Exponent = 0 then
         return 1.0;
      else
         return Base * Power (Base, Exponent - 1);
      end if;
   end Power;

   -- 重载:幂运算(整数底数)
   function Power (Base : Integer; Exponent : Natural) return Integer is
   begin
      if Exponent = 0 then
         return 1;
      else
         return Base * Power (Base, Exponent - 1);
      end if;
   end Power;

   -- 过程:交换两个整数(in out 模式)
   procedure Swap (A, B : in out Integer) is
      Temp : Integer := A;
   begin
      A := B;
      B := Temp;
   end Swap;

   -- 函数:计算最大值(重载)
   function Max (A, B : Integer) return Integer is
   begin
      if A > B then return A; else return B; end if;
   end Max;

   function Max (A, B : Float) return Float is
   begin
      if A > B then return A; else return B; end if;
   end Max;

   -- 测试变量
   X, Y : Integer := 10;
   Z : Float := 5.5;

begin
   Ada.Text_IO.Put_Line ("=== 二进制打印 ===");
   Print_Binary (42);
   Ada.Text_IO.New_Line;
   Print_Binary (255, Width => 16);
   Ada.Text_IO.New_Line;

   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line ("=== 幂运算 ===");
   Ada.Float_Text_IO.Put (Power (2.0, 10), Fore => 1, Aft => 0, Exp => 0);
   Ada.Text_IO.Put (" (2^10)");
   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line (Integer'Image (Power (2, 10)) & " (2^10)");

   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line ("=== 交换 ===");
   X := 5; Y := 9;
   Ada.Text_IO.Put_Line ("交换前: X = " & Integer'Image (X) & ", Y = " & Integer'Image (Y));
   Swap (X, Y);
   Ada.Text_IO.Put_Line ("交换后: X = " & Integer'Image (X) & ", Y = " & Integer'Image (Y));

   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line ("=== 最大值 ===");
   Ada.Text_IO.Put_Line ("Max(10,20) = " & Integer'Image (Max (10, 20)));
   Ada.Text_IO.Put_Line ("Max(3.14,2.71) = " & Float'Image (Max (3.14, 2.71)));
end Math_Utils;

九、本课总结

  • Ada 子程序分为过程(无返回值)和函数(有返回值)
  • 参数模式 inoutin out 明确数据流向,编译器保证正确性
  • 支持默认参数和命名参数调用,提高可读性
  • 重载允许同名子程序不同参数类型
  • 递归是重要编程技术,但需注意栈深度
  • 子程序可以前向声明,支持互相调用

十、课后练习

  1. 基本练习:编写一个过程 Print_Calendar,接受月份和年份,输出该月的日历(需考虑闰年)。

  2. 默认参数:编写一个函数 Format_Time,接受小时、分钟、秒,默认秒为0,分钟和小时也为0,返回格式化的时间字符串(如 "12:30:00")。测试不同参数组合。

  3. 重载:定义一个过程 Put,重载三种版本:IntegerFloatString,输出时分别添加前缀(如 "Int: "、"Float: "、"Str: ")。

  4. 递归:实现一个函数 GCD(最大公约数),使用欧几里得算法(递归版本)。测试多组数据。

  5. 参数模式:分析以下代码的错误,并解释原因:

    procedure Bad (A : in Integer; B : out Integer) is
    begin
       B := A + B;  -- 哪行错误?为什么?
    end Bad;
    

十一、下节预告

第16课|包与模块化设计

我们将:

  • 掌握包的规范与体的分离
  • 学习信息隐藏与私有类型
  • 理解包的初始化与子程序可见性
  • 应用包进行大型程序模块化

关键术语表

过程(procedure):执行操作而不返回值的子程序

函数(function):计算并返回一个值的子程序

参数模式in(只读)、out(只写)、in out(读写)

默认参数:为参数提供默认值,调用时可省略

命名关联:在调用时显式指定参数名

重载(overloading):同一作用域内同名子程序,参数类型或数量不同

递归(recursion):子程序直接或间接调用自身

前向声明:子程序的提前声明,允许相互调用