常见笔/面试题-之构造函数和析构函数
构造函数是用来初始化一个对象的,而析构函数的作用则是释放对象占用的空间。如果将虚函数、构造函数和析构函数结合起来会有怎么样的效果呢?
构造函数可以是虚函数吗?
答:构造函数不可以是虚函数!基于以下几点原因:
(1)构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间动态确定实际类型的。在构建一个对象时,构造函数执行期间,对象未完全构建完成,编译器无法知道对象的实际类型,如果构造函数为虚函数,虚函数的执行是基于对象类型确定的,然而构建的对象本身自己都无法确定自己的类型,虚函数更加无法正确执行!
(2)虚函数的执行依赖于虚函数表。而虚函数表的初始化工作是在构造函数中完成的,即在构造函数中初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还未被初始化,将导致虚函数无法工作。
(3)虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
析构函数可以是纯虚函数吗?能举个例子吗?
答:析构函数可以是纯虚函数!如果一个类要作为基类存在,最好将该类的析构函数申明虚函数(除非不使用基类指针的方式构造子类)。
(1)拥有纯虚函数的类称为抽象类,专门提供函数接口,不能实例化。如果某个类需要作为一个抽象类,但是其中并没有其他方法,这时可以将析构函数声明为纯虚函数。(因为构造函数不能是虚函数,更不必提纯虚函数了)
(2)例子:#include <iostream> using namespace std; class test { public: test(){ cout << "test constructor" << endl; } virtual ~test() = 0; }; test::~test() { cout << "test destructor! it's specail!" << endl; } class derived:public test { public: derived(){ cout << "derived constructor" << endl; }; virtual ~derived(){cout << "derived destructor" << endl;}; }; int main() { derived d; //system("pause"); return 0; }
注意test基类中的析构函数是纯虚函数,但是该析构函数还是有实现,而且必须要有实现!!!test::~test(){}这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~test的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来(报错error link 1120),最后还是得回去把它添上。虽然抽象类的析构函数可以是纯虚函数,但要实例化其派生类对象,仍必须提供抽象基类中析构函数的函数体。
**通常情况下,抽象类的纯虚函数实现必须且只能由派生类实现,但是对于基类的纯虚析构函数实现可以由自身给出,也可以由派生类给出。
拥有纯虚析构函数的类也是抽象类!!!**下面代码执行后会发生内存泄漏吗?如果存在该如何修改?
#include <iostream> using namespace std; //基类 class Base{ public: Base(){} ~Base(){cout << "Base destructor" << endl;} void dosomething(){cout << "do something in Base" << endl;} }; //派生类 class Derived : public Base{ public: Derived(){} ~Derived(){cout << "Derived destructor" << endl;} void dosomething(){cout << "do something in Derived" << endl;} }; int main(){ Base *base = new Derived; base->dosomething(); delete base; system("pause"); return 0; }
答:此段代码执行后将会发生内存泄漏,由于基类指针指向的是子类对象,但是由于基类的析构函数不是虚函数,基类指针无法找到他所指向的实际类型,从而在delete的时候只能释放derived对象中从基类继承的部分,其余部分将无法得到释放导致内存泄漏!
修改:
#include <iostream> using namespace std; //情景1:普通成员函数和析构函数,都不是虚函数 class Base{ public: Base(){} //父类的析构函数,是虚函数,只做了这里的更改 virtual ~Base(){ cout << "Base destructor" << endl; } //普通成员 void dosomething(){ cout << "do something in Base" << endl; } }; //派生类 class Derived : public Base{ public: Derived(){} ~Derived(){ cout << "Derived destructor" << endl; } void dosomething(){ cout << "do something in Derived" << endl; } }; int main(){ //这个时候,虽然父类指针实际指向子类,可是没有虚函数表, //所以不能调用实际的类型,因此输出的只能是父类指针自身能看到的内容 Base *base = new Derived; base->dosomething();//输出的是父类的函数 delete base;//调用的是父类的析构函数 system("pause"); return 0; }
由此可以看出,这个时候,就真正实现了将实际类型对象进行释放的。同时可以得到,如果父类析构是虚函数,子类调用析构函数的话,会先调用子类的析构函数,之后会调用父类的析构函数其实这里父类的析构函数加上了virtual,并不是说pbase释放的时候,同时调用了子类的析构函数和父类的析构函数,它实际上指向的是子类的虚函数表,那么就是说父类指针最终只调用了子类的析构函数,由C++类本身特性,当子类析构函数调用的时候,会自动调用父类的析构函数,完成了释放。
相关小结
对于Base *pbase = new Derived;
如果父类函数不是析构函数,那么pbase只能“看见”父类本身的函数,这是因为没有虚函数表让它可以找到本身
如果父类析构函数是虚函数,如果delete pbase,将会先调用子类函数的析构函数,然后子类析构函数自动调用父类的析构函数,真正实现了资源释放,防止了内存泄露构造派生类的时候,会先构造基类部分,然后构造子类部分;撤销派生类对象的时候,会先撤销派生类部分,然后撤销基类部分