本课目标
完成本课后,你将能够:
-
理解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库信息文件,记录依赖与接口信息
增量编译:只重新编译修改过的单元及其依赖者
提示警告:本课程内容(包括但不限于文字、图片、音频、视频等)版权归原作者所有,未经授权严禁转载、复制、翻录、传播或以任何方式用于商业用途。本课程仅供个人学习使用,请尊重知识产权,共同维护良好的创作环境。如有疑问或需授权合作,请联系版权方。感谢您的理解与支持!