【C++】13.多态
创始人
2025-05-30 01:59:24

1.多态的概念

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

不同的状态

2.多态的定义及实现

1°条件

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2°虚函数

即被virtual修饰的类成员函数称为虚函数。

#include 
using namespace std;class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:int _a;char _ch;
};//成员函数加virtual->虚函数
//普通函数不行int main()
{cout << sizeof(Person) << endl;//要多算 会有多的指针return 0;
}

3°虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚

函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函

数。

//虚函数
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)//指针或者引用都可以 如果传值都会调用全价 如果不是virtual也会调用两个全价 两个函数构成隐藏
{p.BuyTicket();//指针就用箭头
}//1.满足多态的条件:跟对象有关 指向哪个对象就调用它的虚函数
//2.不满足的多态的条件:对类型有关 调用的类型是谁 调用就是谁的int main()
{Person ps;Student st;Func(ps);Func(st);//调的是不同的函数 因为传的对象不一样return 0;
}

  • 满足多态的条件:跟对象有关 指向哪个对象就调用它的虚函数

  • 不满足的多态的条件:跟类型有关 调用的类型是谁 调用就是谁的

  • 小结

不同对象去做同一件事的效果不一样

多态的两个条件

1.虚函数的重写 virtual关键字

virtual关键字

(1)可以修饰成原函数 为了完成虚函数的重写 满足多态的条件之一 (2)可以在菱形继承中 去完成虚继承 解决数据冗余和二义性

虚函数重写:返回值类型 函数名字 参数列表完全相同 注意有特殊情况

两个地方使用了同一个关键字 但是他们互相之间没有一点关联

2.父类对象的指针或者引用去调用虚函数

满足多态:跟指向对象有关 指向哪个对象调用就是他的虚函数

不满足多态:跟调用对象的类型有关 类型是什么就调用谁的虚函数

<1>协变

基类与派生类虚函数返回值类型不同

//虚函数重写的两个例外 小心选择题设坑
//1.协变 子类和父类函数返回值类型不同 也要是本类型的返回值类型 也可以形成多态
class Person {
public:virtual Person* BuyTicket() //Person*{ cout << "Person::买票-全价" << endl; return nullptr;}
};
class Student : public Person {
public:Student* BuyTicket() //Student* //继承后可以不写virtual 也可以构成多态 子类可以不写 父类必须写{ cout << "Student::买票-半价" << endl; return nullptr;}
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

<2>析构函数的重写

//2.析构函数的重写
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }//析构函数的函数名会被处理成destructor
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }//析构函数的函数名会被处理成destructor
};int main()
{//Person p;//Student s;//先Student析构 再调父类析构 父类再自己析构 没有问题//Person* p1 = new Person;//没有问题//delete p1;Person* p2 = new Student;//如果无virtual 会析构两次父类 delete p2;//如果student析构函数中有资源释放 这里没有被调用到 就会出现内存泄漏 //不加virtual 不构成多态:看指针类型 对应析构函数//加virtual 构成多态:看对象类型 对应析构函数return 0;
}

<3>练习

输出?

//练习
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }//A* this
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }//子类可以不写virtual//继承下来 缺省参数也会被父类替换 只有函数内容不一样 缺省值为1
};
int main(int argc, char* argv[])
{B* p = new B;//指向子类对象 调子类的p->test();//p->test(p)return 0;
}

答案:B->1

继承下来后 派生类虚函数的缺省值也会被基类虚函数的缺省值替换

所以最后会打印1

4°C++关键字

  • 加一个final关键字 不能被重写和继承
  • 加一个override检查子类的虚函数是否重写了父类的虚函数

5°重载 重写 隐藏的定义

  • 重载:

    1.两个函数在同一作用域

    2.函数名相同 参数不同

  • 重写:

    1.两个函数分别在父类和子类的作用域

    2.函数名/参数/返回值都必须相同(协变例外)

    3.两个函数都必须是虚函数

  • 重定义(隐藏)

    1.两个函数分别在父类和子类的作用域

    2.函数名相同

    3.两个父类和子类的同名函数不构成重写就是重定义

3.抽象类

不能实例化出对象

//抽象类
//特点:不能实例化出对象
class Car
{
public:virtual void Drive() = 0;//不需要实现 纯虚函数
};class Benz :public Car
{
public:virtual void Drive(){}
};int main()
{//Car car;//不行Benz bz;//继承的类也不可以实例化出对象 继承了父类的纯虚函数 还是抽象类//解决:重写一下//纯虚函数:1.强制子类去完成重写//2.表示抽象的类型 抽象就是在显示中没有对应的实体的return 0;
}

子类重写虚函数后

// 包含纯虚函数的类抽象类 抽象类不能实例化出对象
class Car
{
public:virtual void Drive() = 0;//纯虚函数 不需要实现
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};int main()
{Benz benz;//重写后可以实例化出对象return 0;
}

4.多态的原理

1°虚函数表

  • 笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;//4个字节//还会多一个指针//应该是8 32位下//或者16 64位下 4+8=12加对齐就是16//虚函数表指针:简称虚表指针//虚函数表其实就是一个指针数组(虚函数指针)
};

注意会多一个虚函数表指针

2°原理

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }int _id = 1;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }int _s = 2;
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

  • 调用的时候去找不同的地址(虚指针) 去找对应的位置

从而找到和调用子类和父类的虚函数

p指向的Person类型的对象就调用Person虚函数

p指向的Student类型的对象就调用Student虚函数

  • 如何实现的指向谁调谁?

多态是在运行时到指向的对象的虚表中查找要调用的虚函数的地址来进行调用

编译时直接确定通过p的类型确定要调用函数的地址

普通函数和虚函数都会被编译成指令以后 存在代码段

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 func()
{//取出对象的前4个字节Base b1;printf("vftptr:%p\n", *(int*)&b1);//取对象地址强转int*再解引用拿到地址int i = 0;int* p1 = &i;int* p2 = new int;const char* p3 = "hello";printf("栈变量:%p\n", p1);printf("堆变量:%p\n", p2);printf("代码段常量:%p\n", p3);printf("虚函数地址:%p\n", &Base::func2);printf("普通函数地址:%p\n", func);
}int main()
{Base b;Derive d;func();//可以发现vftptr 虚函数 普通函数与代码段的地址是比较接近的 三者都在代码段
}

  • 虚函数存在哪?虚表存在哪?

虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只

是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

指针数组中多个指针 以0x00000000结束 栈是错的 代码段(常量区) 为同类型的对象公

用一个虚表就不会在栈上 公共区域代码段

3°动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多

态,比如:函数重载

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具

体行为,调用具体的函数,也称为动态多态。

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() //func1重写 func2没有重写 监视看不到func3和func4 { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};void f1(double d)
{}int main()
{int i = 0;double d = 1.1;//静态绑定 静态的多态 (静态:编译时确定函数地址) cout << i << endl;cout << d << endl;f1(i);f1(d);//动态绑定 动态的多态 (动态:运行时到虚表中找虚函数地址)Base* p = new Base;p->func1();p = new Derive;p->func1();return 0;
}

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() //func1重写 func2没有重写 监视看不到func3和func4 {cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}virtual void func4(){cout << "Derive::func4" << endl;}
private:int b;
};//void(*p)();//定义一个函数指针 通过typedef简化
typedef void(*VF_PTR)();void PrintVFtable(VF_PTR* pTable)//打印虚函数表
{for (size_t i = 0; pTable[i] != 0; ++i){printf("vfTable[%d]:%p\n", i, pTable[i]);VF_PTR f = pTable[i];f();}
}int main()
{Base b;Derive d;//取对象前4个字节的虚表指针PrintVFtable((VF_PTR*)(*(int*)&b));cout << endl;//除了func2没有重写 调的是父类 其余都是调自己的PrintVFtable((VF_PTR*)(*(int*)&d));//会有很多个 编译器的BUG 需要重新清理一下 生成里面return 0;
}

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; }//自己的func3是往第一个虚表走的
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()
{cout << sizeof(Derive) << endl;//20 为什么不是24?//Base1   8 //Base2   8//Derive  4 自己虚表指针不用算 算了也会覆盖父类的//20Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//Base1头4个字节 先强转char* 再加sizeofPrintVTable(vTableb2);return 0;
}

 

  • 区分虚表和虚继表

虚基表存的是偏移量 解决菱形继承的数据冗余和二义性

虚表是一个函数指针数组,数组里存放的都是函数指针,指向虚函数所在的位置

【C++】13.多态 完

相关内容

热门资讯

普通插槽、具名插槽、作用域插槽 插槽 插槽就是子组件提供给父组件的占位符,用slot来表示,父组件可以在...
Go语言必知必会100问题-0... 减少代码的嵌套层数 软件开发中的“心智模型”用于描述开发人员在编码时心理活动,每段代码...
CSRF漏洞的概念、利用方式、... CSRF漏洞1.CSRF的概念1.1 什么是CSRF?1.2 基本攻击流程2.CSRF...
基于springboot开发的... 基于springboot开发的学生考勤管理系统 如需更多资料请前往文章底部获取联系方式 系统设计主要...
cocosCreator 之 ... cocosCreator版本: 3.7 简介 cocosCreator的工作流程是通...
vue2中$set的原理_它对... $set的作用背景动态添加属性,不使用$set动态添加属性,使用$set...
3/19考后总结 时间安排 8:30–8:50 读题,T1 感觉是乱搞题,T2 貌似可以二...
【JavaWeb】JDBC 目录 一、JDBC概述 1 JDBC概念 2 JDBC本质 3 JDBC好处 二,J...
python 多任务 一些概念 1.多任务 简单地说,就是同时可以运行多个任务。打个比方,你一...
基于springboot框架实... 基于springboot框架实现自习室预订系统的设计与实现 开发语言:Java 框架...
VMware Centos7 ... 接着开始在VMware中搭建我们的大前端环境。这里我已经在我本机安装好VMware虚拟机了。 Win...
JUC高级二: Java锁(上... JUC高级二: Java锁(上篇) 1. 乐观锁和悲观锁 synchronized关键字和Lock...
Go分布式爬虫笔记(八) 08_项目需求分析与架构设计 需求分析 业务需求 爬虫引擎为基础的推送系统 提供 快速的热点事...
【计量经济学】第一次作业(7、... 第二次 7.假设有人做了如下的回归: y i = β 0 ^ + β 1 ^ x i + e i ...
Java Iterator(迭... 简介 在Java中,如果我们需要遍历一个集合(Collection&#x...
作物杂交/记忆化搜索/记忆化数... 题解:本题用到了记忆化搜索:记忆化搜索,本质还是 动态规划...
Linux下是实现的 HTTP... 项目功能:(1)能接收客户端的GET请求;&...
运营数据分析模型—标签体系 标签体系 标签体系上是描述对象的一种机制,而标签作为标签体系的基本组成元素,用来描述对象具有某种属性...
在openSUSE-Leap-... 在openSUSE-Leap-15.4-DVD-x86_64中使用百度网盘AppImage 打开百度...
鸟哥的Linux私房菜 She... 第十二章、学习 Shell Scripts https://linux.vbird.org/linu...
2分钟快速了解!全网最详细的性...  目录:导读 Redis 简介 Redis 优势 Redis与其他key-value存...
设计模式-02 4,创建型模式 4.2 工厂模式 4.2.1 概述 需求:设计一个咖啡店...
纠错码中的汉明码,NAND F...      纠错码是一种用在不可靠的或者噪音比较大的通信信道中用来控制数据传输错误的技术。这种技术的核...
【Java】P14 面向对象(... 类的封装性封装性高内聚与低耦合何为封装性如何封装get 与 set 方法案例 封装性 高内聚与低耦...
【FPGA】Xilinx Co... 功能描述 1、Rotate Rotate 实现的功能是坐标的旋转。 输入 X, Y, Phase 输...
【Pytorch】使用Pyto... 文章目录1. 题目描述2. 代码实现验证写在最后 1. 题目描述 在这个例子中网络结构如下所示&#...
client-go disco... 1. 概述 discovery包主要用来发现服务器支持的API组、版本和资源的方法,及...
[LeetCode周赛复盘] ... [LeetCode周赛复盘] 第 100 场双周赛20230318 一、本周周赛总结二、 [Easy...
Matlab基础教学入门 Matlab是一种非常强大的数学计算工具,广泛应用于科学和工程领域。本篇文章将介绍一些...
设计模式-02 4,创建型模式 4.2 工厂模式 4.2.1 概述 需求:设计一个咖啡店...