多态——C++

时间:2024-04-15 15:17:54

这里写目录标题

  • 衔接继承总结
  • 继承和组合
    • 白箱复用
    • 黑箱复用
  • 多态的概念
  • 多态的定义以及实现
  • 虚函数重写的两个例外
    • 协变
    • 面试题
    • 析构函数的重写
  • final
  • voerride
  • 重载
  • 隐藏(重定义)
  • 重写(覆盖)
  • 抽象类
    • 什么是抽象类?
  • 实现继承和接口继承
  • 多态的原理
    • 虚函数表
  • 那多态的调用是怎么实现的呢?
  • 动态绑定和静态绑定
    • 切片的区别。
    • 多态的两种形态?
  • 单继承中的虚函数表
    • 虚表存在哪个区域?
  • 多继承中的虚函数表

衔接继承总结

继承的概念和定义,基类和派生类对象赋值转换,这点可以去学学。

一般子类继承级就用公有继承。

继承中的作用域就了解一下就OK。

虚拟继承半了解就行。
我们自己设计尽量不要用菱形继承。但是可以用多继承。

继承和组合

继承是有复用组合是相互独立

优先使用对象组合

白箱复用

在这里插入图片描述
继承这种通过生成派生类的复用称为白箱复用。因为基类的内部细节对子类可见,这样这一定程度破坏了封装。基类的改变,对派生类影响很大。派生类和基类的依赖关系很强,耦合度高。

黑箱复用

黑箱复用对象的内部细节不可见。没有很强的依赖关系,耦合度低
实际中多去使用组合,耦合度低,代码维护性好。

学软件工程一定要画UML图
要学会画类图

C只要不修改公有.就不会影响D。在这里插入图片描述
对象组合是类继承之外的另一种复用的选择。
C对象公有成员D不能直接用。
C对象保护成员D不能直接用。

多态的概念

多态就是:同一个对象去调用同一个函数时会产生不同的效果

多态的关键字是:virtual
在函数面前加上virtual就实现了多态。

构成条件
1.是虚函数+virtual
2.函数名/参数/返回值相同,构成重写,重写也叫作覆盖

注意:要和继承隐藏区分开,隐藏是没有+virtual。

多态有两个要求;
1.子类虚函数重写父类的虚函数。(重写就是三同+virtual)
(三同就是参数,返回值,函数名相同)
2.必须用父类指针或者引用去调用虚函数

为什么要用父类的?不能用子类的?因为只有父类的话,传参的话,符合对象赋值兼容的规则

不能用普通对象调用。

假如用普通对象调用,调用的都是普通对象的,也就是普通人的票
这点为什么不同呢?可以看下面的多态的实现原理。

多态的定义以及实现

怎么让不同的对象调用不同的函数,这里涉及到对象赋值转换,切割

#include <iostream>
using namespace std;
//普通票
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票——全价" << endl;
	}
};
//学生票 继承普通人
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票——半价" << endl;
	}
};
//教师票 继承普通人
class Teacher : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票——免费" << endl;
	}
};

void Pay(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person p;
	Student s;
	Teacher t;
	Pay(&t);

	return 0; 
}

虚函数重写的两个例外

协变

这个是C++的缺陷。

首先满足协变,需要是对象

Student继承了Person,假如返回值不同,三同中的返回值不同。
子函数需要加virtual,这是好习惯。

class A
{

};
class B :public A
{

};
class Person
{
public:
	virtual A* f()
	{
		cout << "virtual A* Person::f()" << endl;
		return new A;
	}
};
class Student : public Person
{
public:
	virtual B* f()
	{
		cout << "virtual B* Student::f()" << endl;
		return new B;
	}
};

面试题

接口继承。
子类 继承 重写父类虚函数
把virtual继承了下来,缺省参数也继承了下来。

1.接口继承(所以B中func不写virtual也是虚函数,复合多态条件,缺省参数也是用的A::func的1)

2.重写的函数实现。

析构函数的重写

析构函数+virtual构成重写,析构函数的名字统一会被改成destructer

基类的析构函数一般要定义为虚函数,定义为虚函数可以完成析构函数的重写。这样可以不容易导致析构的时候发生错误。

final

final中文叫做最后,可以理解为:最后的类。
1.在函数后面添加final关键字,修饰虚函数,表示该函数不能被重写
2.在基类的后面加final,表示该类不能被继承

voerride

override是写在子类中的,用于检查子类是否完成了重写,如果没有完成重写,就会报错
比如:
1.忘记加了virtual
2.函数仓鼠不同。

重载

1.两个函数在同一作用域
2.函数名/参数不同

隐藏(重定义)

隐藏针对的是两个普通函数
1.两个函数分别在基类和派生类的作用域
2.函数名相同
3.两个基类和派生类的同名函数不构成重写就是重定义(隐藏)。

重写(覆盖)

1.两个函数分别在基类和派生类的作用域
2.函数名/参数/返回值必须相同(协变除外)
3.两个函数必须是虚函数。

抽象类

概念:C++中的包含有纯虚函数的类就叫做抽象类抽象类的不能实例化处对象

子类继承抽象类,必须重写虚函数,才能实例化对象。

什么是抽象类?

抽象的意思就是:在现实中一般没有对应的实体
举个例子:

不能实例化出来对象。比如在学校中,不是具体的实体,而老师学生是具体的实体。

所以就是抽象类

抽象类间接要求子类必须重写,才能实例化对象

实现继承和接口继承

实现继承:普通函数的继承是实现继承。继承的是函数的实现,也就是函数体。

接口继承:虚函数的继承是接口继承,继承的是虚函数的参数/函数名/返回值继承的是接口,目的是为了重写,达成多态

所以不重写不实现多态,就不要虚函数

多态的原理

多态就是实现指针指向哪儿,调用哪的。
怎么实现的呢?

虚函数表

虚函数表里面存放的是虚函数的指针

父类的对象模型是:
1.指向虚函数表的指针
2.成员变量

子类的对象模型:
1.从父类继承下来的指针和变量
2.自己的变量

不同的是子类的虚函数表,在子类的虚函数表中,继承下来的重写的函数不一样

子类中重写的函数覆盖了父类的函数。

这就说明重写:是语法层的概念,对函数实现进行了重写。
原理层的概念:就是子类的虚表,拷贝父类的虚表,然后覆盖重写的那个虚函数

那多态的调用是怎么实现的呢?

多态调用的实现,依靠运行时决议,去指向对象的虚表查看调用函数的地址

普通函数的调用:编译时决议,编译时确定调用函数的地址


指向谁我在谁的虚表里找到调用的。
父类赋值给子类对象,也可以切片,为什么实现不了多态?

为什么对象实现不了多态。只有指针和引用可以实现

原理:编译器很死板,在编译的时候就确定了,编译器在检查时发现不构成多态

怎么才能让对象可以支持多态?对象是不可以的。

动态绑定和静态绑定

1.在程序编译期间确定了程序的行为,
2.构成多态就叫做动态绑定,也叫晚绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数

切片的区别。

对象的切片是把子类中父类的值,拷贝过去,自定义类型是深拷贝。
但是不拷贝虚表指针,因为都指向了同一个虚函数表。导致混乱,不知道调用谁。

对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针,假如拷贝就混乱了,父类对象中到底是父类的虚表指针,还是子类的虚表指针?到底该调用谁就混乱了。

但是多态是指向谁调用谁,这不就混乱了,父类指向子类调用的还是子类。所以对象不可能实现多态。

在这里插入图片描述在这里插入图片描述

但是引用和指针切片的话就是直接指向父类或引用父类的那一部分,不存在拷贝的苦恼

多态的两种形态?

静态的多态:函数重载。
为什么有静态的多态,重载实际是在编译期间,根据函数名修饰规则找到不同的函数。

动态的多态:就是上面所讲的。是在运行时候去指向的虚函数表找,而实现的两种形态。

在这里插入图片描述

单继承中的虚函数表

虚函数理论而言要进虚表,但如果只有子类有的虚函数,监视窗口只有父类的虚表,难道子类只有的虚函数不进虚表吗?编译器规定,虚函数都要进虚表的,但是子类一般是拷贝父类的的虚表,然后重写需要重写的虚函数进行覆盖。因为监视窗口一般都是骗人的。但在内存中可以看到。

为了验证这个现象,我们可以写一段程序来打印虚表

虚函数表是一个数组,数组里的每一个元素是函数指针。

这里说明vs的监视窗口看到的虚函数表不一定是真实的,可能会被处理过。

虚表存在哪个区域?

同一个类型的对象,共用一个虚表。
在这里插入图片描述

所以虚表存在的区域只有静态区(数据段)或常量区(代码段)
那么到底在哪个区域呢。
常量区
常量区更加合理,因为静态区一般是全局变量.

我们也可以打印来验证,会发现虚标的地址和常量区的地址最为接近

多继承中的虚函数表

多继承中虚函数表中虚函数的位置

派生类的未重写的虚函数会放在继承的第一个基类部分的虚函数表。

在这里插入图片描述