• Object Pascal 中类型的一些注意 转发 狼窝


    bject Pascal 中类型的一些注意

    ---------------------------------------------------------

    原创文章,如转载,请注明出处

    ---------------------------------------------------------

    以下内容为备忘,你可能早已知道或者早已注意。

    1, 区分类的forward声明和继承自TObject的子类
    例如:
    type
    TFirst = class;   //forward声明, 类TFirst的具体声明在该声明区域后部
    TSecond = class   //一个完整的类定义,定义了类型TSecond
    end;
    TThird = class(TObject); //同样是一个完整的类型定义,定义了类型TThird,
         //这个类不包含任何数据成员和方法

    2, constructor 与 destructor
    我谈两点:用对象名和类名调用create的不同、构造和析构的虚与实
    首先需要说的是,对象被动态分配内存块,内存块结构由类型决定,同时类型也决定了对该类型的“合法”操作!
    一个对象的构造函数用来得到这个内存块。
    <1>, 用对象名和类名调用create的不同
    构造函数是一个类方法,通常应该由类来调用,如下:
    AMan := TMan.Create;
    这条语句,会在堆中分配内存块(当然,它不仅仅干这些,实际上它还会把类型中所有的有序类型字段置0,
    置所有字符串为空,置所有指针类型为nil,所有variant为Unassigned;实际上,构造函数只是把内存块进行
    了清零,内存块清零意味着对所有的数据成员清零),并把这个内存块的首地址给AMan。
    但如果你用下面的方式调用,

    //对象名.Create
    AMan2 := AMan.Create; //假设AMan已经被构造, 如未被构造,会产生运行时异常,
             //本质上,对未构造的对象的操作是对非法内存的操作,结果不
             //可预知!
    实际上相当于调用一个普通的方法,不会分配内存块,当然也不会自动初始化。这和你调用下面的方法
    类似,
            AMan.DoSomething; //DoSomething 为类 TMan的普通方法
    当然,构造函数毕竟是函数,它会有一个返回值,这个返回值就是对象的地址,所以AMan2和AMan指向同
    一地址。
    Note:不要试图通过用对象名调用create方法来实现对象初始化!!因为把构造函数当做普通的方法来调用
    并不会实现自动初始化,所以用对象名来调用create方法就显得很鸡肋了,当然,某些场合,这是一种技巧。

    //对象名.Create
    <2>, 构造和析构的虚与实
    构造函数和析构函数应该是虚的还是实的?这很疑惑!
    看代码:
    ---------------------------------
    type
    TMan = class(TObject)
    public
       constructor Create;
       destructor Destroy;
    end;

    TChinese = class(TMan)
    public
       constructor create;
       destructor Destroy;
    end;

    TBeijing = class(TChinese)
    public
       constructor Create;
       destructor Destroy;
    end;
    ....
            var
       AMan, AMan2, AMan3: TMan;
       AChinese: TChinese;
       ABeijing: TBeijing;
    begin
       AMan := TChinese.Create;
       AMan2 := TMan.Create;
       AMan3 := TBeijing.Create;
      
       AMan.Free;
       AMan2.Free;
       AMan3.Free;
    end;
    如果构造都是“实”的,无论如何,对于上面的用法,对象都可以被正确构造。但是对于析构,上述代码有
    问题!如果加入测试代码,你会发现所有的析构方法根本不会被执行。知道,Free方法继承自TObject,其
    源码如下:
    procedure TObject.Free;
    begin
       if Self <> nil then
       Destroy;
    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
       procedure Test;
    end;
    TChina = class(TCountry)
    public
       procedure Test;
    end;

    ....

    var
       ACoun, ACoun1: TCountry;
       AChina: TChina;
    begin
       ACoun := TCountry.Create;
       ACoun1 := TChina.Create;
       AChina := TChina.Create;

       ACoun.Test; //调用TCountry.Test
       ACoun1.Test; //调用TCountry.Test
       AChina.Test; //调用TChina.Test

       ACoun.Free;
       AChina.Free;
    end;
    对于隐藏的情况,具体调用哪一个方法,取决于调用者本身的类型。
    很显然,一个实的析构方法有问题!可能会造成内存泄露。那么它们是虚的好了。问题来了,
    type
    TMan = class(TObject)
    public
       constructor Create;
       destructor Destroy; virtual; override;
    end;

    TChinese = class(TMan)
    public
       constructor create;
       destructor Destroy; virtual; override;
    end;

    TBeijing = class(TChinese)
    public
       constructor Create;
       destructor Destroy; virtual; override;
    end;
    语法错误!上述的代码编译器会提示语法错误。ok,正确的应该是下面
    TMan = class(TObject)
    public
       constructor Create;
       destructor Destroy; override;
    end;

    TChinese = class(TMan)
    public
       constructor create;
       destructor Destroy; override;
    end;

    TBeijing = class(TChinese)
    public
       constructor Create;
       destructor Destroy; override;
    end;

    疑问来了,不是只有virtual 和 dynamic 才能被覆盖么?在Delphi中为了保证对象被完全的析构,
    所有的Destroy方法“天生”为虚方法,可以在任何地方被覆盖!虽然这破坏了语言语法上的一致性,
    但这保证一个对象不管从哪里(继承)来,都只有唯一的析构方法。但是,编译器不会强制你使用
    override关键字,所以,你可以像下面这样来定义析构。
    TChinaese = class(TMan)
    public
       destructor Destroy;
    end;
    这可能会带来问题,虽然不总是会带来问题。因为在TChinese类中存在多个Destroy方法,分别是:
    TObject.Destroy;
    TMan.Destroy;
    TChinese.Destroy;
    所以,结论是,无论在什么地方定义Destroy,覆盖它!!以确保整个类谱中只有一个Destroy;

    TChinese = class(TMan)
    public
       constructor create; override;

    //这块代码有问题,constructor Create;是静态的,不能override;constructor Create; virtual;
    end;

    TBeijing = class(TChinese)
    public
       constructor Create; override;
    end;
      var
       AMan, AMan2, AMan3: TMan;
       AChinese: TChinese;
       ABeijing: TBeijing;
    begin
       AMan := TChinese.Create;
       AMan2 := TMan.Create;
       AMan3 := TBeijing.Create;
      
       AMan.Free;
       AMan2.Free;
       AMan3.Free;
    end;
    Ok,可以,工作正常。原因在于我们对对象的使用方法。我们总是用"确定"的类名来构建的,这总可以保证
    我们调用正确的方法。但如果我们使用类引用的话,情况不同了,如下:

    type
       TManClass = class of TMan;
    var
       AManClass: TManClass;
       AObj, AObj2, AObj3: TMan;
    begin
       AManClass := TMan;   //AManClass is TMan
       AObj := AManClass.Create; //调用TMan.create

       AManClass := TChinese; //AManClass is TChinese
       AObj2 := AManClass.Create;    //调用那一个create??

       AManClass := TBeijing;
       AObj3 := AManClass.Create;   //which create???

       ....
    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
       class function Create: TMan;
    end;

    class function TMan.Create: TMan; //模拟构造函数
    begin
       result := inherited Create;
    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;对构造函数而言,虚和实取决
    于的使用方法,但一般而言使用虚更安全,在使用类引用的情况下一定要保证子类覆盖基类的构造方法。

  • 相关阅读:
    String类之indexOf--->查找某字对应的位置
    5、文件过滤器
    String类之endsWith方法--->检测该字符串以xx为结尾
    for循环
    java-成员方法/变量、类方法/变量等区别
    4、File类之获取方法
    3、File类之创建、删除、重命名、判断方法
    2、创建File类对象
    Java 实现Redis客户端,服务端
    Phoenix踩坑填坑记录
  • 原文地址:https://www.cnblogs.com/luckForever/p/7254687.html
Copyright © 2020-2023  润新知