C++ 多态实现机制

news2024/11/19 4:47:09

考虑下面的 C++ 程序:

class A
{
     void func(){}
};
 
class B:public A
{
      void func(){}
};
 
int main(void)
{
     cout << sizeof(A) << " " << sizeof(B) << endl;
     return 0;
}

输出结果是:1 1

再考虑下面很相似的程序:

class A
{
     virtual void funcA(){}
};
 
class B:public A
{
     virtual void funcB(){}
};
 
int main(void)
{
     cout << sizeof(A) << " " << sizeof(B) << endl;
     return 0;
}

为了帮助您更好地入门并深入掌握C++,我们精心准备了一系列丰富的学习资源包,包括但不限于基础语法教程、实战项目案例、核心概念解析以及进阶技巧指导等。

您只扫码上方二维码,即可免费获取这份专属的学习礼包。我们的教程覆盖了C++语言的各个方面,旨在让您在理论学习与实践操作中不断进步,提升编程技能。

同时,我们也鼓励您在学习过程中遇到任何问题时积极提问,我们会尽全力提供解答和帮助。期待您在C++编程的道路上越走越远,早日成为一位优秀的C++开发

 

输出结果是:4 4

再来考虑下面的形似的程序:

class A
{
     virtual void funcA(){}
};
 
class B:virtual public A
{
     virtual void funcB(){}
};
 
int main(void)
{
     cout << sizeof(A) << " " << sizeof(B) << endl;
     return 0;
}

输出结果是:4 12

对于第一种情况,没有出现虚函数,也无任何成员变量,因此是一个空类,空类理论上可以进行实例化,每个实例在内存中都有独一无二的地址来标明,所以会占用 1B 的空间,无可厚非。

但第二种情况和第三种情况加入了虚函数(virtual function),而且在第三种情况当中,引入了虚基类(virtual base class)的概念,所得到的结果大相径庭,这是 C++ 引入了 virtual function 和 virtual base class,即多态,更形象的解释是「以一个 public base class 的指针或者引用,寻址出一个 derived class object」,但多态带了一定空间上的开销,在效率上也有折损。

其实, 多态机制可以归结为下面三这句话:

  • 一般而言, 我们无法知道指针 ptr 所指的对象的真正类型. 但经由 ptr 总是可以存取到对象的 virtual table.
  • 虚函数 fn() 总是放在 virtual table 中的固定位置, 用一个固定的索引值就可以 fetch 到.
  • 唯一一个执行期需要知道的是 ptr 所指的对象.

单一继承

下面是单一继承里经常看到的一个程序:


class A
{
public:
    int a;
    void foo(){}
    virtual void funcA(){}
    virtual void func()
    {cout << "class A's func." << endl;}
};
 
classB : public A
{
public:
    int b;
    void foo(){}
    virtual void funcB(){}
    virtual void func()
    {cout << "class B's func." << endl;}
};
 
int main(void)
{
    A *pa = newB;
    pa->func();
}

输出结果是:class B'sfunc.

多态就是多种状态,一个事物可能有多种表现形式,譬如动物,有十二生肖甚至更多的表现形式。当基类里实现了某个虚函数,但派生类没有实现,那么类 B 的实例里的虚函数表中放置的就是 &A::func。此外,派生类也实现了虚函数,那么类 B 实例里的虚函数表中放置的就是 B::func。A *pa = new B; 因为 B 实现了 func,那么它被放入 A 实例的虚拟函数表中,从而代替 A 实例本身的虚拟函数。pa->func(); 调用的结果就不稀奇了,这是虚函数机制带来的。

class A 和 class B 的内存布局和 vptr 可能是下面的样子:

----------
|   int a |
----------
|    vptr | -------->|      &A::funcA()
----------             -------------------------------------------------
                          |      &A::func()
                         -------------------------------------------------
----------
|   int a |
----------
|    vptr | -------->|     &A::funcA() 依旧是 A 的虚函数
----------             -------------------------------------------------
|   int b |              |     &B::func() A::func()
----------             -------------------------------------------------
                          |     &B::funcB()
                          -------------------------------------------------

倘若 虚函数 以外的就没有「多态」效果了,除非进行强制类型转换:

  • pa->a;          //     成功,因为 pa 的类型就是 A
  • pa->b;          //     失败,因为 B::b
  • pa->funcB();  //     失败,因为B::funcB() 不是虚函数
  • pa->funcA();  //     成功,因为A::funcA()

总结一下:

  • 当引入虚函数的时候,会添加 vptr 和 其指向的一个虚拟函数表从而增加额外的空间,这些信息在编译期间就已经确定,而且在执行期不会插足修改任何内容。
  • 在类的构造和析构函数当中添加对应的代码,从而能够为 vptr 设定初值或者调整 vptr,这些动作由编译器完成,class 会产生膨胀。
  • 当出现继承关系时,虚拟函数表可能需要改写,即当用基类的指针指向一个派生类的实体地址,然后通过这个指针来调用虚函数。这里要分两种情况,当派生类已经改写同名虚函数时,那么此时调用的结果是派生类的实现;而如果派生类没有实现,那么调用依然是基类的虚函数实现,而且仅仅在多态仅仅在虚函数上表现。
  • 多态仅仅在虚函数上表现,意即倘若同样用基类的指针指向一个派生类的实体地址,那么这个指针将不能访问和调用派生类的成员变量和成员函数。
  • 所谓执行期确定的东西,就是基类指针所指向的实体地址是什么类型了,这是唯一执行期确定的。以上是单一继承的情况,在多重继承的情况会更为复杂。

多重继承

下面是少有看到的程序代码:


class A
{
public:
    virtual ~A(){cout << "A destruction" << endl;}
    int a;
    void fooA(){}
    virtual void func(){cout << "A func." << endl;};
    virtual void funcA(){cout << "funcA." << endl;}
};
 
class B
{
public:
    virtual ~B(){cout << "B destruction" << endl;}
    int b;
    void fooB(){}
    virtual void func(){cout << "B func." << endl;};
    virtual void funcB(){cout << "funcB." << endl;}
};
 
class C : public A,public B
{
public:
    virtual ~C(){cout << "C destruction" << endl;}
    int c;
    void fooC(){}
    virtual void func(){cout << "C func." << endl;};
    virtual void funcC(){cout << "funcC." << endl;}
};
 
int main(void) 
{  
    return 0;
}

当用基类的指针指向一个派生类的实体地址,基类有两种情况,一种是 class A 和 class B,如果是 A,问题容易解决,几乎和上面单一继承情况类似;但倘若是 B,要做地址上的转换,情况会比前者复杂。先展现class A,B,C 的内存布局和 vptr:

----------
|   int a |
----------
|    vptr | -------->|      &A::~A()
----------             -------------------------------------------------
                            |      &A::func()
                            -------------------------------------------------
                            |      &A::funcA()
                            -------------------------------------------------
----------
|   int b |
----------
|    vptr | -------->|     &B::~B()
----------             -------------------------------------------------
                            |     &B::func()
                            -------------------------------------------------
                            |     &B::funcB()
                            --------------------------------------------------
 

                            |      &C::~C() &A::~A()
----------             -------------------------------------------------
|   int a |               |      &C::func() &A::func()
----------             -------------------------------------------------
----------             |      &C::funcC()
|    vptr | -------->-------------------------------------------------
----------             |      &A::funcA()
----------             -------------------------------------------------
|   int b |               |      &B::funcB() 跳
----------             -------------------------------------------------
----------
|    vptr | -------->|     &C::~C() &B::~B() 跳
----------             -------------------------------------------------
|   int c |               |     &C::func() &B::func() 跳
----------             -------------------------------------------------
                           |     &B::funcB()
                            --------------------------------------------------

多重继承中,会有保留两个虚拟函数表,一个是与 A 共享的,一个是与 B 相关的,他们都在原有的基础上进行了修改:

对于 A 的虚拟函数表:

  • 覆盖派生类实现的同名虚函数,并用派生类实现的析构函数覆盖原有虚函数
  • 添加了派生类独有的虚函数
  • 添加了右端父类即 B 的独有虚函数,需跳转

对于 B 的虚拟函数表:

  • 覆盖派生类实现的同名虚函数,并用派生类实现的析构函数覆盖原有虚函数,但需跳转
int main(void)
{
     A *pa = new C;
     B *pb = new C;
     C *pc = new C;
     pa->func();
     pb->func();
     pc->funcC();
     delete pb;
     delete pa;
     delete pc;
}

输出结果是:

C func.
C func.
funcC.
C destruction
B destruction
A destruction
C destruction
B destruction
A destruction
C destruction
B destruction
A destruction

7 行和 8 行的行为有很大的区别,7 行的调用和上面的单一继承的情况类似,不赘述。8 行的 pb->func(); 中,pb 所指向的是上图第 9 行的位置,编译器已在内部做了转换,也就是 pa 和 pb 所指的位置不一样,pa 指向的是上图第 3 行的位置。接着需要注意的是,pb->func(); 调用时,在虚拟函数表中找到的地址需要再进行一次跳转,目标是 A 的虚拟函数表中的 &C::func(),然后才真正执行此函数。所以,上面的情况作了指针的调整。

那什么时候会出现跳,常见的有两种情况:

  1. 右端基类,对应上面的具体是 B,调用派生类虚拟函数,比如 pb->~C() 和 pb->func()
  2. 派生类调用右端基类的虚拟函数,比如 pc->funcB()

所以 delete pa; 和 delete pa; 的操作是不一样的,pb->funcB(); 和 pc->funcB(); 也不一样。

C++ 为实现多态引入虚函数机制,带来了空间和执行上的折损。

单一/多重继承的构造和析构

单一继承中,构造函数调用顺序是从上到下(单一继承),从左到右(多重继承),析构函数调用顺序反过来。在上一段程序中,

  1.      delete pa;
  2.      delete pb;
  3.      delete pc;

都自动调用了基类和派生类的析构函数,其中只有 delete pc; 涉及了虚拟函数机制。《Effective C++》中07条款中有这样一句话:当derived class 对象经由一个 base 指针被删除,而该对象带有一个 non-virtual 析构函数,其结果未有定义---实际执行时通常发生的是对象的 derived 成分未被销毁。

特地,写了下面的程序:


class A
{
public:
    ~A(){cout << "A destruction" << endl;}
    int a;
};
 
class B
{
public:
    ~B(){cout << "B destruction" << endl;}
};
 
class C : public A,public B
{
public:
    ~C(){cout << "C destruction" << endl;}
};
 
int main(void)
{
    A *pa = new C;
    B *pb = new C;
    C *pc = new C;
    delete pa;     // 没有问题
    delete pb;     // 出错
    delete pc;     // 没有问题
}

所说的「未定义」就在 delete pa; 和 delete pb; 体现出来。

强烈建议,在设计继承关系的时候,为每一个基类实现 virtual 析构函数。

回到开始的问题:

  1. 第一种情况是因为编译器安插了一个字节,为的是一个类的对象能再内存有独一无二的地址,无可厚非。
  2. 第二种情况是因为编译器安插了 vptr。
  3. 第三种情况是因为编译器除了安插 A 和 B 的 vptr 外,还有一个指向虚基类的指针。

另外,虚拟继承在应用比较少应用,一个例子就是:


class ios {...};
 
class istream : public virtual ios {...};
 
calss ostream : public virtual ios {...};
 
class iostream : public istream,public ostream {...};
这里 istream,ostream,iostream 共享同一份 ios。要和下面的情况区分开来:

class ios {...};
 
class istream : public ios {...};
 
calss ostream : public ios {...};
 
class iostream : public istream,public ostream {...};

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

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

相关文章

QtCreater 使用

QtCreater 创建项目 1.刚进入 QtCreater 的界面是这样的一个界面 ① 创建一个新的文件&#xff0c;那么我们就选择左上角的 “文件” ② 点击新建文件&#xff0c;或者也可以直接使用快捷键 CtrlN 此时就会弹出对话框&#xff0c;让我们选择想要创建的文件&#xff1a; Appli…

今天掏心窝子!聊聊35岁了程序员何去何从?

今天的内容不聊技术&#xff0c;聊聊轻松的话题&#xff0c;脑子高速转了好几周&#xff0c;停下来思考一下人生…… 不对&#xff0c;关于35岁的问题好像也不轻松&#xff0c;些许有点沉重&#xff0c;反正不是技术&#xff0c;不用高速转动脑细胞了&#xff0c;哈哈。 兄弟…

Spring源码刨析之配置文件的解析和bean的创建以及生命周期

public void test1(){XmlBeanFactory xmlBeanFactory new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));user u xmlBeanFactory.getBean("user",org.xhpcd.user.class);// System.out.println(u.getStu());}先介绍一个类XmlBeanFac…

1027: 舞伴问题

解法&#xff1a; #include<iostream> #include<vector> using namespace std; int main() {int n, m, k;cin >> n;vector<char> man(n);for (int i 0; i < n; i) cin >> man[i];cin >> m;vector<char> woman(m);for (int i 0…

VMware Workstation部署最新版OpenWrt 23.05.3

正文共&#xff1a;1456 字 51 图&#xff0c;预估阅读时间&#xff1a;2 分钟 我们之前介绍了如何在VMware Workstation上安装OpenWrt&#xff08;软路由是啥&#xff1f;OpenWrt又是啥&#xff1f;长啥样&#xff1f;在VMware装一个瞅瞅&#xff09;&#xff0c;也介绍了如何…

C++类和对象中下篇

赋值运算符重载 运算符重载 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也具有其返回值类型&#xff0c;函数名字以及参数列表&#xff0c;其返回值类型与参数列表与普通的函数类似。 函数名字为&#xff1a;关键字ope…

【数学】主成分分析(PCA)的详细深度推导过程

本文基于Deep Learning (2017, MIT)&#xff0c;推导过程补全了所涉及的知识及书中推导过程中跳跃和省略的部分。 blog 1 概述 现代数据集&#xff0c;如网络索引、高分辨率图像、气象学、实验测量等&#xff0c;通常包含高维特征&#xff0c;高纬度的数据可能不清晰、冗余&am…

QQ农场-phpYeFarm添加数据教程

前置知识 plugin\qqfarm\core\data D:\study-project\testweb\upload\source\plugin\qqfarm\core\data 也就是plugin\qqfarm\core\data是一个缓存文件,如果更新农场数据后,必须要删除才可以 解决种子限制(必须要做才可以添加成功) 你不更改加入了id大于2000直接删除种子 D…

Bridge 桥接

意图 将抽象部分与其显示部分分离&#xff0c;使他们都可以独立地变化。 结构 其中&#xff1a; Abstraction定义抽象类的接口&#xff0c;维护一个指向Implementer类型对象的指针。RefinedAbstraction扩展由Abstraction定义的接口。Implementor定义实现类的接口&#xff0c…

2024年DTC的回顾与思考

刚结束了2024的数据库技术嘉年华 这是我从2017年开始就参加的技术大会。中途因为疫情的耽误。正常来说我是连续的。知道我的朋友都知道我习惯炫耀一下。 按照惯例&#xff0c;此时此刻群友都在写大会回顾。只是有几个不讲武德的人已经发送了。下面有主观和客观的分析。 主观上…

亚马逊云科技CTO带你学习云计算降本增效秘诀

2023亚马逊云科技一年一度的重磅春晚--Re:invent上有诸多不同话题的主题Keynote&#xff0c;这次小李哥带大家复盘来自亚马逊CTO: Wener博士的主题演讲: 云架构节俭之道1️⃣节俭对于云计算为什么重要&#xff1f; ▶️企业基础设施投入大&#xff0c;利用好降本策略可以减少巨…

记录linux从0部署java项目(宝塔)

目录 一、安装宝塔可视化界面 二、部署前端 三、部署后端 1、配置并连接Mysql数据库 2、配置并连接redis 3、安装jdk 这里先记录一个安装后遇到的问题 安装openJDK 四、检查 一、安装宝塔可视化界面 宝塔面板下载&#xff0c;免费全能的服务器运维软件 运行安装脚本 安…

步骤大全:网站建设3个基本流程详解

一.领取一个免费域名和SSL证书&#xff0c;和CDN 1.打开网站链接&#xff1a;https://www.rainyun.com/z22_ 2.在网站主页上&#xff0c;您会看到一个"登陆/注册"的选项。 3.点击"登陆/注册"&#xff0c;然后选择"微信登录"选项。 4.使用您的…

时间序列模型:lag-Llama

项目地址&#xff1a;GitHub - time-series-foundation-models/lag-llama: Lag-Llama: Towards Foundation Models for Probabilistic Time Series Forecasting 论文地址&#xff1a;https://arxiv.org/pdf/2310.08278.pdf hugging-face镜像&#xff1a;https://hf-mirror.c…

C++11 设计模式2. 简单工厂模式

简单工厂&#xff08;Simple Factory&#xff09;模式 我们从实际例子出发&#xff0c;来看在什么情况下&#xff0c;应用简单工厂模式。 还是以一个游戏举例 //策划&#xff1a;亡灵类怪物&#xff0c;元素类怪物&#xff0c;机械类怪物&#xff1a;都有生命值&#xff0…

【一刷《剑指Offer》】面试题 3:二维数组中的查找

力扣对应题目链接&#xff1a;240. 搜索二维矩阵 II - 力扣&#xff08;LeetCode&#xff09; 核心考点&#xff1a;数组相关&#xff0c;特性观察&#xff0c;时间复杂度把握。 一、《剑指Offer》对应内容 二、分析题目 正常查找的过程本质就是排除的过程&#xff0c;谁排除…

傲基科技冲刺上市:依赖单一产品,元气未恢复,有股东提前退出

近日&#xff0c;傲基科技股份有限公司&#xff08;下称“傲基科技”&#xff09;递交招股书&#xff0c;准备在港交所主板上市&#xff0c;华泰证券为其独家保荐人。 据招股书介绍&#xff0c;傲基科技是一家提供家具家居类产品的品牌运营商及出口物流服务商。傲基科技在招股…

进程与线程的区别?

并发和并行 在聊进程和线程的概念之前&#xff0c;首先了解一下操作系统相关概念&#xff0c;大部分操作系统&#xff08;如Windos、Linux&#xff09;的任务调度是采用时间片轮转的抢占式调度方式&#xff0c;也就是一个任务执行一小段时间后强制暂停去执行下一个任务&#x…

音频变速python版

音频变速 如何能在不改变音频其他特点的情况下&#xff0c;只改变语速呢&#xff1f; 有几个python的库可以实现该功能&#xff0c;下面一一介绍。 pydub库 首先&#xff0c;确保安装了pydub和ffmpeg。 下面是一个简单的Python脚本&#xff0c;展示如何改变音频的播放速度&a…

通讯录的实现(顺序表版本)

我们知道通讯录是基于顺序表的前提下&#xff0c;要写好通讯录我们就要深入了解好顺序表。我们先来看看什么是顺序表。&#xff08;注意今天代码量有点多&#xff0c;坚持一下&#xff09;。冲啊&#xff01;兄弟们&#xff01; 顺序表的简单理解 对于顺序表&#xff0c;我们首…