【C++笔记】多态的原理、单继承和多继承关系的虚函数表、 override 和 final、抽象类、重载、覆盖(重写)、隐藏(重定义)的对比

news2025/1/13 15:36:57

1.final关键字

引出:设计一个不能被继承的类。有如下方法:

class A
{
private:
	A(int a=0)
		:_a(a)
	{}
public:
	static A CreateOBj(int a=0)
	{
		return A(a);
	}
protected:
	int _a;
}
//简介限制,子类构成函数无法调用父类构造函数初始化
//子类的构造函数一定去调用父类的构造函数去初始化那一部分
//而父类的构造函数继承下来后为不可见
class B:public A
{}
int main()
{
	A aa=;//构造函数被私有化,报错
	//可以通过如下方法进行A类对象的创建
	A aa=A::CreateOBj(10);
}

以上的方法虽然可以实现我们的需求,但是会对类进行强制性的封装,所有我们在C++11中还有更好的方法,通过final关键字来实现。

  1. C++11中final声明表示为最终类,表明该类不能被继承。
  2. 修饰函数,限制函数不能被重写。
//修饰类
class A final
{
protected:
	int _a;
};
//直接限制报错,A不可被继承
class B:public A
{};
//修饰函数
class C
{
public:
	virtual void f() final
	{
		cout<<"C::f()"<<endl;
	}
}
class D:public C
{
public:
	//直接报错
	virtual void f(){}
}

2. override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

3. 重载、覆盖(重写)、隐藏(重定义)的对比

  • 重载:
    1、两个函数在同一作用域
    2、函数名/参数相同
  • 重写(覆盖):
    1、两个函数分别在基类和派生类的作用域
    2、函数名/参数/返回值都必须相同(协变例外)
    3、两个函数必须是虚函数
  • 重定义(隐藏):
    1、两个函数分别在基类和派生类的作用域
    2、函数名相同
    3、两个基类和派生类的同名函数不构成重写就是重定义

4.纯虚函数

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

class Student
{
public:
	virtual void show()=0;
};

5.抽象类

5.1概念

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承:本质上强制子类去完成虚函数的重写。

 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;
    }
 };

5.2接口继承和实现继承

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

6.多态的原理

6.1认识虚函数表

先来看一道经典的问题,sizeof(Base)是多少?

class Base
 {
 public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
 
private:
    int _b = 1;
 };

很多一部分的答案是4Byte,但实际正确答案是8Byte,这是怎么一回事呢?看Visual Studion下的监视窗口。如下图。
图1
结论:
1.除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
2.一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
继续深入:如果一个类的继承了该类,那么派生类中这个表放了些什么呢?

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
 class Base
 {
 public:
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }
 
    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
 
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
 
private:
    int _b = 1;
 };
 
class Derive : public Base
 {
 public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
 private:
    int _d = 2;
 };
 
int main()
 {
    Base b;
    Derive d;
 
    return 0;
 }

看Visual Studion下的监视窗口。如下图。
在这里插入图片描述
通过对监视窗口的分析可以得出如下结论:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在的部分,另一部分是自己的成员
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。 但是很多同学都是这样深以为然的。注意:虚函数表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚函数表中。另外对象中存的不是虚函数表,存的是虚函数表指针。那么虚表存在哪的呢?代码段! 感兴趣的同学可以用如下代码去打印一下虚表的地址,来验证一下。
typedef void(*VF_PTR)();

//void PrintVFTable(VF_PTR table[])
// 打印虚函数表中内容
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl << endl;
}
int main()
{
	Base b;
	PrintVFTable((VF_PTR*)(*(void**)&b));

	Derive d;
	PrintVFTable((VF_PTR*)(*(void**)&d));

	return 0;
}

7.在函数调用过程中,对象先找到虚函数表指针,通过该指针找到虚函数表,再在虚函数表中找到对应的函数指针,通过函数指针找到所需要的哪个函数。如下图所示。
在这里插入图片描述

6.2多态的原理

 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 Mike;
    Func(&Mike);
    
    Student Johnson;
    Func(&Johnson);
 
    return 0;
 }

在这里插入图片描述

  1. 观察上图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察上图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?单纯的对象的话所有的对象的虚函数表都是相同的,利用指针或者引用可以看作指向对象当中父类那一部分的成员或者指针。覆盖使其不同的虚表中,指向的虚函数不同,从而完成多种状态
  5. 看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
    !!!一图搞懂多态的原理
    在这里插入图片描述

7.多继承中的虚函数表

 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(*VFPTR) ();
 	void PrintVTable(VFPTR vTable[])
 	{
 		cout << " 虚表地址>" << vTable << endl;
 		for (int i = 0; vTable[i] != nullptr; ++i)
 		{
 			printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
 			VFPTR f = vTable[i];
 			f();
 		}
 		cout << endl;
 }
 int main()
 {
	Derive d;
 	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
 	PrintVTable(vTableb1);
 	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
 	PrintVTable(vTableb2);
	return 0;
 }

在这里插入图片描述
分析上述图片可以得出如下结论:
1、多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
2、多继承时,子类重写了Base1和Base2虚函数func1,但是虚函数表中重写的func1的地址却不一样,但是没关系,他们最终还是调到同一个函数。这是因为存在偏移量的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1104974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从零开始搭建第一个django项目

目录 配置环境创建 Django 项目和 APP项目组成  ‍子目录文件组成应用文件组成 配置 settings.py启动项目 数据表创建models.pyDjango-models的常用字段和常用配置 Django-admin 引入admin后台和管理员外键views.pyurls.pypostman接口测试 QuerySetInstance功能APIView 的概念…

Mistral AI发布一个拥有 73 亿参数模型Mistral 7B

导读法国人工智能初创公司 Mistral AI 宣布推出其首款大语言模型 Mistral 7B 是一个&#xff0c;号称是迄今为止同规模产品中最强大的语言模型&#xff1b;在 Apache-2.0 许可下开源&#xff0c;可完全免费使用&#xff0c;不受任何限制。 Mistral AI 是一个成立仅六个月的初创…

python实现TCPclient

python实现TCPclient是一件简单的事情&#xff0c;只要通过socket这个模块就可以实现。 一、实现步骤 1、导入模块&#xff1a; 首先&#xff0c;你需要导入Python的socket模块。 import socket2、创建Socket对象&#xff1a; 使用socket.socket()函数创建一个新的socket对…

美国开源数据库ScyllaDB完成4300万美元融资

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于美国位于加州桑尼维尔的开源数据库ScyllaDB今日宣布完成4300万美元融资。 本轮融资由风险投资公司Eight Roads Ventures和AB Private Credit Investors领投。其他投资者包括TLV Partners&…

视觉 注意力机制——通道注意力、空间注意力、自注意力

前言 本文介绍注意力机制的概念和基本原理&#xff0c;并站在计算机视觉CV角度&#xff0c;进一步介绍通道注意力、空间注意力、混合注意力、自注意力等。 目录 前言 一、注意力机制 二、通道注意力机制 三、空间注意力机制 四、混合注意力机制 五、自注意力机制 六、…

基于多尺度超图的特征对齐网络--细胞定位

Paper Link&#xff1a;Multi-scale Hypergraph-based Feature Alignment Network for Cell Localization Code&#xff1a;https://github.com/Boli-trainee/MHFAN/tree/main 核心思想&#xff1a;利用多尺度超图来统一解决定位任务中形状、尺度和颜色方面的显著变化带来的挑…

elementui select组件下拉框底部增加自定义按钮

elementui select组件下拉框底部增加自定义按钮 el-select组件的visible-change 事件&#xff08;下拉框出现/隐藏时触发&#xff09; <el-selectref"select":value"value"placeholder"请选择"visible-change"visibleChange">&…

网络工程师知识点7

111、IS-IS路由器的三种类型&#xff1f; Level-1路由器&#xff08;只能创建level-1的LSDB&#xff09; Level-2路由器&#xff08;只能创建level-2的LSDB&#xff09; Level-1-2路由器&#xff08;路由器默认的类型&#xff0c;能同时创建level-1和level-2的LSDB&#xff09;…

如何开发微信小程序

前言 因为最近沉迷和朋友们一起下班去打麻将&#xff0c;他们推荐了一个计分的小程序&#xff0c;就不需要每局都转账或者用扑克牌记录了&#xff0c;但是这个小程序不仅打开有广告&#xff0c;各个页面都植入了广告&#xff0c;用起来十分不适。 于是我就心里暗自下定决心&a…

蓝桥杯每日一题2023.10.18

题目描述 特别数的和 - 蓝桥云课 (lanqiao.cn) 题目分析 简单枚举每一个可行的数 #include<bits/stdc.h> using namespace std; int flag, ans; int main() {int n;cin >> n;for(int i 1; i < n; i ){flag 0;int x i;while(x){int y x % 10;if(y 2 || y…

NewStarCTF2023week3-Rabin‘s RSA

根据题目提示是Rabin算法 先将N分解得到P和Q 导入e&#xff0c;n&#xff0c;p&#xff0c;q&#xff0c;c 使用Rabin算法直接计算明文&#xff0c;再将明文转字符串即可 我们也可以通过脚本来理解原理 import gmpy2 import libnump 13934102561950901579 q 144504527390…

全自动打包机检测不到货物怎么办?

全自动打包机也称无人化打包机或无人化捆扎机&#xff0c;是指在工作的过程中不需要人工干预&#xff0c;机器可以自动检测、自动输送、自动打包的设备。但最近有一些客户反映打包机不能自己检测到货物了。这是为什么呢&#xff1f; 1、电子眼感应太弱。电子眼的感应程度是可以…

棋盘覆盖问题(分治法)

裁判测试程序样例&#xff1a; #include <iostream> #include<fstream> #include <iomanip> #define MAX 1025 using namespace std; int board[MAX][MAX]; int tile1;void ChessBoard(int tr,int tc,int dr,int dc,int size);int main() { int dr,dc,size;…

GeoServer改造Springboot启动三(集成jdbcconfig和jdbcstore)

1、集成jdbc插件 1.1 由于GeoServer所有数据都在数据目录下,如果需要将数据存储数据库,这需要引入如图 13所示的两个插件。 图 13jdbc插件位置 1.2 右键两个插件的“pom.xml”,选择“Add as Maven Project”,模块就会加入maven关联。 图 14jdbc右键“Add as Maven Proje…

python代码调用文件或数据库中保存的脚本

这里采用的读取excel 1、先写一个测试方法 def demo5():import xlrdimport randomwb xlrd.open_workbook("code.xls")st wb.sheet_by_index(0)code st.cell_value(0, 0)list ["6666", asd, 1ad23, 1f23, 12g3, 1b3, 12r3]code2 st.cell_value(0, 1)…

Mendix 创客访谈录|用移动审批和三维可视化打造客户满意的高逻辑应用

本期创客 朱成 能科瑞元数字技术有限公司 Mendix业务部 朱成任职于能科瑞元数字技术有限公司Mendix业务部&#xff0c;主要负责工业互联网软件开发实施。本人主要负责Mendix Teamcenter集成开发、Mendix应用实施部署以及低代码平台技术支持。作为Mendix实施开发工程师参与了中…

iOS 借助定位实现“保活”策略

疑惑 你是否有过类似的体验,当你刚刚来到一个商业区,命名没打开任何APP,手机就会收到push给你推荐周围的“吃喝玩乐”,那他们又是怎么做到的呢? ##解密 其实,我们可以通过监听当位置变化,在用户无感知的情况下在后台悄悄拉齐我们的进行来处理特定的逻辑。 不是感觉很…

智能合同和TikTok:揭示加密技术的前景

在当今数字化时代&#xff0c;智能合同和加密技术都成为了技术和商业世界中的热门话题。它们代表了一个崭新的未来&#xff0c;有着潜在的巨大影响。 然而&#xff0c;你或许从未想过将这两者联系在一起&#xff0c;直到今天。本文将探讨智能合同和TikTok之间的联系&#xff0…

maven构建jar包运行后出现中文乱码问题解决

问题描述&#xff1a; 最近在接手一个坑时&#xff0c;发现本地打出来的jar包&#xff0c;到环境中运行之后总是出现中文乱码问题&#xff0c;但又不是全部中文乱码。经过排查发现&#xff0c;只有写在代码里的中文返回到前端之后出现了乱码。再通过解压打出来的jar包&#xff…

使用目标之间的先验关系提升目标检测器性能

今天跟大家分享阿姆斯特丹大学等提出的用于提升目标检测和实例分割性能的新方法RP-FEM&#xff0c;该方法将目标之间位置的先验关系融入到feature中。 论文标题&#xff1a;Relational Prior Knowledge Graphs for Detection and Instance Segmentation机构&#xff1a;阿姆斯特…