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关键字。
使用虚函数代表会增加一个指针内存开销。