关于Delphi的构造函数为虚函数问题 ( 积分: 200 )

  • 主题发起人 主题发起人 SS2000
  • 开始时间 开始时间
S

SS2000

Unregistered / Unconfirmed
GUEST, unregistred user!
大家知道,C++不允许构造函数为虚函数,可是Delphi允许,开始没在意,现在
觉得不对劲,到底Delphi的构造函数声明为虚函数有什么用呢?
还有,对一个变量调用Create有什么用?
比如
TMyClass = class
constractor Create;
end;

MyClassA: TMyClass;
MyClassB: TMyClass;

MyClassA := TMyClass.Create;
MyClassB := MyClassA.Create
//这句话发生了什么事?
 
大家知道,C++不允许构造函数为虚函数,可是Delphi允许,开始没在意,现在
觉得不对劲,到底Delphi的构造函数声明为虚函数有什么用呢?
还有,对一个变量调用Create有什么用?
比如
TMyClass = class
constractor Create;
end;

MyClassA: TMyClass;
MyClassB: TMyClass;

MyClassA := TMyClass.Create;
MyClassB := MyClassA.Create
//这句话发生了什么事?
 
经过我的测试,这样的写法MyClassA和MyClassB指向同一个变量
type
TMyClass =class
public
InfoStr:string;
constructor Create;
end;
constructor TMyClass.Create;
begin
InfoStr:=IntToStr(GetTickCount);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyA,MyB:TMyClass;
begin
MyB:=TMyClass.Create;
sleep(100);
MyA:=MyB.Create;
ShowMessage(MyB.InfoStr+'-'+MYA.InfoStr);
ShowMessage(IntToStr(Integer(Pointer(MyB)))+'-'+IntToStr(Integer(Pointer(MyA))));
end;
如果换成如下写法,则两个是不同的变量了,具体细节可以参看李纬的深入VCL的书,上面好像有吧,忘了:)。TApplication.CreateForm也是采用类似的写法
MyB:=TMyClass.Create;
sleep(100);
MyA:=TMyClass.NewInstance as TMyClass;
MyA.Create;
ShowMessage(MyB.InfoStr+'-'+MYA.InfoStr);
ShowMessage(IntToStr(Integer(Pointer(MyB)))+'-'+IntToStr(Integer(Pointer(MyA))));
 
占个座儿,听课[8D]
 
virtual->就能override->实现多态.
MyClassB := MyClassA.Create
//这句话发生了什么事?
实现了对象的克隆.
 
我不太懂冒个傻气
MyClassA := TMyClass.Create;
类是用堆来实现的,
MyClassA := TMyClass.Create;
说是声称一个TMyClass类的实例,增加一个指向该堆的计数器
MyClassB := MyClassA.Create;
我八成是在这个堆上有增加一个计数器,不同的是,他的释放跟MyClassA有关系
 
C++没有 class reference, Delphi里有。通过 class reference 调用构造函数
可以实现构造多态。
TControlClass = class of TControl;
function CreateMyControl(aControlClass:TControlClass):TControl;
begin
Result := aControlClass.Create(nil);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i:Integer;
aControlClass:TControlClass;
begin
for i :=0 to Self.ControlCount - 1 do
CreateMyControl(TControlClass(Self.Controls.ClassType));

end;
 
yf_zq:
>>virtual->就能override->实现多态.
对于普通函数是这样的,可是对于构造函数,如何实现?能举个例?
要是那样为什么C++不能支持构造函数为虚函数?
 
>>MyClassA := TMyClass.Create;
>>MyClassB := MyClassA.Create
//这句话发生了什么事?
各位可以打开CPU调试窗口看看生成的汇编代码就知道了:
即使是一个空的构造函数,Delphi生成的汇编代码也有调用_ClassCreate这个方法,在_ClassCreate中调用TObject的NewInstance方法创建一个新的实例;
因为MyClassA是一个已经存在的实例所以不再调用NewInstance方法了,这是和TMyClass.Create执行时唯一不同的地方
 
C++的构造函数没有必要为虚,具体原因Xeen已经讲的很清楚了
 
我把测试结果给大家看看证明结果如下
MyClassA := TMyClass.Create;
创造一个tmyclass的实例,让MyClassA指向它
MyClassB := MyClassA.Create
//这句话发生了什么事?
把MyClassA的引用给MyClassB,并且给这个指针一个空间
这时他们都指向一个实力那就tmyclass
他们做的修改都是对一个堆进行修改
如果MyClassA.free,那么这个实例就释放了,另个类就指向一个无效的内存空间了,呵呵,起码结果是这样
 
恩,怎么说呢classcreate
只要你用到构造函数都会调用他,用到继承,或者虚函数只是调用create里的代码
可是classcreate在调用代码之前会先执行,也许是编译器做的手脚吧
 
MyClassB := MyClassA.Create
//这句话发生了什么事?
------------------------------------------------------
这种语句是没有意义的,很肯能引起错误,等于对于一个对象调用了两次
构造函数,所以在Object Pascal里不要用类似的语句 。
 
肯定的说,
MyClassB := MyClassA.Create

MyClassB := TMyClass.Create

执行结果是不同的,到底不同在什么地方?
比如说,通常Create函数里会有初始化,那么在执行
MyClassB := MyClassA.Create

除了MyClassB指向MyClassA外,MyClassA的内容是否又被初始化一次?
确实象xeen所说,这种语句是没有意义的,很肯能引起错误,我就是因为
不小心造成了错误,才有此疑惑。
在此主要是做纯技术讨论,是想深入研究一下Delphi,声明一下
MyClassB := MyClassA.Create
这种语句确实不该用,起码我现在从未用过。

感谢xeen对多态构造的讲解,使我搞清楚了Delphi确实是多态构造,而且在
大量使用。当初学习C++时,看到专门论述构造函数不能为虚函数,并且因此
还有专门的技术实现虚拟构造。呵呵,没想到Delphi在这个问题上比C++强。
 
构造函数的功能分类为两部分:
1.分配内存空间
2.初始化各字段
C++设计的一个重要思想就是用户不会为不需要的功能付出代价.因此C++的运行效率较好,但是开发效率并不是最好的。
基本上说不管是C++/Delphi,他们的构造函数中用户可以定制的部分仅仅是初始化字段。而分配内存空间的功能C++由new操作符,Delphi由NewInstance实现。所以他们没有本质的区别。
在C++当中,很容易实现Delphi中的这种效果
只需要定义若干个初始化函数,接受不同参数,然后调用基本的构造函数,然后再调用对应的初始化函数即可。而这些初始化函数是可以为虚函数的

所以大家所讨论的概念基本上其实可以称作 初始化函数 的问题
 
>>在C++当中,很容易实现Delphi中的这种效果
>>只需要定义若干个初始化函数,接受不同参数,然后调用基本的构造函数,然后再调用对应的初始化函数即可。而这些初始化函数是可以为虚函数的
呵呵,这种方法和所讨论的虚构造是两回事,如果用zjan521所说的方法,子类都不能在构造函数里做任何事情,不能有自己的构造函数,只能重载基类中构造函数调用的虚函数,这中方法和这里的虚构造函数已经不是一回事了。
C++中可以有方法是Virtualizing constructors,可以达到Delphi的虚构造函数的效果,但是不是很容易看懂的,也完全不是zjan521上面所说的方法
 
虚构造函数是语言本事支持的,C++不支持这种语法,用自己的方法实现语言(编译器)不支持的功能,是很难很麻烦的。
 
确实,这是一个有趣的问题。
我们看看Delphi在Create一个对象之前做了什么。
在执行创建函数的时候,我看看它的汇编码。
mov eax,[xxxxx] //第一个参数,就是VMT指针
test dl,dl //比较第二个参数,如果是True的话就跳到add esp,-$10指令处
jz +$08 //为false(0)的话跳过下面两行代码,其实什么都没有做,具体还可以看后面的相关代码
add esp,-$10 //栈中分配16字节,主要放置异常处理的一些处理信息,后面的代码中还要用到(也可以忽略这部分)
call @ClassCreate//关键部分,调用_ClassCreate方法
test dl,dl
......
下面看看关键的代码:
function _ClassCreate(AClass: TClass
Alloc: Boolean): TObject;
asm
{ -> EAX = pointer to VMT }
{ <- EAX = pointer to instance }
PUSH EDX
PUSH ECX
PUSH EBX
TEST DL,DL //如果为false的话调用下面的@@noAlloc不分配内存
JL @@noAlloc
CALL dword ptr [EAX].vmtNewInstance //这里是关键,下面会分析!调用NewInstance方法(这个方法就是平时看到的虚方法NewInstance 注意它是类方法)
{下面是一些其他的处理,你完全可以忽略不管的!}
@@noAlloc:
{$IFNDEF PC_MAPPED_EXCEPTIONS}
XOR EDX,EDX
LEA ECX,[ESP+16]
MOV EBX,FS:[EDX]
MOV [ECX].TExcFrame.next,EBX //这里就用到了刚才说到的栈中分配的内存
MOV [ECX].TExcFrame.hEBP,EBP
MOV [ECX].TExcFrame.desc,offset @desc
MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }
MOV FS:[EDX],ECX
{$ENDIF}
POP EBX
POP ECX
POP EDX
RET

{$IFNDEF PC_MAPPED_EXCEPTIONS}
@desc:
JMP _HandleAnyException

{ destroy the object }

MOV EAX,[ESP+8+9*4]
MOV EAX,[EAX].TExcFrame.ConstructedObject
TEST EAX,EAX
JE @@skip
MOV ECX,[EAX]
MOV DL,$81
PUSH EAX
CALL dword ptr [ECX].vmtDestroy
POP EAX
CALL _ClassDestroy
@@skip:
{ reraise the exception }
CALL _RaiseAgain
{$ENDIF}
end;
//如果真的要分配的话,下面这个函数会被调用!
class function TObject.NewInstance: TObject;
begin
Result := InitInstance(_GetMem(InstanceSize));
//很简单啊,就是根据对象大小(InstanceSize)调用_GetMem分配内存!注意这里我们还没有到相应的Create方法哟,内存中对象框架已经搭好了!
end;
现在可以简单总结一下:
在delphi中,真正创建对象的方法是_ClassCreate,它会调用虚方法NewInstance方法,这个方法默认会分配内存(其实你可以不让它分配的,有兴趣的可以试试),由于有一个类后,编译器就知道类对象的大小(InstanceSize)并可以在编译的时候确定VMT的地址,所以利用虚方法来创建对象是可以的。
当所有的工作都做好了,它才会调用我们的构造器函数(例如:Create方法),到这里,这个函数好像不再具有它名字赋予的意义了,其实它的确可以改为任何名字,这个方法已经变成一个初始化的过程了(因为内存构造在这个函数之前就完成了,这里只是在这片分配好的内存中放入适当的数据而已)。那什么时候编译器会调用到_ClassCreate方法呢??其实这里有一个玄机:就是Constructor关键字了,你可以认为它是一个编译器关键字,只要编译器看到你调用的方法是一个Constructor类型的,它就会在这个函数之前插入一些代码(当然就是调用_ClassCreate之类的了),这就是所谓的“编译器魔法”,所以你可以定义类似下面的函数:
Constructor TMyClass.Delete;
虽然名字是删除的意思,但你如果这样调用:
a:TMyClass;
a:=TMyClass.Delete;
a变量其实已经指向了一个存在的实例了,编译器会在delete函数前分配好内存,然后再调用Delete方法初始化,这就是Constructor的作用了,^_^
至于各种编译器使用不同方式的构造方法其实不必关心,因为各种编译器工作机制不同,其中掺杂了编译器和语言创造人自己的喜好。
说了这么多,不知道说清楚了没有,呵呵!
 
补充一点:
如果你的代码是使用TClass作为参数的话,delphi会插入_ClassCreate,例如:
var a:TMyClass;
a:=TMyClass.Create;// 这里的参数为TMyClass,其实就是VMT地址,这里会分配内存,分解一下(示意性代码):
a:=NewInstance(TMyClass.InstanceSize)
//分配内存
a.Create
//初始化a内存
如果你的代码不是使用TClass作为参数,那么就不会分配内存,前面的代码也讲到过,就是布尔值为false时候再调用_ClassCreate,那么_ClassCreate不分配内存,它其实直接调用你呼叫的方法,例如:
a.Create
//再次初始化内存了,因为它并没有NewInstance过程的调用!
补充完成了^_^
 
后退
顶部