第4课|程序结构与编译流程

15 阅读9分钟

本课目标

完成本课后,你将能够:

  • 理解Ada库单元、子单元、主程序的层级关系

  • 掌握 gnatmake 的依赖分析原理与构建流程

  • 区分分离编译与分别编译的概念与应用

  • 构建并管理第一个多文件Ada项目

  • 使用 gnat 工具链进行项目构建与清理


一、Ada程序的单元体系

1.1 什么是程序单元?

Ada将代码组织为程序单元(Program Units),这是模块化的核心概念:

单元类型文件扩展名作用类比
子程序(Subprogram).adb可执行的过程或函数C的函数
包(Package).ads + .adb封装数据与操作Java的类/C++的命名空间
泛型单元(Generic).ads + .adb参数化模板C++模板/Java泛型
任务单元(Task).adb并发执行实体操作系统线程
保护单元(Protected.adb同步数据访问互斥锁

1.2 库单元 vs 子单元

这是Ada独特的两级组织方式:

库单元(Library Unit)          ← 顶层,可被其他单元直接引用
    │
    ├── 子单元(Subunit)       ← 嵌套,只能被父单元引用
    │       └── 孙单元(Sub-subunit)
    │
    └── 子单元

关键区别

特性库单元子单元
引用方式with 子句separate 子句
可见范围全局可见仅父单元可见
文件命名直接对应单元名父单元名.子单元名
编译独立性完全独立编译依赖父单元上下文

二、包的规范与实现分离

2.1 包的两部分结构

Ada强制将接口与实现分离,这是工业级可靠性的基石:

-- 文件名: calculator.ads
-- 包规范(Package Specification):对外可见的接口

package Calculator is
   
   -- 类型声明(对外可见)
   type Operation is (Add, Subtract, Multiply, Divide);
   
   -- 子程序声明(仅签名,无实现)
   function Calculate (A, B : Float; Op : Operation) return Float;
   
   -- 常量声明
   Error_Value : constant Float := Float'Last;
   
private  -- 私有部分,对外可见但不可访问细节
   
   -- 这里可以放置实现细节,但外部只能知道存在,无法直接使用
   Internal_Precision : constant := 6;
   
end Calculator;
-- 文件名: calculator.adb
-- 包体(Package Body):实现细节,对外隐藏

package body Calculator is

   function Calculate (A, B : Float; Op : Operation) return Float is
   begin
      case Op is
         when Add      => return A + B;
         when Subtract => return A - B;
         when Multiply => return A * B;
         when Divide   =>
            if B /= 0.0 then
               return A / B;
            else
               return Error_Value;
            end if;
      end case;
   end Calculate;

end Calculator;

2.2 信息隐藏的三层级别

级别关键字可见性用途
公开package 后至 private所有客户端可见接口、常量、类型名
私有private 后至 end可见但不可访问内容完整类型定义、实现细节
包体内package body仅包体内部可见辅助子程序、内部状态

三、多文件项目实战

3.1 项目结构规划

创建项目目录 math_project

math_project/
├── math_utils.ads      # 数学工具包规范
├── math_utils.adb      # 数学工具包体
├── io_handler.ads      # 输入输出处理包规范
├── io_handler.adb      # 输入输出处理包体
└── main.adb            # 主程序

3.2 编写各文件

math_utils.ads(规范):

package Math_Utils is
   
   function Factorial (N : Natural) return Natural;
   -- 计算阶乘,N必须 <= 12(防止溢出)
   
   function Is_Prime (N : Positive) return Boolean;
   -- 判断是否为素数
   
   Max_Factorial_Input : constant := 12;
   
end Math_Utils;

math_utils.adb(包体):

package body Math_Utils is

   function Factorial (N : Natural) return Natural is
      Result : Natural := 1;
   begin
      for I in 2 .. N loop
         Result := Result * I;
      end loop;
      return Result;
   end Factorial;

   function Is_Prime (N : Positive) return Boolean is
   begin
      if N < 2 then
         return False;
      end if;
      
      for I in 2 .. N / 2 loop
         if N mod I = 0 then
            return False;
         end if;
      end loop;
      
      return True;
   end Is_Prime;

end Math_Utils;

io_handler.ads(规范):

with Math_Utils;  -- 依赖math_utils包

package IO_Handler is
   
   procedure Print_Result (N : Natural; Is_Fact : Boolean);
   -- 打印阶乘或素数检查结果
   
   function Get_Number return Natural;
   -- 从用户获取一个非负整数
   
end IO_Handler;

io_handler.adb(包体):

with Ada.Text_IO;
with Ada.Integer_Text_IO;

package body IO_Handler is

   procedure Print_Result (N : Natural; Is_Fact : Boolean) is
      use Ada.Text_IO;
   begin
      if Is_Fact then
         Put ("Factorial of ");
         Ada.Integer_Text_IO.Put (N, Width => 0);
         Put (" is ");
         Ada.Integer_Text_IO.Put (Math_Utils.Factorial (N), Width => 0);
         New_Line;
      else
         Put ("Is ");
         Ada.Integer_Text_IO.Put (N, Width => 0);
         Put (" prime? ");
         
         if Math_Utils.Is_Prime (N) then
            Put_Line ("Yes");
         else
            Put_Line ("No");
         end if;
      end if;
   end Print_Result;

   function Get_Number return Natural is
      Result : Natural;
   begin
      Ada.Text_IO.Put ("Enter a number (0-12 for factorial): ");
      Ada.Integer_Text_IO.Get (Result);
      return Result;
   end Get_Number;

end IO_Handler;

main.adb(主程序):

with Ada.Text_IO;
with IO_Handler;   -- 间接依赖math_utils

procedure Main is
   Choice : Character;
   Number : Natural;
begin
   
   loop
      Ada.Text_IO.Put_Line ("=== Math Tool ===");
      Ada.Text_IO.Put_Line ("1. Factorial");
      Ada.Text_IO.Put_Line ("2. Prime Check");
      Ada.Text_IO.Put_Line ("3. Exit");
      Ada.Text_IO.Put ("Choice: ");
      Ada.Text_IO.Get (Choice);
      Ada.Text_IO.Skip_Line;  -- 消耗换行符
      
      case Choice is
         when '1' =>
            Number := IO_Handler.Get_Number;
            if Number <= Math_Utils.Max_Factorial_Input then
               IO_Handler.Print_Result (Number, True);
            else
               Ada.Text_IO.Put_Line ("Number too large!");
            end if;
            
         when '2' =>
            Number := IO_Handler.Get_Number;
            IO_Handler.Print_Result (Number, False);
            
         when '3' =>
            Ada.Text_IO.Put_Line ("Goodbye!");
            exit;
            
         when others =>
            Ada.Text_IO.Put_Line ("Invalid choice!");
      end case;
      
      Ada.Text_IO.New_Line;
   end loop;
   
end Main;

3.3 编译多文件项目

进入项目目录,执行:

gnatmake main.adb

gnatmake的自动处理流程

1.解析依赖:读取 main.adb ,发现 with IO_Handler

2.递归解析:读取 io_handler.ads ,发现 with Math_Utils

3.检查规范:读取 math_utils.ads ,无进一步依赖

4.编译顺序

  • 编译 math_utils.ads (规范)

  • 编译 math_utils.adb (包体)

  • 编译 io_handler.ads (规范)

  • 编译 io_handler.adb (包体)

  • 编译 main.adb (主程序)

5.绑定链接:生成可执行文件 main

输出示例

gcc -c math_utils.ads
gcc -c math_utils.adb
gcc -c io_handler.ads
gcc -c io_handler.adb
gcc -c main.adb
gnatbind main.ali
gnatlink main.ali

四、编译系统深度解析

4.1 ALI文件的作用

编译后生成的 .ali 文件(Ada Library Information)包含:

  • 依赖关系:本单元依赖哪些其他单元

  • 版本信息:编译时的Ada标准版本

  • 接口信息:供其他单元使用的符号表

  • 优化信息:用于链接时优化

ALI文件是 gnatmake 进行增量编译的依据。修改源文件后,gnatmake通过比较时间戳和ALI内容,只重新编译必要的单元。


4.2 增量编译演示;

修改 math_utils.adb 中的注释,重新编译:

gnatmake main.adb

输出:

gcc -c math_utils.adb
gnatbind main.ali
gnatlink main.ali

仅重新编译修改的文件及其依赖者io_handler 未改动则跳过。


4.3 手动编译步骤(理解原理)

如需完全控制编译流程:

# 1. 编译规范(生成.ali和.o)
gcc -c math_utils.ads
gcc -c io_handler.ads

# 2. 编译包体(需要对应.ali已存在)
gcc -c math_utils.adb
gcc -c io_handler.adb

# 3. 编译主程序
gcc -c main.adb

# 4. 绑定(生成binder文件)
gnatbind main.ali

# 5. 链接(生成可执行文件)
gnatlink main.ali -o my_program

日常开发使用 gnatmake 即可,手动步骤用于理解底层或特殊构建需求。


五、子单元(Subunit)机制

5.1 何时使用子单元?

当包体过于庞大时,可将部分子程序分离为子单元,实现物理上的分离编译

5.2 子单元示例

主包体data_processor.adb ):

package body Data_Processor is

   procedure Process_Large_Data (Data : in out Data_Array) is
      separate;  -- 标记为子单元,实现在单独文件
   -- 注意:此处无begin/end,实现完全分离

   procedure Process_Small_Data (Data : in out Data_Array) is
   begin
      -- 简单处理,直接在此实现
      for I in Data'Range loop
         Data (I) := Data (I) * 2;
      end loop;
   end Process_Small_Data;

end Data_Processor;

子单元文件data_processor-process_large_data.adb ):

separate (Data_Processor)  -- 声明所属父单元

procedure Process_Large_Data (Data : in out Data_Array) is
   -- 子单元可以访问父单元的所有声明
   Temp : Integer;
begin
   for I in Data'Range loop
      Temp := Data (I);
      -- 复杂处理逻辑...
      Data (I) := Temp ** 2;
   end loop;
end Process_Large_Data;

命名规则:子单元文件名 = 父单元名-子程序名.adb (连字符分隔)


六、项目构建最佳实践

6.1 目录结构规范

my_project/
├── src/                    # 源代码
│   ├── main.adb
│   ├── utils/
│   │   ├── math_utils.ads
│   │   └── math_utils.adb
│   └── io/
│       ├── io_handler.ads
│       └── io_handler.adb
├── obj/                    # 编译产物(.o, .ali)
├── bin/                    # 可执行文件
└── Makefile 或 gprbuild配置

6.2 使用gprbuild(现代推荐)

创建 my_project.gpr 项目文件:

project My_Project is
   
   for Source_Dirs use ("src", "src/utils", "src/io");
   for Object_Dir use "obj";
   for Exec_Dir use "bin";
   for Main use ("main.adb");
   
   package Builder is
      for Default_Switches ("Ada") use ("-s");  -- 重新编译时显示命令
   end Builder;
   
   package Compiler is
      for Default_Switches ("Ada") use ("-g", "-O0", "-gnatwa");
      -- -g: 调试信息, -O0: 无优化, -gnatwa: 激活所有警告
   end Compiler;
   
end My_Project;

构建命令

gprbuild my_project.gpr

七、清理与维护

7.1 清理编译产物

gnatclean main  # 清理main及其依赖的所有产物

或手动删除:

  • *.o (目标文件)

  • *.ali (库信息文件)

  • 可执行文件

7.2 完整重建

gnatclean main
gnatmake main.adb

八、常见构建错误

错误信息原因解决
file "xxx.ads" not foundwith 引用的包不存在或路径错误检查文件名、路径、环境变量
xxx must be compiled before yyy规范未编译就编译包体先编译.ads文件,或直接用gnatmake
ambiguous dependency循环依赖(A依赖B,B依赖A)重构设计,打破循环
subunit not found子单元文件名不匹配检查 separate (Parent) 与文件名
body not found有规范无对应包体创建.adb文件或删除规范中的实现

九、本课总结

  • Ada程序由库单元(全局可见)和子单元(父单元私有)组成

  • 包规范(.ads)定义接口,包体(.adb)隐藏实现,强制分离

  • gnatmake 自动分析依赖、增量编译,生成可执行文件

  • 子单元通过 separate 实现物理分离,适用于大型子程序

  • 现代项目推荐使用 gprbuild.gpr 项目文件管理构建


十、课后练习

1.添加功能:在 math_utils 包中添加 GCD (最大公约数)函数,并在主程序中调用。

2.创建新包:创建 string_utils 包,提供 Reverse_String 函数,独立于现有包。

3.子单元实践:将 io_handler 中的 Print_Result 分离为子单元。

4.错误排查:故意删除 math_utils.adb ,观察编译错误并理解原因。

5.gprbuild配置:为当前项目创建 .gpr 文件,使用 gprbuild 构建。


十一、下节预告

第5课|标识符与保留字

我们将:

  • 系统学习Ada 73个保留字的分类与用法

  • 掌握标识符的完整命名规则与编码规范

  • 理解Unicode标识符支持与限制

  • 学习代码风格检查工具 gnatcheck 的使用


关键术语表

库单元:顶层编译单元,可被其他单元直接引用

子单元:嵌套在父单元中的单元,仅父单元可见

规范(Specification):包的接口声明,.ads文件

包体(Body):包的实现细节,.adb文件

ALI文件:Ada库信息文件,记录依赖与接口信息

增量编译:只重新编译修改过的单元及其依赖者


提示警告:本课程内容(包括但不限于文字、图片、音频、视频等)版权归原作者所有,未经授权严禁转载、复制、翻录、传播或以任何方式用于商业用途。本课程仅供个人学习使用,请尊重知识产权,共同维护良好的创作环境。如有疑问或需授权合作,请联系版权方。感谢您的理解与支持!