跳转到内容

Ada 编程/类型/访问

来自 Wikibooks,开放世界中的开放书籍

Ada. Time-tested, safe and secure.
Ada。经久耐用、安全可靠。

什么是访问类型?

[编辑 | 编辑源代码]

Ada 中的访问类型是其他语言中称为指针的东西。它们指向位于特定地址的对象。因此,通常可以将访问类型视为简单的地址(这种简化观点存在例外情况)。Ada 不说“指向”,而是说“授予访问权限”或“指定”某个对象。

访问类型的对象隐式地初始化为null,即,如果未显式初始化,它们将不指向任何内容。

在 Ada 中应该很少使用访问类型。在其他语言中使用指针的许多情况下,还有其他不使用指针的方法。如果您需要动态数据结构,请先检查是否可以使用 Ada 容器库。特别是对于不定记录或数组组件,Ada 2012 包 Ada.Containers.Indefinite_Holders(RM A.18.18 [注释])可以用来代替指针。

Ada 中有四种访问类型:池访问类型 - 一般访问类型 - 匿名访问类型 - 访问子程序类型。

池访问

[编辑 | 编辑源代码]

池访问类型处理对在特定堆(或 Ada 中称为存储池)上创建的对象的访问。这些类型的指针不能指向堆栈或库级别(静态)对象,也不能指向其他存储池中的对象。因此,池访问类型之间的转换是非法的。(可以使用 Unchecked_Conversion,但请注意,通过与分配池不同的存储池的访问对象进行释放是错误的。)

type Person is record
  First_Name : String (1..30);
  Last_Name  : String (1..20);
end record;

type Person_Access is access Person;

可以使用存储大小子句来限制相应的(实现定义的匿名)存储池。存储大小子句为 0 将禁用分配器的调用。

for Person_Access'Storage_Size use 0;

如果没有指定,存储池是实现定义的。Ada 支持用户定义的存储池,因此可以使用以下方法定义存储池:

for Person_Access'Storage_Pool use Pool_Name;

存储池中的对象是用关键字创建的new:

Father: Person_Access := new Person;                                          -- uninitialized
Mother: Person_Access := new Person'(Mothers_First_Name, Mothers_Last_Name);  -- initialized

通过附加 .all 访问存储池中的对象。Mother.all 是完整的记录;组件用通常的点表示法表示:Mother.all.First_Name。访问组件时,隐式解引用(即省略all)可以作为一种便捷的简写

Mother.all := (Last_Name => Father.Last_Name, First_Name => Mother.First_Name);  -- marriage

隐式解引用也适用于数组

  type Vector is array (1 .. 3) of Complex;
  type Vector_Access is access Vector;

  VA: Vector_Access := new Vector;
  VB: array (1 .. 3) of Vector_Access := (others => new Vector);

  C1: Complex := VA (3);    -- a shorter equivalent for VA   .all (3)
  C2: Complex := VB (3)(1); -- a shorter equivalent for VB(3).all (1)

使用访问对象进行复制时,请注意区分深拷贝和浅拷贝

Obj1.all := Obj2.all;  -- Deep copy: Obj1 still refers to an object different from Obj2, but it has the same content
Obj1 := Obj2;          -- Shallow copy: Obj1 now refers to the same object as Obj2

从存储池中删除对象

[编辑 | 编辑源代码]

虽然 Ada 标准提到了垃圾收集器,它会自动删除所有在堆上创建的无用对象(当没有定义存储池时),但只有针对像 Java 或 .NET 这样的虚拟机的 Ada 编译器实际上具有垃圾收集器。

当访问类型超出范围时,相应仍然分配的数据项将以任意顺序被最终化(即它们不再存在);但是,分配的内存只有在通过属性定义子句为访问类型定义了属性 Storage_Size 后才会释放。(注意:最终化释放是不同的!)

以下是 Ada 参考手册的摘录。省略号代表与本案无关的部分。

RM 3.10(7/1) 存在 ... 访问对象类型,其值指定对象... 与访问对象类型相关联的是一个存储池;几个访问类型可以共享同一个存储池。... 存储池是用于存储动态分配的对象(称为池元素)的存储区域,由分配器创建。

(8) 访问对象类型进一步细分为特定池访问类型,其值只能指定其关联存储池的元素...

RM 7.6(1) ... 每个对象在被销毁之前都会被最终化(例如,通过离开包含对象声明的子程序体,或通过调用 Unchecked_Deallocation 的实例)...

RM 7.6.1(5) 对于对象的最终化

(6/3) 如果对象的完整类型是基本类型,则最终化没有效果;

(7/3) 如果对象的完整类型是标记类型,并且对象的标记标识受控类型,则将调用该受控类型的 Finalize 过程;

(10) 在 Unchecked_Deallocation 的实例回收对象的存储空间之前,将对该对象进行最终化。如果从未对由分配器创建的对象应用 Unchecked_Deallocation 的实例,则该对象在相应的 master 完成时仍然存在,并且将在那时被最终化。

(11.1/3) 每个非派生访问类型 T 都有一个关联的集合,它是通过 T 的分配器(或从 T 派生的类型)创建的对象集。Unchecked_Deallocation 从其集合中删除对象。集合的最终化包括对集合中每个对象的最终化,顺序任意...

RM 13.11(1) 每个访问对象类型都有一个关联的存储池。分配器分配的存储空间来自该池;Unchecked_Deallocation 实例将存储空间返回该池。多个访问类型可以共享同一个池。

(2/2) 存储池是根植于 Root_Storage_Pool 的类型的变量,Root_Storage_Pool 是一个抽象受限受控类型。默认情况下,实现为每个访问对象类型选择一个标准存储池...

(11) 存储池类型(或池类型)是 Root_Storage_Pool 的后代。存储池的元素是由分配器在池中分配的对象。

(15) 可以通过属性定义子句为非派生访问对象类型指定 Storage_Size 或 Storage_Pool...

(17) 如果未为由访问对象定义定义的类型指定 Storage_Pool,则实现将以实现定义的方式为其选择一个标准存储池...

(18/4) 如果为访问类型 T 指定了 Storage_Size,则为该类型使用一个实现定义的池 PP 的 Storage_Size 至少为请求的大小,并且在包含访问类型声明的主体退出时回收 P 的存储空间...

以下程序可以编译,但在运行时会因异常而无法通过可访问性检查。

with Ada.Text_IO;
use Ada.Text_IO;

procedure Main is
   function Accessibility_Check_Fail
     return access String
   is
      -- Declare a new access type locally.
      -- All memory with this type will be finalized but not freed
      -- when the this type goes out of scope.
      type A_Type is access String;  -- no Storage_Size defined
      
      X : A_Type := new String'("x");  -- storage will be lost
      Y : access String;  -- defined locally
   begin
      Y := X;  -- data defined in a local pool will be finalized when function returns
      return Y;  -- exception should be raised
   end Accessibility_Check_Fail;
begin
   -- Accessibility check will fail because the accessiblity level associated
   -- with Y is deeper than the accessibility level of this scope.
   Put_Line(Accessibility_Check_Fail.all);
end Main;

还有一个pragma Controlled,当应用于此类访问类型时,可以防止使用它创建的对象被自动垃圾回收。 请注意,pragma Controlled 已从 Ada 2012 中移除,存储管理的子池已取代它。请参见 RM 2012 13.11.3 [带注释的]13.11.4 [带注释的]

因此,要从堆中删除对象,您需要通用单元 Ada.Unchecked_Deallocation。在释放对象时,要格外小心,不要创建悬空指针,如以下示例所示。(请注意,当相应的存储池不同时,使用与创建对象时不同的访问类型释放对象是错误的。)

with Ada.Unchecked_Deallocation;

procedure Deallocation_Sample is

   type Vector     is array (Integer range <>) of Float;
   type Vector_Ref is access Vector;

   procedure Free_Vector is new Ada.Unchecked_Deallocation
      (Object => Vector, Name => Vector_Ref);
  
   VA, VB: Vector_Ref;
   V     : Vector;

begin

   VA     := new Vector (1 .. 10);
   VB     := VA;  -- points to the same location as VA

   VA.all := (others => 0.0);

   --  ... Do whatever you need to do with the vector

   Free_Vector (VA); -- The memory is deallocated and VA is now null

   V := VB.all;  -- VB is not null, access to a dangling pointer is erroneous

end Deallocation_Sample;

正是由于存在悬空指针问题,释放操作被称为unchecked。程序员有责任确保这种情况不会发生。

由于 Ada 允许用户定义存储池,您也可以尝试使用 垃圾收集器库

构建引用计数指针

[edit | edit source]

您可以在网上找到一些引用计数指针的实现,称为安全智能指针。使用这种类型可以避免担心释放,因为当不再有指向对象的指针时,将自动执行释放。但请注意,大多数这些实现并不能阻止故意释放,因此会破坏使用它们所获得的所谓安全性。

在 AdaCore 网站上的一系列 Gems 中可以找到有关如何构建这种类型的良好教程。

Gem #97:Ada 中的引用计数 - 第 1 部分 此小宝石构建了一个简单的引用计数指针,它不阻止释放,即本质上是不安全的。

Gem #107:防止引用计数类型的释放 此宝石进一步描述了如何获得一种指针类型,其安全性无法被破坏(除任务问题外)。这种改进的安全性的代价是笨拙的语法。

Gem #123:Ada 2012 中的隐式解引用 此宝石展示了如何使用新的 Ada 2012 生成简化语法。(诚然,此宝石与引用计数有点关系,因为新的语言功能可以应用于任何类型的容器。)

通用访问

[edit | edit source]

通用访问类型允许访问在任何存储池、堆栈或库级别(静态)创建的对象。它们有两种版本,分别授予读写访问权限或只读访问权限。允许在通用访问类型之间进行转换,但要遵守某些访问级别检查。

解引用与池访问类型类似。要引用的对象(池对象除外)必须声明为aliased,并使用属性 'Access 创建对它们的引用。访问级别限制可以防止访问超出被访问对象生存期的对象,这会导致程序出错。属性 'Unchecked_Access 会省略相应的检查。

访问变量

[edit | edit source]

当关键字all 用于定义时,它们会授予读写访问权限。

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access all Day_Of_Month;

访问常量

[edit | edit source]

授予对被引用对象只读访问权限的通用访问类型使用关键字constant 在其定义中。被引用对象可以是常量或变量。

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access constant Day_Of_Month;

一些示例

[edit | edit source]
 type General_Pointer  is access all      Integer;
 type Constant_Pointer is access constant Integer;

 I1: aliased constant Integer := 10;
 I2: aliased Integer;

 P1: General_Pointer  := I1'Access;  -- illegal
 P2: Constant_Pointer := I1'Access;  -- OK, read only
 P3: General_Pointer  := I2'Access;  -- OK, read and write
 P4: Constant_Pointer := I2'Access;  -- OK, read only

 P5: constant General_Pointer := I2'Access;  -- read and write only to I2

匿名访问

[edit | edit source]

匿名访问类型也有两种版本,类似于通用访问类型,分别授予读写访问权限或只读访问权限,具体取决于关键字constant 是否出现。

匿名访问可以用作子程序的参数或作为区分符。以下是一些示例

procedure Modify (Some_Day: access          Day_Of_Month);
procedure Test   (Some_Day: access constant Day_Of_Month);  -- Ada 2005 only
task type Thread (Execute_For_Day: access Day_Of_Month) is
   ...
end Thread;
type Day_Data (Store_For_Day: access Day_Of_Month) is record
  -- components
end record;

在使用匿名访问之前,您应该考虑命名访问类型,或者更好的是,考虑是否“out”或“in out”修饰符更合适。

此语言功能仅从 Ada 2005 开始可用。

在 Ada 2005 中,匿名访问在更多情况下是允许的

type Object is record
  M   : Integer;
  Next: access Object;
end record;

X: access Integer;

function F return access constant Float;

隐式解引用

[edit | edit source]

此语言功能已在 Ada 2012 中引入。

Ada 2012 使用新的语法简化了通过指针访问对象。

假设您有一个包含某种元素的容器。

type Container   is private;
type Element_Ptr is access Element;

procedure Put (X: Element; Into: in out Container);

现在,如何访问存储在容器中的元素。当然,您可以通过

function Get (From: Container) return Element;

来检索它们,但这会复制元素,如果元素很大,这是不利的。您可以使用

function Get (From: Container) return Element_Ptr;

获得直接访问权限,但指针很危险,因为您很容易创建悬空指针,例如

P: Element_Ptr := Get (Cont);
P.all := E;
Free (P);
... Get (Cont) -- this is now a dangling pointer

使用访问器对象而不是访问类型可以防止意外释放(这仍然是 Ada 2005)

type Accessor (Data: not null access Element) is limited private;  -- read/write access
function Get (From: Container) return Accessor;

(对于空排除not null 在区分符的声明中,请参见下文)。通过此类访问器进行的访问是安全的:区分符只能用于解引用,不能将其复制到 Element_Ptr 类型的对象,因为其访问级别更深。在上面的形式中,访问器提供了读写访问权限。如果添加关键字constant,则只能进行读访问。

type Accessor (Data: not null access constant Element) is limited private;  -- only read access

现在访问容器对象的方式如下

Get (Cont).all      := E;  -- via access type: dangerous
Get (Cont).Data.all := E;  -- via accessor: safe, but ugly

这里,新的 Ada 2012 方面功能非常有用;对于这种情况,我们需要使用 Implicit_Dereference 方面

type Accessor (Data: not null access Element) is limited private
   with Implicit_Dereference => Data;

现在,无需再编写上面冗长且难看的函数调用,我们可以简单地省略区分符及其解引用,如下所示

Get (Cont).Data.all := E;  -- Ada 2005 via accessor: safe, but ugly
Get (Cont)          := E;  -- Ada 2012 implicit dereference

请注意,调用 Get (Cont) 是重载的 - 它可以表示访问器对象或元素,编译器会根据上下文选择正确的解释。

空排除

[edit | edit source]

此语言功能仅从 Ada 2005 开始可用。

所有访问子类型都可以用not null 修改,此类子类型的对象永远不会有空值,因此必须进行初始化。

type    Day_Of_Month_Access          is access   Day_Of_Month;
subtype Day_Of_Month_Not_Null_Access is not null Day_Of_Month_Access;

该语言还允许直接使用空排除声明第一个子类型

type Day_Of_Month_Access is not null access Day_Of_Month;

但是,在几乎所有情况下,这不是一个好主意,因为它会使该类型对象的可用性变得很差(例如,您无法释放分配的内存)。非空访问旨在用于访问子类型、对象声明和子程序参数[1]

访问子程序

[edit | edit source]

访问子程序允许调用方调用 子程序,而无需知道其名称或声明位置。这种访问方式的一种应用是众所周知的回调。

type Callback_Procedure is access procedure (Id  : Integer;
                                             Text: String);

type Callback_Function is access function (The_Alarm: Alarm) return Natural;

要获取对子程序的访问权,需要将属性 Access 应用于子程序名称,并使用适当的参数和结果概要。

procedure Process_Event (Id  : Integer;
                         Text: String);

My_Callback: Callback_Procedure := Process_Event'Access;

匿名访问子程序

[编辑 | 编辑源代码]

此语言功能仅从 Ada 2005 开始可用。

procedure Test (Call_Back: access procedure (Id: Integer; Text: String));

现在,一个序列中关键字的数量不再受限制。

function F return access function return access function return access Some_Type;

这是一个函数,它返回对一个函数的访问,该函数又返回对一个函数的访问,该函数返回对某种类型的访问。

访问常见问题解答

[编辑 | 编辑源代码]

关于 Ada 的访问类型,一些 "常见问题" 和 "常见问题" (主要来自 C 用户)。

访问 vs. 访问所有

[编辑 | 编辑源代码]

一个访问 all 可以执行任何一个简单的access 可以执行的操作。因此有人可能会问:"为什么还要使用简单的access 呢?" - 实际上,一些程序员从来不使用简单的访问.

Unchecked_Deallocation 如果使用不当,始终是危险的。将池特定的对象释放两次和释放堆栈对象一样容易,也同样危险。"访问所有" 的优势在于,你可能根本不需要使用 Unchecked_Deallocation。

道德:如果你有(或可能会有)将 '访问或 'Unchecked_Access 存储到访问对象的有效理由,那么使用 "访问所有" 并且不要担心。如果没有,"最小权限" 的口号建议应该省略 "所有" (不要启用你不会使用的功能)。

以下(可能灾难性的)示例将尝试释放一个堆栈对象

declare

  type Day_Of_Month is range 1 .. 31;            
  type Day_Of_Month_Access is access all Day_Of_Month;

  procedure Free is new Ada.Unchecked_Deallocation
      (Object => Day_Of_Month,
       Name   => Day_Of_Month_Access);

  A  : aliased Day_Of_Month;
  Ptr: Day_Of_Month_Access := A'Access;

begin

   Free(Ptr);

end;

使用一个简单的access 你至少知道你不会尝试释放一个堆栈对象。原因是access 不允许从堆栈对象创建指针。

访问 vs. System.Address

[编辑 | 编辑源代码]

访问可以与一个简单的内存地址不同,它可能包含更多内容。例如,"对字符串的访问" 通常还需要某种方法来存储字符串大小。如果你需要一个简单的地址并且不关心强类型,请使用 System.Address 类型。

C 兼容指针

[编辑 | 编辑源代码]

创建 C 兼容访问的正确方法是使用pragma Convention

type Day_Of_Month is range 1 .. 31;
for  Day_Of_Month'Size use Interfaces.C.int'Size;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month);

type Day_Of_Month_Access is access Day_Of_Month;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month_Access);

pragma Convention 应该用于你想要在 C 中使用的任何类型。如果该类型无法与 C 兼容,编译器会发出警告。

在声明 Day_Of_Month 时,你也可以考虑以下更短的替代方法

type Day_Of_Month is new Interfaces.C.int range 1 .. 31;

在 C 中使用访问类型之前,你应该考虑使用普通的 "in"、"out" 和 "in out" 修饰符。pragma Exportpragma Import 知道参数通常如何在 C 中传递,并且会在 C 使用指针传递参数的情况下自动使用指针来传递参数。当然,RM 包含关于何时为 "in"、"out" 和 "in out" 使用指针的精确规则 - 请参阅 "B.3: Interfacing with C [Annotated]"。

void* 在哪里?

[编辑 | 编辑源代码]

虽然实际上是 "与 C 交互" 的问题,这里有一些可能的解决方案

procedure Test is

  subtype Pvoid is System.Address;

  -- the declaration in C looks like this:
  -- int C_fun(int *)
  function C_fun (pv: Pvoid) return Integer;
  pragma Import (Convention    => C,
                 Entity        => C_fun,     -- any Ada name
                 External_Name => "C_fun");  -- the C name

  Pointer: Pvoid;

  Input_Parameter: aliased Integer := 32;
  Return_Value   : Integer;

begin

  Pointer      := Input_Parameter'Address;
  Return_Value := C_fun (Pointer);

end Test;

可移植性较差,但可能更易用 (对于 32 位 CPU)

type void is mod 2 ** 32;
for void'Size use 32;

使用 GNAT,你可以通过使用以下方法获得 32/64 位可移植性

type void is mod System.Memory_Size;
for void'Size use System.Word_Size;

更接近 void 的本质 - 指向大小为零的元素的指针是指向空记录的指针。这也具有为 voidvoid* 提供表示的优势

type Void is null record;
pragma Convention (C, Void);

type Void_Ptr is access all Void;
pragma Convention (C, Void_Ptr);

瘦访问类型和胖访问类型

[编辑 | 编辑源代码]

访问类型和地址之间的区别将在下面详细说明。使用术语 指针 是因为这是常用的术语。

有一个预定义的单元 System.Address_to_Access_Conversion 用于在访问值和地址之间来回转换。请谨慎使用这些转换,如下文所述。

瘦指针

[编辑 | 编辑源代码]

瘦指针允许访问约束子类型。

type Int     is range -100 .. +500;
type Acc_Int is access Int;

type Arr     is array (1 .. 80) of Character;
type Acc_Arr is access Arr;

此类子类型的对象具有静态大小,因此只需一个简单的地址即可访问它们。在数组的情况下,这通常是第一个元素的地址。

对于这种类型的指针,使用 System.Address_to_Access_Conversion 是安全的。

胖指针

[编辑 | 编辑源代码]
type Unc     is array (Integer range <>) of Character;
type Acc_Unc is access Unc;

子类型 Unc 的对象需要约束,即起始和终止索引,因此指向它们的指针也需要包含这些索引。因此,像第一个组件的地址这样的简单地址是不够的。请注意,对于任何数组对象,A'Address 与 A(A'First)'Address 相同。

对于这种类型的指针,System.Address_to_Access_Conversion 可能无法正常工作。

CO: aliased Unc (-1 .. +1) := (-1 .. +1 => ' ');
UO: aliased Unc            := (-1 .. +1 => ' ');

在这里,CO 是一个 名义约束 对象,指向它的指针不需要存储约束,即一个瘦指针就足够了。相反,UO 是一个 名义未约束 子类型的对象,它的 实际子类型 由初始值约束。

A: Acc_Unc            := CO'Access;  -- illegal
B: Acc_Unc            := UO'Access;  -- OK
C: Acc_Unc (CO'Range) := CO'Access;  -- also illegal

RM 中的相关段落很难理解。简而言之

访问类型的目标类型称为 指定子类型,在我们这个示例中是 Unc。RM 3.10.2 [Annotated](27.1/2) 要求 Acc_Unc 的指定子类型在静态上与对象的 名义子类型 匹配。

现在,CO 的名义子类型是约束匿名子类型 Unc (-1 .. +1)UO 的名义子类型是未约束子类型 Unc。在非法情况下,指定子类型和名义子类型在静态上不匹配。

另请参阅

[编辑 | 编辑源代码]

维基教科书

[编辑 | 编辑源代码]

Ada 参考手册

[编辑 | 编辑源代码]

最新 RM

[编辑 | 编辑源代码]

Ada 质量和风格指南

[编辑 | 编辑源代码]

参考资料

[编辑 | 编辑源代码]


华夏公益教科书