c++中虚函数的部分细节和基类必须要是虚析构函数的理由
一.说明
过程有些繁琐,如果只对结果感兴趣的同学可以直接跳转结论
二.过程
1.对无虚函数继承类的说明
如下,类C继承了类A,此时类A和类C中的存储结构如下(左A右C,以下默认),可以看见,两者是独立分开的,互不干扰。此时我们开辟了一个C类的空间,并定义了一个A类指针指向它,想调用C类的函数。但此时调用的内容依然是基于A类定义的内容,这显然不合理。而且能看见A和C的构造函数都被调用了,但结束后只调用了A类的析构函数,很明显,C类对象没有被释放,存在内存泄露的可能。
class A
{
public:
A() { cout << "A" << endl; };
~A() { cout << "delete A" << endl; };
void k() { cout << "AAA" << endl; };
int a;
};
class C :public A {
public:
C() { cout << "C" << endl; };
~C() { cout << "delete C" << endl; };
void k() { cout << "CCC" << endl; };
void c1() { cout << "CCC" << endl; };
int c;
};
void main()
{
A *t = new C;
t->k();
delete t;
}
2. 对有虚函数继承类的说明
通过1,我们知道这并不能满足我们的要求,那么怎么办呢?如果你学习过c++,那么肯定会回答将函数虚化,成为虚函数。没错,使用virtual将函数虚化后得到了我们想要的功能。而且将析构函数虚化后我们也成功将C类对象释放了。看上去皆大欢喜了,可喜可贺,可喜可贺。
class A
{
public:
A() { cout << "A" << endl; };
virtual ~A() { cout << "delete A" << endl; };
virtual void k() { cout << "AAA" << endl; };
int a;
};
class C :public A {
public:
C() { cout << "C" << endl; };
~C() { cout << "delete C" << endl; };
void k() { cout << "CCC" << endl; };
void c1() { cout << "CCC" << endl; };
int c;
};
void main()
{
A *t = new C;
t->k();
delete t;
}
3. 为何成功了?
①存储结构说明
通过2,我们成功实现了我们想要的功能:定义的A类指针指向C类,同时调用C类的函数。但为什么?因为这是virtual的特性?那它又是怎么实现的这个特性的呢?
让我们回到存储结构,还记得一开始1中的结构吗?没错,两个独立的结构。那加入virtual后会怎么变化呢?让我们看下图所示,两个结构多了个什么?两个指针?指向了函数表?没错,virtual一个很重要的功能就是让使用了这个关键字的类会生成一个vbptr指针,即虚表指针,和一个vfptr,即虚基类指针。vbptr指向了一个表,表中存放着所有被virtual修饰的函数;vfprt指向了被继承对象的虚函数列表。而且继承的对象生成的列表和基类并不相通,即是分开的。这带来了一个好处,即重写也不会有问题,而且重写后的函数是被分开保存的(如下第二张图)。
②调用说明
分开保存有什么好处呢?我们不妨先问一下,如何调用虚函数呢?很显然,通过虚函数列表找到对应函数就能调用。有没有发现为什么要特意拐一个弯来实现?没错,如果只是正常声明一个对象,在调用,显然多此一举,甚至还导致了性能的下降。但是我们是要做什么?声明A类指针指向堆中的C类对象。如果不拐弯,就会像1一样自己调用自己函数定义的内容。那拐弯就能实现了吗?是的,但如何实现的呢?
在弄清楚怎么实现的之前,我们要先弄清楚,虚函数是存放在哪的?这不显而易见的嘛,虚函数列表里。那么,虚函数列表是什么样的呢?或者说谁构建的虚函数列表呢?相信你应该逐渐清晰了,没错,由于是new 的C类,所以虚函数列表是C类构建的。调用的自然也是C类系函数列表中k(),输出自然是C类中的定义内容。正因如此,虚函数列表才能实现调用C类函数。说明结束,皆大欢喜,皆大欢喜。了……吗?
4.虚析构函数
还记得我们上面说的吗?函数重写后覆盖了原函数,所以能成功调用。但析构函数呢?它没被重写为什么虚化后能调用子类析构函数?这是因为当父类析构函数虚化后,子类创建时会将其塞入自己的析构函数,即父类想析构必须先找到子类的虚构函数进去,之后才能找到自己的析构函数。自然子类析构函数会被调用。若不虚化,则是类似普通函数一样,被正常调用,自然轮不到子类析构函数出场。
三.结论
父类析构函数虚化后子类继承时会将其放在自己的析构函数中,因此父类析构函数想执行必须先进入子类的析构函数才能找到自己的析构函数。