Ada 编程/类型/访问
Ada 中的访问类型是其他语言中称为指针的东西。它们指向位于特定地址的对象。因此,通常可以将访问类型视为简单的地址(这种简化观点存在例外情况)。Ada 不说“指向”,而是说“授予访问权限”或“指定”某个对象。
访问类型的对象隐式地初始化为null,即,如果未显式初始化,它们将不指向任何内容。
在 Ada 中应该很少使用访问类型。在其他语言中使用指针的许多情况下,还有其他不使用指针的方法。如果您需要动态数据结构,请先检查是否可以使用 Ada 容器库。特别是对于不定记录或数组组件,Ada 2012 包 Ada.Containers.Indefinite_Holders(RM A.18.18 [注释])可以用来代替指针。
Ada 中有四种访问类型:池访问类型 - 一般访问类型 - 匿名访问类型 - 访问子程序类型。
池访问类型处理对在特定堆(或 Ada 中称为存储池)上创建的对象的访问。这些类型的指针不能指向堆栈或库级别(静态)对象,也不能指向其他存储池中的对象。因此,池访问类型之间的转换是非法的。(可以使用 Unchecked_Conversion,但请注意,通过与分配池不同的存储池的访问对象进行释放是错误的。)
typePersonisrecordFirst_Name : String (1..30); Last_Name : String (1..20);endrecord;typePerson_AccessisaccessPerson;
可以使用存储大小子句来限制相应的(实现定义的匿名)存储池。存储大小子句为 0 将禁用分配器的调用。
forPerson_Access'Storage_Sizeuse0;
如果没有指定,存储池是实现定义的。Ada 支持用户定义的存储池,因此可以使用以下方法定义存储池:
forPerson_Access'Storage_PoolusePool_Name;
存储池中的对象是用关键字创建的new:
Father: Person_Access :=newPerson; -- uninitialized Mother: Person_Access :=newPerson'(Mothers_First_Name, Mothers_Last_Name); -- initialized
通过附加 . 访问存储池中的对象。allMother. 是完整的记录;组件用通常的点表示法表示:allMother.。访问组件时,隐式解引用(即省略all.First_Nameall)可以作为一种便捷的简写
Mother.all := (Last_Name => Father.Last_Name, First_Name => Mother.First_Name); -- marriage
隐式解引用也适用于数组
typeVectorisarray(1 .. 3)ofComplex;typeVector_AccessisaccessVector; VA: Vector_Access :=newVector; VB:array(1 .. 3)ofVector_Access := (others=>newVector); 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 的存储空间...
以下程序可以编译,但在运行时会因异常而无法通过可访问性检查。
withAda.Text_IO;useAda.Text_IO;procedureMainisfunctionAccessibility_Check_FailreturnaccessStringis-- 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.typeA_TypeisaccessString; -- no Storage_Size defined X : A_Type :=newString'("x"); -- storage will be lost Y :accessString; -- defined locallybeginY := X; -- data defined in a local pool will be finalized when function returnsreturnY; -- exception should be raisedendAccessibility_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);endMain;
还有一个 请注意,,当应用于此类访问类型时,可以防止使用它创建的对象被自动垃圾回收。pragma Controlled 已从 Ada 2012 中移除,存储管理的子池已取代它。请参见 RM 2012 13.11.3 [带注释的] 和 13.11.4 [带注释的]。pragma Controlled
因此,要从堆中删除对象,您需要通用单元 Ada.Unchecked_Deallocation。在释放对象时,要格外小心,不要创建悬空指针,如以下示例所示。(请注意,当相应的存储池不同时,使用与创建对象时不同的访问类型释放对象是错误的。)
withAda.Unchecked_Deallocation;procedureDeallocation_SampleistypeVectorisarray(Integerrange<>)ofFloat;typeVector_RefisaccessVector;procedureFree_VectorisnewAda.Unchecked_Deallocation (Object => Vector, Name => Vector_Ref); VA, VB: Vector_Ref; V : Vector;beginVA :=newVector (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 erroneousendDeallocation_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 用于定义时,它们会授予读写访问权限。
typeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessallDay_Of_Month;
访问常量
[edit | edit source]授予对被引用对象只读访问权限的通用访问类型使用关键字constant 在其定义中。被引用对象可以是常量或变量。
typeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessconstantDay_Of_Month;
一些示例
[edit | edit source]typeGeneral_PointerisaccessallInteger;typeConstant_PointerisaccessconstantInteger; I1:aliasedconstantInteger := 10; I2:aliasedInteger; 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:constantGeneral_Pointer := I2'Access; -- read and write only to I2
匿名访问
[edit | edit source]匿名访问类型也有两种版本,类似于通用访问类型,分别授予读写访问权限或只读访问权限,具体取决于关键字constant 是否出现。
匿名访问可以用作子程序的参数或作为区分符。以下是一些示例
procedureModify (Some_Day:accessDay_Of_Month);procedureTest (Some_Day:accessconstantDay_Of_Month); -- Ada 2005 only
tasktypeThread (Execute_For_Day:accessDay_Of_Month)is...endThread;
typeDay_Data (Store_For_Day:accessDay_Of_Month)isrecord-- componentsendrecord;
在使用匿名访问之前,您应该考虑命名访问类型,或者更好的是,考虑是否“out”或“in out”修饰符更合适。
此语言功能仅从 Ada 2005 开始可用。
在 Ada 2005 中,匿名访问在更多情况下是允许的
typeObjectisrecordM : Integer; Next:accessObject;endrecord; X:accessInteger;functionFreturnaccessconstantFloat;
隐式解引用
[edit | edit source]此语言功能已在 Ada 2012 中引入。
Ada 2012 使用新的语法简化了通过指针访问对象。
假设您有一个包含某种元素的容器。
typeContainerisprivate;typeElement_PtrisaccessElement;procedurePut (X: Element; Into:inoutContainer);
现在,如何访问存储在容器中的元素。当然,您可以通过
functionGet (From: Container)returnElement;
来检索它们,但这会复制元素,如果元素很大,这是不利的。您可以使用
functionGet (From: Container)returnElement_Ptr;
获得直接访问权限,但指针很危险,因为您很容易创建悬空指针,例如
P: Element_Ptr := Get (Cont);
P.all := E;
Free (P);
... Get (Cont) -- this is now a dangling pointer
使用访问器对象而不是访问类型可以防止意外释放(这仍然是 Ada 2005)
typeAccessor (Data:notnullaccessElement)islimitedprivate; -- read/write accessfunctionGet (From: Container)returnAccessor;
(对于空排除not null 在区分符的声明中,请参见下文)。通过此类访问器进行的访问是安全的:区分符只能用于解引用,不能将其复制到 Element_Ptr 类型的对象,因为其访问级别更深。在上面的形式中,访问器提供了读写访问权限。如果添加关键字constant,则只能进行读访问。
typeAccessor (Data:notnullaccessconstantElement)islimitedprivate; -- 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 方面
typeAccessor (Data:notnullaccessElement)islimitedprivatewithImplicit_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 修改,此类子类型的对象永远不会有空值,因此必须进行初始化。
typeDay_Of_Month_AccessisaccessDay_Of_Month;subtypeDay_Of_Month_Not_Null_AccessisnotnullDay_Of_Month_Access;
该语言还允许直接使用空排除声明第一个子类型
typeDay_Of_Month_AccessisnotnullaccessDay_Of_Month;
但是,在几乎所有情况下,这不是一个好主意,因为它会使该类型对象的可用性变得很差(例如,您无法释放分配的内存)。非空访问旨在用于访问子类型、对象声明和子程序参数。[1]
访问子程序
[edit | edit source]访问子程序允许调用方调用 子程序,而无需知道其名称或声明位置。这种访问方式的一种应用是众所周知的回调。
typeCallback_Procedureisaccessprocedure(Id : Integer; Text: String);typeCallback_Functionisaccessfunction(The_Alarm: Alarm)returnNatural;
要获取对子程序的访问权,需要将属性 Access 应用于子程序名称,并使用适当的参数和结果概要。
procedure Process_Event (Id : Integer;
Text: String);
My_Callback: Callback_Procedure := Process_Event'Access;
此语言功能仅从 Ada 2005 开始可用。
procedureTest (Call_Back:accessprocedure(Id: Integer; Text: String));
现在,一个序列中关键字的数量不再受限制。
functionFreturnaccessfunctionreturnaccessfunctionreturnaccessSome_Type;
这是一个函数,它返回对一个函数的访问,该函数又返回对一个函数的访问,该函数返回对某种类型的访问。
关于 Ada 的访问类型,一些 "常见问题" 和 "常见问题" (主要来自 C 用户)。
一个访问 all 可以执行任何一个简单的access 可以执行的操作。因此有人可能会问:"为什么还要使用简单的access 呢?" - 实际上,一些程序员从来不使用简单的访问.
Unchecked_Deallocation 如果使用不当,始终是危险的。将池特定的对象释放两次和释放堆栈对象一样容易,也同样危险。"访问所有" 的优势在于,你可能根本不需要使用 Unchecked_Deallocation。
道德:如果你有(或可能会有)将 '访问或 'Unchecked_Access 存储到访问对象的有效理由,那么使用 "访问所有" 并且不要担心。如果没有,"最小权限" 的口号建议应该省略 "所有" (不要启用你不会使用的功能)。
以下(可能灾难性的)示例将尝试释放一个堆栈对象
declaretypeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessallDay_Of_Month;procedureFreeisnewAda.Unchecked_Deallocation (Object => Day_Of_Month, Name => Day_Of_Month_Access); A :aliasedDay_Of_Month; Ptr: Day_Of_Month_Access := A'Access;beginFree(Ptr);end;
使用一个简单的access 你至少知道你不会尝试释放一个堆栈对象。原因是access 不允许从堆栈对象创建指针。
访问可以与一个简单的内存地址不同,它可能包含更多内容。例如,"对字符串的访问" 通常还需要某种方法来存储字符串大小。如果你需要一个简单的地址并且不关心强类型,请使用 System.Address 类型。
创建 C 兼容访问的正确方法是使用pragma Convention
typeDay_Of_Monthisrange1 .. 31;forDay_Of_Month'SizeuseInterfaces.C.int'Size;pragmaConvention (Convention => C, Entity => Day_Of_Month);typeDay_Of_Month_AccessisaccessDay_Of_Month;pragmaConvention (Convention => C, Entity => Day_Of_Month_Access);
pragma Convention 应该用于你想要在 C 中使用的任何类型。如果该类型无法与 C 兼容,编译器会发出警告。
在声明 Day_Of_Month 时,你也可以考虑以下更短的替代方法
typeDay_Of_MonthisnewInterfaces.C.intrange1 .. 31;
在 C 中使用访问类型之前,你应该考虑使用普通的 "in"、"out" 和 "in out" 修饰符。pragma Export 和pragma Import 知道参数通常如何在 C 中传递,并且会在 C 使用指针传递参数的情况下自动使用指针来传递参数。当然,RM 包含关于何时为 "in"、"out" 和 "in out" 使用指针的精确规则 - 请参阅 "B.3: Interfacing with C [Annotated]"。
虽然实际上是 "与 C 交互" 的问题,这里有一些可能的解决方案
procedureTestissubtypePvoidisSystem.Address; -- the declaration in C looks like this: -- int C_fun(int *)functionC_fun (pv: Pvoid)returnInteger;pragmaImport (Convention => C, Entity => C_fun, -- any Ada name External_Name => "C_fun"); -- the C name Pointer: Pvoid; Input_Parameter:aliasedInteger := 32; Return_Value : Integer;beginPointer := Input_Parameter'Address; Return_Value := C_fun (Pointer);endTest;
可移植性较差,但可能更易用 (对于 32 位 CPU)
typevoidismod2 ** 32;forvoid'Sizeuse32;
使用 GNAT,你可以通过使用以下方法获得 32/64 位可移植性
typevoidismodSystem.Memory_Size;forvoid'SizeuseSystem.Word_Size;
更接近 void 的本质 - 指向大小为零的元素的指针是指向空记录的指针。这也具有为 void 和 void* 提供表示的优势
typeVoidisnullrecord;pragmaConvention (C, Void);typeVoid_PtrisaccessallVoid;pragmaConvention (C, Void_Ptr);
访问类型和地址之间的区别将在下面详细说明。使用术语 指针 是因为这是常用的术语。
有一个预定义的单元 System.Address_to_Access_Conversion 用于在访问值和地址之间来回转换。请谨慎使用这些转换,如下文所述。
瘦指针允许访问约束子类型。
typeIntisrange-100 .. +500;typeAcc_IntisaccessInt;typeArrisarray(1 .. 80)ofCharacter;typeAcc_ArrisaccessArr;
此类子类型的对象具有静态大小,因此只需一个简单的地址即可访问它们。在数组的情况下,这通常是第一个元素的地址。
对于这种类型的指针,使用 System.Address_to_Access_Conversion 是安全的。
typeUncisarray(Integerrange<>)ofCharacter;typeAcc_UncisaccessUnc;
子类型 Unc 的对象需要约束,即起始和终止索引,因此指向它们的指针也需要包含这些索引。因此,像第一个组件的地址这样的简单地址是不够的。请注意,对于任何数组对象,A'Address 与 A(A'First)'Address 相同。
对于这种类型的指针,System.Address_to_Access_Conversion 可能无法正常工作。
CO:aliasedUnc (-1 .. +1) := (-1 .. +1 => ' '); UO:aliasedUnc := (-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: 存储管理 [注释]
