代码之家  ›  专栏  ›  技术社区  ›  zeus

如何在调整数组大小后保持指向动态数组的指针有效?

  •  -1
  • zeus  · 技术社区  · 10 月前

    我有一个动态数组,我需要维护一个指向它的指针。设置如下:

    var
      Controls: TArray<TControls>;
      Items: ^TArray<TControls>;
    begin
      Items := @Controls;
    end;
    

    问题: 什么时候? Controls 调整大小(例如,通过 SetLength() ),Delphi可能会重新分配数组,使存储在中的指针无效 Items .

    1. Delphi在调整动态数组大小时是否总是重新分配内存?

    2. 我如何确保 项目 始终指向更新 控制 ?

    3. 在这里使用指针是最好的方法吗,还是我应该使用另一种机制?

    2 回复  |  直到 10 月前
        1
  •  3
  •   Andreas Rejbrand    10 月前

    我觉得你有点困惑。

    首先,回想一下德尔菲 dynamic arrays 是引用类型( 没有 写时复制(COW)语义)。动态数组变量的值是指向 dynamic array heap object .

    所以,如果你有

    var
      A: TArray<Integer>;
      B: TArray<Integer>;
    begin
      A := [1, 2, 3, 4];
      B := A;
    

    然后 A B 两者都将是引擎盖下的指针,它们将指向 相同的 动态数组堆对象,其引用计数为2:

    Screenshot of heap object memory: Reference count 2, length 4, values 1, 2, 3, and 4.

    因此,如果你尝试类似的东西

      Writeln(NativeInt(A));
      Writeln(NativeInt(B));
    

    您将获得相同的地址(此处:02E62D48)。

    现在,如果你打电话来 SetLength A. ,那么RTL当然可能需要在其他地址重新分配并创建一个全新的堆对象。事实上,如果原始堆对象的引用计数大于1,则总是会发生这种情况。从 the documentation :

    接到电话后 设定长度 ,[参数数组] S 保证引用一个唯一的字符串或数组,即引用计数为1的字符串或阵列。

    然后 B 将左指向旧堆对象,该对象现在也具有引用计数1。

    例如,如果我重做我的 Writeln(NativeInt(A)); Writeln(NativeInt(B)) 我可能会得到

    A initially:  $02E62D48 (refcount 2)
    B initially:  $02E62D48
    A after:      $032CB378 (refcount 1)
    B after:      $02E62D48 (refcount 1)
    

    现在,你的代码

    var
      A: TArray<Integer>;
      B: ^TArray<Integer>;
    begin
      A := [1, 2, 3, 4];
      B := @A;
    

    做一些不同的事情。在这里, B 指向 A. 变量本身,它又指向动态数组堆对象(引用计数仅为1)。

    因此,从 A. 对于堆对象,您只需跟随一个指针。但要从 B 对于堆对象,您需要遵循两个指针。你先跟着 B 查找 A. ,在这里您可以找到堆对象的所需地址。

    打电话 SetLength(A) 确实可能改变 A. (动态数组堆对象的地址), 但它永远不会改变地址 @A 属于 A. .

    因此, B 仍然有效。它将指向同一个地址 A. ,但这里可能有一个不同的地址,动态数组堆对象的新地址。

    为了看到这一点,例如,

      A := [1, 2, 3, 4];
      B := @A;
    
      Writeln(NativeInt(A));
      Writeln(NativeInt(B));
    
      SetLength(A, 66);
    
      Writeln(NativeInt(A));
      Writeln(NativeInt(B));
    

    例如,

    43199816 (first address of array heap object)
    6531208 (address of local variable A)
    43168632 (new address of array heap object)
    6531208 (address of local variable A)
    

    但如果你访问 B^ 相反,您会得到相同的对象(不同的run):

    53161288 (first address of array heap object)
    53161288 (first address of array heap object)
    53130104 (new address of array heap object)
    53130104 (new address of array heap object)
    

    所以打电话 设定长度 A. 不会毁了你的 B 指针。再次,这是因为 B 指向局部变量 A. (不会移动),它又包含与关联的数组堆对象的始终最新地址 A. .

    Delphi在调整动态数组大小时是否总是重新分配内存?

    无法保证堆对象 不会 更改地址。如果堆对象最初的引用计数>1,那么 获取一个新地址。一般来说,预计地址会更改。

    我如何确保项目始终指向更新的控件?

    它总是这样。

    在这里使用指针是最好的方法吗,还是我应该使用另一种机制?

    在现代Delphi中,指针很少是“正确”的方法,除非你是一名从事相当高级工作的专家开发人员。

    我不知道你的背景,但也许你最好买一辆现代车 TList<Integer> .

        2
  •  2
  •   Remy Lebeau    10 月前

    什么时候? Controls 调整大小(例如,通过 SetLength ),Delphi可能会重新分配数组,使存储在中的指针无效 Items .

    这是不正确的。你指的是 控制 变量本身,而不是数组 控制 调整数组大小不会改变变量,因此不会使指针无效。

    如果你指向实际的数组,那么你就会遇到你描述的问题,例如:

    type
      PControls = ^TControls;
    var
      Controls: TArray<TControls>;
      Items: PControls;
    begin
      SetLength(Controls, ...);
      Items := @Controls[0];
      // or:
      // Items := PControls(Controls);
      SetLength(Controls, ...); // <-- Items invalidated here!
      ...
    end;
    

    Delphi在调整动态数组大小时是否总是重新分配内存?

    不幸的是,是的。如果新数组大小小于或等于当前数组大小,RTL不够智能,无法避免重新分配。

    我如何确保 项目 始终指向更新 控制 ?

    在你的例子中,它已经做到了。