【C++】-----多态及原理

目录

前言

一、是什么?

二、怎么样?

Ⅰ、构成条件

Ⅱ、虚函数

Ⅲ、虚函数的重写

1.常规情况下

2.虚函数重写的三个例外

①返回值的类型可以不同

②析构函数的重写

③子类虚函数可以不加virtual关键字(不建议)

3.override和final关键字

Ⅳ、重载/重写/重定义(隐藏)区别

三、虚函数表(细致)

场景引入

定义

单继承的虚表

如何打印虚表(指针)?

多继承的虚表

菱形虚拟继承的虚表

总结:

四、底层原理

原理

多态调用与普通调用

五、抽象类

定义

特点


前言

都说面向对象的三大特性为封装,继承,多态,前面已经介绍了封装继承,下面来看看C++多态是怎么个事。。

一、是什么?

多态,多态,通俗点来说就是一个事物有多种形态,具体点就是对于同一件事情,不同对象去完成时会产生不同的形态,结果不一样!

比如:买车票这件事,有普通票,有学生票半价,军人就优先买票等等

二、怎么样?

多态是在不同继承关系的类对象,去调用同一函数,从而产生了不同的行为!!

多态的特点:指向谁就调用谁!

Ⅰ、构成条件

  • 子类完成对父类虚函数的重写
  • 通过父类的指针或者引用去调用虚函数

因此实现多态,指向谁,就调用谁!!

Ⅱ、虚函数

所谓虚函数,就是在成员函数前面加上virtual关键字 。

注意:

①是成员函数,不是函数!!!不能在类外声明和定义

②静态成员函数不能设置为虚函数,因为没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

③inline成员函数可以是虚函数。编译器会忽略inline属性。

如:

virtual void Buytick()

Ⅲ、虚函数的重写

1.常规情况下

重写也称为覆盖,即子类中有一个和父类完全一样(函数名,参数列表,返回值三者)的成员函数,称为子类重写了父类的虚函数。。

可以看到,没有完成虚函数重写,不构成多态,结果就不是多态的结果!

以上常规情况下必须完成三同的条件,但是你知道的,这是C++,必然会有例外,也叫坑!

2.虚函数重写的三个例外

①返回值的类型可以不同

这种情况也叫做协变,类型不同,但是仅限父类虚函数返回父类的指针/引用,子类虚函数返回子类的指针/引用。

②析构函数的重写

如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然函数名不同。看似违背重写规则,其实不然,因为 实际上编译器统一将析构函数的名称处理为destructor。这样函数名实际就是相同的。构成重写。

有以下场景:

可以看到上面的结果,如果子类中申请了空间,但是没有释放,必然会引起内存的泄漏 ,所以为了防止上述场景的发生,我们希望指向谁就调用谁的析构,那就要构成多态。所以建议在继承体系中,将析构函数定义为虚函数。。构成多态。。

③子类虚函数可以不加virtual关键字(不建议)

对于这种例外,不建议这样干,建议最好全加上。

注意:构成多态,父类不能不加!!

为什么可以不加呢?

重写更深一层的含义:子类继承父类,实际上将虚函数头继承了,也就是说虚函数头实际还是和父类完全一样,子类只是完成功能上的重写,也就是把这个虚函数头的函数体换了。所以可以不加。

3.override和final关键字

  • final,修饰虚函数,表示该虚函数不能继承;修饰类时,表示该类不能被继承。加父类
  • override,检查子类虚函数是否重写父类的某个虚函数,没有重写就报错。加子类

Ⅳ、重载/重写/重定义(隐藏)区别

  • 重载

①两个函数在同一作用域

②函数名相同,参数不同

  • 重写

①两个函数必须是虚函数

②两个函数分别在父类和子类

③两个函数满足三同(函数名,参数,返回值),协变例外

  • 重定义(隐藏)

①两个函数分别在父类和子类中

②两函数名相同即可

三、虚函数表(细致)

场景引入

问题:以下代码类对象的大小是多大?

cpp 复制代码
class Base
{
public:
	virtual void Func() 
	{
		cout << "Is Base::Func()" << endl;
	}
private:
	int _a=1;
};

Base b;
sizeof(b)=?
	

按照正常理解,类对象中存的是类成员变量,所以大小应该是4,但是事实并非如此!

可以看到结果是8,为啥呢?调试看看

除了有成员变量以外,还有一个_vfptr,这什么玩意啊?实际上就是一个指针,也叫做虚函数表指针。在32位环境下,一个指针的大小就是4字节,所以上面的结果就是 int + 指针大小=8!

定义

虚函数表就是存放虚函数地址的一个表。实际上,对于一个有虚函数的类至少都有一个虚函数表指针,该指针指向一个虚函数表,虚函数表简称虚表,虚函数表指针简称虚表指针!

注意区分:

①虚表里面存的不是虚函数,而是虚函数的地址(指针)!!!只有虚函数才能进入这个表!

②虚表的本质就是一个存虚函数指针的指针数组,一般这个数组最后一个放nullptr!

③虚表是在编译阶段就生成的,一般在常量区!

④虚表指针存在于实例化的对象上!

⑤虚函数跟普通函数一样存在代码段,不在虚表!

⑥**继承部分提到的虚基表,** 存的当前位置距离虚基类部分的偏移量,解决菱形继承数据冗余和二义性

单继承的虚表

上图我们说过,只有一个函数是虚函数时,才能将其地址放入虚表中,那来看看下面的例子

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Is Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
};
class Devire:public Base
{
public:
	virtual void Func1()
	{
		cout << "Is Devire::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Is Devire::Func3()" << endl;
	}
};

Base b;
Devire d;

实际上这个可以理解为编译器的一个BUG。。将Func3给隐藏了,不正常现象!那Fun3究竟在哪?只能打印虚函数表来看看了。

如何打印虚表(指针)?

虚表实际上是一个函数指针数组 ,元素是函数指针,也就是每一个元素都为void(*)()类型。所以为了方便我们可以重命名。

cpp 复制代码
typedef void(*VFR)();将函数指针类型重命名为VFR

VFR p[10];函数指针数组。元素VFR

虚表的访问需要使用对象中虚表指针才能进行访问,所以得取出对象的虚表指针(数组指针),指向虚表。32位环境下,一个指针的大小为4字节,所以就要取出对象的头4字节。怎么取?直接强制类型转化不可以!

①取出d对象地址,强制类型转化成int*,在解引用,此时就拿到一个整型的大小,4个字节。这个值就是指向虚表的指针。。

②在强转成VFR*, 因为虚表就是一个存VFR类型(函数指针类型)的数组。

cpp 复制代码
VFR* ptr = (VFR*)(*(int*)&d);//取出虚表指针。

打印函数:

cpp 复制代码
//void func(VFR ptr[])
void func(VFR* ptr)
{
	cout << "虚表地址->>" << ptr << endl;
	for (size_t i = 0; i<3; i++)
	{
		printf("第%d虚函数的地址:%p->",i, ptr[i]);
		VFR f = ptr[i];//取出数组的每一个元素,类型为函数指针。

		(*f)();//指针解引用,函数调用
	}
}

验证一下结果:

可以看到Func3也在对象d中的虚表中。。。只是被编译器隐藏了!!!BUG

所以实际上上图,单继承子类的对象模型是:

多继承的虚表

实际上,多继承体系中,子类没有重写的虚函数(自己的虚函数),会被放在第一个继承基类部分的虚函数表中!

分析如下:

cpp 复制代码
class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int _a;
};
class B
{
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func2() { cout << "B::func2" << endl; }
private:
	int _b;
};
class C : public B, public A
{
public:
	virtual void func1() { cout << "C::func1" << endl; }
	virtual void func3() { cout << "C::func2" << endl; }
private:
	int _c;
};

C c

上图的监视窗口可以看到,子类C重写了B和C的func1,那么C自己的虚函数Func3在哪?是否可能会在两个父类的虚表中的一个?打印虚表看看。

从上图可以很清楚的看出,C自己的虚函数放在了父类B的虚表中!!

注意:找到父类A的虚表指针,更加保险的写法:

cpp 复制代码
A* ptr = &c;
VFR* vTableb2 = (VFR*)(*(int*)ptr);
func(vTableb2);

所以实际上,C的对象模型是这样的:

菱形虚拟继承的虚表

有如下继承关系:

cpp 复制代码
class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
private:
	int _a=1;
};
class B:virtual public A
{
public:
	virtual void func2() { cout << "B::func2" << endl; }
private:
	int _b=2;
};
class C :virtual public A
{
public:
	virtual void func3() { cout << "C::func3" << endl; }
private:
	int _c=3;
};
class D :public B, public C
{
public:
	virtual void func4() { cout << "D::func4" << endl; }
private:
	int _d=4;
};

D d
sizeof(d)=?

对于菱形虚拟继承,实际上存在三张虚表,A、B、C都有虚表。因为虚基类A是共享的,B和C的虚函数不可能同时放入A的虚表中,所以B和C存在各自的虚表,另外又是虚拟继承,所以还存在着一个虚基表指针,虚基表中存的偏移值,为了去寻找父类A,方便切片赋值!

总结:

  • 父类没有虚表或者是共享类的时候,子类都需要单独创建虚表存自己的虚函数。如上A是B、C的共享类,B、C就要创建虚表
  • 父类存在虚表,子类对象就不需要单独创建了,直接放继承下来的

不要写菱形虚拟继承,太复杂了!!

四、底层原理

原理

结论:若程序满足多态条件,运行时,就会去指向对象的虚函数表中找到对应的虚函数地址,再根据虚函数地址调用对应的虚函数。指向父类调用父类的虚函数,指向子类调用子类的虚函数!

分析如下:

买票代码:

cpp 复制代码
class Person
{
public:
	virtual void Buytick() 
	{
		cout << "普通-全价" << endl;
	}
};
class Stu :public Person
{
public:
	virtual void Buytick()
	{
		cout << "学生-半价" << endl;
	}
};
void Func(Person& p)
{
	p.Buytick();
}
int main()
{
	Person p;
	Stu s;
	return 0;
}

来看看子类Stu的虚表是怎样的。

①子类会将父类的虚表内容拷贝一份放到自己的虚表中,如果子类重写了父类中的某个虚函数,那就会用自己重写后的虚函数去覆盖父类的虚函数(如上Buytick)。子类自己新增的虚函数依次按声明顺序放入虚表。

②当你将子类对象赋值给父类指针或者引用时,切出去的父类虚函数已经被子类的覆盖了。

综上可得:父类和子类的虚函数表是完全不一样的两张表即同类型的对象共用一张虚表,不同类型的对象有各自的虚表。正因如此,才能实现指向谁就调用谁。

多态调用与普通调用

①多态调用:满足多态条件以后的函数调用,不是在编译时确定的,而是程序运行起来以后到对象中找的。因为对象是在运行时才有的,而虚表指针又存在对象中。。

运行起来才确定所调用的函数行为,也称为动态绑定(晚期绑定)
**②普通调用:**不满足多态的函数调用,在编译时就确定了。根据调用对象的类型,确定调用函数。

在程序编译期间确定了程序的行为,**也称为静态多态,**比如:函数重载

五、抽象类

定义

在C++中的抽象类和Java不一样,C++的抽象类:在虚函数的后面写上 =0,该函数为纯虚函数!包含这种纯虚函数的类叫做抽象类

cpp 复制代码
//Car为抽象类
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};

特点

抽象类不能实例化出对象

②若子类没有重写纯虚函数,该子类也不能实例化出对象,重写后才可以实例化!但父类依旧不能实例化出对象

间接强制派生类重写虚函数

和override的区别就是:override是检查语法是否有问题,能实例化对象!抽象类不能实例化出对象!

看需求,若不希望父类实例化出对象,那就搞成抽象类!
①普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
②虚函数的继承是一种接口继承 ,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。
③如果不实现多态,不要把函数定义成虚函数。


好了,今天的分享就到这里,如果对你有帮助,欢迎三连,你的支持和认可就是我前进的动力!

相关推荐
Eiceblue2 分钟前
使用Python获取PDF文本和图片的精确位置
开发语言·python·pdf
xianwu54310 分钟前
反向代理模块。开发
linux·开发语言·网络·c++·git
xiaocaibao77716 分钟前
Java语言的网络编程
开发语言·后端·golang
Bucai_不才32 分钟前
【C++】初识C++之C语言加入光荣的进化(上)
c语言·c++·面向对象
木向34 分钟前
leetcode22:括号问题
开发语言·c++·leetcode
comli_cn36 分钟前
使用清华源安装python包
开发语言·python
筑基.42 分钟前
basic_ios及其衍生库(附 GCC libstdc++源代码)
开发语言·c++
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年12月认证C++三级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
雨颜纸伞(hzs)1 小时前
C语言介绍
c语言·开发语言·软件工程
J总裁的小芒果1 小时前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv