IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++—多态 -> 正文阅读

[C++知识库]C++—多态

一、多态的概念

多态就是多种形态,不同的对象去完成会产生不同的效果!

静态的多态

函数重载,看起来调用同一个函数,却有着不同的行为
原理:编译时实现

动态的多态

一个父类对象的引用或者指针去调用同一个函数,传递不同的对象,会调用不同的函数
原理:运行时实现

二、多态的定义及其实现

2.1 构成多态的两个条件

  • 必须通过父类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且子类必须对父类的虚函数完成了重写

2.2 虚函数

virtual关键字修饰的类成员函数就是虚函数

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

2.3 虚函数的重写

虚函数的重写也叫做覆盖:子类中有一个与父类完全相同的虚函数,他们两个的虚函数满足三同返回值函数名参数列表完全相同,则称子类的虚函数重写了父类的虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);//这行传参会完成切片
	return 0;
}

这里就完成了我们的多态,如果传参传的是父类对象,就会调用父类的函数;如果传参传的是子类对象,就会调用子类的函数。
在这里插入图片描述
本质:不同的人做相同的事情,结果不同!

注意

  1. 虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
  2. 在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数的属性,其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual,虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写,那么他也算是虚函数。
//不建议这样使用
void BuyTicket() { cout << "买票-半价" << endl; }

在这里插入图片描述

1??构成多态,跟p类型没有关系,传参传的是哪个类型的对象,调用的就是哪个类型对象中的虚函数——(跟对象有关)
2??不构成多态,调用的就是p类型函数——(跟类型有关)

void Func(Person p)//此处不是指针或者引用
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

结果:
在这里插入图片描述
这里就没有构成多态了!
在这里插入图片描述
在这里插入图片描述

虚函数重写的两个例外

  1. 协变
    父类与子类的虚函数构成重写,返回值相同有个例外,即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,称为协变。
class Person {
public:
	virtual Person* BuyTicket() { cout << "买票-全价" << endl; return  nullptr; }
};
class Student : public Person {
public:
	virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
  1. 析构函数的重写
    析构函数是虚函数,是否构成重写?——构成
    因为析构函数名被特殊处理成了destructor
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person p;
	Student s;
	
	return 0;
}

结果:
在这里插入图片描述
下面再来看看这个代码:

class Person {
public:
	 ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	 ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* p1=new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
	
	return 0;
}

这份两个函数并没有构成多态,我们析构的时候,想要父类调用父类的析构,而子类调用子类的析构,而并没有构成多态,就会跟类型有关,p1 p2都是person类型的,都会去调用子类的析构函数。万一子类对象中有些动态开辟的资源,没有被释放,就会很危险!
在这里插入图片描述
构成多态即可解决这个问题!重写里面的虚函数,加上virtual关键字:
在这里插入图片描述

2.4 C++11 final 和 override

1.final

限制类的继承
修饰虚函数表示该虚函数不能被重写

如何设计一个类无法被继承?
可以父类的构造函数私有(private)

class A
{
private:
	A(int a)
		:_a(a)
	{}
public:
	static A CreateObj(int a=10)
	{
		return A(20);//创建一个对象出来
	}
protected:
	int _a = 10;
};
// 间接限制,子类的构造函数无法调用父类的构造函数初始化,没办法实例化出对象,因为是private私有权限
class B :public A
{

};
int main()
{
	A aa=A::CreateObj(100);
	// A类的构造函数是私有的,类内能用,但是在类外却不能实例化
	// 所以调用一个公共的函数接口,来实例化
	return 0;
}

以上的方法太过复杂,C++11新增加了一个语法,final关键字

// 直接限制
class A final
{
protected:
	int _a = 10;
};
class B :public A
{

};

限制虚函数的重写

class C 
{
public:
	virtual void f() final
	{
		cout << "C::f()" << endl;
	}
};

class D :public C
{
public:
	virtual void f()
	{
		cout << "D::f()" << endl;
	}// 无法重写final函数
};

2.override

放在子类重写的虚函数的后面,检查是否完成重写,没有重写就报错

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

2.5 重载 重写 隐藏对比

特别爱考

在这里插入图片描述

三、 抽象类

3.1 抽象类的概念

虚函数的后面写上=0,则这个函数就是纯虚函数。包含纯虚函数的类就叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数一般只声明,不实现,因为没有价值。
子类继承后也是不能实例化出对象的,只有重写纯虚函数。纯虚函数规范了子类必须重写,另外纯虚函数更体现了接口继承。

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

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

void Test() 
{ 
Car* pBenz = new Benz; 
pBenz->Drive(); 
Car* pBMW = new BMW;
pBMW->Drive(); 
} 

为什么这里的指针可以调用?而对象调用就会崩溃?
要用后面的原理来解释!

对比纯虚函数与override

1. 纯虚函数的类,本质上强制子类去完成虚函数的重写
2. override只是语法上检查是否完成重写

3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,从而达成多态,所以如果不实现多态,不要把函数定义成虚函数。

四、多态的原理

4.1 虚函数表

常考笔试题:sizeof(Base)是多少呢?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char c = 'A';
};
int main()
{
	cout << sizeof(Base) << endl;// 12
	return 0;
}

我们观察发现是12字节!除了显示的对象成员(_b,c)以外,还有一个_vfptr放在对象的前面,(有了虚函数,就会多存在这个指针)这个指针我们叫做虚函数表指针,简称虚表指针——virtual function pointer
在这里插入图片描述
一个含有虚函数的类中,都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
如下:含有多个虚函数
在这里插入图片描述
具体展示:
在这里插入图片描述

4.2 多态的原理

原理
8页的图片
模型解析:我们可以看到,student有两部分构成,一部分是从父类person中继承的,另外的是自己剩下的一部分。并且父子的虚表是各不相同的,分别存储了自己的虚函数,其中子类继承的虚函数如果重写了,就会被覆盖。

多态原理总结:基类的指针或者引用,调用谁,就去谁的数函数表中找到对应的位置的虚函数,就实现了对应的多态的功能。
如上述所示:
传递的是Mike对象,就直接去父类虚表中寻找该虚函数的地址
传递的是Johnson对象,先会完成切片操作,相当于现在是子类中父类那部分对象的别名,再去子类中继承自父类中去寻找该类中重写的虚函数的地址,进而完成调用,实现多态。

那么为什么必须是父类的指针或者引用?对象不行呢?
我们知道普通的函数调用,都是编译时确定地址,而虚函数的调用是运行时确定地址。
传入的是对象:就是传入的值,是值拷贝
我们再来看看对象的引用r1和对象本身p,切片过后内存布局,如下图所示:
我们注意到,引用的r1其实就是Johnson中继承自分类那部分对象的别名,里面虚表是一样的
对象切片的时候,我们会把值(注意_a变量)给拷贝过去,不会把子类的虚表指针给赋值过去,如果赋值成功,父类的虚表指针会指向子类的虚表,那么多态就不可能实现了。
在这里插入图片描述
易错:同类型的对象,虚表指针是相同的,指向同一张虚表
如下所示,三个同类型的person对象都指向同一张虚表。
在这里插入图片描述
易错:普通函数和虚函数的存储位置是否一样?
答案:一样的!都是在代码段
只不过虚函数要把地址存一份到虚表中,方便实现多态

注意:如果子类的虚函数是私有的,那么也是能够实现多态的。
从语法上来看,重写是一种接口继承,继承父类的接口,重写函数体内容,编译器检查不出来,因为是运行时,去p指向对象的虚表中,找到虚函数的地址,所以私有的限制不起作用。
在这里插入图片描述
总结
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
派生类的虚表生成:
a.先将父类中的虚表内容拷贝一份到子类虚表中
b.如果子类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数
c.子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。

在这里插入图片描述
我们观察虚函数的地址时,发现虚表中存放的地址和函数真实的地址是不一样的,那是因为VS对此作了处理
在这里插入图片描述

4.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

五、单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("vft[%d]:%p\n",i,table[i]);
	}
}
int main()
{
	Base b;
	PrintVTable((VF_PTR*)(*(int*)&b));

	Derive d;
	return 0;
}

思路:取出b对象的头4bytes,就是虚表的指针

  1. 先取b的地址,强制类型转换为int*的指针
  2. 在解引用取值,就取到了b对象头4bytes,这个值就是指向虚表的指针
  3. 再强转成VF_PTR*,因为虚表就是一个存放VF_PTR虚函数指针类型的数组

在这里插入图片描述

5.2 多继承中的虚函数表

// 多继承中的虚表
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("vft[%d]:%p\n",i,table[i]);
	}
}
int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	PrintVTable((VF_PTR*)(*(void**)&d));
	PrintVTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
	return 0;
}

在这里插入图片描述

5.3 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 18:48:02  更:2022-06-29 18:50:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年3日历 -2024/3/29 1:10:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码