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)
并与之合作.
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]
不是动态数组,而是指向动态数组中间的字节的指针.两者不兼容.通过在未对类型进行类型检查的默认模式下运行,编译器可以让您犯下这个可怕的错误.这就是为什么这是默认模式完全超出我的范围.
动态数组不仅仅是指向第一个元素的指针 - 还有长度和引用计数等元数据.该元数据存储在距第一个元素的偏移处.因此,您的整个设计都存在缺陷.将类型代码存储在单独的变量中,而不是作为动态数组的一部分.
动态数组是内部指针,与指针分配兼容; 但是赋值右侧唯一正确的指针是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]
是一个动态数组,而不仅仅是一个指针.
归档时间: |
|
查看次数: |
1945 次 |
最近记录: |