C++多态的实现及原理

引入的相关概念:

  • 静态类型:声明变量的类型-----编译器编译期间已经确定。
  • 动态类型:实际指向空间的类型。

C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时就会根据对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类中的函数,如果对象类型是基类,就调用基类函数。
1.用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
2.存在虚函数的类都有一个一维的虚函数的虚表,类的对象都有一个指向虚表头的指针。虚表是类的,虚表指针是对象的。
3.多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
4.多态用虚函数来实现,结合动态绑定。
5.纯虚函数是虚函数再加上=0。
6.抽象类是☞包括至少一个虚函数的类。
备注:纯虚函数:virtual void func() = 0;即抽象类,即先有名称,没有内容,在派生类中实现内容。

#include <iostream>

using namespace std;

class Base
{
public:
	void Do()
	{
		cout << "Base::Do" << endl;
	}

	void Say()
	{
		cout << "Base::Say" << endl;
	}
};

class Derived :public Base
{
public:
	void Say()
	{
		cout << "Derived::Say" << endl;
	}
};

void test()
{
	Derived D;
	Base* pB = &D;
	pB->Say();
}

结果为:
在这里插入图片描述
我们在test()函数中定义了一个Derived类的对象D,接着定义了一个Base类的指针变量pB,然后利用该变量调用Say()函数,认为D实际上是Derived的对象,应该调用Derived类的Say(),但是结果不是。

  • 从编译的角度来看:
    C++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这成为早绑定,当我们将Derived类的对象D的地址赋值给pB时,C++编译器进行了类型转换,此时C++编译器认为pB变量保存的就是Base对象的地址,当在函数调用时,执行pB->Say()时,调用的当然就是Base类的Say()函数。

  • 从内存的角度看:
    Derived类的对象模型如图:
    在这里插入图片描述
    我们构造Derived类的对象时,首先调用Base类的构造函数去构造Base类的对象,然后再调用Derived类的构造函数完成自身部分的构造,从而拼接出完整的Derived类对象,当我们将Derived类对象转换为Base类型时,该对象就被认为是原对象模型的上半部分,也就是图中Base的对象所占内存,那么当我们利用类型转换后的对象指针去调用它的函数时,当然调用它所在内存中的函数,所以输出"Base::Say()"也就正常了。
    在代码里面,我们知道pB实际指向的时Derived类的对象,我们希望输出的结果是Derived类的函数,那么就需要使用虚函数了。
    需要注意的是一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。
    代码稍微改动一下:

class Base
{
public:
	void Do()
	{
		cout << "Base::Do" << endl;
	}

	void virtual Say()
	{
		cout << "Base::Say" << endl;
	}
};

class Derived :public Base
{
public:
	void Say()
	{
		cout << "Derived::Say" << endl;
	}
};

void test()
{
	Derived D;
	Base* pB = &D;
	pB->Say();
}

运行结果如下:
在这里插入图片描述
我们发现结果是"Derived::Say",也就是根据对象的类型调用了正确的函数,那么当我们将Say()函数声明为virtual时,背后发生了什么。
编译器在变异的时候,发现Base类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址。
在这里插入图片描述
如何定位虚表,编译器另外还为每个对象提供了一个虚表指针(vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr指向正确的类的虚表,从而在调用虚函数的时候,能够正确找到虚函数,对于第二段程序,由于pB实际指向的对象的类型是Derived,因此vptr指向Derived类的vtable,当调用pB->Say()时,根据虚表的函数地址找到的就是Derived的Say()函数。
问题:
由于每个对象调用的虚函数都是用过虚表指针来索引的,也就决定了虚表指针的正确的初始化时非常重要的,换句话说,如果虚表指针没有初始化,我们就不能调用虚函数,那么虚表指针在什么时候,什么地方初始化?
答案:
在构造函数中进行虚表指针的创建及其初始化,在构造子类对象时,要先调用基类的构造函数,此时编译器只看到了基类,并不知道后面还有继承者,它初始化基类的虚表指针,该虚表指针指向基类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
简单总结(基类有虚函数):
1.每个类都有虚表
2.如果子类没有重写虚函数,那么子类虚表仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,如果基类有3个虚函数,那么基类的虚表中就有3项(虚函数的地址),派生类也会有虚表,至少有3项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚表中就会添加该项。
3.派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同:
这就是C++的多态性,当C++编译器在编译的时候,发现某个类的某个函数是虚函数,这个时候C++就会采用晚绑定技术,也就是在编译时并不确定具体调用的函数,而是在运行时,依据对象的类型确认调用哪一个函数,这就叫做C++的多态性,我们没有在函数前加virtual时,C++编译器就确定了那个函数被调用,这就叫做早绑定
C++的多态性就是通过晚绑定技术实现的。
基类虚表构建过程:
(i)按照虚函数的声明次序添加到虚表中
派生类虚表构建过程:
(i)将基类虚表中的内容拷贝一份放到派生类的虚表中
(ii)如果派生类重写了基类中的虚函数,用派生类自己虚函数替换虚表中相同偏移量位置的基类虚函数
(iii)派生类对于自己新增加的虚函数将其按照在派生类中的声明次序放在派生类虚表的最后

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值