1 问题
在C++中,运行时多态是通过虚函数来实现的。本文将讨论关于虚函数实现多态的问题:
- 相信大家都知道怎么使用虚函数实现多态,但是它的原理是什么呢?
- 构造函数可以声明为虚函数吗?为什么?
- 析构函数可以声明为虚函数吗?为什么?
2 概念
2.1 虚表和虚表指针
要理解虚函数实现多态的原理,我们首先要知道 虚表 和 虚表指针 的概念:
- 含有虚成员函数的类称为虚类,如果一个类是虚类,那么编译器会自动为其添加一个虚表(虚函数表)和一个称为虚表指针(指向虚表的的指针,__vptr)的数据成员;
虚表
- 每个虚类,都有一个虚函数表;
- 本质上是一个指针数组,其中元素为指向该类的虚成员函数的函数指针;
- 属于类,为该类的所有对象所共用;(static?)
- 在编译阶段就已经被确定了。(const?)
虚表指针
- 属于对象,是编译器自动添加的一个指针成员变量;
- 虚表指针,在构造函数中初始化;
2.2 虚函数调用过程
现有基类对象指针p和基类虚函数func1
class Base {
public:
...
virtual void func1() {
...
}
...
};
int main() {
Base* p = new Base();
p->func1();
return 0;
}
在执行 “p->func1();” 时,主要步骤有:
- 首先发现 p 是一个指针,且调用的函数 func1 是一个虚函数;
- 此时会通过该对象的虚表指针 __ptr 访问到它的虚表,然后在虚表中查找虚函数func1所对应的函数指针;
- 根据函数指针找到函数func1,并执行。
3 虚函数实现多态原理
现假设已有 Base 和 Derived 两个类,具体情况如下:
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() {
cout << "Base func1 called." << endl;
}
virtual void func2() {
cout << "Base func2 called." << endl;
}
void func3() {
cout << "Base func3 called." << endl;
}
private:
int x;
int y;
};
class Derived : public Base {
public:
virtual void func1() {
cout << "Derived func1 called." << endl;
}
virtual void func2() {
cout << "Derived func2 called." << endl;
}
virtual void func4() {
cout << "Derived func4 called." << endl;
}
private:
int z;
};
基类Base具有三个成员函数,其中func1和func2为虚函数,func3为普通成员函数;派生类Derived对func1和func2进行了重写,并新添加了一个虚函数func4。以下为多态的示例代码:
Base* ptrBase = new Derived();
ptrBase ->func1(); //显然输出:Derived func1 called.
ptrBase ->func2(); //显然输出:Derived func2 called.
ptrBase ->func3(); //显然输出:Base func3 called.
ptrBase ->func4(); //报错
分析如下
- 基类Base和派生类Derived都具有虚函数,均为虚类,因此都具有虚表和虚表指针。Base类的虚表vTableBase包含两个函数指针,第一个指向func1,第二个指向func2;Derived类的虚表vTableDerived包含三个函数指针,第一个指向func1,第二个指向func2,第三个指向func4。
- 首先,“new Derived();” 会调用构造函数,生成一个Derived类对象的指针(ptrDerived),并使该对象的虚表指针指向Derived类的虚表;然后,使用ptrDerived对ptrBase的数据成员(当然也包括虚表指针 __ptr )进行初始化,使得ptrBase的虚表指针不再指向Base类的虚表,而是指向Derived类的虚表(实际上是Derived类虚表的子集,由于func4,不是Base的成员函数,所以该虚表中不会包含有func4的函数指针)即:此时 ptrBase所指对象的虚表依然包含2个函数指针,不过它们不再指向基类Base的成员函数func1 和 func2,而是指向派生类Derived的成员函数 func1 和 func2。
- 结合虚函数的调用过程:首先,ptrBase 是一个对象指针,且 func1 是一个虚函数,然后根据该对象的虚表指针 __vptr,找到其所指的虚表,接着找到虚函数 func1 的函数指针,并对其执行。由于此时虚表中虚函数 func1 的函数指针指向派生类Derived的虚函数 func1 ,所以,执行的是派生类的虚函数 func1。
4 构造函数可以声明为虚函数吗?
显然构造函数是不能声明为虚函数的。
我们知道,在调用虚函数前,需要先访问虚表指针,得到虚表,然后再执行虚表中相对应的函数。假设,现在将构造函数声明为虚函数:调用构造函数时,发现构造函数是一个虚函数,然后去访问虚表指针,可是虚表指针是在构造函数中进行初始化的,而目前构造并没有执行,也就是说,虚表指针还没有初始化,只是一个空值,理所当然的,也就找不到指向构造函数的函数指针,因此无法完成构造函数的调用。可见,构造函数是不能声明为虚函数的。
5 析构函数可以声明为虚函数吗?
1、当然是可以的,而且,为了防止内存泄漏,基类的析构函数必须声明为虚函数!
2、为什么将基类析构函数声明为虚函数,就可以防止内存泄漏?
- 如果没有将基类析构函数声明为虚函数,在释放指向派生类对象的基类指针的时候,只会调用基类的析构函数,而派生类的析构函数不会被调用,导致属于派生类的新添加的数据得不到释放,从而导致内存泄漏。
- 如果将析构函数声明为虚函数,在释放指向派生类对象的基类指针的时候,会调用派生类的析构函数,而派生类的析构函数会自动调用基类的析构函数,从而释放所有内存,避免了内存泄漏。
3、可是,有个问题,为什么将基类析构函数声明为虚函数之后,在释放指向派生类对象的基类指针时,调用的是派生类的析构函数?难道派生类的析构函数重写了基类的析构函数?不可能啊,基类析构函数和派生类析构函数的函数名不同,不能构成重写啊。
- 其实,析构函数是一个特殊的函数,编译器在编译时,析构函数的名字统一为destucter;
- 所以只要将基类的析构函数声明为虚函数,不管子类的析构函数前是否加virtual,都构成重写。这也就可以解释为什么将基类析构函数声明为虚函数,释放指向派生类对象的基类指针时,会调用派生类的析构函数,因为虚表中的函数指针指向的是派生类的析构函数。
6 参考文献
https://blog.csdn.net/lihao21/article/details/50688337(推荐,讲得很好)
https://blog.csdn.net/han8040laixin/article/details/81704165