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

C++:多态(协变,override,final,纯虚函数抽象类,原理)

目录

编译时多态

函数重载

模板

运行时多态

多态的实现

实现多态的条件

协变

析构函数的重写

override 关键字

final 关键字

重载、重写、隐藏对比

纯虚函数和抽象类

多态的原理


多态是什么?

多态就是有多种形态

多态有两种,分别是编译时多态(静态多态)、运行时多态(动态多态)

编译时多态

函数重载

函数重载就是其中的一种多态

void print(int i) {  
    cout << "Printing int: " << i << endl;  
}  
  
void print(double f) {  
    cout << "Printing float: " << f << endl;  
}  

我们用一个print函数就可以实现上面的两种状态,由于是编译时完成的多态所以叫做编译时多态 

模板

模板也是其中的一种多态

template<class T>  
void print(T value) {  
    cout << value << endl;  
} 

 这里我们可以给print函数传任意类型的参数,这里也可以体现出多态

在我们传参时在编译时期就会生成对应的函数,所以它也是编译时多态

运行时多态

运行时多态就是我们需要完成某一个任务(函数),那么当我们传不同的对象时所能产生的效果是不一样的

例如:

当我们买火车票的时候,普通人买票是全价,学生买票可以打折,如果是军人那甚至可以优先买票

当我们在完成买票这个过程的时候,不同的人来买票所产生的结果是不一样的,这就是多态

下面来具体解释运行时多态

多态的实现

多态的构成

首先多态是一个继承关系下的对象去调用同一个函数,从而产生了不同的行为

实现多态的条件

  • 被调用的函数必须是虚函数
  • 必须是指针或者引用调用虚函数
  • 派生类需要对基类的虚函数进行重写/覆盖(只有这样才会两个相同的函数有不同的行为)
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

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

void func(Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person p;
	Student s;

	func(&p);
	func(&s);

	return 0;
}

首先Student和Person是继承关系

其次基类Person所需要实现多态的函数是虚函数

子类里面的函数可以写virtual也可以不写,因为Person继承下来后无论写不写本身都是虚函数

为了能有不同的行为,我们需要重写该函数

调用函数的时候使用的是指针或者引用

这样我们多态的条件就全部都有了,下面只需要传相应的对象就可以完成对应的行为了

协变

当派生类重写基类的虚函数时,派生类和基类的返回值不同

基类虚函数返回其它基类对象的指针或者引用,派生类虚函数返回其它派生类对象的指针或者引用,称为协变

class A {};
class B : public A {};

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

class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(&ps);
	Func(&st);

	return 0;
}

析构函数的重写

class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;

	return 0;
}

如上代码A类的析构函数和B类的析构函数其实是构成重写的

看起来两个类的析构函数名字不相同,但是在编译器进行处理的时候会统一将析构函数的名字处理成destructor 

所以即使我们是两个A类型的指针,它们new出来的对象如果不构成多态是不会调用B的析构函数的,只有构成多态,A对象才会调用A的析构,B对象调用B的析构

这里已经满足了三个条件:

A和B的继承关系

析构函数为虚函数

指针或引用调用虚函数 

如果这里不构成多态那么就会发生内存泄漏的问题! 

override 关键字

由于C++对重写的要求较为严格,因此C++11提供了override关键字

它的作用是可以帮助我们检查是否重写

class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	~B() override
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

若是把A的virtual给去掉,则构不成重写

final 关键字

如果我们不想让派生类重写这个虚函数,那么我们可以加上关键字final

一旦我们试图对基类某个虚函数带上了final关键字进行重写,那么则会报错

class A
{
public:
	virtual ~A() final
	{
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

重载、重写、隐藏对比

纯虚函数和抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数

class Person
{
public:
	virtual void BuyTicket() = 0;
};

纯虚函数不需要定义实现

因为实现没有意义,需要被派生类重写。只需要声明即可 

这个时候的这个Person类叫做抽象类

有纯虚函数的类就是抽象类

抽象类是不可以定义出对象的 

如果派生类继承后不重写虚函数,那么该派生类也是抽象类

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就无法实例化出对象

多态的原理

当一个类中有虚函数时,这个类是会多出一个成员指针变量__vfptr,我们把它叫做虚函数表指针

v代表virtual,f代表function,ptr则是指针

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
private:
	int _age;
	char _name[20];
};

int main()
{
	Person p;
	cout << sizeof(p) << endl;

	return 0;
}

从上面的实验可以看出,类中是会有一个虚函数表指针的,除了age和name占24个字节以外,还多了一个虚函数表指针占了4个字节,所以是28个字节

从底层的角度来想,为什么我们满足了多态的条件后,可以通过Person指针所指向的对象来精确的找到我们要使用的函数呢? 

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
private:
	int _age;
	char _name[20];
};

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

int main()
{
	Person p;
	Student s;

	return 0;
}

通过上图得知

当我们满足多态的条件时,底层不再是编译时通过对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数地址,所以就能实现我们是哪个对象就调用哪个对象的类中的虚函数

这也是为什么它是运行时多态的原因

虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址
  • 派生类会继承基类的虚函数表指针,但两者并不是同一个虚函数表指针
  • 派生类的虚函数表中包含 
  1. 基类的虚函数地址
  2. 派生类重写的虚函数地址
  3. 派生类自己的虚函数地址
  • 派生类重写基类的虚函数后,派生类的虚函数表里对应的虚函数就会被覆盖成派生类重写的虚函数地址
  • 虚函数和普通函数一样,都是存在代码段中的,只是虚函数的地址又存在虚表中
  • 虚函数表存在哪个地方并没有严格的规定,由编译器自己决定,具体在哪里我们可以依靠代码验证

验证代码: 

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

	void func()
	{
		cout << "void func()" << endl;
	}
private:
	int _age;
	char _name[20];
};

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

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	Person b;
	Student d;
	Person* p3 = &b;
	Student* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Person::BuyTicket);
	printf("普通函数地址:%p\n", &Person::func);
	return 0;
}


http://www.kler.cn/news/316103.html

相关文章:

  • 10 vue3之全局组件,局部组件,递归组件,动态组件
  • 博睿谷IT认证-订阅试学习
  • 利用H5无插件播放RTSP流的实现方案
  • Vue3 路由传参:玩转 params,让页面交互更流畅!
  • 什么是堡垒机?运维为什么需要堡垒机?
  • ES 索引或索引模板
  • 【图像匹配】基于SIFT算法的图像匹配,matlab实现
  • ECMAScript与JavaScript的区别:深入解析与代码实践
  • 出厂非澎湃OS手机解BL锁
  • STM32篇:通用输入输出端口GPIO
  • 智谱清影的魅力:使用CogVideoX-2b生成6秒视频的真实体验!
  • 信息安全工程师(10)网络信息安全法律与政策文件
  • jvm中的程序计数器、虚拟机栈和本地方法栈
  • Spring8-事务
  • git安装geographiclib失败解决办法
  • GPT对话知识库——编写IIC驱动的过程
  • 位图与布隆过滤器
  • docker minio启动命令
  • ARM/Linux嵌入式面经(三六):中科曙光
  • Docker:安装Apache Pulsar 消息队列的详细指南
  • Python 课程16-Pygame
  • LabVIEW软件维护的内容是什么呢?
  • [2025]基于微信小程序慢性呼吸系统疾病的健康管理(源码+文档+解答)
  • 【数据结构与算法 | 灵神题单 | 栈基础篇】力扣155, 1472, 1381
  • 微信小程序03-页面交互
  • vue3中使用iframe不成功的问题
  • 逻辑回归 和 支持向量机(SVM)比较
  • 【深入理解SpringCloud微服务】了解微服务的熔断、限流、降级,手写实现一个微服务熔断限流器
  • 【spring】引入 Jackson 依赖 对java对象序列号和反序列化
  • 基于单片机的智能温控风扇系统的设计