虚函数实现多态原理

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 
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

你可能感兴趣的:(C/C++)