本课目标
完成本课后,你将能够:
- 理解访问类型(指针)与动态分派的结合,掌握
access T'Class的用途 - 区分通用访问类型(
access all)与匿名访问(access参数)的适用场景 - 掌握访问类型在容器(链表、动态数组)和回调(函数指针)中的应用
- 理解多种内存管理策略:手动释放、存储池、引用计数,并分析其优劣
- 运用访问类型的安全性限制(
aliased、not null等)编写可靠代码
一、回顾与引入
在第14课中,我们学习了访问类型(指针)的基础:使用 new 分配对象、通过 .all 解引用、手动释放内存。然而,在实际工程中,访问类型常常与面向对象(标记记录)和泛型结合,用于构建灵活的容器、回调机制以及复杂数据结构。本课将在第14课的基础上,深入探讨访问类型的高级特性,尤其是如何安全高效地管理动态内存,并与多态协同工作。
Ada 的访问类型设计原则是“在保证安全的前提下提供必要的灵活性”。与 C 语言不同,Ada 不允许任意地址转换,也不允许取局部变量的地址(除非显式标记 aliased)。这些限制避免了大量常见指针错误,但同时也要求程序员更清晰地表达意图。本课将展示如何在限制内实现强大的功能。
二、访问类型与动态分派的结合
2.1 access T'Class:多态指针
在第21课中,我们学习了类宽类型 T'Class 可以实现动态分派,但无法直接声明 T'Class 的变量。要持有不同派生类型的对象,通常使用访问类型指向 T'Class。例如:
type Shape_Ptr is access all Shape'Class;
这种指针可以指向任何 Shape 的派生对象(如 Circle、Rectangle),并通过该指针调用 primitive operations,实现多态行为。
S1 : Shape_Ptr := new Circle'(Center => (0.0,0.0), Radius => 5.0);
S2 : Shape_Ptr := new Rectangle'(Center => (10.0,10.0), Width => 4.0, Height => 3.0);
S1.Draw; -- 动态分派,调用 Circle 的 Draw
S2.Draw; -- 动态分派,调用 Rectangle 的 Draw
内存布局:Shape_Ptr 本身是一个指针(4或8字节),指向堆上分配的对象。对象头部包含 tag,因此通过指针访问时,动态分派仍然有效。
2.2 容器中的多态元素
使用 access T'Class 可以构建异构容器。例如,一个链表可以存储不同类型的形状:
type Node;
type Node_Ptr is access Node;
type Node is record
Shape : Shape_Ptr; -- 指向任意形状
Next : Node_Ptr;
end record;
遍历容器时,调用 Node.Shape.Draw 将根据实际类型动态分派。这是面向对象容器的基础。
2.3 注意事项:生命周期与内存泄漏
使用 access T'Class 时,必须明确谁负责释放对象。如果容器负责释放,则需要在删除节点时调用 Ada.Unchecked_Deallocation 释放 Shape 对象。否则会造成内存泄漏。同时,避免悬挂指针:释放后应将指针置为 null。
三、通用访问类型与匿名访问
3.1 access all:可指向任何 aliased 对象
普通的访问类型(如 access Integer)只能指向堆上分配的对象(通过 new),不能指向栈上的局部变量。access all 则放宽了限制,可以指向任何 aliased 的对象(包括栈变量和全局变量)。这在需要临时引用局部数据时很有用,但必须确保引用不超过被引用对象的生命周期。
type Int_Acc is access all Integer;
X : aliased Integer := 10;
P : Int_Acc := X'Access; -- 合法,X 是 aliased
危险示例:返回局部变量的 'Access 会导致悬挂指针,编译器通常能检测并拒绝(在安全模式下)。Ada 的作用域规则会阻止这种逃逸,但使用 access all 时需格外小心。
3.2 匿名访问类型:作为子程序参数
匿名访问类型直接在子程序参数中使用 access 关键字,无需单独命名类型。这常用于实现回调或临时访问。
procedure Process (Data : access Integer) is
begin
Data.all := Data.all + 1;
end Process;
调用时,只能传递 aliased 变量的 'Access:
X : aliased Integer := 5;
Process (X'Access);
匿名访问不能用于声明变量(只能用于参数),因此更安全,不会产生悬挂指针(因为参数生命周期受限于子程序)。
3.3 对比总结
| 特性 | 普通 access T | access all T | 匿名 access 参数 |
|---|---|---|---|
| 可指向堆对象 | 是 | 是 | 是(需通过 new 或 'Access) |
| 可指向栈变量 | 否 | 是(需 aliased) | 是(需 aliased) |
| 可声明变量 | 是 | 是 | 否(仅参数) |
| 典型用途 | 动态数据结构 | 临时引用、全局变量 | 回调、内部辅助 |
四、访问类型在容器和回调中的应用
4.1 动态数组的简化实现
使用访问类型可以实现类似 C++ std::vector 的动态数组。一种常见模式:记录中包含指向堆数组的指针和长度、容量。
type Int_Array is array (Positive range <>) of Integer;
type Int_Array_Acc is access Int_Array;
type Vector is record
Data : Int_Array_Acc;
Len : Natural := 0;
Cap : Positive;
end record;
初始化时分配 new Int_Array (1 .. Initial_Capacity),当长度超过容量时重新分配更大的数组并复制元素。这种模式避免了每次添加都重新分配,提高了效率。
4.2 回调机制:函数指针
Ada 允许访问类型指向子程序,这称为“访问子程序类型”。可用于实现回调、策略模式等。
type Callback is access procedure (Value : Integer);
-- 或者带返回值的函数
type Comparator is access function (A, B : Integer) return Boolean;
procedure Sort (Arr : in out Int_Array; Less : Comparator) is ...;
实例化时,使用 'Access 获取子程序的访问值,要求子程序在作用域内且满足调用约定。
function Ascending (A, B : Integer) return Boolean is (A < B);
Sort (My_Array, Ascending'Access);
安全性:访问子程序类型不能指向局部子程序(因为局部子程序的生命周期随栈帧结束),这防止了悬空回调。
4.3 回调与动态分派结合
可以将访问子程序类型作为标记记录的组件,实现类似“委托”或“策略”模式。例如,一个 Button 类可以包含一个 OnClick 回调,用户可动态设置。
五、内存管理策略
Ada 默认不提供垃圾回收,动态内存需要程序员手动管理。但 Ada 提供了多种机制来简化管理并提高安全性。
5.1 手动释放:Ada.Unchecked_Deallocation
最基础的方式,如同第14课所述。需要为每种访问类型实例化一个释放过程。缺点:容易忘记释放、产生悬挂指针。
procedure Free_Int is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);
5.2 存储池(Storage Pools)
存储池允许自定义内存分配策略。Ada 为每个访问类型关联一个存储池,默认是 System.Pool_Global(类似 malloc/free)。通过创建自己的存储池,可以实现:
- 从固定大小的静态缓冲区分配(适用于嵌入式系统,无堆碎片)。
- 自动引用计数(通过池的
Allocate和Deallocate跟踪)。 - 内存调试(记录分配/释放的调用栈)。
定义存储池需要继承 System.Storage_Pools.Root_Storage_Pool 并重写 Allocate、Deallocate、Storage_Size 函数。然后将访问类型的 Storage_Pool 属性设置为此池。
type My_Pool is new Root_Storage_Pool with private;
for Int_Ptr'Storage_Pool use My_Pool;
存储池较为底层,通常用于嵌入式或高性能场景。标准库中提供了 Ada.Unchecked_Deallocation 已经足够多数情况。
5.3 引用计数:Ada.Containers.Reference_Counted(Ada 2012+)
Ada 2012 引入的 Ada.Containers.Reference_Counted 提供了对引用计数智能指针的支持。它类似于 C++ 的 shared_ptr。使用 Reference_Counted.Types 中的 Holder 类型,可以自动管理引用计数,当最后一个引用消失时释放对象。
with Ada.Containers.Reference_Counted;
package Int_Holders is new Ada.Containers.Reference_Counted (Integer);
use Int_Holders;
P : Holder := To_Holder (new Integer'(42));
Q : Holder := P; -- 引用计数增加
-- 当 P 和 Q 都离开作用域时,对象自动释放
引用计数避免了手动释放,但需注意循环引用问题(可通过弱引用解决,但 Ada 标准库未提供)。对于树或图结构,引用计数不适用,仍需手动管理或使用其他策略。
5.4 基于作用域的资源管理:Ada.Finalization
对于需要确定性释放的资源(如文件句柄),可以使用 Controlled 类型,在 Finalize 中释放资源。这是一种 RAII(资源获取即初始化)模式,类似于 C++ 的析构函数。这对于管理堆内存同样有效:在 Initialize 中分配,在 Finalize 中释放,然后将该对象作为栈上的组件,离开作用域自动释放。
type Smart_Int_Ptr is new Ada.Finalization.Controlled with record
P : Int_Ptr;
end record;
overriding procedure Finalize (S : in out Smart_Int_Ptr) is
procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);
begin
Free (S.P);
end Finalize;
这样,声明 Smart_Int_Ptr 变量时,无需显式释放。这是推荐的做法。
六、安全性限制与最佳实践
6.1 aliased 的必要性
只有标记为 aliased 的对象才能被 'Access 引用。这迫使程序员思考是否真的需要指针,减少了无意的别名。
6.2 not null 约束
从 Ada 2005 开始,可以声明非空的访问类型:
type Non_Null_Ptr is not null access Integer;
这样的变量不能赋值为 null,减少了空指针解引用错误。在子程序参数中,access 默认允许 null,但可以用 not null access 强制非空。
6.3 池访问类型(Pool-specific access)
使用 new 分配的对象默认从全局堆分配。如果希望从特定存储池分配,可以使用 Storage_Pool 属性。还有“池访问类型”(access T 可以指定 Storage_Pool),但较少用。
6.4 避免悬挂指针的规则
- 不要从子程序返回局部变量的
'Access(编译器通常阻止)。 - 释放对象后,将所有指向它的指针置为
null。 - 优先使用智能指针(引用计数或
Controlled)代替手动管理。
6.5 使用场景建议
- 动态数据结构(链表、树):手动管理或使用
Controlled包装节点。 - 多态容器:
access T'Class配合Controlled自动释放。 - 回调:匿名访问子程序参数或
access all子程序类型,注意生命周期。 - 性能关键路径:避免动态分配,或使用自定义存储池。
七、完整示例:多态形状容器(自动释放)
-- 文件名:shape_container.ads
with Ada.Finalization;
with Shapes; use Shapes;
package Shape_Container is
type Shape_List is new Ada.Finalization.Controlled with private;
procedure Add (List : in out Shape_List; S : access Shape'Class);
procedure Display_All (List : Shape_List);
private
type Node;
type Node_Acc is access Node;
type Node is record
Shape : access Shape'Class; -- 多态指针
Next : Node_Acc;
end record;
type Shape_List is new Ada.Finalization.Controlled with record
Head : Node_Acc;
end record;
overriding procedure Initialize (List : in out Shape_List);
overriding procedure Adjust (List : in out Shape_List);
overriding procedure Finalize (List : in out Shape_List);
end Shape_Container;
-- 文件名:shape_container.adb
with Ada.Unchecked_Deallocation;
package body Shape_Container is
procedure Free_Node is new Ada.Unchecked_Deallocation (Node, Node_Acc);
procedure Add (List : in out Shape_List; S : access Shape'Class) is
New_Node : Node_Acc := new Node'(Shape => S, Next => List.Head);
begin
List.Head := New_Node;
end Add;
procedure Display_All (List : Shape_List) is
Current : Node_Acc := List.Head;
begin
while Current /= null loop
Current.Shape.Draw; -- 动态分派
Current := Current.Next;
end loop;
end Display_All;
overriding procedure Initialize (List : in out Shape_List) is
begin
List.Head := null;
end Initialize;
overriding procedure Adjust (List : in out Shape_List) is
-- 深拷贝:需要复制整个链表,且复制 Shape 对象
-- 为简化,本例不支持复制,故留空或 raise
begin
null; -- 实际应实现深拷贝,但超出范围
end Adjust;
overriding procedure Finalize (List : in out Shape_List) is
Current : Node_Acc := List.Head;
Next_Node : Node_Acc;
procedure Free_Shape is new Ada.Unchecked_Deallocation (Shape'Class, access Shape'Class);
begin
while Current /= null loop
Next_Node := Current.Next;
Free_Shape (Current.Shape); -- 释放形状对象
Free_Node (Current);
Current := Next_Node;
end loop;
end Finalize;
end Shape_Container;
-- 文件名:main.adb
with Shape_Container; use Shape_Container;
with Shapes; use Shapes;
procedure Main is
List : Shape_List;
begin
Add (List, new Circle'(Center => (0.0,0.0), Radius => 5.0));
Add (List, new Rectangle'(Center => (10.0,10.0), Width => 4.0, Height => 3.0));
Display_All (List);
-- 离开作用域时,List 自动释放所有节点和形状对象
end Main;
八、本课总结
access T'Class是多态指针,用于构建异构容器和动态分派。- 通用访问类型
access all可以指向任何aliased对象,但需谨慎生命周期。 - 匿名访问参数(
access)适合回调,更安全。 - 访问子程序类型实现函数指针,支持策略模式。
- 内存管理策略:手动释放、存储池、引用计数、
ControlledRAII。推荐使用Controlled或引用计数减少错误。 - 安全性限制(
aliased、not null、作用域规则)是 Ada 指针安全的基石。
九、课后练习
-
多态指针练习:声明一个
Shape_Ptr指向Shape'Class,动态分配一个Circle和一个Rectangle,并调用它们的Area和Draw。释放内存。 -
匿名访问参数:编写一个过程
Apply,接受一个匿名访问Integer参数和一个函数指针(访问函数类型),将函数应用到该整数上并更新。测试。 -
通用访问类型:声明一个
access all Integer变量,分别指向堆对象和栈上的aliased变量。解释各自的生命周期注意事项。 -
动态数组实现:实现一个泛型
Vector包,内部使用访问类型管理动态数组,支持Append、Pop、Get、Length。要求使用Controlled自动释放内存。 -
回调与策略模式:实现一个排序过程,接受一个
Comparator访问函数类型(access function (A,B:Integer) return Boolean)。测试使用升序和降序比较器。 -
存储池探索:编写一个自定义存储池,从固定大小的静态缓冲区分配,并绑定到一个访问类型。分配几个对象,观察地址是否在缓冲区内。
-
引用计数实验:使用
Ada.Containers.Reference_Counted创建一个Integer的引用计数句柄,测试多个句柄指向同一对象时自动释放。 -
循环引用问题:设计一个双向链表,节点包含前后指针。使用引用计数管理节点,观察是否发生内存泄漏(因为循环引用导致计数永不为零)。提出解决方案(如弱指针)。
-
悬挂指针检测:编写一个函数,返回一个局部变量的
'Access,观察编译器是否报错。尝试使用access all绕过,并解释风险。 -
性能比较:实现一个频繁插入删除的链表,分别使用手动释放和
Controlled自动释放,测试性能(使用Ada.Calendar计时)。分析开销。
十、下节预告
第23课|异常与错误处理深度实践
我们将:
- 深入异常传播机制与异常规范
- 学习异常与资源管理的结合(RAII)
- 掌握自定义异常链与异常消息
- 应用异常处理构建健壮系统
关键术语表
多态指针:
access T'Class,指向类宽类型的指针,支持动态分派。通用访问类型:
access all,可指向任何aliased对象的访问类型。匿名访问:子程序参数中直接使用的
access,无类型名,用于回调。访问子程序类型:指向子程序的访问类型,用于回调或策略模式。
存储池(Storage Pool):管理访问类型内存分配的后台对象,可自定义。
引用计数(Reference Count):通过计数跟踪对象引用,自动释放。
Ada.Finalization.Controlled:提供初始化、调整、终结操作的混合类型,用于 RAII。
aliased:标记对象可以被'Access引用。
not null:约束访问类型不能为空。