C++ 学习系列 -- C++ 中的多态行为

一    多态是什么?

多态是面向对象三大特征中重要一项,另外两项分别是封装与继承。

所谓多态,指的是多种不同的形态,也就是去完成某个具体的行为,多个不同的对象去操作同一个函数时,会产生不同的行为,进而出现不同的状态。

二   C++ 类中的普通成员函数

普通成员函数面临着两个问题:

            1.  无法实现多态行为

            2.  派生类同名函数会覆盖基类的同名函数,即使函数的参数不同也会导致覆盖

// base.h
#include<iostream>class Base
{
public:Base(){}~Base(){}void func1(){std::cout << "Base::func1()---" << std::endl;}void func2(){std::cout << "Base::func2()---" << std::endl;}
};class Derive1 : public Base
{
public:Derive1(){}~Derive1(){}void func1(){std::cout << "Derive1::func1()---" << std::endl;}void func2(int num){std::cout << "Derive1::func2(int num)---" << std::endl;}
};// main.cpp
#include"base.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数// 1. 普通函数 是没有多态行为的// 2. 派生类的同名函数会覆盖基类同名函数,即使参数不同也会被覆盖Base* b1 = new Derive1;b1->func1(); // 调用的是 Base::func1() 函数delete b1;// b1->func2(2); // 编译不过,因为 Base::func2() 函数是不带参数的Derive1* d1 = new Derive1;// d1->func2(); // 编译不过,Derive1::func2(int num) 覆盖了同名函数 Base::func2() 函数,delete d1;return 0;
}

输出:

Base::func1()---

Derive1::func1()---

三   C++ 中的虚函数

1. virtual 关键字

c++ 中的 virtual 关键字来常有以下两种使用,

  1.1 修饰函数

被 virtual 关键字修饰的类成员函数,被称为虚函数。虚函数分为两种:

1.  纯虚函数 (也叫做抽象函数)

       含有纯虚函数的类是无法被实例化的,因为在编译器看来,纯虚函数并不是一个完整的函数,它没有具体的实现,若是在使用时调用它,编译器也不知道到底该怎么办

2.  非纯虚函数

      若是类的虚函数都是非纯虚函数,那么这个类是可以实例化的

            友元函数、构造函数与 static 函数 是不能被 virtual 关键字修饰的。

      虚函数具有继承性,基类中 virtual关键字修饰的虚函数,派生类重写时,可以不再用 virtual 修饰,重写的基类虚函数会自动成为虚函数。 

// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }// 虚函数virtual void vfunc1(){std::cout << "A::vfunc1()---" << std::endl;}// 纯虚函数virtual void vfunc() = 0;private:int m_data1;
};class B : public A
{
public:B(){ }~B() { }void vfunc1(){std::cout << "B::vfunc1()---" << std::endl;}void vfunc(){std::cout << "B::vfunc()---" << std::endl;}
private:int m_data2;
};// main.cpp
#include"A.h"int main()
{// A a1;          // 类中有纯虚函数,无法实例化,编译不过// A* a2 = new A; // 类中有纯虚函数,无法实例化,编译不过B b1;             // B class 实现了 A class  的纯虚函数,可以实例化B* b2 = new B;    // B class 实现了 A class  的纯虚函数,可以实例化delete b2;return 0;
}

1.2 修饰类继承

C++虚继承和虚基类详解 - 知乎 (zhihu.com)   

1.2.1 public、protected 与 private 继承的区别

我们知道 类之间的继承有三种:public、protected 与 private

   三者区别为:

  •   public 继承
  •  基类中原 public 的成员 在派生类中仍然是 public
  •  基类中原 protected 的成员 在派生类中仍然是 protected
  •  基类中原 private 的成员 在派生类中仍然是 private   
  •  protected
  •  基类中原 public 的成员 在派生类中变为了 protected
  •  基类中原 protected 的成员 在派生类中仍然是 protected
  •  基类中原 private 的成员 在派生类中仍然是 private   
  • private
  •  基类中原 public 的成员 在派生类中变为了 private
  •  基类中原 protected 的成员 在派生类中仍然是 private
  •  基类中原 private 的成员 在派生类中仍然是 private   
class A
{};class B1 : public A
{};class B2 : protected A
{};class B3 : private A
{};

那么 virtual 继承是用于解决什么问题呢?下面让我们看以下的场景:

有这样一种继承关系,菱形继承:

  

// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){// m_a = a; // 编译不通过,这里有歧义,可能是 BB::m_a ,也可能是 CC::m_a}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{D d;return 0;
}

输出:

 通过以上代码与输出我们可以得出下面两点结论:      

    1.  AA 类的构造函数与析构函数分别被执行了两次,分别是 DD 继承 BB 类时执行,DD继承 CC 类时执行。

    2.  m_a = a 这行代码是无法通过编译的,因为编译器无法判断 m_a 是从路径 AA -> BB -> DD 还是从路径 AA -> CC -> DD 得来的,产生了歧义。

     那么是否有办法消除歧义呢?

     答案是有,可以通过如下的代码来消除歧义

// 使用 BB 类路径的 m_a
void set_a(int a)
{BB::m_a = a;
}
// 使用 CC 类路径的 m_a
void set_a(int a)
{CC::m_a = a;
}

  上面的多继承的方式主要有两点问题:

    1. 派生类 DD 有两个 m_a 成员变量,分别来自于 AA -> BB -> DD 于 AA -> CC -> DD,这样会造成冗余与内存的浪费

   2. 同名变量与函数会出现命名的冲突

1.2.2 虚继承

   

为了解决多继承这两点问题,c++引入了虚继承的概念

虚继承代码如下:

// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : virtual public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : virtual public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){m_a = a; // 编译通过}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{DD dd;return 0;
}

 输出:

  

    通过输出可以看出,AA类的构造函数与析构函数只被执行了一次,这说明在虚继承时,不再存在两种路径 AA  -> BB -> DD 与 AA -> CC -> DD。

   上述的虚继承代码解决了菱形继承中 m_a 数据冗余的问题,所以 D 类中直接访问 m_a 就不再有歧义的问题了。

// 虚继承中访问 m_a 无冗余问题
void  set_a(int a)
{m_a = a;
}

总结

       虚继承的目的是为了表明某个类的基类是可以被这个类的所有派生类共享,这个基类也被称作虚基类(Virtual Base Class),上述代码中的 class AA 就是 class BB 与 class CC 的虚基类。

不管虚基类在派生类中出现多少次,最终在派生类中都只有一份虚基类的成员变量与成员函数。

2. 虚函数指针与虚函数表

     2.1 c++多态原理

      c++ 实现多态的原理就是利用 虚函数表指针与虚函数表。

      若是定义的类实现了一个或者多个虚函数,那么这个类会有一张对应的虚函数表,表中存放的是对应虚函数的指针。

      如下图所示:

若是派生类 B 重写了基类 A 中的 虚函数 vfunc1,那么在 class B 的虚函数表中对应的 vfunc1 虚函数的地址就是派生类 B 重写的虚函数 B::vfunc1 地址;

若是 派生类 B 没有重写了基类 A 中的 虚函数 vfunc2,那么在 class B 的虚函数表中对应的 vfunc2 虚函数的地址就是基类 A 的虚函数 A::vfunc2 地址。

    当新生成 class B或者 class C 对象时,会在对象内部自动生成一个指向虚函数表地址的虚函数表指针 vptr,如果我们用 sizeof(A)  发现其大小为 16 ,计算方式为 两个 int 类型的成员变量大小分别为 4 ,有一个默认的虚函数指针大小为 8  (这里是 64 位操作系统,如果是 32 位操作系统的话,指针大小为 4)所以 sizeof(B) = 4 * 2 + 8

// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }virtual void vfunc1() override{std::cout << "A::vfunc1()---" << std::endl;}virtual void vfunc2(){std::cout << "A::vfunc2()---" << std::endl;}
private:int m_data1;int m_data2;
};class B : public A
{
public:B(){ }~B() { }void vfunc1() override{std::cout << "B::vfunc1()---" << std::endl;}
private:int m_data2;int m_data3;};class C : public B
{
public:C(){ }~C(){ }void vfunc1() override{std::cout << "C::vfunc1()---" << std::endl;}
private:int m_data1;int m_data4;
};// main.cpp
#include"A.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数A* a  = new A;A* b = new B;A* c = new C;a->vfunc1(); // A::vfunc1a->vfunc2(); // A::vfunc2b->vfunc1(); // B::vfunc1b->vfunc2(); // A::vfunc2c->vfunc1(); // C::vfunc1c->vfunc2(); // A::vfunc2return 0;
}

 输出

     通过输出可以看出,指针为 A* 的 B 类对象调用的 vfunc1 函数为 B 类中重写的 vfunc1 虚函数; 指针为 A* 的 C 类对象调用的 vfunc1 函数为 C 类中重写的 vfunc1 虚函数。

2.2 虚函数表分析

    以下通过类的内存布局来看一下虚函数表是什么样的:

   通过debug 方式,查看  A类、B类 与 C 类生成对象中虚函数表中的内存分配,操作系统是 windows 10 ,64位。

   

   

  

       A类虚函数表 vptr 中的头两个元素存放的是 其析构函数 A::~A() 的地址,第三个元素是虚函数   A::vfunc1() 的地址,第四个元素 是虚函数   A::vfunc2() 的地址;

      B类虚函数表 vptr 中的头两个元素存放的是 其析构函数 B::~B() 的地址,第三个元素是虚函数   B::vfunc1() 的地址,第四个元素 是虚函数   A::vfunc2() 的地址;

      C类虚函数表 vptr 中的头两个元素存放的是 其析构函数 C::~C() 的地址,第三个元素是虚函数   C::vfunc1() 的地址,第四个元素 是虚函数   A::vfunc2() 的地址;

如何获取到类 Object 的虚函数表呢?对于有虚函数的对象 object 来说:

1. &object 代表对象 object 的起始地址

2. (intptr_t *)&object 代表获取对象起始地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),而这前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)则是虚函数表的指针

3. *(intptr_t *)&object 则是取前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)中的数据,也就是虚函数表 vptr 的地址

4. 取 虚函数表 vptr 地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),为虚函数表中第一个元素的地址  (intptr_t *) *(intptr_t *)&object,取出虚函数表中第一个元素 *((intptr_t *) *(intptr_t *)&object), 则取出虚函数表中第 n 个元素为  *((intptr_t *) *(intptr_t *)&object + n),所取元素即为 虚函数的地址

5. 定义一个函数指针:typedef void(*pFunc)(); 函数指针 pFunc 代表 形如 void print(); 的函数的指针类型。通过前面的 1 - 4 步,获取道虚函数的地址,将地址赋给函数指针 pFunc ,就可以通过 pFunc 调用对应的虚函数了。     深入浅出——理解c/c++函数指针 - 知乎 (zhihu.com)

我们可以把类对象的虚函数表中的虚函数指针获取出来,直接调用虚函数,代码如下所示:

// main.cpp
#include"a.h"int main()
{typedef void(*pFunc)() ;A a;printf("the addr of A::vfunc1 is 0x%p\n", &A::vfunc1);printf("the addr of the third function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+2));printf("the addr of the fourth function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+3));pFunc pa_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&a+2);pFunc pa_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&a+3);pa_vfunc1();pa_vfunc2();B b;printf("the addr of B::vfunc1 is 0x%p\n", &B::vfunc1);printf("the addr of the third function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+2));printf("the addr of the fourth function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+3));pFunc pb_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&b+2);pFunc pb_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&b+3);pb_vfunc1();pb_vfunc2();C c;printf("the addr of B::vfunc1 is 0x%p\n", &C::vfunc1);printf("the addr of the third function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+2));printf("the addr of the fourth function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+3));pFunc pc_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&c+2);pFunc pc_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&c+3);pc_vfunc1();pc_vfunc2();return 0;
}

输出:

2.3  c++ 多态实现条件

 C++ 多态的实现条件有三条:

1. 指针

2. 指针向上转型

3. 虚函数

   多态可以通过下面的例子来去实现:

// main.cpp
#include"A.h"int main()
{// 多态三要素:1. 指针 2. 向上转型 3. 调用虚函数A* a = new A; // 指针a->vfunc1();a->vfunc2();B b;a = &b;       // 向上转型a->vfunc1();  // 调用虚函数a->vfunc2();C c;a = &c;      // 向上转型a->vfunc1();  // 调用虚函数a->vfunc2();return 0;
}

输出:

四   常见问题

1. 虚函数指针属于类还是对象?

     答:虚函数指针属于对象,每个含有虚函数的类对象都有一个默认的虚函数指针,通过虚函数指针可以获取到虚函数表,进而实现动态调用虚函数,实现多态行为

2. 虚函数表属于类还是对象?

   答:虚函数表属于类,让我们站在设计者的角度思考一下,虚函数表本身是占有一定的内存空间的,而每个对象的虚函数表都是相同的,要是每个对象都有一个,那得耗费多少冗余内存啊,所有对象共用一份虚函数表即可。

3. 基类的析构函数为什么要加 virtual 关键字

答:若是基类的析构函数不加 virtual 关键字的话,则 delete 多态时的对象指针是,会出现只调用基类的析构函数,未调用派生类的析构函数,那么派生类的数据就可能未被释放掉,会出现内存泄漏的现象。

实验如下:

// base.h
#include<iostream>class Base
{
public:Base(){std::cout << "Base constructor." << std::endl;}~Base(){std::cout << "Base destructor." << std::endl;}virtual void func(){std::cout << "virtual Base::func()---" << std::endl;}};class Derive1 : public Base
{
public:Derive1(){std::cout << "Derive1 constructor." << std::endl;}virtual ~Derive1(){std::cout << "Derive1 constructor." << std::endl;}void func() override{std::cout << "virtual Derive1::func()---" << std::endl;}};// main.cpp
#include"base.h"int main()
{Base* d1 = new Derive1;d1->func();delete d1;return 0;
}

   输出:

    

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

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

相关文章

全国(山东、安徽)职业技能大赛--信息安全管理与评估大赛题目+答案讲解

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

方舟开发框架(ArkUI)概述

目录 1、基本概念 2、两种开发范式 3、开发框架的特性 4、UI开发&#xff08;ArkTS声明式开发范式&#xff09;概述 4.1、特点 4.2、整体架构 4.3、开发流程 方舟开发框架&#xff08;简称ArkUI&#xff09;为HarmonyOS应用的UI开发提供了完整的基础设施&#xff0c;包…

智能优化算法应用:基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.食肉植物算法4.实验参数设定5.算法结果6.…

ISP 状态机轮转和bubble恢复机制

1 ISP的中断类型 ISP中断类型 SOF: 一帧图像数据开始传输 EOF: 一帧图像数据传输完成 REG_UPDATE: ISP寄存器更新完成(每个reg group都有独立的这个中断) EPOCH: ISP某一行结尾(默认20)就会产生此中断 BUFFER DONE: 一帧图像数据ISP完全写到DDR了 2 ISP驱动状态机 通过camer…

java八股 redis

Redis篇-01-redis开篇_哔哩哔哩_bilibili 1.缓存穿透 2.缓存击穿 逻辑过期里的互斥锁是为了保证只有一个线程去缓存重建 3.缓存雪崩 4.双写一致性 4.1要求一致性&#xff08;延迟双删/互斥锁&#xff09; 延迟双删无法保证强一致性 那么前两步删缓和更新数据库哪个先呢&#xf…

车手互联是不是杀手锏,来听听一家头部手机厂的座舱方法论

作者 |Amy 编辑 |德新 十年前&#xff0c; 苹果CarPlay和谷歌Android Auto相继推出&#xff0c;手机与车机两个此前貌似无关的品类&#xff0c;从此开始产生交集。 科技巨头看好车机的硬生态&#xff0c;汽车大鳄们则垂涎于科技圈的软实力。 CarPlay和Android Auto的出现&am…

锐捷配置重发布RIP进OSPF中

一、实验拓扑 二、实验目的 使用两种动态路由协议&#xff0c;并使两种协议间的路由可以传递 三、实验配置 第一步&#xff1a;配置全网基本IP R1 Ruijie>enable Ruijie#configure terminal Ruijie(config)#interface gigabitEthernet 0/0 Ruijie(config-if-GigabitEthe…

使用 Postman 进行并发请求:实用教程与最佳实践

背景介绍 最近&#xff0c;我们发起了一个在线图书管理系统的项目。我负责的一个关键模块包括三个主要后台接口&#xff1a; 实现对books数据的检索。实施对likes数据的获取。通过collections端点访问数据。 应对高流量的挑战 在设计并部署接口时&#xff0c;我们不可避免地…

基于计算机视觉的棋盘图像识别

本期我们将一起学习如何使用计算机视觉技术识别棋子及其在棋盘上的位置 我们利用计算机视觉技术和卷积神经网络&#xff08;CNN&#xff09;为这个项目创建分类算法&#xff0c;并确定棋子在棋盘上的位置。最终的应用程序会保存整个图像并可视化的表现出来&#xff0c;同时输出…

Java多线程技术四——定时器

1 定时器的使用 在JDK库中Timer类主要负责计划任务的功能&#xff0c;也就是在指定的时间开始执行某一个任务&#xff0c;Timer类的方法列表如下&#xff1a; Timer类的主要作用就是设置计划任务&#xff0c;封装任务的类却是TimerTask&#xff0c;该类的结构如下图 因为TimerT…

Http---查看HTTP协议的通信过程

1. 谷歌浏览器开发者工具的使用 首先需要安装Google Chrome浏览器&#xff0c;然后Windows和Linux平台按F12调出开发者工具, mac OS选择 视图 -> 开发者 -> 开发者工具或者直接使用 altcommandi 这个快捷键&#xff0c;还有一个多平台通用的操作就是在网页右击选择检查。…

uni-app 工程目录结构介绍

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…