• 继承条件下的对象创建与销毁


    在第3章介绍过类的构造函数,当使用new关键字创建对象时,类的构造函数被自动调用,如果没有定义专门的构造函数,一个默认的无参数构造函数被调用。

    在继承条件下,因为父类和子类都可以有自己的构造函数,情况就变得比较复杂了。

    1.子类、父类构造函数的调用次序

    请看以下代码(示例项目Constructors):

    class Parent

    {

        public Parent()

        {

            System.Console.WriteLine("Parent的默认构造函数被调用");

        }

    }

    class Child : Parent

    {

        public Child()

        {

            System.Console.WriteLine("Child的默认构造函数被调用");

        }

    }

    调用代码如下:

    Child c = new Child();

    输出结果如下:

    Parent的默认构造函数被调用

    Child的默认构造函数被调用

    可以看到,在创建子类对象时,首先调用父类构造函数,再调用子类构造函数。如果父类还有一个“祖父类”存在,则会先上溯到“祖父类”,调用“祖父类”构造函数,接着是父类构造函数,最后才是子类构造函数[①]。

    继承条件下类构造函数的这种调用次序可以称之为“尊重长辈”原则。

    向读者提个问题:

    为什么子类的构造函数在运行之前,会调用父类的构造函数,反过来行不行?

    给个线索:构造函数的主要作用是什么?可以从这个方面去想。

    空几行,请读者不要“偷看答案”。

    ……

    ……

    ……

    答案揭晓:

    类的构造函数主要用于初始化类的数据成员,而父类中的数据成员可能会被子类构造函数所访问,因此,显然应该先初始化最顶层的父类数据成员,再依派生顺序初始化其子类的数据成员。

    当对象析构时,会首先调用子类的析构函数,再调用父类的析构函数,刚好与对象创建时构造函数的调用顺序相反。

    由于C#和Visual Basic.NET中对象的回收由CLR的垃圾收集机制负责,因此本节使用C++来展示对象的析构过程。

    2.非托管代码对象的创建与回收问题

    请参看以下代码(示例项目ObjectCreateAndDestoryForCPP):

    class Parent

    {

        public:

            Parent()

            {

                cout<<"Parent的默认构造函数被调用"<<endl;

            }

            ~Parent()

            {

                cout<<"Parent的默认析构函数被调用"<<endl;

            }

    };

    class Child: public Parent

    {

        public:

            Child()

            {

                cout<<"Child的默认构造函数被调用"<<endl;

            }

            ~Child()

            {

                cout<<"Child的默认析构函数被调用"<<endl;

            }

    };

    程序中定义了两个有继承关系的类Parent和Child,并分别定义了一个构造函数与析构函数。

    调用代码如下:

    Child *pC=new Child();  //创建对象

    delete pC;  //销毁对象

    运行结果:

    Parent的默认构造函数被调用

    Child的默认构造函数被调用

    Child的默认析构函数被调用

    Parent的默认析构函数被调用

    程序的运行结果验证了我们的结论:

    对象销毁时,先调用子类的析构函数,再调用父类的析构函数。

    使用C++编写非托管的应用程序时,必须高度注意构造函数与析构函数的调用顺序问题。尤其是析构函数,一定要保证析构函数中不能有代码访问已被销毁的对象,否则将会引发内存存取冲突。

    修改ObjectCreateAndDestoryForCPP示例的代码,以展示C++如何在对象构造与析构之时分配和释放资源。

    首先,增加一个类A,其中有一个公有字段i。

    class A

    {

        public:

            int i;

    };

    接着,在Parent类中增加一个保护类型的成员pA,在构造函数中设置pA所指对象的i字段初值为100。

    class Parent

    {

    protected:

        A *pA;

    public:

        Parent()

        {

            pA->i=100;

            cout<<"Parent的默认构造函数被调用"<<endl;

        }

        ~Parent()

        {

            cout<<"Parent的默认析构函数被调用"<<endl;

        }

    };

    在Child类的构造函数中创建A对象,然后,在析构函数中销毁对象A。

    class Child: public Parent

    {

    public:

        Child()

        {

            pA=new A(); //创建对象A

            cout<<"Child的默认构造函数被调用"<<endl;

        }

        ~Child()

        {

            delete pA; //删除对象A

            cout<<"Child的默认析构函数被调用"<<endl;

        }

    };

    调用代码不变:

    Child *pC=new Child();  //创建对象

    delete pC;  //销毁对象

    当编译程序并运行时,Visual Studio 2005报告发生内存访问冲突(见图4-6)。

    图4-6  访问未创建的对象时引发内存访问冲突

    引发以上错误的原因在于:父类Parent构造函数先于子类Child构造函数运行,而pA所指对象是在子类Child构造函数中创建的,因此,在Parent类构造函数运行时,pA所指对象还未创建,因而犯了“访问未创建对象”的错误。

    如果在Parent类构造函数中删除“pA->i=100;”这句,并将其移到子类的构造函数中,则一切正常。

    class Child: public Parent

    {

    public:

        Child()

        {

            pA=new A(); //创建对象A

            pA->i=100;  //访问pA所指对象的字段i

            cout<<"Child的默认构造函数被调用"<<endl;

        }

        //……

    };

    然而,要注意这时在Parent类的析构函数中不能有任何代码访问pA对象,为了检验这点,我们在Parent类的析构函数中加一句代码访问pA->i。

    class Parent

    {

    public:

        ~Parent()

        {

            cout<<pA->i<<endl;  //访问pA所指对象的字段i

            cout<<"Parent的默认析构函数被调用"<<endl;

        }

        //……

    };

    运行结果为:

    Parent的默认构造函数被调用

    Child的默认构造函数被调用

    Child的默认析构函数被调用

    -17891602

    Parent的默认析构函数被调用

    可以看到,有一个很奇怪的数字“-17891602”出现,不是预想中的100。

    根本原因在于子类Child析构函数最先运行,它销毁了pA所指的对象,这样,当父类Parent的析构函数运行时,就犯了“访问已销毁对象”的错误。由于在销毁pA时,未及时地设置其为NULL指针,所指向的内存单元已不再有效(可能被其他进程所使用)。所以,输出的是内存中此单元的当时内容,因而出现了这样奇怪的数字。

    要更正很简单,请遵循一个基本编程原则:

    每个类都负责创建与销毁归自己管的资源。

    由于pA是类Parent的数据成员,因此,由Parent类负责其创建与回收工作,其子类只管使用就行了。正确的代码如下:

    class Parent

    {

    protected:

        A *pA; 

    public:

        Parent()

        {

            pA=new A(); //创建对象A

            pA->i=100;  //访问pA所指对象的字段i

            cout<<"Parent的默认构造函数被调用"<<endl;

        }

        ~Parent()

        {

            cout<<pA->i<<endl;  //访问pA所指对象的字段i

            delete pA; //删除对象A

            pA=NULL;//删除完对象之后,记住要及时将指针置为NULL

            cout<<"Parent的默认析构函数被调用"<<endl;

        }

    };

    现在,类Child只管使用pA,不再理会pA对象的创建与销毁问题。

    可以看到,使用C++编程,对象的创建与销毁是比较复杂的,只要稍有不慎,就会引发错误。而在.NET下编程,不管是C#还是Visual Basic.NET,程序员都可以不用理会对象的销毁工作,麻烦事全让CLR代劳了,从而使程序员可以摆脱各种技术细节的纠缠,更高效地编程,应该是一件值得欢迎的事。

    3.子类与父类构造函数的重载

    父类中可以有多个构造函数,子类可以根据需要有目的地选择一个调用。请看以下代码(示例项目Constructors):

    class Parent

    {

        public Parent()

        {

            System.Console.WriteLine("Parent的默认构造函数被调用");

        }

        public Parent(String info)

        {

            System.Console.WriteLine("Parent.Parent(String)被调用:"+info);

        }

    }

    上述代码中类Parent提供了两个构造函数。类Child可选择调用父类任意一个构造函数。

    class Child : Parent

    {

        public Child() //调用父类默认构造函数

        {

            System.Console.WriteLine("Child的默认构造函数被调用");

        }

        public Child(String info):base(info)//调用父类重载的构造函数

        {

            System.Console.WriteLine("Child.Child(String)被调用:"+info);

        }

    }

    特别注意C#使用base关键字调用父类重载的构造函数,并且这一调用声明紧跟在子类构造函数声明的后面,用冒号隔开。

    调用代码如下:

    Child c = new Child("Hello");

    运行结果:

    Parent.Parent(String)被调用:Hello

    Child.Child(String)被调用:Hello

     

    试一试:Visual Basic.NET子类使用关键字MyBase调用父类的构造函数,详情请查询Visual Studio 2005文档。读者可将上述C#代码转换为Visual Basic.NET作为练习。

  • 相关阅读:
    mac下配置openCV
    K最短路 A*算法
    KMP算法
    北航复试机试题
    1385重建二叉树
    二维数组中的查找
    简单的单向链表
    Getting Started with WebRTC [note]
    我的c漏洞
    PeerConnection
  • 原文地址:https://www.cnblogs.com/itgmhujia/p/1145289.html
Copyright © 2020-2023  润新知