bject Pascal 中类型的一些注意
---------------------------------------------------------
原创文章,如转载,请注明出处
---------------------------------------------------------
以下内容为备忘,你可能早已知道或者早已注意。
1, 区分类的forward声明和继承自TObject的子类
例如:
type
TFirst = class;
TSecond = class
end;
TThird = class(TObject); //同样是一个完整的类型定义,定义了类型TThird,
2, constructor 与 destructor
我谈两点:用对象名和类名调用create的不同、构造和析构的虚与实
首先需要说的是,对象被动态分配内存块,内存块结构由类型决定,同时类型也决定了对该类型的“合法”操作!
一个对象的构造函数用来得到这个内存块。
<1>, 用对象名和类名调用create的不同
构造函数是一个类方法,通常应该由类来调用,如下:
AMan := TMan.Create;
这条语句,会在堆中分配内存块(当然,它不仅仅干这些,实际上它还会把类型中所有的有序类型字段置0,
置所有字符串为空,置所有指针类型为nil,所有variant为Unassigned;实际上,构造函数只是把内存块进行
了清零,内存块清零意味着对所有的数据成员清零),并把这个内存块的首地址给AMan。
但如果你用下面的方式调用,
//对象名.Create
AMan2 := AMan.Create; //假设AMan已经被构造,
如未被构造,会产生运行时异常,
这实际上相当于调用一个普通的方法,不会分配内存块,当然也不会自动初始化。这和你调用下面的方法
类似,
当然,构造函数毕竟是函数,它会有一个返回值,这个返回值就是对象的地址,所以AMan2和AMan指向同
一地址。
Note:不要试图通过用对象名调用create方法来实现对象初始化!!因为把构造函数当做普通的方法来调用
并不会实现自动初始化,所以用对象名来调用create方法就显得很鸡肋了,当然,某些场合,这是一种技巧。
//对象名.Create
<2>, 构造和析构的虚与实
构造函数和析构函数应该是虚的还是实的?这很疑惑!
看代码:
---------------------------------
type
TMan = class(TObject)
public
end;
TChinese = class(TMan)
public
end;
TBeijing = class(TChinese)
public
end;
....
begin
end;
如果构造都是“实”的,无论如何,对于上面的用法,对象都可以被正确构造。但是对于析构,上述代码有
问题!如果加入测试代码,你会发现所有的析构方法根本不会被执行。知道,Free方法继承自TObject,其
源码如下:
procedure TObject.Free;
begin
end;
constructor TObject.Create;
begin
end;
destructor TObject.Destroy;
begin
end;
Note:通常,self内置参数是指向对象本身,而在类方法中self是指向类本身。
很显然,在前面的代码中, AMan, AMan2, AMan3不是nil,free方法执行了。但是很遗憾的是,它所执行的
Destroy其实是基类TObject.Destroy;
而TObject.Destroy什么也不做。要理解这一点,需要理解类的内存组织
结构,理解“隐藏”的概念。所谓隐藏,指的是基类和子类有同名的方法(不考虑虚、动态的情况), 子类的该名
方法隐藏基类的方法名。运行时,到底调用哪一个方法取决于调用者类型。如下代码:
type
TCountry = class
public
end;
TChina = class(TCountry)
public
end;
....
var
begin
end;
对于隐藏的情况,具体调用哪一个方法,取决于调用者本身的类型。
很显然,一个实的析构方法有问题!可能会造成内存泄露。那么它们是虚的好了。问题来了,
type
TMan = class(TObject)
public
end;
TChinese = class(TMan)
public
end;
TBeijing = class(TChinese)
public
end;
语法错误!上述的代码编译器会提示语法错误。ok,正确的应该是下面
TMan = class(TObject)
public
end;
TChinese = class(TMan)
public
end;
TBeijing = class(TChinese)
public
end;
疑问来了,不是只有virtual 和 dynamic 才能被覆盖么?在Delphi中为了保证对象被完全的析构,
所有的Destroy方法“天生”为虚方法,可以在任何地方被覆盖!虽然这破坏了语言语法上的一致性,
但这保证一个对象不管从哪里(继承)来,都只有唯一的析构方法。但是,编译器不会强制你使用
override关键字,所以,你可以像下面这样来定义析构。
TChinaese = class(TMan)
public
end;
这可能会带来问题,虽然不总是会带来问题。因为在TChinese类中存在多个Destroy方法,分别是:
TObject.Destroy;
TMan.Destroy;
TChinese.Destroy;
所以,结论是,无论在什么地方定义Destroy,覆盖它!!以确保整个类谱中只有一个Destroy;
TChinese = class(TMan)
public
//这块代码有问题,constructor
Create;是静态的,不能override;constructor Create; virtual;
end;
TBeijing = class(TChinese)
public
end;
begin
end;
Ok,可以,工作正常。原因在于我们对对象的使用方法。我们总是用"确定"的类名来构建的,这总可以保证
我们调用正确的方法。但如果我们使用类引用的话,情况不同了,如下:
type
var
begin
end;
和前面讨论析构的情况类似,这取决于方法的内存布局,注意我在最初提到过类型决定布局。方法的内存布局
取决于它是virtual, override, overload, dynamic.当TMan.create 是virtual,
并且,TChinese.create是
override时,AObj2 := AManClass.Create 调用的是 TChinese.Create;
如果不是,则是TMan.Create;
上面的解释仍然是疑惑的,问题的关键在于什么是“类引用”!从语义上说,类引用是这样一个类型:它代表了
一系列相关的存在继承关系的类型,一个类引用变量代表一系列类型。区别于一般的变量,一般的变量表示的
是一系列实体,如:var count: integer; 意思是你定义了一个实例count,它的类型是integer; count
对应
于堆或栈中的一个内存块,count是数据。这是一般情况下我们定义一个变量所隐含的意思。但类引用变量稍有
不同,如上,AManClass变量,它代表的是一系列和TMan兼容的“类型”,它的类型是TManClass,它没有内存块
(你没办法说,一个类型的类型对应什么内存块),实际上,可以认为类引用类型指向了一个“代码块”。类引用
变量的值是“类”!这很重要(稍后会进一步解释)!Delphi对类引用的实现实际上就是一个32位的指针(一个普通
指针),它指向了类的虚方法表(VMT).
类引用的值是“类”,这决定了任何时候使用类引用变量只能调用类方法!一个构造函数是类方法,所以使用类
引用调用Create方法是合理的,但是你不可以使用类引用调用非类方法,比如Destroy方法,实际上在类引用中
你也找不到Free方法。
需要提及的是,构造函数虽然很接近于类方法,甚至于你也可以使用类方法来模拟构造函数:
TMan = class
public
end;
class function TMan.Create: TMan; //模拟构造函数
begin
end;
但构造函数做的更多,除了前面提到的自动初始化外,构造函数还会在构造对象时将对象的VMT指针指向类的VMT。
此外,构造函数中的self指的是对象本身,类方法中self指的是类本身。总之,我们可以认为构造函数是一个
特殊的类函数。
Ok,到此,我们确信:类引用可以调用类方法,构造函数可以使用类引用来调用。
问题产生:由于类引用表示的是“一系列”类型,那么调用构造方法的时候,到底调用的是那一个构造方法呢?
对于TManClass类型变量而言,它只能调用的一定是TMan类的create方法,但如果TMan类的子类覆盖了TMan的create
方法,调用TMan类的create方法实际上就是调用子类的create方法。这里的关键是理解“覆盖”的概念。这实际上意味
着对使用类引用调用构造方法,虚的构造方法和实的构造方法是不同的。对于前面的例子而言,
AManClass := TChinese;
AObj2 := AManClass.create; //调用了TChinese.Create;
AManClass := TBeijing;
AObj3 := AManClass.create; //调用了TBeijing.Create;
但如果构造不是覆盖虚函数,那么它们统统是调用TMan.Create;显然这不是我们期望的。
Note:每个类有一个虚方法表(VMT),而不是每个对象有一个!
结论:构造函数是可以被类本身、类引用、对象调用,在这三种情况下构造函数的表现是不同的,在使用类引用的情况
下虚的构造通常是必要的。
那么,构造函数总是虚的是否更合理呢?我个人倾向于总是虚的更合理,因为这样可以保证一个对象总是被正确的构造。
实际上,VCL中组件继承的大多Create都被声明成虚方法了。但Delphi并未将TObject的构造实现为虚,也许是出于兼容的
考虑,我不太清楚。我听说,Java中的构造总是虚的。当然,总是虚的也有弊端,例如:你不可以通过调用Create方法来
只初始化父类的成员,因为在类的继承体系中只有一个Create方法。但是静态的构造可以实现父类的成员初始化。私下里,
我猜想也许是因为构造函数承担了太多的义务,使得Delphi设计者没有像析构那样以总是虚来处理。
另,在C++中有一条法则,不要在构造中使用虚!但在Delphi中可以随便,根本原因在于RTTI(运行时类型信息),Delphi在
运行时保存了完整的类型信息。这是C++和Delphi的重要不同之处。
总结:对析构函数而言,无论是否声明为virtual,它总是虚的,你总需要使用override;对构造函数而言,虚和实取决
于的使用方法,但一般而言使用虚更安全,在使用类引用的情况下一定要保证子类覆盖基类的构造方法。