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

本课目标

完成本课后,你将能够:

  • 理解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独特的两级组织方式:

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

关键区别

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

二、包的规范与实现分离

2.1 包的两部分结构

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

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;
ada 复制代码
-- 文件名: 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

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

3.2 编写各文件

math_utils.ads(规范):

ada 复制代码
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(包体):

ada 复制代码
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(规范):

ada 复制代码
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(包体):

ada 复制代码
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(主程序):

ada 复制代码
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 编译多文件项目

进入项目目录,执行:

bash 复制代码
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

输出示例

text 复制代码
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 中的注释,重新编译:

bash 复制代码
gnatmake main.adb

输出:

text 复制代码
gcc -c math_utils.adb
gnatbind main.ali
gnatlink main.ali

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


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

如需完全控制编译流程:

bash 复制代码
# 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 ):

ada 复制代码
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 ):

ada 复制代码
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 目录结构规范

text 复制代码
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 项目文件:

ada 复制代码
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;

构建命令

bash 复制代码
gprbuild my_project.gpr

七、清理与维护

7.1 清理编译产物

bash 复制代码
gnatclean main  # 清理main及其依赖的所有产物

或手动删除:

  • *.o (目标文件)

  • *.ali (库信息文件)

  • 可执行文件

7.2 完整重建

bash 复制代码
gnatclean main
gnatmake main.adb

八、常见构建错误

错误信息 原因 解决
file "xxx.ads" not found with 引用的包不存在或路径错误 检查文件名、路径、环境变量
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库信息文件,记录依赖与接口信息

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


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

相关推荐
名字还在想1 小时前
SpringBoot 自动装配-自定义Stater
后端
茶杯梦轩1 小时前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
服务器·后端·面试
风象南2 小时前
终于找到了!这个开源框架让 AI 真正融入开发流程
后端
Java面试题总结2 小时前
Go-依赖注入
开发语言·后端·golang
Java面试题总结2 小时前
Go 泛型中的 [0]func(T)
开发语言·后端·golang
rannn_1112 小时前
【Redis|基础篇】初识、Redis的安装与启动、Redis命令、Java客户端
java·redis·后端·缓存·nosql
minh_coo2 小时前
Spring单元测试之反射利器:ReflectionTestUtils
java·后端·spring·单元测试·intellij-idea
圆师傅2 小时前
Spring Boot中的日志log原理与自定义日志格式
spring boot·后端·logging
野生技术架构师2 小时前
Spring Boot + JPackage:构建独立安装包!
java·spring boot·后端