【C++取经之路】多态及其原理

目录

什么是多态?

构成多态的条件

虚函数

虚函数的重写

C++11 override和final 

拓展:重载、重写(覆盖)、重定义(隐藏)的对比 

抽象类

接口继承和实现继承 

多态的原理 

虚函数表(vftable)

虚函数表指针(vfptr)

动态绑定和静态绑定

继承与虚函数表

再谈虚函数和虚函数表


什么是多态?

多态,通俗的说就就是多种形态,也就是说,不同的对象去完成同一行为时会产生不同的形态。比如,买高铁票,学生的话是可以打75折的,而非学生的成年人就是全价买票了。

构成多态的条件

多态必须满足以下3个条件:

1)继承

 继承是多态的基础,它允许子类继承父类的属性和方法,并可以对这些方法进行重写,从而调用同一函数表现出不同的行为(多态)

2)必须通过基类的指针或引用调用虚函数

3)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

下面解释一下什么是虚函数。

虚函数

被virtual修饰的类成员函数称为虚函数。下面的BuyTicket就是一个虚函数。

class person
{
public:
	virtual void BuyTicket() { cout << " 买票 — 全价" << endl; }
};

虚函数的重写

虚函数的重写,也叫覆盖。派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名、函数参数列表),则称子类的虚函数重写了基类的虚函数。请看下面的例子~

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void BuyTicket() { cout << "买票 — 全价" << endl; }
};

class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票 — 75折" << endl; }
	//下面这种写法也是正确的,但不建议
	//void BuyTicket() { cout << "买票 — 75折" << endl; }
};

void Func(Person& p)
{
	//基类的指针或引用调用虚函数
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

上述例子的子类中完成了虚函数的重写(BuyTicket),运行结果如下:

我们可以看到,不同的对象去完成同一行为的结果是不一样的,这就是多态。 

一个特例:

析构函数的重写(基类的析构函数和派生类的析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。很显然,这两个析构函数的函数名看起来是不同的,似乎违背了重写的规则,其实不然,因为编译器在编译后析构函数的名称统一处理成了destructor。 

C++11 override和final 

final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
	virtual void Drive() final {}
};

class Benz : public Car
{
public:
	virtual void Drive() { cout << "Benz - 舒适" << endl; }
};

override:检查派生类的虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错 

class Car
{
public:
	virtual void Drive() {}
};

class Benz : public Car
{
public:
	virtual void Drive() override { cout << "Benz - 舒适" << endl; }
};

知道了override的用法,下面我们再来谈谈它存在的意义。

#include <iostream>
using namespace std;

class Car
{
public:
	virtual void Drive() {}
};

class Benz : public Car
{
public:
	virtual void drive()  { cout << "Benz - 舒适" << endl; }
};

int main()
{
	Benz b;
	return 0;
}

在上面的代码中,我没有使用关键字override,但实际上我尝试在派生类中重写基类的的方法(重写基类的虚函数),只是不小心把大写的D写成小写的d了,但程序是可以编译通过的,毫无疑问运行结果不符合预期。

这就导致了我没能及时的发现问题,如果加上override会怎样?我们看一看吧~

#include <iostream>
using namespace std;

class Car
{
public:
	virtual void Drive() {}
};

class Benz : public Car
{
public:
	virtual void drive() override { cout << "Benz - 舒适" << endl; }
};

int main()
{
	Benz b;
	return 0;
}

 编译结果:

用override关键字修饰之后,在编译阶段就发现了问题,所以说override关键字有助于我们提前发现错误并修正。

拓展:重载、重写(覆盖)、重定义(隐藏)的对比 

抽象类

在虚函数的后面加上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。抽象类不能实例化出对象。继承自抽象类的派生类,如果派生类中不对纯虚函数进行重写,那么派生类就包含了纯虚函数,它便是抽象类,不能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;//纯虚函数
};

int main()
{
	Car c;   //Car为抽象类
	return 0;
}

编译结果很好的说明了抽象类是不能实例化出对象的。 

 

接口继承和实现继承 

接口继承和实现继承是继承的两种主要形式。

1)接口继承主要关注定义一组方法,但不实现它们,然后由继承它的派生类来重写这些方法。

2)实现继承(也叫功能继承)就是派生类直接使用继承自基类的方法。

普通函数的继承是一种实现继承(功能继承),派生类继承了基类的函数就可以直接使用,继承的是函数的实现(或者说继承的是函数的功能)。虚函数的继承是一种接口继承,派生类继承的是虚函数的接口,目的是为了重写,达成多态。

多态的原理 

在讲多态的原理之前,需要铺垫几个新的概念,了解了必要的概念后再串起来讲多态的原理。

虚函数表(vftable)

虚函数表(简称虚表)本质上是一个「函数指针数组」,里面存放着虚函数的地址。

虚函数表指针(vfptr)

顾名思义,虚函数表指针就是指向虚函数表的指针。下面我们通过监视窗口观察。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	Base b;
	return 0;
}

在监视窗口中,可以看到__vfptr和vftable。除了_b成员,还有一个__vfptr放在对象的前面,这个就是虚函数表指针。其中v代表virtual,f代表function。

到这先进行一个简单总结:

在C++中,每个包含至少一个虚函数的类都会有一张虚函数表,这张虚函数表包含了该类所有的虚函数地址,当类对象被创建时,它们会拥有一个虚函数指针,这个指针指向类的虚函数表。顺便提一下,一个类只有一张虚函数表,所有类的对象共用同一张虚函数表。

 

动态绑定和静态绑定

1)静态绑定

静态绑定又称为前期绑定或早绑定,在程序编译期间确定了程序的行为。比如函数重载和函数模板的实例化(又被成为静态多态,广义上来讲,多态分为静态多态的动态多态)。

2)动态绑定

动态绑定又称为后期绑定或晚绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

在编译阶段,编译器会判断行为是否符合多态,符合就动态绑定,不符合就选择静态绑定。

总结起来,多态的实现机制就是:通过虚函数表指针找到虚函数表,在虚函数表中找到对应的虚函数并调用,完成多态。虚函数的调用顺序是这样子的:this指针 —> vfptr —> vftable —> 虚函数。

上面总结的还是比较抽象,这里再对多态的实现原理更详细的阐述一遍:

多态的实现原理主要是通过虚函数表和虚函数表指针来实现的。每个含有虚函数的类,以及从这样的类派生出来的类,都会有一张虚函数表,这张表中存储了虚函数的地址。类实例化出的对象中都含有一个虚函数指针,指向这张虚函数表。当我们通过基类的指针或引用调用虚函数时,会通过虚函数表指针找到虚函数表,然后在表中查找并调用相应的函数。这个过程是运行时完成的,所以可以实现运行时多态。

继承与虚函数表

1)当子类继承自一个或多个基类时,它的虚函数表将包含所有继承的虚函数的地址,以及子类自己定义的虚函数地址。如果子类重写了基类的虚函数,那么子类的虚函数表中将替换为新的函数地址(覆盖)。

2)如果子类没有重写基类的虚函数,那么它们将共享相同的虚函数地址。

再谈虚函数和虚函数表

1)虚函数表的作用

虚函数表的主要作用是实现动态绑定,即在程序运行时确定调用哪个函数。当通过基类的指针或引用调用虚函数时,虚函数表提供了一种机制来查找并调用正确的函数版本。

2)虚函数通常比非虚函数的调用稍慢,因为调用虚函数需要一些额外的间接跳转。

3)虚函数还增加了对象的大小(因为每个对象需要一个虚函数表指针vfptr),同时如果把没必要写成虚函数的函数硬要写成虚函数,还会增加虚函数表的大小,所以写成普通函数就可以的函数不建议硬要写成虚函数。


完~

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-21 04:46:01       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-21 04:46:01       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-21 04:46:01       45 阅读
  4. Python语言-面向对象

    2024-07-21 04:46:01       55 阅读

热门阅读

  1. 【Socket 编程】基于UDP协议建立多人聊天室

    2024-07-21 04:46:01       16 阅读
  2. 会Excel就会sql?

    2024-07-21 04:46:01       18 阅读
  3. Android笔试面试题AI答之布局Layout(1)

    2024-07-21 04:46:01       13 阅读
  4. SpringBootWeb 篇-入门了解 Swagger 的具体使用

    2024-07-21 04:46:01       23 阅读