德尔福型铸造

时间:2013-01-31 12:19:57

标签: delphi types casting

我需要澄清Delphi Type Casting
我写了两个类的例子:TClassA和TClassB,TClassB派生自TClassA。

代码如下:

program TEST;

{$APPTYPE CONSOLE}


uses
  System.SysUtils;

type
  TClassA = class(TObject)
  public
    Member1:Integer;
    constructor Create();
    function ToString():String; override;
  end;

type
  TClassB = class(TClassA)
  public
    Member2:Integer;
    constructor Create();
    function ToString():String; override;
    function MyToString():String;
  end;

{ TClassA }

constructor TClassA.Create;
begin
  Member1 := 0;
end;

function TClassA.ToString: String;
begin
  Result := IntToStr(Member1);
end;

{ TClassB }

constructor TClassB.Create;
begin
  Member1 := 0;
  Member2 := 10;
end;

function TClassB.MyToString: String;
begin
  Result := Format('My Values is: %u AND %u',[Member1,Member2]);
end;

function TClassB.ToString: String;
begin
  Result := IntToStr(Member1) + ' - ' + IntToStr(Member2);
end;


procedure ShowInstances();
var
  a: TClassA;
  b: TClassB;
begin
  a := TClassA.Create;
  b := TClassB(a); // Casting (B and A point to the same Memory Address)
  b.Member1 := 5;
  b.Member2 := 150; // why no error? (1)

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); // (2)
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); // (3)
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); // (4)
  Writeln(b.MyToString); // why no error? (5)

  readln;
end;

begin
  try
    ShowInstances;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

程序输出为:

ToString: a = 5, a = 5
Class Name: a=TClassA, b=TClassA
Address: a=0012FF44, b=0012FF40
My Values is: 5 AND 150

(1)会员2的地址是什么?这可能是“访问违规”吗? (2)ok,ToString()方法指向同一地址
(3)为什么a和b具有相同的ClassName?
(4)ok,a和b是两个不同的变量
(5)如果b是TClassA,为什么可以使用“MyToString”方法?

3 个答案:

答案 0 :(得分:23)

您正在对变量应用 hard 类型转换。当你这样做时,你告诉编译器你知道你在做什么,编译器信任你。

  

(1)会员2的地址是什么?这可能是“访问冲突”吗?

当您为类的成员分配值时,编译器会使用该变量的类定义来计算该成员在内存空间中的偏移量,因此当您有类似这样的类声明时:

type
  TMyClass = class(TObject)
    Member1: Integer; //4 bytes
    Member2: Integer; //4 bytes
  end;

此对象的内存中表示如下所示:

reference (Pointer) to the object
|
|
--------> [VMT][Member1][Member 2][Monitor]
Offset     0    4        8         12

当您发出如下声明时:

MyObject.Member2 := 20;

编译器只使用该信息来计算应用该赋值的内存地址。在这种情况下,编译器的赋值可以转换为

PInteger(NativeUInt(MyObject) + 8)^ := 20;

所以,你的任务成功只是因为(默认)内存管理器的工作方式。当您尝试访问不属于您的程序的内存地址时,操作系统会发起AV。在这种情况下,您的程序从操作系统获取的内存超过了所需的内存。恕我直言,当你没有获得AV时,你实际上是不吉利的,因为你的程序内存现在可能会被无声地破坏。碰巧驻留在该地址的任何其他变量可能已更改其值(或元数据),并且会导致未定义的行为。

  

(2)ToString()方法指向相同的地址

由于ToString()方法是虚方法,因此该方法的地址存储在VMT中,并且在运行时确定调用。请查看What data does a TObject contain?,并阅读参考书籍章节:The Delphi Object Model

  

(3)为什么a和b具有相同的ClassName?

类名也是对象的运行时元数据的一部分。您将错误的模具应用于对象这一事实并不会改变对象本身。

  

(4)a和b是两个不同的变量

当然,你宣布了它,看看你的代码:

var
  a: TClassA;
  b: TClassB;

嗯,两个不同的变量。在Delphi中,对象变量是引用,因此,在某些代码行之后都引用相同的地址,但这是另一回事。

  

(5)如果b是TClassA,为什么可以使用“MyToString”方法?

因为你告诉编译器没问题,如上所述,编译器信任你。这很糟糕,但Delphi也是一种低级语言,如果你愿意,你可以做很多疯狂的事情,但是:

安全播放

如果你想(并且你肯定希望大部分时间)安全,不要在你的代码中应用这样的硬强制转换。使用as operator

  

as 运算符执行检查的类型转换。表达式

     

对象 as class

     

返回对象作为同一对象的引用,但是使用类给出的类型。在运行时,对象必须是由类或其后代之一表示的类的实例,或者为nil;否则会引发异常。如果声明的对象类型与类无关 - 也就是说,如果类型是不同的并且一个不是另一个的祖先 - 则会产生编译错误。

因此,使用as运算符,无论是在编译时还是在运行时,都是安全的。

将您的代码更改为:

procedure ShowInstance(A: TClassA);
var
  b: TClassB;
begin
  b := A as TClassB; //runtime exception, the rest of the compiled code 
                     //won't be executed if a is not TClassB
  b.Member1 := 5;
  b.Member2 := 150; 

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); 
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); 
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); 
  Writeln(b.MyToString); 

  readln;
end;

procedure ShowInstances();
begin
  ShowInstance(TClassB.Create); //success
  ShowInstance(TClassA.Create); //runtime failure, no memory corrupted.
end;

答案 1 :(得分:4)

  1. Member2的地址未由内存管理器分配。写入Member2的可能结果是堆损坏,后续访问冲突在程序的完全不同的部分。这是一个非常讨厌的bug,编译器无法帮助你。在进行不安全的类型转换时,您必须知道自己在做什么。

  2. 这是因为ToString方法是虚方法,因此其地址由正在创建的类实例的实际类型决定。如果您通过静态替换虚方法(在您的情况下通过override替换reintroduce指令),结果将会有所不同。

  3. 因为ClassName方法也是虚拟的(实际上不是VMT的成员,但这是无关紧要的实现细节)。

  4. 是的,ab是对同一个实例的两次引用。

  5. 因为ToMyString方法是静态的。实例的实际类型对静态方法无关紧要。

答案 2 :(得分:3)

  

(1)会员2的地址是什么?这可能是“访问冲突”吗?

是。 AV是可能的。在你的情况下,你有运气:)

  

好的,ToString()方法指向同一个地址

是的,因为VTable在创建时担心。

  

(3)为什么a和b具有相同的ClassName?

与(2)中的答案相同。

  

(4)好的,a和b是两个不同的变量

不是真的。你从堆栈打印地址:)

  

(5)如果b是TClassA,为什么可以使用“MyToString”方法?

b是TClassB,但错误指向TClassA实例。

您应该使用作为运算符进行此类强制转换。在这种情况下,它会失败。

相关问题