Ada 编程/类型/访问
Ada 中的访问类型是其他语言中称为指针的东西。它们指向位于特定地址的对象。因此,通常可以将访问类型视为简单的地址(这种简化观点存在例外情况)。Ada 不说“指向”,而是说“授予访问权限”或“指定”某个对象。
访问类型的对象隐式地初始化为null
,即,如果未显式初始化,它们将不指向任何内容。
在 Ada 中应该很少使用访问类型。在其他语言中使用指针的许多情况下,还有其他不使用指针的方法。如果您需要动态数据结构,请先检查是否可以使用 Ada 容器库。特别是对于不定记录或数组组件,Ada 2012 包 Ada.Containers.Indefinite_Holders(RM A.18.18 [注释])可以用来代替指针。
Ada 中有四种访问类型:池访问类型 - 一般访问类型 - 匿名访问类型 - 访问子程序类型。
池访问类型处理对在特定堆(或 Ada 中称为存储池)上创建的对象的访问。这些类型的指针不能指向堆栈或库级别(静态)对象,也不能指向其他存储池中的对象。因此,池访问类型之间的转换是非法的。(可以使用 Unchecked_Conversion,但请注意,通过与分配池不同的存储池的访问对象进行释放是错误的。)
type
Personis
record
First_Name : String (1..30); Last_Name : String (1..20);end
record
;type
Person_Accessis
access
Person;
可以使用存储大小子句来限制相应的(实现定义的匿名)存储池。存储大小子句为 0 将禁用分配器的调用。
for
Person_Access'Storage_Sizeuse
0;
如果没有指定,存储池是实现定义的。Ada 支持用户定义的存储池,因此可以使用以下方法定义存储池:
for
Person_Access'Storage_Pooluse
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_Nameall
)可以作为一种便捷的简写
Mother.all
:= (Last_Name => Father.Last_Name, First_Name => Mother.First_Name); -- marriage
隐式解引用也适用于数组
type
Vectoris
array
(1 .. 3)of
Complex;type
Vector_Accessis
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,则为该类型使用一个实现定义的池 P。P 的 Storage_Size 至少为请求的大小,并且在包含访问类型声明的主体退出时回收 P 的存储空间...
以下程序可以编译,但在运行时会因异常而无法通过可访问性检查。
with
Ada.Text_IO;use
Ada.Text_IO;procedure
Mainis
function
Accessibility_Check_Failreturn
access
Stringis
-- 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_Typeis
access
String; -- no Storage_Size defined X : A_Type :=new
String'("x"); -- storage will be lost Y :access
String; -- defined locallybegin
Y := X; -- data defined in a local pool will be finalized when function returnsreturn
Y; -- exception should be raisedend
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
已从 Ada 2012 中移除,存储管理的子池已取代它。请参见 RM 2012 13.11.3 [带注释的] 和 13.11.4 [带注释的]。pragma
Controlled
因此,要从堆中删除对象,您需要通用单元 Ada.Unchecked_Deallocation。在释放对象时,要格外小心,不要创建悬空指针,如以下示例所示。(请注意,当相应的存储池不同时,使用与创建对象时不同的访问类型释放对象是错误的。)
with
Ada.Unchecked_Deallocation;procedure
Deallocation_Sampleis
type
Vectoris
array
(Integerrange
<>)of
Float;type
Vector_Refis
access
Vector;procedure
Free_Vectoris
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 erroneousend
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_Monthis
range
1 .. 31;type
Day_Of_Month_Accessis
access
all
Day_Of_Month;
访问常量
[edit | edit source]授予对被引用对象只读访问权限的通用访问类型使用关键字constant
在其定义中。被引用对象可以是常量或变量。
type
Day_Of_Monthis
range
1 .. 31;type
Day_Of_Month_Accessis
access
constant
Day_Of_Month;
一些示例
[edit | edit source]type
General_Pointeris
access
all
Integer;type
Constant_Pointeris
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
-- componentsend
record
;
在使用匿名访问之前,您应该考虑命名访问类型,或者更好的是,考虑是否“out
”或“in
out
”修饰符更合适。
此语言功能仅从 Ada 2005 开始可用。
在 Ada 2005 中,匿名访问在更多情况下是允许的
type
Objectis
record
M : Integer; Next:access
Object;end
record
; X:access
Integer;function
Freturn
access
constant
Float;
隐式解引用
[edit | edit source]此语言功能已在 Ada 2012 中引入。
Ada 2012 使用新的语法简化了通过指针访问对象。
假设您有一个包含某种元素的容器。
type
Containeris
private
;type
Element_Ptris
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 accessfunction
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_Accessis
access
Day_Of_Month;subtype
Day_Of_Month_Not_Null_Accessis
not
null
Day_Of_Month_Access;
该语言还允许直接使用空排除声明第一个子类型
type
Day_Of_Month_Accessis
not
null
access
Day_Of_Month;
但是,在几乎所有情况下,这不是一个好主意,因为它会使该类型对象的可用性变得很差(例如,您无法释放分配的内存)。非空访问旨在用于访问子类型、对象声明和子程序参数。[1]
访问子程序
[edit | edit source]访问子程序允许调用方调用 子程序,而无需知道其名称或声明位置。这种访问方式的一种应用是众所周知的回调。
type
Callback_Procedureis
access
procedure
(Id : Integer; Text: String);type
Callback_Functionis
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
Freturn
access
function
return
access
function
return
access
Some_Type;
这是一个函数,它返回对一个函数的访问,该函数又返回对一个函数的访问,该函数返回对某种类型的访问。
关于 Ada 的访问类型,一些 "常见问题" 和 "常见问题" (主要来自 C 用户)。
一个访问
all
可以执行任何一个简单的access
可以执行的操作。因此有人可能会问:"为什么还要使用简单的access
呢?" - 实际上,一些程序员从来不使用简单的访问
.
Unchecked_Deallocation 如果使用不当,始终是危险的。将池特定的对象释放两次和释放堆栈对象一样容易,也同样危险。"访问所有" 的优势在于,你可能根本不需要使用 Unchecked_Deallocation。
道德:如果你有(或可能会有)将 '访问或 'Unchecked_Access 存储到访问对象的有效理由,那么使用 "访问所有" 并且不要担心。如果没有,"最小权限" 的口号建议应该省略 "所有" (不要启用你不会使用的功能)。
以下(可能灾难性的)示例将尝试释放一个堆栈对象
declare
type
Day_Of_Monthis
range
1 .. 31;type
Day_Of_Month_Accessis
access
all
Day_Of_Month;procedure
Freeis
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
不允许从堆栈对象创建指针。
访问可以与一个简单的内存地址不同,它可能包含更多内容。例如,"对字符串的访问" 通常还需要某种方法来存储字符串大小。如果你需要一个简单的地址并且不关心强类型,请使用 System.Address 类型。
创建 C 兼容访问的正确方法是使用pragma
Convention
type
Day_Of_Monthis
range
1 .. 31;for
Day_Of_Month'Sizeuse
Interfaces.C.int'Size;pragma
Convention (Convention => C, Entity => Day_Of_Month);type
Day_Of_Month_Accessis
access
Day_Of_Month;pragma
Convention (Convention => C, Entity => Day_Of_Month_Access);
pragma
Convention 应该用于你想要在 C 中使用的任何类型。如果该类型无法与 C 兼容,编译器会发出警告。
在声明 Day_Of_Month 时,你也可以考虑以下更短的替代方法
type
Day_Of_Monthis
new
Interfaces.C.intrange
1 .. 31;
在 C 中使用访问类型之前,你应该考虑使用普通的 "in"、"out" 和 "in out" 修饰符。pragma
Export 和pragma
Import 知道参数通常如何在 C 中传递,并且会在 C 使用指针传递参数的情况下自动使用指针来传递参数。当然,RM 包含关于何时为 "in"、"out" 和 "in out" 使用指针的精确规则 - 请参阅 "B.3: Interfacing with C [Annotated]"。
虽然实际上是 "与 C 交互" 的问题,这里有一些可能的解决方案
procedure
Testis
subtype
Pvoidis
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
voidis
mod
2 ** 32;for
void'Sizeuse
32;
使用 GNAT,你可以通过使用以下方法获得 32/64 位可移植性
type
voidis
mod
System.Memory_Size;for
void'Sizeuse
System.Word_Size;
更接近 void 的本质 - 指向大小为零的元素的指针是指向空记录的指针。这也具有为 void
和 void*
提供表示的优势
type
Voidis
null
record
;pragma
Convention (C, Void);type
Void_Ptris
access
all
Void;pragma
Convention (C, Void_Ptr);
访问类型和地址之间的区别将在下面详细说明。使用术语 指针 是因为这是常用的术语。
有一个预定义的单元 System.Address_to_Access_Conversion
用于在访问值和地址之间来回转换。请谨慎使用这些转换,如下文所述。
瘦指针允许访问约束子类型。
type
Intis
range
-100 .. +500;type
Acc_Intis
access
Int;type
Arris
array
(1 .. 80)of
Character;type
Acc_Arris
access
Arr;
此类子类型的对象具有静态大小,因此只需一个简单的地址即可访问它们。在数组的情况下,这通常是第一个元素的地址。
对于这种类型的指针,使用 System.Address_to_Access_Conversion
是安全的。
type
Uncis
array
(Integerrange
<>)of
Character;type
Acc_Uncis
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
。在非法情况下,指定子类型和名义子类型在静态上不匹配。
- 4.8: 分配器 [注释]
- 13.11: 存储管理 [注释]
- 13.11.2: 未检查存储释放 [注释]
- 3.7: 辨别式 [注释]
- 3.10: 访问类型 [注释]
- 6.1: 子程序声明 [注释]
- B.3: 与 C 交互 [注释]
- 4.8: 分配器 [注释]
- 13.11: 存储管理 [注释]
- 13.11.2: 未检查存储释放 [注释]
- 3.7: 辨别式 [注释]
- 3.10: 访问类型 [注释]
- 6.1: 子程序声明 [注释]
- B.3: 与 C 交互 [注释]
- 3.10: 访问类型 [注释]
- 7.6: 赋值和终结 [注释]
- 7.6.1: 完成和终结 [注释]
- 13.11: 存储管理 [注释]