重名成员的访问
我们知道,在C++中,访问父类的同名的成员很容易,只要使用a.Parent::b这样的代码就可以了。例如
class TA
{
public
char *Text;
}
class TB:public TA
{
public
char *Text;
}
TB:b;
那么要访问父类的Text,可以这样即可:b.TA::Text,而在Delphi中,如何来访问父类的成员呢?答案是类型强制!
TA=class
public
Text:string;
end;
TB=class(TA)
public
Text:string;
end;
var
b:TB;
...
TA(b).Text:='Text TA'; ///访问父类的成员
完整的测试代码如下:
program Project1 ;
{$APPTYPE CONSOLE}
uses SysUtils ;
type
TA = class
public
FName : string ;
end ;
TB = Class ( TA )
public
FName : string ;
end ;
var
b : TB ;
begin
// Insert user code here
b := TB . Create ;
b . FName := 'abc' ;
TA ( B ). FName := 'def' ;
writeln ( 'TB.Name=' , b . FName , ' TB.TA.Name=' , TA ( b ). FName );
readln ;
end .
---------------------------------------------------------------------------------------------------------------------
类的继承---跨过中间类,继承祖先的方法
有3个类 A,B,C,继承关系 A-->B-->C,
A中有一个虚拟对象方法 Fun、B、C分别重载这个方法,
现在C要继承 A.Fun 而不是 B.Fun.
如何才能做到?
type
A = class
public
procedure Fun; virtual;
end;
B = class(A)
public
procedure Fun; override;
end;
C = class(B)
public
procedure Fun; override;
end;
implementation
procedure A.Fun;
begin
ShowMessage('A');
end;
procedure B.Fun;
begin
ShowMessage('B');
end;
procedure C.Fun;
begin
//在这里调用A中的Fun,怎么可以做到???
//这样试过:
//1 (Self as A).Fun 可以编译通过,但结果不对。
//2 A(Self).Fun; 可以编译通过,但结果不对。
end;
回复人: findcsdn(findcsdn) ( ) 信誉:106 2002-11-22 10:49:30 得分:12
//这样写就可以了。
procedure C.Fun;
var
tmp: pointer;
begin
tmp := A;
asm
mov eax, self
mov edx, [tmp]
call dword ptr[edx + VMTOFFSET a.fun]
end;
end;
回复人: xzgyb(老达摩) ( ) 信誉:110 2002-11-22 10:57:51 得分:0
louislingjjw(云 意) :
asm
call TA.Fun
end;
就是调用直接调用TA.Fun
直接写这条指令就会直接跑到TA.Fun处执行
如果
TA.Fun里有对成员的操作出错是因为这时EAX为零
TA.Fun对数据成员的操作是根据self的偏移量执行的,在这里这个self
也就是EAX
改成这样
procedure TC.Fun(s: string);
begin
asm
push eax
mov eax, self
call TA.Fun
pop eax
end;
end;
可以出来你要的那个效果,主要是存一下EAX值
刚学了点汇编,瞎写的,也不知对不对,有没有什么别的副作用
回复人: xzgyb(老达摩) ( ) 信誉:110 2002-11-22 15:14:59 得分:0
摩托兄:
呵呵,我只是瞎试的,不知道对不对
而且跟FindCsdn学了一招
VMTOFFSET和DMTINDEX指令
VMTOFFSET返回虚方法在虚表中的偏移量
DMTINDEX返回动态方法的Index值
而且帮助中的那个例子也挺有意思
program Project2;
type
TExample = class
procedure DynamicMethod; dynamic;
procedure VirtualMethod; virtual;
end;
procedure TExample.DynamicMethod;
begin
end;
procedure TExample.VirtualMethod;
begin
end;
procedure CallDynamicMethod(e: TExample);
asm
// Save ESI register
PUSH ESI
// Instance pointer needs to be in EAX
MOV EAX, e
// DMT entry index needs to be in (E)SI
MOV ESI, DMTINDEX TExample.DynamicMethod
// Now call the method
CALL System.@CallDynaInst
// Restore ESI register
POP ESI
end;
procedure CallVirtualMethod(e: TExample);
asm
// Instance pointer needs to be in EAX
MOV EAX, e
// Retrieve VMT table entry
MOV EDX, [EAX]
// Now call the method at offset VMTOFFSET
CALL DWORD PTR [EDX + VMTOFFSET TExample.VirtualMethod]
end;
var
e: TExample;
begin
e := TExample.Create;
try
CallDynamicMethod(e);
CallVirtualMethod(e);
finally
e.Free;
end;
end.
goodloop:
就是这样
如
TTest = class
private
FTest: Integer;
FTest1: Integer;
public
constructor Create;
procedure ShowTest;
end;
...
implementation
constructor TTest.Create;
begin
FTest := 20;
FTest1 := 30;
end;
procedure TTest.ShowTest;
begin
ShowMessage(IntToStr(FTest));
ShowMessage(IntToStr(FTest1));
end;
procedure TMainForm.Button2Click(Sender: TObject);
var
a: TTest;
begin
a := TTest.Create;
a.ShowTest;
a.Free;
end;
对于 a := TTest.Create;汇编如下
mov dl, $01
mov eax, [$0046afbc]
call TTest.Create
mov ebx, eax //ebx保存了实例的引用值
a.ShowTest 如下
mov eax, ebx
call TTest.ShowTest
因为object pascal函数的调用默认是Fastcall 的,也就是参数先传到EAX,EDX,ECX,剩下的在压栈,而在类的函数里用的self值其实是隐藏的传过来
的第一个参数,也就是EAX里的值
在看一下TTest.ShowTest
...
mov ebx,eax
...
//对应于ShowMessage(IntToStr(FTest))
lea eax, [ebp-$04] //这是一个临时的字符串变量
mov edx, [ebx+$04]
call IntToStr
//[ebx+$04]即为FTest的值,[ebx]为vptr, [ebx+$08]为FTest1, 所以是访问类里的数据成员是通过相对于self的值的偏移量来访问
这些可以打开cpu窗口来察看
回复人: louislingjjw(云 意) ( ) 信誉:100 2002-11-28 9:06:01 得分:0
谢谢各位的热心帮助!
这个问题几天前已经解决了,方法有四种,其中有些原理是一样的。
方法一:(指针)
1)
procedure C.Fun;
var
P: Pointer;
begin
P := Pointer(ClassParent.ClassParent);
//获得祖先类虚拟方法表入口地址;
if Integer(P) <> 0 then
A(Integer(P) - 76).Fun;
//取得self指针,强制类型转换.
end;
2)
只要知道Self指针就指向了虚拟方法表的入口, Self指针负的偏移量是
一些类方法和RTTI信息的地址就行了
procedure TC.fun(X: string; Y: Integer; Z: TDateTime);
var
p: pointer;
begin
p := Pointer(classparent.ClassParent);
//获得祖父类虚拟方法表入口地址;
if integer(p) <> 0 then
ta(@p).Fun(x,y,z);
//取得self指针,强制类型转换.
end;
方法二:(汇编)
1) 没有参数时
procedure TC.Fun;
begin
asm
call TA.Fun; //直接跑到TA.Fun处执行
end;
end;
2) 有参数时,主要是存一下 EAX 值
如果 TA.Fun 里有对成员的操作出错是因为这时 EAX 为零
TA.Fun对数据成员的操作是根据 self 的偏移量执行的,
在这里这个 self 也就是EAX改成这样
procedure TC.Fun(s: string);
begin
asm
push eax
mov eax, self
call TA.Fun
pop eax
end;
end;
3)
procedure C.Fun;
var
tmp: pointer;
begin
tmp := A;
asm
mov eax, self
mov edx, [tmp]
call dword ptr[edx + VMTOFFSET a.fun]
end;
end;
说明:
寄存器的用法:
对于一般的 procedure ,function:
在入口:
eax: 保存 procedure or function 的第一个参数值(如果存在的话)
edx: 保存 procedure or function 的第二个参数值
ecx: 保存 procedure or function 的第三个参数值
ebx: 保存 procedure or function 的地址
在出口:
eax: 对于function是保存结果;对于procedure一般是保存相关的自定义错误代码
ebx: 仍是保存procedure or function的地址
对于对象的方法:
在入口:
eax 保存parent对象的地址
ebx 保存 procedure or function 的地址
ecx 保存第二个参数值
edx 保存第一个参数值
在出口:
eax: 对于function是保存结果;对于procedure一般是保存相关的自定义错误代码
ebx: 仍是保存procedure or function的地址
方法三:(参考类)
1.
A,B两个类与C在不同的单元内,那么C中Fun不要override,因为A,B中的Fun是在Public或
Protected中,所以C的Fun中也可以用Inherited Fun,即在C中调用父类(即B)中的Fun。
如果要跨过B直接调用A中的Fun,我们一般定义一个参考类,如:
TARef=class(A)
public
procedure Fun;
end;
...
procedure TARef.Fun;
begin
Inherited Fun;
end;
procedure TC.Fun;
begin
TARef(Self).Fun
end;
2.
更多的情况,我们只是为了要在C中改变A中的私有变量,即改变不在同一单元中类的私有
变量。解决这个问题,我们也是定义一个参考类。假设A类如下:
A=class
private
X:Integer;
Y:string;
....
end;
参考类则:
TARef=class //这里老子不是A,而是A的老子
private
X:Integer;
Y:string;
...
end;
其实就是将A的定义搬过来,只是改了类名(注意只要私有域,不要私有方法)。
这样在C的Fun中(或其它需要的地方)可以:
TARef(Self).X:=123;
TARef(Self).Y:='123';
不过,这种方法有一个问题就是:一旦A的定义改了,我们的源程序也得改。这也是
每次Delphi升级时我们的程序必须检查的地方。
所谓定义参考类和指针操作private变量是一个原理。
定义的参考类其实就是为了确定你所关心的变量相对于实例入口的偏移。
和指针操作的唯一区别就是参考类方法是由编译器帮你决定访问的指针偏移量,
我的方法是由你自己计算这个指针偏移量而已。
比如要在TC中访问TA.FNum3的话:
TA = class(Tobject)
private
FNum1: string;
FNum2: Integer;
FNum3: Float;
FNum4: DateTime;
FNum5: string;
FNum6: string;
public
function temp1(X: string;): string; virtual;
procedure temp2(X: Integer; Y: TDateTime): string; virtual;
procedure fun(X: string; Y: Integer; Z: TDateTime);virtual;
end;
TB = class(TA)
private
FNote1: string;
FNote2: TDateTime;
FNote3: Integer;
protected
X1: Integer;
public
function temp3(S: string): string; virtual;
procedure temp4(I: Integer): string; virtual;
procedure fun(X: string; Y: Integer; Z: TDateTime);override;
end;
TC = class(TB)
private
FNote4: string;
FNote5: TDateTime;
FNote6: Integer;
protected
X2: string;
public
function temp5(S: string): string;
procedure temp6(S: string); string;
procedure fun(X: string; Y: Integer; Z: TDateTime);override;
end;
指针:
type
TRefRecord=record // TB里我们所要跳过的私有变量
FNum1: string;
FNum2: Integer;
FNum3: Float;
end;
PRefRecord=^TRefRecord;
var
pt: PRefRecord;
c: TC;
...
pt := Pointer(Integer(c)+TObject.InstanceSize);
pt^.FNum3 := 5.5;
参考类:
TRef = class
private
FNum1: string;
FNum2: Integer;
FNum3: Float;
end;
var
c: TC;
...
TRef(c).FNum3 := 5.5
这两种方法操作的都是TA.FNum3这个私有变量
同理: 要访问TB.FNote2的话
指针:
type
TRefRecord=record
FNote1: string;
FNote2: TDateTime;
end;
var
pt: ^TRefRecord;
c: TC;
pt := pointer(integer(c)+TA.InstanceSize);
pt^.FNote2 := now;
参考类:
TRef = class(TA)
private
FNote1: string;
FNote2: TDateTime;
end;
var
c: TC;
TRef(c).FNote2 := now;
方法四:(汇编+指针)
TCallFun = procedure (X: string; Y: Integer; Z: TDateTime) of object;
procedure TC.fun(X: string; Y: Integer; Z: TDateTime);
var
m: TMethod;
voffset: Integer;
begin
asm
mov voffset, VMTOFFSET TA.fun
end;
m.Code := Pointer(Pointer((Integer(Classparent.ClassParent) + voffset))^);
m.Data := self;
TCallFun(m)(X, Y, Z);
end;
注:修改父类的指针变量
//修改父类的私有变量
procedure TC.Fun(S: string);
var
P: PString;
begin
P := Pointer(Integer(Self) + TObject.InstanceSize); // p 现在指向TA.FNode
P^ := 'A' + S;
ShowMessage(P^);
end;
//说明
这个self指的是TC的实例所在的地址,在那里数据是这样排列的:
TC.VMT入口地址
TObject所有的Private,protected,public变量
TA所有private,protected,public变量
TB所有private,protected,public变量
TC所有private,protected,public变量
...(是否还有其他的就不知道了)
这样的话要获得TA某个private变量的地址只要跳过它前面的所有变量的偏移量即可。
Integer(self)-> 入口地址 + TObject.InstanceSize-->跳过所有TObject的变量和VMT
这时指针已经指向TA第一个private变量了。这里恰好正是我们关心的FNode
接下来对这个指针所在的数据进行操作所修改的就是TA的Private变量值了。