C++ 详解虚函数和纯虚函数

多态性是面向对象三大特性之一。封装、继承和多态——三大特性。

多态性分为编译时的多态性和运行时候的多态性。

多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。

编译时: 重载函数、运算符重载

运行时:虚函数和继承实现

纯虚函数是在定义的时候在末尾加上 =0

纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

定义一个函数为纯虚函数,代表函数没有被实现,等待派生类对其进行实现。

定义一个函数为虚函数,不代表没有被实现,只是为了允许基类的指针指向派生类对象的时候,可以调用对应的派生类函数。

1
2
virtual void funtion1()=0

带有纯虚函数的类叫做抽象类。纯虚函数最显著的特征是,他们必学在 继承类中重新声明函数。

抽象类只能作为基类使用,抽象类不能定义实例,但是可以声明指向该抽象类的具体类的指针或者引用。

友元函数不是成员函数,可以访问私有成员,但是只有成员函数才是可以虚拟的,所以友元函数不能是虚拟函数。

虚函数和纯虚函数的区别

1、虚函数和纯虚函数可以出现在同一个类中,该类称为抽象类基类

2、使用方式不同,虚函数可以直接使用,但是纯虚函数必须在派生类中实现后才能使用

3、定义形式不同,纯虚函数需要virtual关键字+ =0,虚函数只加virtual关键字就可以了

4、虚函数必须实现,否则编译器会报错

5、虚函数和纯虚函数都可以在派生类中重写

6、基类的析构函数最好定义为虚函数,否则派生类对象可能会造成内存泄漏,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
int baseData;
~Base() {}
};

class Derived : public Base {
public:
int derivedData;
};

int main() {
Base* b = new Derived();
delete b; // 由于 Base 的析构函数不是虚函数,
// 这里调用的是 Base 的析构函数,
// 而非 Derived 的析构函数,
// 因此,Derived 里的 derivedData 没有被释放,
// 因此,内存泄漏。
}

如果一个类被其他类继承,那么析构函数最好声明为虚函数。

虚函数的实现机制

虚函数是通过虚函数表实现的,虚函数表中,包含了虚函数的地址。虚函数表是和类绑定的,也就是类的所有对象用的都是同一张虚函数表。在类的对象所存在的内存空间中,保存了指向虚函数表的指针,被称虚表指针,通过虚表指针可以找到类的虚函数表。所以虚表指针是和对象绑定的,每个对象的虚表指针不一样,但是指向的是同一张表。

无虚函数覆盖情况下的虚函数表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class Base
{
public:
virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
public:
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
Base *p = new Derive();
p->B_fun1(); // Base::B_fun1()
return 0;
}

基类和派生类的继承关系

基类的虚函数表:

派生类的虚函数表:

子类没有重写虚函数的时候,子类对象的虚表指针指向基类的虚函数表,子类没有虚函数表。但是当子类重写基类虚函数或者添加了一个新的虚函数的时候,子类会有一个自己的虚函数表,表中会存放基类虚函数的地址

单继承和多继承的虚函数表结构

两个父类的多继承(其他以此类推)总结:

两个虚函数指针分别指两个虚函数表。每个虚函数表保存每个父类的虚函数地址。即,继承多个基类的时候,子类有多个虚表指针。
内存布局与继承的父类的顺序有关,子类的虚函数插入到第一个虚指针所指的虚函数表中。
特别关注子类的虚析构函数。第二个虚指针调用虚析构函数时,会跳转到第一个虚函数表调用子类虚析构函数。
子类的虚函数表中虚函数的顺序与父类一样,若子类重写父类虚函数,即在虚函数表中原位置覆盖即可。