c++多态

前言

封装,继承和多态是面向对象编程语言的三大特性。在前面我们介绍过了封装和继承,今天我们就来简单的认识一下多态,补全这最后一块拼图

多态初识

简单来说,多态就是多种形态,对于不同的对象会有不同的“应对”,当然了,多态是建立在继承体系之上的,只有在应对子类和父类时才可能产生不同的“应对”

多态分类

多态分为编译时多态(静态多态)和运⾏时多态(动态多态)

编译时多态(静态多态)主要就是我们前⾯介绍的函数重载和函数模板,它表现为在编译时期就确定

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态

对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定

满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定

至于虚函数是什么,虚函数表是什么,我们在后面详细展开介绍

多态构成条件

除了我们前面介绍到的必须在继承体系之下还有两个必要条件

必须是基类的指针或者引用调用,被调用的必须是虚函数

前者是因为只有基类的指针或引⽤才能既指向派⽣类对象(涉及到我们在继承中介绍的切片)

后者是因为基类的虚函数才能被派⽣类重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到

虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修
饰,static和virtual不能同时使用

虚函数的重写/覆盖

虚函数的重写需要满足三同,函数名相同,函数参数相同,函数返回值相同(这里会有一个协变的特例,我们在后面介绍)

在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承
后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性。我们建议写完整

注意,我们强烈建议父类和重写的子类虚函数,如果有缺省参数,那么这个缺省参数强烈建议相同,这可以避免很多意想不到的错误。重写可以理解为调用父类函数原型和子类函数体的一个拼接体

协变

我们允许要重写的虚函数返回值不同,但父类返回父类对象的指针或者引用,子类返回子类对象的指针或者引用,注意,我们不要求返回值的父子类必须是这个虚函数的父子类,只需要满足父子类的关系即可

析构函数的重写

基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写

我们如此设计是为了解决一些问题

比如B继承于A,B有一些需要动态申请释放的资源。我们实现一个辅助析构的函数,形参是父类对象的指针或者引用,此时可以传入子类或者父类,我们希望用一个函数统一父子类的析构。如果析构函数没有被重写,那么此时调用的全是父类的析构函数,可能造成内存泄漏,为了解决这个问题,我们统一析构函数的函数名以符合重写的条件,从而使父子类调用这个辅助析构函数时可以调到自己的析构函数避免内存泄漏

override和final 关键字

c++对于重写的要求是比较严格的,但是如果重写失败只有在运行时才会报错,c++11引入override关键字,它的作用是在语法层面上检查是否成功重写,如果重写失败直接在语法层面报错。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。当然了final这个关键字我们在继承中介绍过,它的作用是修饰一个不能被继承的最终类

重载,隐藏和重写(覆盖)

这里我们简单的区分一下这三个容易混淆的概念

重载,在同一作用域下,函数名相同,参数列表不同,返回值不做要求

重写,继承体系下在父子类域中,函数名相同,参数列表相同,返回值相同(注意协变的特例)

隐藏,继承体系下在父子类域中,函数名相同的不是重写就是隐藏,成员变量也有隐藏

纯虚函数和抽象类

当我们给定义的虚函数后写上 =0 ,这就是纯函数。纯虚函数一般需要实现函数体,只需要声明即可,想实现函数体也是可以的,但我们一般不这样做。包含纯虚函数的类称为抽象类,抽象类无法实例化对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象

虽然抽象类无法实例化,但它可以被继承,继承之后抽象类的指针或者引用可以接收子类从而实现多态。可以说抽象类就是为了被继承的

多态原理

下面我们就来简单的介绍一下多态的运行原理

在开始之前,我们来简单的看一段程序

class Parent {
public:
	virtual void isTest()
	{
		cout << "Parent::test"<<endl;
	}
};

class Child :public Parent
{
public:
	virtual void isTest()
	{
		cout << "Child::test" << endl;
	}
};

int main()
{
	Parent* pp = new Parent();
	Parent* pc = (Parent*)new Child();
	pp->isTest();
	pc->isTest();
	return 0;
}

这里我们简单的实现了一个多态,我们可以用调试窗口看看这两个指针指向的空间里是如何存储数据的

我们发现了一个很有意思的现象——我们没有定义过任何属性,在观察时却能看见vfptr,这个什么东西呢,里面存储着什么信息呢,它又有什么作用呢

 经过查阅资料我们可以知道,vfptr是一个叫虚表指针的东西,v代表virtual,f代表function,里面存储的是虚函数地址,作用是实现多态的基础

我们也发现在父类和子类的对象中各有一张虚表,我们可以观察一下这两种虚表。父类和子类的虚表不是同一张表,我们在上面的例子中实现了多态,虚表中存储的地址不相同,我们大胆猜测如果我们重写了父类的虚函数,那么对应虚函数的位置会被子类的地址覆盖,通过传入不同的对象调用不同的虚表以实现多态的效果。那么我们来验证一下这个猜想,如果这个猜想合理,我们有理由认为如果我们不重写父类的虚函数那么父子类的虚表中应该存储的是同一个函数的地址,因为这里不符合多态触发的条件

class Parent {
public:
	virtual void isTest()
	{
		cout << "Parent::test"<<endl;
	}
};

class Child :public Parent
{
public:
	/*virtual void isTest()
	{
		cout << "Child::test" << endl;
	}*/
};

int main()
{
	Parent* pp = new Parent();
	Parent* pc = (Parent*)new Child();
	pp->isTest();
	pc->isTest();
	return 0;
}

此时虚表中的函数地址是一致的,也侧面的印证了我们前面的理论 

注意,只有含有虚函数的类才会有虚表指针

虚函数表

现在我们再来谈谈虚函数表

父类的虚函数表会存储父类中所有的虚函数的地址

子类会在父类的虚函数表的基础上添加自己的虚函数地址,但它俩指向不一样的两张表,但一个类的对象共享同一张表

子类重写会覆盖子类相对应位置上虚函数的地址

虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标
记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000
标记,g++系列编译不会放)

虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函
数的地址⼜存到了虚表中。

虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定。vs下是存在代码段(常量区)

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值