本课目标
完成本课后,你将能够:
- 理解 Ada 访问类型与普通指针的本质区别
- 掌握访问类型的声明与使用
- 熟练进行动态内存分配与释放
- 理解访问类型的安全性机制(作用域、null 排除)
- 应用访问类型构建链表、树等动态数据结构
一、访问类型基础
1.1 什么是指针(访问类型)
指针是存储其他变量地址的变量。在 Ada 中,指针被称为访问类型(access type),比 C 的指针更安全。
-- 声明一个指向 Integer 的访问类型
type Int_Ptr is access Integer;
-- 使用
P : Int_Ptr;
P := new Integer'(42); -- 在堆上分配一个整数,值为42
Value : Integer := P.all; -- 解引用,读取值
P.all := 100; -- 修改堆上的值
1.2 访问类型的声明
访问类型可以指向任何类型,包括标量、数组、记录等:
type Int_Acc is access Integer;
type Float_Acc is access Float;
type String_Acc is access String;
type Point_Acc is access Point; -- Point 是记录类型
1.3 空访问值
访问类型可以包含空值(null),表示不指向任何对象:
P : Int_Ptr := null; -- 初始化为空
if P = null then
Ada.Text_IO.Put_Line ("指针为空");
end if;
访问 null 会导致 Constraint_Error:
P := null;
-- Value := P.all; -- 会触发异常!
二、动态内存分配
2.1 new 分配器
使用 new 关键字在堆上分配对象,返回访问值:
-- 分配并初始化
P1 : Int_Ptr := new Integer'(42);
P2 : Int_Ptr := new Integer; -- 未初始化,值不确定
P3 : Int_Ptr := new Integer'(5); -- 显式初始化
-- 分配记录
type Person is record
Name : String (1 .. 20);
Age : Integer;
end record;
type Person_Acc is access Person;
John : Person_Acc := new Person'(Name => "John Doe ", Age => 30);
2.2 无约束类型的分配
对于无约束数组,需要在分配时指定范围:
type Int_Vector is array (Integer range <>) of Integer;
type Int_Vector_Acc is access Int_Vector;
V : Int_Vector_Acc := new Int_Vector (1 .. 100); -- 分配100个元素
V (1) := 10; -- 直接访问,无需 .all
注意:对于无约束数组,分配时指定范围,之后可以直接用 V (I) 访问,编译器自动解引用。
2.3 初始化规则
访问类型变量的默认值是 null(除非显式初始化)。new 分配的对象如果没有显式初始化,其内容是不确定的(类似未初始化的变量)。
三、访问值的操作
3.1 解引用与组件访问
使用 .all 显式解引用,但对于记录和数组,可以省略 .all 直接访问组件:
type Point is record
X, Y : Float;
end record;
type Point_Acc is access Point;
P : Point_Acc := new Point'(10.0, 20.0);
-- 显式解引用
P.all.X := 15.0;
-- 隐式解引用(推荐)
P.X := 25.0; -- 编译器自动解引用
-- 对于数组同理
type Int_Array is array (1 .. 10) of Integer;
type Arr_Acc is access Int_Array;
A : Arr_Acc := new Int_Array'(1 .. 10 => 0);
A (3) := 100; -- 隐式解引用
3.2 访问类型的比较
访问类型支持 = 和 /= 比较,比较的是地址而非内容:
P1, P2 : Int_Ptr;
P1 := new Integer'(10);
P2 := new Integer'(10);
if P1 = P2 then
Ada.Text_IO.Put_Line ("指向同一对象");
else
Ada.Text_IO.Put_Line ("指向不同对象");
end if;
要比较内容,需要解引用后比较:
if P1.all = P2.all then
Ada.Text_IO.Put_Line ("内容相同");
end if;
3.3 赋值与别名
访问类型赋值会复制地址,导致多个指针指向同一对象:
P1 := new Integer'(42);
P2 := P1; -- P2 和 P1 指向同一对象
P1.all := 100;
-- 此时 P2.all 也是 100
四、动态内存释放
4.1 Ada.Unchecked_Deallocation
Ada 默认不自动回收堆内存(无垃圾回收,除非显式使用),需要手动释放:
with Ada.Unchecked_Deallocation;
procedure Memory_Management is
type Int_Ptr is access Integer;
procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);
P : Int_Ptr := new Integer'(42);
begin
Ada.Text_IO.Put_Line (Integer'Image (P.all));
Free (P); -- 释放内存
-- 此后 P 变为未定义,访问会出错
P := null; -- 建议置空,防止野指针
end Memory_Management;
4.2 释放与悬挂指针
释放后,如果还有其他指针指向同一对象,那些指针变成悬挂指针(dangling pointer),访问它们是危险的。Ada 无法自动检测,需要程序员小心管理。
P1 := new Integer'(42);
P2 := P1;
Free (P1);
-- P2 现在悬挂,P2.all 可能出错
4.3 避免内存泄漏
确保每个 new 分配的对象最终都被释放,否则会造成内存泄漏。可以使用智能指针模式(Ada 2012 引入了 Ada.Finalization 来管理,将在后续课程讲解)。
五、访问类型的安全性机制
Ada 的访问类型比 C 指针安全,主要通过以下机制:
5.1 作用域规则
Ada 的访问类型不能指向局部变量(除非使用 access all 或 access 参数模式)。这防止了返回局部变量的地址。
function Bad return Int_Ptr is
X : Integer := 10;
begin
return X'Access; -- 错误!不能取局部变量的访问值
end Bad;
5.2 access 参数模式
子程序参数可以使用 access 模式,允许传递访问值,但必须符合作用域规则:
procedure Modify (P : access Integer) is
begin
P.all := P.all + 1;
end Modify;
-- 调用
X : aliased Integer := 5; -- aliased 标记可被取地址
Modify (X'Access); -- 传递局部变量的访问值
5.3 aliased 关键字
只有标记为 aliased 的对象才能被取地址('Access):
A : aliased Integer := 10;
B : Integer := 20;
P : Int_Ptr := A'Access; -- OK
-- P := B'Access; -- 错误!B 不是 aliased
5.4 通用访问类型
access all 可以指向任何 aliased 对象,包括局部变量:
type Any_Int_Acc is access all Integer;
P : Any_Int_Acc;
X : aliased Integer := 10;
P := X'Access; -- OK
5.5 命名访问类型 vs 匿名访问类型
- 命名访问类型:
type T is access Integer;可以创建多个变量。 - 匿名访问类型:子程序参数中直接写
access,不单独定义类型。
匿名访问更受限制,但更安全。
六、访问类型的应用
6.1 链表实现
-- 简单单向链表
type Node;
type Node_Acc is access Node;
type Node is record
Value : Integer;
Next : Node_Acc;
end record;
-- 创建链表
Head : Node_Acc := null;
procedure Insert (Value : Integer) is
New_Node : Node_Acc := new Node'(Value => Value, Next => Head);
begin
Head := New_Node;
end Insert;
-- 遍历链表
procedure Print_List is
Current : Node_Acc := Head;
begin
while Current /= null loop
Ada.Text_IO.Put (Integer'Image (Current.Value));
Current := Current.Next;
end loop;
Ada.Text_IO.New_Line;
end Print_List;
6.2 二叉树实现
type Tree_Node;
type Tree_Node_Acc is access Tree_Node;
type Tree_Node is record
Value : Integer;
Left : Tree_Node_Acc := null;
Right : Tree_Node_Acc := null;
end record;
procedure Insert (Root : in out Tree_Node_Acc; Value : Integer) is
begin
if Root = null then
Root := new Tree_Node'(Value => Value, Left => null, Right => null);
elsif Value < Root.Value then
Insert (Root.Left, Value);
else
Insert (Root.Right, Value);
end if;
end Insert;
6.3 循环数据结构
Ada 的访问类型支持创建循环结构,但要注意释放时的顺序。
type Node;
type Node_Acc is access Node;
type Node is record
Next : Node_Acc;
Data : Integer;
end record;
-- 创建循环链表
A, B : Node_Acc;
A := new Node'(Data => 1, Next => null);
B := new Node'(Data => 2, Next => A);
A.Next := B; -- 形成循环
七、完整示例:学生成绩链表
-- 文件名: grade_list.adb
-- 功能:使用链表管理学生成绩
with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Float_Text_IO;
with Ada.Unchecked_Deallocation;
procedure Grade_List is
-- 学生节点
type Student;
type Student_Acc is access Student;
type Student is record
Name : String (1 .. 20);
Grade : Float;
Next : Student_Acc := null;
end record;
-- 释放过程
procedure Free is new Ada.Unchecked_Deallocation (Student, Student_Acc);
Head : Student_Acc := null;
-- 添加学生到链表开头
procedure Add_Student (Name : String; Grade : Float) is
begin
Head := new Student'(Name => Name,
Grade => Grade,
Next => Head);
end Add_Student;
-- 打印所有学生
procedure Print_All is
Current : Student_Acc := Head;
Count : Integer := 0;
begin
Ada.Text_IO.Put_Line ("=== 学生成绩列表 ===");
while Current /= null loop
Count := Count + 1;
Ada.Text_IO.Put (Integer'Image (Count) & ". ");
Ada.Text_IO.Put (Current.Name);
Ada.Text_IO.Put (" : ");
Ada.Float_Text_IO.Put (Current.Grade, Fore => 1, Aft => 2, Exp => 0);
Ada.Text_IO.New_Line;
Current := Current.Next;
end loop;
Ada.Text_IO.Put_Line ("共 " & Integer'Image (Count) & " 人");
end Print_All;
-- 计算平均分
function Average_Grade return Float is
Current : Student_Acc := Head;
Sum : Float := 0.0;
Count : Integer := 0;
begin
while Current /= null loop
Sum := Sum + Current.Grade;
Count := Count + 1;
Current := Current.Next;
end loop;
if Count = 0 then
return 0.0;
else
return Sum / Float (Count);
end if;
end Average_Grade;
-- 释放所有内存
procedure Free_All is
Current : Student_Acc := Head;
Temp : Student_Acc;
begin
while Current /= null loop
Temp := Current;
Current := Current.Next;
Free (Temp);
end loop;
Head := null;
end Free_All;
begin
-- 添加示例数据
Add_Student ("Alice ", 85.5);
Add_Student ("Bob ", 92.0);
Add_Student ("Charlie ", 78.5);
Add_Student ("Diana ", 88.0);
Add_Student ("Eve ", 95.5);
-- 显示数据
Print_All;
Ada.Text_IO.New_Line;
Ada.Text_IO.Put ("平均分: ");
Ada.Float_Text_IO.Put (Average_Grade, Fore => 1, Aft => 2, Exp => 0);
Ada.Text_IO.New_Line;
-- 释放内存
Free_All;
end Grade_List;
八、本课总结
- Ada 的访问类型(指针)通过
access关键字声明,比 C 指针更安全 - 使用
new在堆上分配对象,返回访问值 - 使用
.all解引用,但对记录和数组可隐式解引用 - 手动释放内存使用
Ada.Unchecked_Deallocation,需要小心悬挂指针 - Ada 的安全性机制:
aliased标记、作用域限制、访问模式参数 - 访问类型是构建动态数据结构(链表、树等)的基础
九、课后练习
-
基础练习:定义一个访问类型指向
Float,动态分配一个浮点数并赋值,打印后释放。 -
链表操作:扩展链表示例,增加删除指定名字的学生、按成绩排序的功能。
-
二叉树:实现一个二叉搜索树,支持插入、查找、中序遍历(按顺序输出)。
-
循环链表:实现一个循环链表,并编写一个函数检测链表是否循环。
-
内存管理:编写程序分配大量节点,然后释放,观察内存使用情况(可使用操作系统工具)。
十、下节预告
第15课|子程序与参数传递
我们将:
- 掌握过程与函数的声明与使用
- 深入理解参数模式(
in、out、in out) - 学习默认参数与命名参数调用
- 理解子程序的重载与递归
- 应用子程序进行模块化设计
关键术语表
访问类型:Ada 中指针的正式名称,通过
access定义
new:在堆上分配对象的操作符
.all:显式解引用操作符,用于访问指针指向的对象
null:表示不指向任何对象的访问值
aliased:标记对象可以被取地址的关键字
'Access:返回对象访问值的属性悬挂指针:指向已释放内存的指针,访问可能导致错误
Ada.Unchecked_Deallocation:用于释放动态内存的泛型过程匿名访问类型:子程序参数中直接使用的
access,不单独命名链表:动态数据结构,由节点通过指针链接而成