C/C++语言基础--C++神奇的多态

embedded/2024/10/15 18:22:14/

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 通过前面几节课,我们学习了抽象、封装、继承相关的概念,接下来我们将讲解多态,多态他非常神奇,正式有了他,类才能出现多样性特征;
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

文章目录

    • 问题思考?
    • 面向对象新需求
    • 多态的意义探究
      • 面向对象三大概念:
      • 多态成立的三要素:(结合解决方案)
    • 虚析构
    • 函数的重载、重写、重定义
      • 函数重载
      • 函数重定义
      • 虚函数重写
    • 纯虚函数和抽象类
      • 纯虚函数
      • 抽象类与接口
        • 抽象类特征
      • 关键字
        • abstract
        • final
    • 多态探究
      • 多态的理论基础
      • 获得虚函数表
      • 多态的本质(原理)
      • 虚函数简单介绍:
      • 虚函数图像
      • 如何证明vptr指针存在
      • 如何找到vptr指针呢

问题思考?

如果子类定义了与父类中定义相同函数会发生什么?如下面代码所示:

#include <iostream>using namespace std;class Parent
{
public:void show() {cout << "I am father" << endl;}
};class Son : public Parent
{
public:void show() {cout << "I am son" << endl;}
};void print(Parent& p) {p.show();
}int main()
{Parent pa;Son so;print(pa);print(so);  // 子赋值给父亲,可以当作父亲用return 0;
}

输出:

I am father
I am father

但是,如何在传入不同对象的时候输出相应的数据呢? 这个就是我们接下来要学的多态。

面向对象新需求

上面的这一种场景,需要C++需要做的事情是:

  • print函数中,传递什么对象调用什么对象的show函数,传递父类的,就调用父类的,传递子类的,就调用子类的。

解决方案:虚函数

  • 在父类中,在能让子类重写的函数前面必须加上virtual关键字
  • 在子类中,在重写的父类的虚函数后面加上override关键字,表示是虚函数重写(非必须,但是加上可以防止重写的虚函数写错)

虚函数重写概念

派生类(父类)中有一个跟基类(子类)完全相同的函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同)

多态的意义探究

面向对象三大概念:

封装:提取事物的属性与方法
继承:代码复用——可以用父类的代码
多态:在代码复用基础上,实现不同功能

案例:打印矩形和圆形坐标和面积

矩形:x,y,length,width

圆形:x,y,radius

在这个案例中,我们可以划分:

  • 封装:将矩形和圆形共有属性抽象出来,这里是x,y坐标,同时将共有方法抽象出来,这里是打印坐标和面积,将这些封装成一个基类A
  • 继承:分别定义矩形、圆形类,继承基类A,同时定义属于自己的属性或者方法,这里是矩形中定义属性length,width,圆形定义radius;
  • 多态:基类中定义了方法(打印坐标和面积),在圆形和矩形中分别重写这两个方法;
  • 测试:利用子类可以赋值给父类的特征,实现传入什么类就输出什么类对应的API。

代码实现如下:

#include <iostream>using namespace std;class Geometry
{
public:Geometry(int x, int y) : m_x(x), m_y(y) {}virtual void print_coordinates() {}virtual void print_area() {}int m_x;			// 测试:整形int m_y;
};class Rectangle : public Geometry
{
public:Rectangle(int x, int y, int width, int length): Geometry(x, y),m_width(width),m_length(length) {}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Rectangle area: " << m_width * m_length << std::endl;}int m_width;int m_length;
};class Round : public Geometry
{
public:Round(int x, int y, int riduas): Geometry(x, y),m_riduas(riduas){}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;}int m_riduas;
};void test(Geometry& various)
{various.print_coordinates();various.print_area();
}int main()
{Rectangle rect(1, 2, 3, 4);Round round(5, 6, 1);test(rect);test(round);return 0;
}

输出:

x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

多态成立的三要素:(结合解决方案)

  1. 要有继承:多态发生在父子类之间
  2. 要有虚函数重写:重写了虚函数,才能进行动态绑定
    1. (解决方案)
  3. 要有父类指针(引用)指向子类对象,传递参数的时候必须为引用或者指针,推荐常引用

虚析构

前置知识:

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚的。通过父类指针释放所有的子类资源

虚析构:

虚析构:通过父类去释放子类的时候,如果分类没有虚析构不会调用子类的析构函数的,会调用子类的析构函数,想要通过父类去释放子类, 必须在父类定义虚析构


让我们来看一下,这段代码结果会是什么:

#include <iostream>using namespace std;class Base
{
public:Base(){cout << __FUNCSIG__ << endl;}~Base(){cout << __FUNCSIG__ << endl;}
};class Derive : public Base
{
private:char* _str;
public:Derive(){_str = new char[10] { "wy" };cout << __FUNCSIG__ << endl;}~Derive(){delete _str;cout << __FUNCSIG__ << endl;}
};int main()
{Base* base = new Derive;       delete base;			return 0;
}

结果:

__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Base::~Base(void)

但是这个时候,子类的内存没有释放(_str),这样就造成了内存泄露问题😵😵😵😵😵😵


解决方法:

🔥虚析构:

  • 在父类析构函数中,加上关键字vartual
class Base
{
public:Base(){cout << __FUNCSIG__ << endl;}virtual ~Base()   // 加上virtual{cout << __FUNCSIG__ << endl;}
};

结果:

__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Derive::~Derive(void)
__cdecl Base::~Base(void)

这样子类通过父类去释放,这样就能够自动识别是父类还是子类了,从而避免内存泄露🌝🌝🌝

函数的重载、重写、重定义

函数重载

  • 必须在同一个作用域相同
  • 子类无法重载父类的函数,父类同名函数将被名称覆盖
  • 重载是在编译期间根据参数类型和个数决定函数调用
int add(int a, int b) {  // 函数1return a + b;
}int add(int a, int b, int c) {   // 函数2return a + b + c;
}add(2, 3);  // 调用函数1
add(2, 3, 4);   // 调用函数2

函数重定义

  • 发生于父类和子类之间,如果子类写了个和父类函数原型一样的函数,并且父类中的函数没有声明为虚函数,则子类会直接覆盖掉父类的函数
  • 但是要注意,通过父类指针或引用执行子类对象时,会调用父类的函数

子类继承父类函数,且子类直接调用父类函数:

#include <iostream>using namespace std;class Parent
{
public:void show() {cout << "I am father" << endl;}
};class Son : public Parent
{
public:};int main()
{Parent pa;Son so;pa.show();so.show();return 0;
}

结果

I am father
I am father

但是如果这样:

// 在子类中
class Son : public Parent
{
public:void show() {   // 重新写show函数cout << "I am son" << endl;}
};

结果

I am father
I am son

📖 📖📖📖 ​ ​ 这样通过子类调用show,就调用的是子类定义的show的。

虚函数重写

  • 必须发生于父类和子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 必须使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
  • 多态是在运行期间根据具体对象的类型决定函数调用
// 如这个打印面积的案例
#include <iostream>using namespace std;class Geometry
{
public:Geometry(int x, int y) : m_x(x), m_y(y) {}virtual void print_coordinates() {}   // virtualvirtual void print_area() {}int m_x;			// 测试:整形int m_y;
};class Rectangle : public Geometry
{
public:Rectangle(int x, int y, int width, int length): Geometry(x, y),m_width(width),m_length(length) {}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Rectangle area: " << m_width * m_length << std::endl;}int m_width;int m_length;
};class Round : public Geometry
{
public:Round(int x, int y, int riduas): Geometry(x, y),m_riduas(riduas){}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;}int m_riduas;
};void test(Geometry& various)
{various.print_coordinates();various.print_area();
}int main()
{Rectangle rect(1, 2, 3, 4);Round round(5, 6, 1);test(rect);test(round);return 0;
}

结果:

统一调用:test(rect),test(round)的时候输出的:
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

纯虚函数和抽象类

纯虚函数

纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体,这意味着它没有函数的实现需要让派生类去实现

C++中的纯虚函数,一般在函数签名后使用=0作为此类函数的标志。Java、C#等语言中,则直接使用abstract作为关键字修饰这个函数签名,表示这是抽象函数(纯虚函数)。

简单理解:如果类里面声明了纯虚函数,那么这个类就叫做抽象类,且抽象类无法定义对象

class Animal
{
public:virtual void cry() = 0;     //virtual 为虚函数标志,后面赋值 = 0,代表为这个为纯虚函数,则这个类为抽象类
}

抽象类与接口

接口:在C++里面,就是通过抽象类来实现接口的(不要在接口里面存放任何变量,一般只放虚函数

抽象类:是对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

  • 通常在编程语句中用 abstract 修饰的类是抽象类。在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;在java中,含有抽象方法的类称为抽象类,同样不能生成对象。
  • 抽象类是不完整的,它只能用作基类。
抽象类特征
  1. 抽象类不能实例化
  2. 抽象类和包含抽象方法(纯虚函数)、非抽象方法和属性
  3. 从抽象类派生的非抽象类,必须对继承过来的所有抽象方法实现

关键字

abstract

MSVC独有的关键字,申明类为抽象类

class  Animal abstract
{
};int main()
{Animal a;	//error C3622: “Animal”: 声明为“abstract”的类不能被实例化return 0;
}
final

C++标准关键字,结束的意思

  • 禁用虚函数重写

    class  Animal 
    {
    protected:virtual void show() final{}
    };class Dog final :public Animal
    {
    public:void show()override	//error C3248: “Animal::show”: 声明为“final”的函数无法被“Dog::show”重写{}
    };
  • 禁止该类被继承

    class  Animal  final
    {
    };class Dog final :public Animal //error C3246: "Dog": 无法从 "Animal" 继承,因为它已声明为 "final"
    {
    };

多态探究

参考博客: 详解

🌺 提示:多态概念很重要,但是概念同时也很容易忘记,可以先较为深入学习一下,记一下笔记,收藏一点资料,等要用到的时候再看,可以快速回忆。

多态的理论基础

静态联编和动态联编:联编是指一个程序模块、代码之间互相关联的过程。

  • 静态联编(关联),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
    重载函数使用静态联编。

  • 动态联编(关联),是指程序联编推迟到运行时进行,所以又称为动态联编(迟绑定),将函数体和函数调用关联起来,就叫绑定
    switch 语句和 if 语句是动态联编的例子。

    那么C++中的动态联编是如何实现的呢?
    如果我们声明了类中的成员函数为虚函数,那么C++编译器会为类生成一个虚函数表,通过这个表即可实现动态联编

获得虚函数表

#include<iostream>
//1、得到虚函数
//2、验证不同兄弟类虚函数都是一样的
class A
{
public:virtual void a(){std::cout << __FUNCSIG__ << std::endl;}virtual void b(){std::cout << __FUNCSIG__ << std::endl;}virtual void c(){std::cout << __FUNCSIG__ << std::endl;}
private:int x;int y;
};typedef void(*func)();  //使用函数指针,强制转换成函数int main()
{A a, b;uint64_t* p = (uint64_t*)&a;uint64_t* arr = (uint64_t*)*p;func fa = (func)arr[0];func fb = (func)arr[1];func fc = (func)arr[2];fa();fb();fc();uint64_t* pp = (uint64_t*)&b;uint64_t* arr2 = (uint64_t*)*pp;std::cout << arr << " " << arr2 << std::endl;return 0;
}
  • 继承虚函数
#include<iostream>
/*
* 1、父类虚函数和子类虚函数
* 2、兄弟虚函数
* 3、继承虚函数
*///继承虚函数表和重写
class A
{
public:virtual void a(){std::cout << __FUNCSIG__ << std::endl;}virtual void b(){std::cout << __FUNCSIG__ << std::endl;}virtual void c(){std::cout << __FUNCSIG__ << std::endl;}
private:int x;int y;
};class B : public A
{
public:void b() override{ std::cout << __FUNCSIG__ << std::endl; } 
};typedef void(*func)();  //使用函数指针,强制转换成函数int main()
{A a;B b;uint64_t* pa = (uint64_t*)&a;uint64_t* arra = (uint64_t*)*pa;uint64_t* pb = (uint64_t*)&b;uint64_t* arrb = (uint64_t*)*pb;func faa = (func)arra[0];func fab = (func)arra[1];func fac = (func)arra[2];func fba = (func)arrb[0];func fbb = (func)arrb[1];func fbc = (func)arrb[2];faa();fab(); fac();fba(); fbb(); fbc();return 0;
}

ss

多态的本质(原理)

虚函数表是顺序存放虚函数地址的,虚表是顺序表(数组),依次存放着类里面的虚函数。
虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的

在这里插入图片描述

既然虚函数表,是一个顺序表,那么它的首地址存放在哪里呢?

  • 当我们在类中定义了virtual函数时,C++编译器会偷偷的给对象添加一个vptr指针,vptr指针就是存的虚函数表的首地址

虚函数简单介绍:

  • 虚函数表存放了类的虚函数(就是一个函数指针数组)
  • 虚函数的指针分布初始化:创建子类对象的时候,会先构造父类,构造父类的时候,父类的虚函数指针,指向自己的虚函数(父类构造完后会构造子类,这个时候父类的虚函数指针,会指向类的虚函数标)
  • 在构造函数里面禁止使用虚函数(因为分布初始化还没有完成,可能得到不正确的结果)

虚函数图像

  • 三层

如何证明vptr指针存在

我们可以通过求出类的大小判断是否有vptr的存在

class Dog
{void show() {}
};class Cat
{virtual void show() {}
};int main()
{cout << "Dog size:" << sizeof(Dog) << " Cat size:" << sizeof(Cat) << endl;return 0;
}
output: Dog size:1 Cat size:8

通过调试确实能看到vptr指针的存在,而且存放在对象的第一个元素

在这里插入图片描述

如何找到vptr指针呢

既然vptr指针存在,那么能不能拿到vptr指针,手动来调用函数呢?

答案是可以的,利用它存在对象的第一个元素特征,但是操作起起来很麻烦,以下过程也是我收集资料学习到的。

思路:核心(存放在第一个对象元素)

  • 首先定义一个子类,拿取子类地址;
  • 接着将子类地址转化成long long*类型,再次解引用,这样就告诉编译器,这个指向子类指针,没有子类约束,并且这个类型是long long类型了,这个时候就拿到了对象第一个元素,很绕,但是没办法;
  • 再接着,将这个类型重新转化为指针long long
  • 这个时候就可以通过指针转化成不同定义的函数指针,转化成相应的函数调用。

步骤:

  1. 因为vptr指针在对象的第一个元素(通过证明vptr指针的存在可以看出),所以对对象t取地址可以拿到对象的地址

    Parent* p = &obj;
  2. 现在拿到的指针的步长是对象的大小,因为vptr是指针,只有4/8个字节,所以需要把p强转成int*指针,这样对(int*)&t就得到了vptr指针

    int vptr = *(int*)p;	//拿到了vptr指针的指针
    int* pvptr = (int*)vptr; //把vptr的值转成指针
  3. 因为vptr指针是指向的存储指针数组的首地址,所以拿到vptr指针后先把vptr转成int*指针,这样进行取值的话,刚好是每个指针

    FUN foo = (FUN)*(pvptr+0)  // 获取元素
  4. 接着吧得到的数组里面的元素(指针)转成函数指针,即可直接使用了

🤠 结果

#include <iostream>using namespace std;using FUN = void(*)();   // (*) 代表是一个指针,指向一个void类型函数class Parent
{
public:virtual void func1(){cout << "Parent::func1()" << endl;}virtual void func2(){cout << "Parent::func2()" << endl;}
};class Child : public Parent
{
public:void func1() override{cout << "Child::func1()" << endl;}void func2() override{cout << "Child::func2()" << endl;}
};int main()
{Child obj;Parent* p = &obj;long long vptr = *(long long*)p;long long* pvptr = (long long*)vptr;auto foo = (FUN) * (pvptr + 1);foo();return 0;
}

🍼 输出:

Child::func2()

http://www.ppmy.cn/embedded/127417.html

相关文章

毕设开源 基于python的搜索引擎设计与实现

文章目录 0 简介1 课题简介2 系统设计实现2.1 总体设计2.2 搜索关键流程2.3 推荐算法2.4 数据流的实现 3 实现细节3.1 系统架构3.2 爬取大量网页数据3.3 中文分词3.4 相关度排序第1个排名算法&#xff1a;根据单词位置进行评分的函数第2个排名算法&#xff1a;根据单词频度进行…

消防安全小程序推动社会消防安全意识提升

消防安全小程序在推动社会消防安全意识提升方面发挥着重要作用。以下是对其作用的详细阐述&#xff1a; 一、普及消防知识 消防安全小程序通过图文、音频等多种形式&#xff0c;向公众普及消防安全知识。这些小程序通常包含丰富的消防安全内容&#xff0c;如火灾预防、初期火灾…

用FPGA做一个全画幅无反相机

做一个 FPGA 驱动的全画幅无反光镜数码相机是不是觉得很酷&#xff1f; 就是上图这样。 Sitina 一款开源 35 毫米全画幅 (3624 毫米) CCD 无反光镜可换镜头相机 (MILC)&#xff0c;这个项目最初的目标是打造一款数码相机&#xff0c;将 SLR [单镜头反光] 相机转换为 DSLR [数码…

ubuntu kernel 调试信息输出

1. 配置了 CONFIG_DYNAMIC_DEBUGy 和 CONFIG_DEBUG_KERNELy 2. echo module my_pci_driver p > /sys/kernel/debug/dynamic_debug/control 3. echo 8 > /proc/sys/kernel/printk 使用如下方法不能打印调试信息&#xff1a; echo "file drivers/pci/* p" &…

【附源码】Python :打家劫舍

系列文章目录 Python 算法学习&#xff1a;打家劫舍问题 文章目录 系列文章目录一、算法需求二、解题思路三、具体方法源码方法1&#xff1a;动态规划&#xff08;自底向上&#xff09;方法2&#xff1a;动态规划&#xff08;自顶向下&#xff09;方法3&#xff1a;优化的动态…

MySQL-07.DDL-表结构操作-数据类型

一.MySQL中的数据类型 MySQL中的数据类型主要分为3种&#xff1a;数字类型&#xff0c;字符串类型&#xff0c;日期时间类型。如下图所示&#xff01; 二.数值类型 三.字符串类型 四.日期类型

深度解析python标准库模块json库!

在 Python 中&#xff0c;json库用于处理 JSON&#xff08;JavaScript Object Notation&#xff09;数据。以下是json库的主要用法&#xff1a; 一、将 Python 对象转换为 JSON 字符串 使用dumps()方法&#xff1a; json.dumps()方法可以将 Python 对象转换为 JSON 字符串。例…

高校新生报道管理系统使用SpringBootSSM框架开发

&#xff01;&#xff01;&#xff01;页面底部,文章结尾,加我好友,获取计算机毕设开发资料 目录 一、引言 二、相关技术介绍 三、系统需求分析 四、系统设计 五、关键技术实现 六、测试与优化 七、总结与展望 一、引言 当前高校新生报到过程中存在许多问题&#xff0c;…