第14课|访问类型(指针)

13 阅读8分钟

本课目标

完成本课后,你将能够:

  • 理解 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 allaccess 参数模式)。这防止了返回局部变量的地址。

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 标记、作用域限制、访问模式参数
  • 访问类型是构建动态数据结构(链表、树等)的基础

九、课后练习

  1. 基础练习:定义一个访问类型指向 Float,动态分配一个浮点数并赋值,打印后释放。

  2. 链表操作:扩展链表示例,增加删除指定名字的学生、按成绩排序的功能。

  3. 二叉树:实现一个二叉搜索树,支持插入、查找、中序遍历(按顺序输出)。

  4. 循环链表:实现一个循环链表,并编写一个函数检测链表是否循环。

  5. 内存管理:编写程序分配大量节点,然后释放,观察内存使用情况(可使用操作系统工具)。


十、下节预告

第15课|子程序与参数传递

我们将:

  • 掌握过程与函数的声明与使用
  • 深入理解参数模式(inoutin out
  • 学习默认参数与命名参数调用
  • 理解子程序的重载与递归
  • 应用子程序进行模块化设计

关键术语表

访问类型:Ada 中指针的正式名称,通过 access 定义

new:在堆上分配对象的操作符

.all:显式解引用操作符,用于访问指针指向的对象

null:表示不指向任何对象的访问值

aliased:标记对象可以被取地址的关键字

'Access:返回对象访问值的属性

悬挂指针:指向已释放内存的指针,访问可能导致错误

Ada.Unchecked_Deallocation:用于释放动态内存的泛型过程

匿名访问类型:子程序参数中直接使用的 access,不单独命名

链表:动态数据结构,由节点通过指针链接而成