为什么两个TBy不能使用重叠数据?

Hug*_*nes 17 delphi dynamic-arrays

请考虑以下XE6代码.目的是ThingData应该将两个Thing1&写入控制台Thing2,但事实并非如此.这是为什么?

program BytesFiddle;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TThing = class
  private
    FBuf : TBytes;
    FData : TBytes;
    function GetThingData: TBytes;
    function GetThingType: Byte;
  public
    property ThingType : Byte read GetThingType;
    property ThingData : TBytes read GetThingData;

    constructor CreateThing(const AThingType : Byte; const AThingData: TBytes);
  end;

{ TThing1 }

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + 1);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));

  FData := @FBuf[1];
  SetLength(FData, Length(FBuf) - 1);
end;

function TThing.GetThingData: TBytes;
begin
  Result := FData;
end;

function TThing.GetThingType: Byte;
begin
  Result := FBuf[0];
end;

var
  Thing1, Thing2 : TThing;

begin
  try
    Thing1 := TThing.CreateThing(0, TEncoding.UTF8.GetBytes('Sneetch'));
    Thing2 := TThing.CreateThing(1, TEncoding.UTF8.GetBytes('Star Belly Sneetch'));

    Writeln(TEncoding.UTF8.GetString(Thing2.ThingData));
    Writeln(Format('Type %d', [Thing2.ThingType]));

    Writeln(TEncoding.UTF8.GetString(Thing1.ThingData));
    Writeln(Format('Type %d', [Thing1.ThingType]));

    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Run Code Online (Sandbox Code Playgroud)

Joh*_*ica 33

让我带您了解这段代码失败的方式以及编译器如何让您自己开枪.

如果您使用调试器单步执行代码,您可以看到会发生什么.

在此输入图像描述

初始化之后,Thing1您可以看到FData填充全部为零.
奇怪的Thing2是很好.
因此错误在于CreateThing.让我们进一步调查......

在奇怪命名的构造函数中,CreateThing您有以下行:

FData := @FBuf[1];
Run Code Online (Sandbox Code Playgroud)

这看起来像一个简单的任务,但实际上是一个调用 DynArrayAssign

Project97.dpr.32: FData := @FBuf[1];
0042373A 8B45FC           mov eax,[ebp-$04]
0042373D 83C008           add eax,$08
00423743 8B5204           mov edx,[edx+$04]
00423746 42               inc edx
00423747 8B0DE03C4000     mov ecx,[$00403ce0]
0042374D E8E66DFEFF       call @DynArrayAsg      <<-- lots of stuff happening here.  
Run Code Online (Sandbox Code Playgroud)

其中一项检查DynArrayAsg是检查源动态数组是否为空.
DynArrayAsg还做了一些你需要注意的事情.

我们先来看一下动态数组的结构 ; 它不仅仅是一个指向数组的简单指针!

Offset 32/64  |   Contents     
--------------+--------------------------------------------------------------
-8/-12        | 32 bit reference count
-4/-8         | 32 or 64 bit length indicator 
 0/ 0         | data of the array.
Run Code Online (Sandbox Code Playgroud)

执行FData = @FBuf[1]您正在弄乱动态数组的前缀字段.
前面的4个字节@Fbuf[1]被解释为长度.
对于Thing1,这些是:

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 00 00 00  08 00 00 00  00  'S' 'n' .....
FData:    00 00 00 08  00 00 00 00  .............. //Hey that's a zero length.
Run Code Online (Sandbox Code Playgroud)

哎呀,当DynArrayAsg开始调查它时,看到它认为是分配的源的长度为零,即它认为源是空的并且不分配任何东西.它FData保持不变!

是否Thing2按预期工作?
它看起来确实如此,但实际上却以一种糟糕的方式失败,让我告诉你.

在此输入图像描述

您已成功欺骗运行时相信@FBuf[1]是对动态数组的有效引用.
因此,FData指针已经更新为指向FBuf[1](到目前为止一直很好),并且FData的引用计数增加了1(不好),运行时也增加了将动态数组保存到其认为的内存块是FData(坏)的正确大小.

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 01 00 00  13 00 00 00  01  'S' 'n' .....
FData:    01 00 00 13  00 00 00 01  'S' .............. 
Run Code Online (Sandbox Code Playgroud)

哎呀FData现在的引用数为318,767,105,长度为16,777,216字节.
FBuf也有它的长度增加,但它的引用现在是257.

这就是您需要调用SetLength以撤消内存的大量分配的原因.但这仍然无法修复引用计数.
过度分配可能会导致内存不足错误(尤其是64位),而古怪的refcounts会导致内存泄漏,因为您的阵列永远不会被释放.

解决方案
根据David的回答:启用键入的检查指针:{$TYPEDADDRESS ON}

您可以通过定义FData为普通PAnsiChar或或来修复代码PByte.
如果您确保始终FBuf使用双零FD 终止您的分配,则FData将按预期工作.

FData一个TBuffer像这样:

TBuffer = record
private
  FData : PByte;
  function GetLength: cardinal;
  function GetType: byte;
public
  class operator implicit(const A: TBytes): TBuffer;
  class operator implicit(const A: TBuffer): PByte;
  property Length: cardinal read GetLength;
  property DataType: byte read GetType;
end;
Run Code Online (Sandbox Code Playgroud)

CreateThing像这样改写:

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + Sizeof(AThingType) + 2);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));
  FBuf[Lengh(FBuf)-1]:= 0;
  FBuf[Lengh(FBuf)-2]:= 0;  //trailing zeros for compatibility with pansichar

  FData := FBuf;  //will call the implicit class operator.
end;

class operator TBuffer.implicit(const A: TBytes): TBuffer;
begin
  Result.FData:= PByte(@A[1]);
end;
Run Code Online (Sandbox Code Playgroud)

我不明白所有这些关于试图超越编译器的问题.
为什么不直接声明FData:

type
  TMyData = record
    DataType: byte;
    Buffer: Ansistring;  
    ....
Run Code Online (Sandbox Code Playgroud)

并与之合作.

  • @HughJones,不,它没有,它不会,继续努力. (2认同)

Dav*_*nan 18

通过启用类型检查指针可以很容易地看到问题.将其添加到代码顶部:

{$TYPEDADDRESS ON}
Run Code Online (Sandbox Code Playgroud)

文件说:

$ T指令控制@运算符生成的指针值的类型以及指针类型的兼容性.

在{$ T-}状态中,@运算符的结果始终是与所有其他指针类型兼容的无类型指针(Pointer).当@被应用于{$ T +}状态的变量引用时,结果是一个类型指针,它只与Pointer兼容,并与其他指向变量类型的指针兼容.

在{$ T-}状态中,除指针之外的不同指针类型是不兼容的(即使它们是指向相同类型的指针).在{$ T +}状态中,指向相同类型的指针是兼容的.

通过该更改,您的程序无法编译.此行失败:

FData := @FBuf[1];
Run Code Online (Sandbox Code Playgroud)

错误消息是:

E2010不兼容的类型:'System.TArray<System.Byte>''Pointer'

现在,FData是类型TArray<Byte>@FBuf[1]不是动态数组,而是指向动态数组中间的字节的指针.两者不兼容.通过在未对类型进行类型检查的默认模式下运行,编译器可以让您犯下这个可怕的错误.这就是为什么这是默认模式完全超出我的范围.

动态数组不仅仅是指向第一个元素的指针 - 还有长度和引用计数等元数据.该元数据存储在距第一个元素的偏移处.因此,您的整个设计都存在缺陷.将类型代码存储在单独的变量中,而不是作为动态数组的一部分.

  • 很容易创建一个看起来像数组但实际上映射到实际数组的区域的类.提供**视图**的类(或记录).无论如何,让类型检查指针将来成为你的朋友! (2认同)

klu*_*udg 6

动态数组是内部指针,与指针分配兼容; 但是赋值右侧唯一正确的指针是nil另一个动态数组.FData := @FBuf[1];显然是错误的,但有趣的FData := @FBuf[0];是,即使$TYPEDADDRESS启用也可以.

以下代码在Delphi XE中编译并按预期工作:

program Project19;

{$APPTYPE CONSOLE}
{$TYPEDADDRESS ON}

uses
  SysUtils;

procedure Test;
var
  A, B: TBytes;

begin
  A:= TBytes.Create(11,22,33);
  B:= @A[0];
  Writeln(B[1]);
end;

begin
  try
    Test;
    readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Run Code Online (Sandbox Code Playgroud)

看起来像编译器"知道"这@A[0]是一个动态数组,而不仅仅是一个指针.