当前位置: 首页 > article >正文

【C++第十六章】多态

【C++第十六章】多态

定义🧐

  多态通俗来说,就是多种形态,我们去完成某个行为时,不同对象完成时会产生出不同的状态。比如游乐园中,1.2米以上买票就需要买成人票,1.2米以下就可以买儿童票。

  多态是在不同继承关系的类对象,去调用同一函数,产生不同行为。我们通过下面代码来学习:

#include <iostream>
using namespace std;

class Person {
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
	}
};
class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
};
//多态条件
//1.虚函数重写
//2.父类的指针或者引用去调用虚函数

//虚函数重写要求
//父子继承关系的两个虚函数,要求同函数名、参数、返回

//virtual只能修饰成员
//三同的例外:协变->返回类型可以不同,但必须是父子类关系的指针或者引用
//派生类重写的虚函数可以不加virtual
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);
	
	return 0;
}

  student类继承了person类,并拥有同一函数BuyTicket,此时我们使用**父类的指针或者引用去调用该函数**就可以形成多态。

  总结一下,多态形成的条件为:1.虚函数的重写(由于一些特性,子类虚重写的虚函数可以不加virtual) 2.需要父类指针或者引用去调用该函数。而虚函数重写需要满足三同——函数名、参数、返回类型(除协变)都要相同。普通函数的继承是实现继承,虚函数的继承是接口继承

Pasted image 20240814120002

协变🔎

  协变是三同的例外,协变的返回类型可以不同,但必须是父子类关系的指针或者引用

Pasted image 20240813205432

子类virtual🔎

  子类重写的虚函数可以不加virtual,但父类必须加上virtual

Pasted image 20240813205612

  原因在于,父类指针只会调用父类析构,但是我们可能将父类指针指向子类,而delete由两部分构成——destructor(析构的统一处理,继承章节提到过)和operator delete,析构函数的名称由于多态被统一处理了,所以delete时会先调用析构再调用operator delete,在该代码中,我们想要p指向谁就调用谁的析构,此时就需要用到虚函数,而person和student满足父子关系,也有统一函数名destructor,此时只缺少virtual关键字,所以我们在析构加上virtual就可以变为多态,实现指向谁析构谁。

  不过在设计时,为了不让我们忘记给子类加上virtual而导致内存泄漏,所以统一设计,即使子类不写virtual也可以重写。

#include <iostream>
using namespace std;

class Person {
public:
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;

	return 0;
}

Pasted image 20240813214552

Pasted image 20240813214616

重载、重写(覆盖)、重定义(隐藏)🔎

Pasted image 20240814125718

小试牛刀🔎

Pasted image 20240814124514

  B类创建了一个指针,该指针指向test函数,而test是A类的成员,所以test的参数为A* this,内部为this->fucn(),而this是B,B与A是父子关系,满足虚函数重写,所以是多态调用,但虚函数重写是父类的实现,用的是父类的接口,所以val还是父类的值,则选B

final和override🧐

  final用于修饰虚函数,被修饰的虚函数不能被重写

Pasted image 20240814130212

  override修饰派生类的虚函数,可以检查是否完成重写,没有重写则会报错。

Pasted image 20240814130450

抽象类🧐

  在虚函数后面加上"=0",则这个函数为纯虚函数,含有纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象派生类继承后必须要重写纯虚函数,才能实例化对象。

  纯虚函数规范了抽象类必须要重写,也体现出接口继承。抽象类一般用于不需要实例化的对象,比如person类,我们没有赋予属性前这个类就可以看做是抽象的,当我们继承person类后,重写它的各种属性(身高年龄职业等),让其变为一个具体的对象再进行实例化。

Pasted image 20240814132812

虚函数表🧐

  C++会把虚函数存到虚函数表(_vfptr)中,所以会多开一个指针指向该表(本篇博客代码在32位环境下运行),虚函数编译后也存在代码段中,只不过会把虚函数的地址单独拿出来放在表中。

Pasted image 20240814174804

Pasted image 20240814175304

  虚函数的重写,也叫虚函数的覆盖,虚函数表也会进行覆盖。所以当我们传子类对象时,实际上是父类对子类的切片,通过子类虚函数表找到虚函数地址。

#include <iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func(){}

private:
	int a = 0;
};

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

private:
	int b = 1;
};

void Func(Person ptr)
{
	ptr.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	
	Func(p);
	Func(s);

	return 0;
}

Pasted image 20240814211250

  那么为什么可以用指针和引用,而不能用对象呢?首先引用的底层也是指针,所以它俩都是能指向子类对象中切割出来的父类。用对象也是子类切割出来的父类,成员拷贝给父类,但是不会拷贝虚函数表的指针,原因在于当出现父类=子类的情况时,不一定能调用到父类的虚函数。

Pasted image 20240814205451

  如果父类写了虚函数,子类没写虚函数,虚函数表的内容一样,但是存储在不同的位置,因为多开一个虚函数表不会耗费太多资源,从安全性考虑新开一个表更为保险。

Pasted image 20240814210906

  同一个类可以共用一张虚表。

Pasted image 20240814210537

  我们将虚表地址打印出来,发现虚表更靠近代码段,所以我们认为虚表是存在代码段中的。

Pasted image 20240816134602

  并且,所有的虚函数一定会被放进类的虚函数表中,我们以下面代码来说明:

#include <iostream>
using namespace std;

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func1" << 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;
	}
	void func5()
	{
		cout << "Derive::func5" << endl;
	}
private:
	int _b;
};
class X : public Derive
{
public:
	virtual void func3()
	{
		cout << "X::func3" << endl;
	}
};
//打印虚函数
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
	Base b;
	PrintVFT((VFUNC*)(*((int*)&b)));

	Derive d;
	PrintVFT((VFUNC*)(*((int*)&d)));

	X x;
	PrintVFT((VFUNC*)(*((int*)&x)));
	return 0;
}

  我们用监视窗口发现只有两个虚函数。

Pasted image 20240816144909

  但实际上用函数指针数组打印出虚函数地址,发现实际有四个,不过监视窗口给我们隐藏了。

Pasted image 20240816144939

  我们打印出虚函数地址的原理为,先取到Derive对象的地址,然后强转成int*只取前4个字节,也就是取到虚函数表,解引用拿到虚函数的地址,最后强转一下传过去。

Pasted image 20240816150251

  在多继承下,会有多个虚表存在。

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int _a1;
};

class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int _a2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
	
private:
	int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*((int*)&d)));

	Base2* ptr = &d; //自动切片
	PrintVFT((VFUNC*)*(int*)ptr);

	return 0;
}

Pasted image 20240817142309

  我们打印发现,重写的两个fun1地址不一样。

Pasted image 20240817160641

  我们用下面代码讲解:

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int _a1;
};

class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int _a2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
	
private:
	int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
    Derive d;
	Base1* p1 = &d;
	p1->func1();

	Base1* p2 = &d;
	p2->func1();
    return 0;
}

  我们从p1的汇编指令来看,首先p1所call的不是真正的地址,而是call到jmp,再由jmp跳到真正的地址,开始建立fun1的栈帧,而p2发现call和jmp以及func1的地址也不一样,并且会进行多段跳。

Pasted image 20240817153059

Pasted image 20240817153107

  原因在于func1所接受的是Derive* this,而this应该能够访问整个对象,所以我们需要修正p2让它指向Derive,如图2的ecx-8就是在修正p2,让其指向Derive对象,而p1恰好与this重叠,所以没有多段跳

结尾👍

  以上便是多态的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹


http://www.kler.cn/a/289128.html

相关文章:

  • 限流算法(令牌通漏桶计数器)
  • 九州未来再度入选2024边缘计算TOP100
  • 在 Ubuntu 上安装 `.deb` 软件包有几种方法
  • unity基础,点乘叉乘。
  • AtomicInteger 和 AtomicIntegerFieldUpdater的区别
  • 【计算机网络】Socket编程接口
  • 综合评价 | 基于层次-变异系数-正态云组合法的综合评价模型(Matlab)
  • Vulkan入门系列18 - 计算着色器(Compute Shader)
  • 阳台封窗是在保温上边还是把保温拆了之后封呢?
  • JsonCpp库的使用
  • SQL基础——MySQL的优化
  • SOHO建站
  • 【mysql】SQL语言的概述
  • java03
  • 深入探索Java中的分布式文件系统:从理论到实战
  • LeetCode_sql_day18(1841.联赛信息统计)
  • 维信小程序禁止截屏/录屏
  • React学习day03-components插件安装(仅基于火狐浏览器)、受控表单绑定、在React中获取dom、组件通信(组件间的数据传递)
  • 51单片机-串口通信关于SBUF的问题
  • elementui 表单 tab切换下个光标能不能改成enter键
  • 24数学建模国赛提供助攻(13——灰色系统理论)
  • 611.有效三角形的个数
  • 豆包MarsCode编程助手:让编程更简单
  • 七、场景加载
  • git中的分支是什么?分支有哪些好处?如何建立分支?
  • PyTorch Geometric(torch_geometric)简介