一个小问题: 类的地址。。。?(200分)

X

xbl

Unregistered / Unconfirmed
GUEST, unregistred user!
请先看下面的测试:

Type
TExample = class
private
FMessage: string;
public
constructor Create;
procedure ShowIt;
end;

constructor TExample.Create;
begin
inherited;
FMessage := 'Hello';
end;

procedure TExample.ShowIt;
begin
ShowMessage('I''m TExample');
end;

测试 1:
procedure TForm1.Button7Click(Sender: TObject);
var
obj: TExample;
begin
obj.ShowIt
// I'm TExample
end;

问题:
1. 对象还没有创建,为什么不报错?
2. 如果 ShowIt 方法是 ShowMessage(FMessage),则报错?

//测试 2:
procedure TForm1.Button1Click(Sender: TObject);
var
obj: TExample;
P1, P2, P3, P4: Pointer;
begin
obj := TExample.Create;
P1 := Pointer(Integer(TExample) - 76);
TExample(P1).ShowIt
//I'm Example

P2 := TExample;
TExample(@P2).ShowIt
//I'm Example

P3 := Pointer(Integer(obj) - 76);
TExample(P3).ShowIt
//I'm Example

P4 := Pointer(Integer(TExample) + TObject.InstanceSize);
TExample(P4).ShowIt
//I'm Example

ShowMessage(Format('%d', [Integer(P1) - 76]))
//4570620
ShowMessage(Format('%d', [Integer(@P2)]))
//1243000
ShowMessage(Format('%d', [Integer(P3)]))
//13395420
ShowMessage(Format('%d', [Integer(P4)]))
//450776

obj.Free;
end;

问题:1. 为什么 P1,P2,P3,P4 所指向的地址不同,但是却调用了同一个函数 ShowIt()?
多运行几次,P1,P2,P3,P4 所指的地址也会发生变化?
2. 这 4 个指针所指向的地址是什么意思?
3. 类的实例的地址,类的地址(TExample),类的地址的地址(@TExample)
他们3者之间有什么联系?

//测试 3:

1) 将 TExample.ShowIt 改为:

procedure TExample.ShowIt;
begin
ShowMessage(FMessage);
end;

2) 测试:

procedure TForm1.Button2Click(Sender: TObject);
var
obj: TExample;
P1, P2, P3, P4: Pointer;
begin
obj := TExample.Create;
P1 := Pointer(Integer(TExample) - 76);
TExample(P1).ShowIt
//显示的是一些空格''

P2 := TExample;
TExample(@P2).ShowIt
//??

P3 := Pointer(Integer(obj) - 76);
TExample(P3).ShowIt
//显示的是一些空格''

P4 := Pointer(Integer(TExample) + TObject.InstanceSize);
TExample(P4).ShowIt
//出错: Access violation at address 0040453C in
// module 'Project2.exe'. Read of address 107FFFFC.

ShowMessage(Format('%d', [Integer(P1) - 76]))
//4570620
ShowMessage(Format('%d', [Integer(@P2)]))
//1243000
ShowMessage(Format('%d', [Integer(P3)]))
//13395420
ShowMessage(Format('%d', [Integer(P4)]))
//450776

obj.Free;
end;

问题:同测试 2
 
这个解释起来太长了。不好意思,太不想打字了。这个理解编译器对Class的实现就好理解了。
类的一个方法在实现的时候与普通函数或过程一样,不过编译器加上一个类指针的参数。
所以procedure TExample.ShowIt就有一点像一个procedure ShowIt(Self: TExample);
在obj.ShowIt中,obj没有创建时,相当于Self为空,你在方法中没有使用类的成员
就不会用到Self.所以就不会报错,而你使用的类的成员时,要使用Self这个指针来确定成员
的地址,为Self空,所以就会访问了非法的地址。
2、3的问题你可以与Delphi的Help中Class内存分配相结合起来解释。
 
jiangxiancheng:
那你明天再告诉我好不好?
 
我看看在告诉你
 
好啊,谢谢你 !
 
推荐你看 DFW 中的这篇:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1047021
非常非常伟大的讨论。
 
小雨哥:
谢谢你,好长啊,我先看看!
 
aimingoo 的这篇,我帮他 up 上来,你也可以看看内部实质:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1642063
 
有些牵涉到了类的实质,有些牵涉到了类的实现机制。比如不建立类可以调用函数,确实
是因为实现造成的。因为代码只需要在内存中存在一份,而建立的对象则会建立自己的类
实例的数据。类里面的函数存放形式是一张表,也就是一堆指针,当然都指向一个地方。
每次create都会存在一个对象,而一个变量对其的引用只是为了用他而存在的一个指针
而已,变量不是对象本身,所以哪怕你再写p1=nil也不会使得对象消失(com除外)。
只是你失去了对对象的引用,也就是永远的失去了对这个对象的控制权利。p1更形象点
就是你控制这个对象的权仗啊。
此外还有一个烦恼的地方,就是
obj:=TExample.create

obj是对象,但TExample本身也是一个类指针。这个时候TExample不仅指类这个虚名,而且
指对这个类的实现的引用。否则定义了TExample哪里去找实现方法呢?
TClass = class of TObject;
就是说可以这么用
c:Tclass;
c:=TExample;
obj:=c.create;
这就是类的指针含义。尽量不要用(@TExample)这种晦涩的语法,不建议这样。这样只有
进入无尽的乱麻之中。
 
测试 1:
類由數據和方法構成
類在內存中會實現所有的類方法,由類的方法表來引用
但類不會存放數據,只是定義了數據在內村中的排列方式和數據類型
對象 Create 時,只是來類的定義來開劈內存存放數據,而不會實現
方法代碼.其實對象在內存中就是一個record,因為方法的定義是不會在
運行期改變的,無需重複實現,所以它只需要通過類指針來引用方法就可以了

總結;類的方法是可以直接調用的,就像是直接調用一個全局函數一樣
但在函數中調用中不能由有類數據,因為類沒有數據
C++中的類靜態函數就是用這個道理實現的,你沒看它規定不能調用類數據

测试 2:
這個問題是你自己搞混亂了,首先要理解類型轉換的道理,由於對象只是一
堆數據,而類型轉換是將你指定的一個地址開始的 InstanceSize 大小的一段
內存強行按類定義的數據排列方式和類型進行使用. 而類方法是由類指針來調用
的,和對象中的數據沒有關係.

用你例子:
TExample(P1).ShowIt;
是將 4570620 開始的 InstanceSize 大小一段內存,作為對象數據
但 TExample(P1).ShowIt;
不是 p1.Showit
而是 TExample.ShowIt;

同樣 p1,p2,p3,p4 都是調用了 TExample.ShowIt,
由於 ShowIt沒用用到類數據,所以不會報錯
我就是 p5:=1000000;
再TExample(P5).ShowIt
也是一樣

總結;緊緊記住,類或對象的方法都是通過類指針來調用的,和對象沒有關係

测试 3:
和 测试 2的解悉一樣,由於你用了對像數據,那就會出錯了







 
我看了小雨哥推荐的帖子,的确非常精彩,回去再好好想想。

soul:
“因为代码只需要在内存中存在一份,而建立的对象则会建立自己的类实例的数据。
类里面的函数存放形式是一张表,也就是一堆指针,当然都指向一个地方。”
昨天晚上到处查资料,跟您的观点极为神似。(@TExample),还是想
不明白下面这样的用法:
P2 := TExample;
TExample(@P2).ShowIt
//I'm Example

还有,比如这样来测试:
procedure TForm1.Button9Click(Sender: TObject);
var
obj: TExample;
I, J: Integer;
begin
obj := TExample.Create;
I := obj.InstanceSize;
J := TExample.InstanceSize;
ShowMessage(IntToStr(I))
//8
ShowMessage(IntToStr(J))
//8
obj.Free;
end;

//VMT指针 4 + 4 = 8
I := SizeOf(ptrVMT) + SizeOf(string)

很显然,对象和类的的指针,都是是指向vmt和数据段的堆内存上,
你说的“类里面的函数存放形式是一张表”,那么类TExample是怎么找到函数的入口,
它是通过什么来调用的,是不是有一个什么指针指向这张表?
我猜想可能是通过 VMT,我打开 System.pas来看:
vmtMethodTable -52: pointer to method definition table
从名称来看,好象是这样,不知道实际上是不是?
还有偏移 -76 的位置 vmtSelfPtr,帮助里面说 pointer to virtual method table
意思好象是说指向虚拟方法表,但是你看我上面测试2中的测试:
P3 := Pointer(Integer(obj) - 76);
TExample(P3).ShowIt
//I'm Example
可是,ShowIt 不是虚方法啊???

唉,越看越是一头雾水, 您能不能再解释一下?
 
Delphi的方法是一个结构,查以下Delphi的帮助TMethod
type
TMethod = record
Code: Pointer;//也就是方法的函数指针
Data: Pointer;//也就是方法的调用者的地址,就是我们在方法中使用的Self对象
end;
对象的方法是共享同一空间的(这个空间就在类的空间内而不是对象的空间内),而对象的成员变量才有自己的空间
当你调用方法时也就是相当于调用了方法段Code所指的函数地址,因为你在这个例子中的方法中没有使用
对象的成员变量有关的东西,那么其实只是简单的执行了一个函数。当你的例子调用与成员变量有关
的东西时,就会找不到成员的。
 
lqy:
你说的,有个疑问:通過類指針來引用方法,是怎么引用的,
方法的入口是怎么得到的?
在我的测试中,下面这种情况如何解释?
P2 := TExample;
TExample(@P2).ShowIt
//I'm Example

突然想起一篇文章中的一句话:“类其实就是 VMT”,如果是这样,那么,可以
想象,每个对象也是一个指针,前4位指向 VMT,后面的是数据段,那么调用类的方法,
不管是虚方法还是普通方法,都是通过 VMT 来调用,只是虚方法在运行期间才确定地址,
而普通的类方法在编译期就得到了地址,ptrVMT - 76 应该就是指向虚拟方法表,而
ptrVMT - 52 应该就是指向类的非虚方法了,不知道我理解得对不对?
对象的指针
 
wr960204:
你说得不错,几天前我也看见书上这么说,可是没怎么留意,delphi 的 help 里面说
得好象比较简单,关于 TMethod 的用法,你举个具体的可以调试的例子如何?
 
procedure TExample.ShowIt;
begin
ShowMessage(ClassName);
end;
你在调用就会发现竟然是当前的窗体类名。
为何如此,因为Pascal编译器会优化他,本来应该是随机数的TMethod 的数据段竟然指向了当前的窗体。
但是你要是把编译选项的优化去掉的话就会因为找不到实例而出错的。也就是TMethod的数据段
 
wr960204:
好象不是吧,我测试,
procedure TExample.ShowIt;
begin
ShowMessage(ClassName);
end;

结果是 TExample,我用的是 Delphi6.

 
我用的D7。
 

xbl你好像進了死胡同了,可能是我文筆不好,沒說明白
沒錯,對象的開始4位是指向類 VMT
但強制類型轉換,就是強制地改變對象的VMT(當然沒由實質地改內存)

直白一點的說
當Delpih 解析
p5:=1000000;
TExample(P5).ShowIt
這兩句代碼時,會這樣解析;
CUP你不要理會 1000000-1000003 是指向那個 VMT
而是按 TExample的VMT來執行,數據在 1000004 開始的
TExample.InstanceSize大小的內存中

所以不管 p1,p2,p3,p4 是甚麼對 ShowIT都沒有影響
 
lqy:
如果 ShowIt() 没有访问数据段, 不管 p1,p2,p3,p4 是甚麼對 ShowIT都沒有影響,
我测试过了,事实的确是这样!

我查了一下资料,我有下面的想法,你看看我理解得是否正确:

一个类一创建,就实现了它所有的方法,并确定了所有方法的地址,如果是普通方法,
则保存于一张全局表T1中,如果是虚方法,则保存在表 T2 中,T1, T2 应该就是
TMethod 中的 Code 域了,而 Data 域应该是在调用方法时,传递的隐藏参数 Self,
根据这个来找在方法中引用的对象中的数据,
因此,在调用一个类的方法时,如果不访问类的数据,那么,这个参数Self就没有什么
作用了,而类的方法则在编译期间就已经实现了的,所以,即使没有创建对象,
调用类方法也不出错,然后 T1,T2,均保存到类的VMT中,返回一个指向 VMT 的指
针ptrVMT (VMT = 类),则一个类在编译期间就完成了。
T1, T2 的指向如下:
T1:ptrVMT - 52
T2:ptrVMT - 76

当创建了一个具体对象时,如果有虚方法,则去填写 ptrVMT 中的 T2,也就是在运行期
决定虚方法的调用地址,所谓“滞后联编”。
当调用一个类的方法时,会传递一个隐含指针 Self,根据这个指针来找类的数据。

不知道我的理解对不对?
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
顶部