C++ 构造和析构

2019-03-07  本文已影响0人  宋大壮

构造顺序

C++构造函数按下列顺序被调用:
(1)任何虚拟基类的构造函数按照它们被继承的顺序构造;
(2)任何非虚拟基类的构造函数按照它们被继承的顺序构造;
(3)任何成员对象的构造函数按照它们声明的顺序调用;
(4)类自己的构造函数。

class OBJ1
{
public:
    OBJ1(){ cout <<"OBJ1\n"; }
};

class OBJ2
{
public:
    OBJ2(){ cout <<"OBJ2\n"; }
};

class Base1
{
public:
    Base1(){ cout <<"Base1\n"; }
};

class Base2
{
public:
    Base2(){ cout <<"Base2\n"; }
};

class Base3
{
public:
    Base3(){ cout <<"Base3\n"; }
};

class Base4
{
public:
    Base4(){ cout <<"Base4\n"; }
};

class Derived :public Base1, virtual public Base2,   //虚基类的继承顺序为Base2,Base4
    public Base3, virtual public Base4                       //非虚基类的继承顺序为Base1,Base3
{
public:
    Derived() :Base4(), Base3(), Base2(),       //成员被初始化的顺序和成员初始化表里面的顺序是没有关系的,只和成员的声明顺序有关
        Base1(), obj2(), obj1()                 //这里声明顺序 obj1>obj2
   {                                            //对于const类型和引用类型的成员只能在成员初始化表初始化,以及在类定义中初始化(static const类型还可以在类定以外的程序文本中初始化);
        cout <<"Derived ok.\n";
    }
protected:
    OBJ1 obj1;
    OBJ2 obj2;
};

int main()
{
    Derived aa;
    cout <<"This is ok.\n";

    int i;
    cin >> i;

    return 0;
}

结果

Base2 虚基类
Base4
Base1 非虚基类
Base3
OBJ1 成员
OBJ2
Derived ok 构造函数体

析构则相反

Derived destory
OBJ2 destory
OBJ1 destory
Base3 destory
Base1 destory
Base4 destory
Base2 destory

多重继承的构造顺序

先调用基类的构造函数,如果基类的构造函数中仍然存在基类,那么程序会继续进行向上查找,直到找到它最早的基类进行初始化。

为什么使用初始化成员列表

0.初始化列表能够提高构造函数的性能,在派生类中可以跳过基类构造函数,直接进行赋值操作
1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面;
2.引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面;

  1. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。(只要显示定义了类的构造函数,则编译器不再提供默认构造函数。如果没有定义构造函数,则编译器会提供默认构造函数)
class A {
public:
    A(const string& name, T *ptr);
    ...
private:
    const string& _name; // 必须通过成员初始化列表
                        // 进行初始化
    T * const _ptr; // 必须通过成员初始化列表
                   // 进行初始化
};


class B {
public:
    A(const string& name);
    ...
private:
    string _name; 
};

已知在执行构造函数体内的操作前,成员对象已经初始化,那么

    A(const string& name, T *ptr){
       _name = name;                      //_name将调用string的默认构造函数,并且在函数体中调用operator=函数
}

这样总共有两次对 string 的成员函数的调用:一次是缺省构造函数,另一次是赋值。
而用成员初始化列表来指定 name,则会通过拷贝构造函数以仅一个函数调用的代价被初始化。即使是一个很简单的 string 类型,不必要的函数调用也会造成很高的代价。

构造函数与虚函数

构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

虚析构函数

虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。

class Base
{
public:
    Base(){}
    ~Base()
    {
    }
private:
    int a, b;
};

class Derive : public Base
{
public:
    Derive()
    {
        
    }
    ~Derive()
    {
        // release memeory
        if(pI != NULL)
        {
            delete pI;
        }
    }
private:
    int *pI;
};
...
{
       Base *pD = new Derive;
       delete pD;
 }
...

这段代码中没有使用虚析构函数,当删除动态申请的对象时,只执行了基类的构造函数,而此时,子类里面是有动态申请的内存的,那么这就早成了内存的泄露。
即当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。

原理

由于父类的析构函数为虚函数,所以子类会在所有属性的前面形成虚表,子类的析构函数就在虚表内,且和声明顺序有关
当delete父类的指针时,由于子类的析构函数与父类的析构函数构成多态,所以调用子类的析构函数;
虽然父子的析构函数名字不一样,但是他们只占一个位置(即父子析构函数在虚函数表中的位置是一样的,否则就不存在多态了)
析构时,到特定的位置中调用该类型的析构函数,其析构函数中又嵌套了对父类的析构函数的调用

除此之外

1、 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
2、 非纯的虚函数必须有定义体,不然是一个错误。
3、 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。

4、 纯虚函数通常没有定义体,但也完全可以拥有。

5)、析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

上一篇下一篇

猜你喜欢

热点阅读