C++ 构造函数、析构函数与虚函数的关系

编译环境:windows 10 + VS2105 

 1、构造函数不能为虚函数

虚函数的作用是为了实现C++多态机制。基类定义虚函数,子类可以重写该虚函数。当子类重写父类虚函数后,父类指针指向子类地址时,父类指针根据赋给它不同子类的指针,动态调用该子类的该函数,而不是父类的对应函数(当子类没重写该函数时,则调用父类对应的函数)。且这样的函数调用发生在运行阶段,而不是发生的编译阶段,称为动态联编(注意:函数重载也可以认为是多态,只不过是静态的,在编译阶段确定了函数调用方式。非虚函数静态联编效率比虚函数高,但是不具备动态联编能力)。

class A 
{
public:    
    virtual void fun_1() { std::cout << "A::fun_1" << std::endl; }
    virtual void fun_2() { std::cout << "A::fun_2" << std::endl; }
};
class B :public A
{
public:
    void fun_1() { std::cout << "B::fun_1" << std::endl; }
    //void fun_2() { std::cout << "B::fun_2" << std::endl; }
};

int main()
{    
    B b;
    A* obj = &b;
    obj->fun_1(); //输出 "B::fun_1"
    obj->fun_2(); //子类没重写fun_2,所以调用父类的 fun_2 输出"A::fun_2"
    return 0;
}

因此,虚函数是只知道部分信息情况下完成函数调用的机制,允许我们只知道接口而不知道对象的确切类型。但是要创建一个对象,则需要知道对象的一个完整信息。所以不支持构造函数是虚函数。另外,一般情况下,编译器为虚函数维护一个虚函数列表。类在构造时候需要分配内存来构造对象,构造对象没完成时,虚函数表不存在,如果构造函数是虚函数,这个虚函数表并没有创建出来,因此会陷入死锁。编译器会认为此写法不合法。

2、析构函数可以为虚函数

1)析构顺序

派生类的成员类->派生类->基类

2)基类析构函数为非虚函数时,造成内存泄漏。

下列代码造成内存泄漏,原因是直接给编译器一个A指针,编译器直接调用A的析构函数。

class A 
{
public:        
    ~A() {std::cout << "~A()" << std::endl;}
};
class B :public A
{
public:    
    C* c =nullptr;
    B() { c = new C(); }
    ~B()
    { 
        delete c;
        std::cout << "~B()" << std::endl; 
    }
};

int main()
{     
    A* obj = new B();
    delete obj; //输出"~A()",没调用B的析构函数,有可能造成内存泄漏 (B中 c资源没释放)
    return 0;
}

3)基类析构函数定义成虚函数可避免内存泄漏。

class C{
public:
    ~C() { std::cout << "~C()" << std::endl; }
};
class A 
{
public:        
    virtual~A() {std::cout << "~A()" << std::endl;}
};
class B :public A
{
public:    
    C* c =nullptr;
    B() { c = new C(); }
    ~B()
    { 
        delete c;
        std::cout << "~B()" << std::endl; 
    }
};

int main()
{    
    
    A* obj = new B();
    delete obj; 
    return 0;
}
/*
输出:
~C()
~B()
~A()
内存正常释放
*/

 4)纯虚析构函数

定位为纯虚函数的析构函数称为纯虚析构函数。一般我们把函数设置为纯虚函数时不想这个类实例化,抽象出来的顶层父类,并且这个纯虚函数不能实现。与普通纯虚析构函数区别是不能在类中 = 0之后实现,而需要类外实现。如果不是实现,则编译器会自动加上。同样,编译器仍会对其产生调用。

class C {
public:
    ~C() { std::cout << "~C()" << std::endl; }
};
class A 
{
public:        
    virtual~A() = 0;
};
A::~A() { std::cout << "~A()" << std::endl; }
class B :public A
{
public:    
    C* c = nullptr;
    B() { c = new C(); }
    ~B()
    { 
        delete c;
        std::cout << "~B()" << std::endl; 
    }
};


int main()
{        
    A* obj = new B();
    delete obj; 
    return 0;
}

/*
输出:
~C()
~B()
~A()
内存正常释放
*/

与上一段代码效果一样。

5)关于virtual的隐式传播

class A 
{
public:        
    virtual~A() = 0;
};
A::~A() { std::cout << "~A()" << std::endl; }
class B :public A
{
public:        
    ~B()
    {         
        std::cout << "~B()" << std::endl; 
    }
};
class C:public B {
public:
    ~C() { std::cout << "~C()" << std::endl; }
};
class D:public C {
public:
    ~D() { std::cout << "~D()" << std::endl; }
};

int main()
{        
    A* obj = new D();
    delete obj; 
    return 0;
}

/*
输出:
~D()
~C()
~B()
~A()
*/

当基类是虚函数,无论子类的相同函数是否加virtual关键字均为虚函数。但是为了方便其他开发人员查阅代码,建议把从继承过来的虚函数都加上virtual关键字。

使用虚函数代表会增加一个指针内存开销。

posted @ 2021-04-09 17:02  钟齐峰  阅读(257)  评论(0编辑  收藏  举报