C++多态

目录

  • 多态的概念
  • 多态的定义及实现
  • 协变
  • 析构函数的重写
  • 通过一段代码理解多态
  • C++11 final 和 override
  • 重载、覆盖(重写)、隐藏(重定义)的对比
  • 多态调用原理
  • 单继承中的虚函数表
  • 抽象类
  • 多继承中的虚函数表

多态的概念

概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

多态的定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。 比如Student继承了Person。Person对象买票全价,Student对象买票半价。
只有在继承中多态才存在。

class person
{
public:virtual void buy(){cout << "全价" << endl;}
};
class student:public person
{
public:/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*//*void buy() { cout << "买票-半价" << endl; }*/virtual void buy(){cout << "半价" << endl;}
};
void buy(person& p)
{p.buy();
}
int main()
{person p;student s;buy(p);//全价buy(s);//半价return 0;
}

在继承中要构成多态有两个条件:

  1. 父子类完成虚函数重写

虚函数的重写(覆盖):子类中有一个跟基类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。

  1. 父类的指针或引用去调用虚函数
    在这里插入图片描述
    虚函数:即被virtual修饰的类成员函数称为虚函数。
    这里virtual关键字和菱形虚拟继承那里使用没有任何关系,只是关键字一样。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

协变

子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
协变是多态的一种特殊情况,三同中,返回值不同。

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

返回值只要是父子类的关系即可,子类返回的是子类对象的指针或引用,父类亦是如此。

析构函数的重写

析构函数建议重写为虚函数,原因如下代码说明:

class Person {
public:~Person(){cout << "~Person()" << endl;}
private:int p1;int p2;string ps1;
};
class Student : public Person {
public:~Student(){cout << "~Student()" << endl;}
private:int s1;int s2;string ss1;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

结果为:在这里插入图片描述
只执行了父类的析构,但是空间全部释放了,子类没有执行析构。如果子类中存在空间的申请,那么就会造成内存的泄露。

注意,析构函数并不释放空间,堆上的对象由free释放,栈上的由操作系统释放。

但是如果使用虚函数的重写,就会变为多态的调用,父类的指针,指向子类,那么就调用子类的析构。
为什么没有满足三同也可以执行呢?因为为了满足多态的需要,编译的时候析构函数的名称统一处理成destructor。这也是前期设计时经验不足,后期加上的规则。

所以正确的满足多态的写法是:

class Person {
public:virtual ~Person(){cout << "~Person()" << endl;}
private:int p1;int p2;string ps1;
};
class Student : public Person {
public:virtual ~Student(){cout << "~Student()" << endl;} 
private:int s1;int s2;string ss1;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

这样就满足我们的期望:指向父类调用父类析构,指向子类调用子类析构。所以如果是继承的话,建议析构函数都定义为虚函数。

通过一段代码理解多态

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};
class B : public A
{
public:virtual void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{B* p = new B;p->test();//p->func();//B->0,非多态调用,先在B中查找return 0;
}

执行结果是B->1。
分析:
首先,第一个结论:多态调用是指向谁调用谁,和类型无关,函数调用看类型。
p是B类型即子类类型的指针,指向子类p->test();去调用父类的test(),但是test()this类型是不变的,还是A*,在test()函数中,执行的是this->func()this的值是子类的地址,指向子类,但是是父类类型的指针。所有构成多态调用。所有调用子类的func()。因为this的类型是父类类型指针,所有使用父类的缺省值。
如下代码可以证明:

如果把main函数换为:

int main()
{A* p = new B;p->test();p->func();return 0;
}

执行结果为B->1和B->1。
第二次执行出B->1,便可以证明多态调用时,指针是父类型,函数就调用父类型的缺省值。也可以说多态调用调用父类型的缺省值。 因为不是父类型就不再是多态调用了。

结合两个结论,虽然p是父类型指针,但是指向的是B,所以调用B的函数实现。所以满足多态调用,所以p->func();输出B->1。

结合上述两个案例,最重要的结论就是,要牢记多态调用的条件,满足了虚函数重写,如果是父类型的指针调用了虚函数,那么就会产生多态调用。且指针指向谁调用谁

C++11 final 和 override

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能再被重写

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

上述代码表示Car中的Drive不能被重写,所以会报错
也可以用在类上面,表示不能被继承

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

上述代码表示,Car无法被继承。

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

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

若没有完成重写,就会报错。

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

在这里插入图片描述

多态调用原理

探究多态调用原理可以通过下列代码入手,输出是几呢?

class student
{
public:virtual void buy(int i = 200){cout << "半价" << i << endl;}
private:int a;char b;
};
int main()
{cout << sizeof(student) << endl;return 0;
}

输出结果为12
为什么呢?这就是虚函数和不同函数的区别,当类中存在虚函数的时候,编译器在编译阶段形成虚函数表,即使没有实例化对象,虚函数表也是存在的。对象实例化的时候,存在虚表指针指向虚函数表。每个虚函数的地址都会存在虚函数表中。虚函数表中存的是虚函数的地址。地址就是成员函数所在的地址。多态调用和普通调用都是同一个地址,只是多态调用多走了一个虚表。
在这里插入图片描述
因为多了一个虚表指针,且要满足对齐规则,并且是32位系统下,所以大小是12。

将代码改为这样:

class person
{
public:virtual void buy(int i = 100){cout << "全价" << i << endl;}
private:int a;
};
class student :public person
{
public:virtual void buy(int i = 200){cout << "半价" << i << endl;}
private:int a;char b;
};
void buy(person& p)
{p.buy();
}
int main()
{student s1;cout << sizeof(student) << endl;return 0;
}

student继承了person,其中有buy()这个虚函数
在这里插入图片描述
通过内存观察一下,子类中的vfptr就是虚表指针,指向的一张表就是虚表,虚表中存的是函数地址。可以看到蓝色框到的有student说明是student的虚函数,如果单独调用虚函数而不进行多态调用,可以发现使用的地址都是同一个地址。

每个对象都有自己的虚表指针,且指向自己类的虚表。同一类的对象,虚表使用的是同一个。虚表指针都指向同一个虚表。

在这里插入图片描述

通过上述了解,便可以明白多态调用的原理:
类似下图红色框住的:
在这里插入图片描述
当满足多态调用时,Student会进行切片,对于Func()来说都是父类,并不知道传进来的是什么,传入后,会在虚表中去找调用函数的地址,子类中虚表是子类虚函数的地址,父类中虚表是父类虚函数的地址。
于是就出现了前面说的,满足多态调用时,指向父类调用父类,指向子类调用子类的现象。
在Func()中调用时,多态的调用会转为一串汇编指令,指令就是将调用函数地址的值写入eax。
整个过程是比较复杂的,而是否符合多态的调用是在运行时确定的,并不是在编译时确定的。

单继承中的虚函数表

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;
};
int main()
{Base b;Derive d;return 0;
}

通过监视窗口看一下上述的代码:
在这里插入图片描述
可以发现Derive的窗口有些问题,虚表中没有func3()和func()4,这是编译器的处理的问题,实际上通过内存窗口观察一下。

可以了解的是,d的虚表中func1()还是指向的func1(),可以这样理解,虚表其实是重父类拷贝下来的,然后子类有重写的函数就进行覆盖。覆盖的概念就是在虚表这里,这是形象的说法。

在这里插入图片描述
我们可以看到func1()和func2()都在虚表中,但是另外两个疑似也是某个函数的地址。
可以通过下面的代码证明一下。

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;
};
int main()
{Base b;Derive d;void(**p)() = (void(**)())(*((int*)(&d)));(**p)();(**(p + 1))();(**(p + 2))();(**(p + 3))();return 0;
}

我们知道虚表中存的是函数的地址,那么虚表就可以看作是一个函数指针数组
而对象的第一个元素,即前四个字节存的是虚表的地址。那么我们就可以通过两次解引用去调用函数,看一下函数输出的内容。
那么如何拿到对象的前四个字节呢,我们知道只有相近类型才能够强制类型转换,Derive怎么转为int呢,显然没办法转。但是C语言存在一个类似BUG的行为,指针可以任意转换。
所以就可以取d的地址,然后强转为int*,然后解引用就可以拿到前四个字节了。但是此时具有的是int属性,即为整形。所以还需要一次强转,转为函数指针的地址。所以整个转化就是void(**p)() = (void(**)())(*((int*)(&d)));此时p是函数指针的地址,所以进行解引用就好了,一次解引用是函数指针,两次解引用就是函数自己了。然后调用即可。
输出即为
在这里插入图片描述
所以就证明了,虚函数表中存在另外两个函数。即得,只要是虚函数,就会写入虚函数表。

直接使用函数指针会比较麻烦,所以可以是使用宏,typedef void(*VFPTR)();
整体代码即为:

typedef void(*VFPTR)();
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;
};
void ptintfunc(VFPTR* vfpptr)
{for (int i = 0; i < 4; i++){printf("%p->", vfpptr[i]);(*vfpptr[i])();//printf("\n");}
}
int main()
{Base b;Derive d;VFPTR* pptr = (VFPTR*)(*(int*)(&d));ptintfunc(pptr);return 0;
}

抽象类

在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。**包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。**派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

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

假如基类为一个抽象的对象,没有什么特征可以描述便可以使用抽象类,这样使用抽象类的指针,便可以调用派生类重写的函数。

多继承中的虚函数表

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);Base2* ptr = &d;VFPTR* vTableb2 = (VFPTR*)(*(int*)ptr);//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

如上代码,Derive继承了Base1和Base2,那么子类会有两个虚表指针,指向两个虚表,重写会在两个虚表中都完成重写。但是类似func3()函数,在父类中没有的虚函数只会在第一个虚表中。如果在Base2中存在虚函数func3(),那么只会在第二个虚表中重写,第一个虚表中并不会存在。在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PosgreSQL比MySQL更优秀吗?

一日&#xff0c;一群开发者对PosgreSQL是不是比MySQL更优秀进行了激烈的辩论&#xff0c;双方吵的都要打起来了 正方有以下理由&#xff1a; PostgreSQL严格遵循SQL标准规范&#xff0c;相较MySQL在语法兼容性和功能完整性方面展现出更强的体系化设计&#xff0c;尤其在事务处…

『大模型笔记』Jason Wei: 大语言模型的扩展范式!

Jason Wei: 大语言模型的扩展范式! 文章目录 一. What is scaling and why do it?1. 什么是Scaling?2. 为什么要Scaling?二. Paradigm 1: Scaling next-word prediction1. 下一个词预测2. 极限多任务学习3. Why does scaling work?三. The challenge with next-word predi…

TCP协议(Transmission Control Protocol)

TCP协议&#xff0c;即传输控制协议&#xff0c;其最大的特征就是对传输的数据进行可靠、高效的控制&#xff0c;其段格式如下&#xff1a; 源端口和目的端口号表示数据从哪个进程来&#xff0c;到哪个进程去&#xff0c;四位报头长度表示的是TCP头部有多少个4字节&#xff0c;…

瑞萨RA-T系列芯片ADCGPT功能模块的配合使用

在马达或电源工程中&#xff0c;往往需要采集多路AD信号&#xff0c;且这些信号的优先级和采样时机不相同。本篇介绍在使用RA-T系列芯片建立马达或电源工程时&#xff0c;如何根据需求来设置主要功能模块ADC&GPT&#xff0c;包括采样通道打包和分组&#xff0c;GPT触发启动…

TraeAi上手体验

一、Trae介绍 由于MarsCode 在国内由于规定限制&#xff0c;无法使用 Claude 3.5 Sonnet 模型&#xff0c;字节跳动选择在海外推出 Trae&#xff0c;官网&#xff1a;https://www.trae.ai/。 二、安装 1.下载安装Trae-Setup-x64.exe 2.注册登录 安装完成后&#xff0c;点击登…

三层渗透测试-DMZ区域 二三层设备区域

DMZ区域渗透 信息收集 首先先进行信息收集&#xff0c;这里我们可以选择多种的信息收集方式&#xff0c;例如nmap如此之类的&#xff0c;我的建议是&#xff0c;可以通过自己现有的手里小工具&#xff0c;例如无影&#xff0c;密探这种工具&#xff0c;进行一个信息收集。以免…

DeepSeek-R1:通过强化学习激励大模型的推理能力

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; DeepSeek 第一代推理模型&#xff08;reasoning models&#xff09; &#xff08;所以缩写为 R1&#xff09;的设计和训练过程&#xff1a; 要理解 DeepSeek-R1 的创新之处&#xff0c;可以先阅读 如何训…

演绎推理及其与数学的关系介绍

演绎推理及其与数学的关系介绍 什么是演绎推理&#xff1f; 演绎推理&#xff08;Deductive Reasoning&#xff09;是一种逻辑推理方法&#xff0c;它从一般性的规则或前提出发&#xff0c;得出一个具体的、必然正确的结论。换句话说&#xff0c;只要前提&#xff08;Premise&…

【git】工作场景下的 工作区 <-> 暂存区<-> 本地仓库 命令实战 具体案例

&#x1f680; Git 工作区 → 暂存区 → 本地仓库 → 回退实战 Git 的核心流程是&#xff1a; &#x1f449; 工作区&#xff08;Working Directory&#xff09; → git add → 暂存区&#xff08;Staging Area&#xff09; → git commit → 本地仓库&#xff08;Local Repos…

胶囊网络动态路由算法:突破CNN空间局限性的数学原理与工程实践

一、CNN的空间局限性痛点解析 传统CNN的瓶颈&#xff1a; 池化操作导致空间信息丢失&#xff08;最大池化丢弃85%激活值&#xff09;无法建模层次空间关系&#xff08;旋转/平移等变换不敏感&#xff09;局部感受野限制全局特征整合 示例对比&#xff1a; # CNN最大池化示例…

如何下载AndroidStudio的依赖的 jar,arr文件到本地

一、通过jitpack.io 下载依赖库 若需要下载 com.github.xxxxx:yy-zzz:0.0.2 的 jar则 https://jitpack.io/com/github/xxxxx/yy-zzz/0.0.2/ 下会列出如下build.logyy-zzz-0.0.2.jaryy-zzz-0.0.2.pomyy-zzz-0.0.2.pom.md5yy-zzz-0.0.2.pom.sha1jar 的下载路径为https://jitpack…

Ubuntu中离线安装Docker

Ubuntu中离线安装Docker 前言 本教程将详细介绍如何在 Ubuntu 22.04 系统上&#xff0c;通过 .deb 包离线安装 Docker CE、Docker CE CLI 和 Docker Compose。 适用于无法访问互联网的环境。 准备工作 下载 .deb 包 在可以访问互联网的机器上&#xff0c;下载 Docker CE、…

【科研创新与智能化转型】AI智能体开发与大语言模型的本地化部署、优化技术

智能体&#xff08;Agent&#xff09;是指能够感知环境、自主决策并采取行动以实现特定目标的实体。它可以是一个软件程序、机器人或任何具备自主行为的系统。智能体的核心特征包括自主性、反应性、目标导向性和社会性。 智能体的主要特征 自主性&#xff1a;能够在没有外部干预…

【拒绝算法PUA】LeetCode 1287. 有序数组中出现次数超过25%的元素

系列文章目录 【拒绝算法PUA】0x00-位运算 【拒绝算法PUA】0x01- 区间比较技巧 【拒绝算法PUA】0x02- 区间合并技巧 【拒绝算法PUA】0x03 - LeetCode 排序类型刷题 【拒绝算法PUA】LeetCode每日一题系列刷题汇总-2025年持续刷新中 C刷题技巧总结&#xff1a; [温习C/C]0x04 刷…

6.2.4 基本的数据模型

文章目录 基本的数据模型 基本的数据模型 基本的数据模型包含层次模型&#xff0c;网状模型和关系模型。 层次模型&#xff1a;使用树型结构表示数据间联系。记录间的联系用指针实现&#xff0c;简单高效。但是只能表示1:n的联系&#xff0c;且对插入、删除的限制多。网状模型…

前端JS接口加密攻防实操

前端JS接口加密攻防实操 背景 在爬虫过程中&#xff0c;对数据接口各类加密的经历总结&#xff0c;无头消耗资源效率不高&#xff0c;采用浏览器兜底解密协程并行 青铜版(混淆对称加密|签名nonce等&#xff09; 解&#xff1a;根据API 调用栈&#xff0c;断点找到request参…

嵌入式AI(2)清华大学DeepSeek 01:从入门到精通

嵌入式AI(2)清华大学DeepSeek 01&#xff1a;从入门到精通

SpringBoot+Vue+数据可视化的动漫妆造服务平台(程序+论文+讲解+安装+调试+售后等)

感兴趣的可以先收藏起来&#xff0c;还有大家在毕设选题&#xff0c;项目以及论文编写等相关问题都可以给我留言咨询&#xff0c;我会一一回复&#xff0c;希望帮助更多的人。 系统介绍 在当今数字化高速发展的时代&#xff0c;动漫产业迎来了前所未有的繁荣&#xff0c;动漫…

Ubuntu 22.04.5 LTS 安装企业微信,(2025-02-17安装可行)

一、依赖包(Ubuntu 20.04/Debian 11) 点击下载https://www.spark-app.store/download_dependencies_latest 1、 下载最新的依赖包。 请访问星火应用商店依赖包下载页面, 下载最新的依赖包。2、解压依赖包 </

假面与演员:到底是接口在使用类,还是类在使用接口?编程接口与物理接口的区别又是什么?

前言&#xff1a;本篇文章解释了接口学习过程中的2个常见问题&#xff0c;一个是“为什么是类在使用接口”&#xff0c;另一个一个是“编程接口与物理接口的差异源于所处的抽象层次和交互模式的不同”&#xff0c;旨在揭示编程接口的本质。 Part1.是类在使用接口 当学习接口时…