当前位置: 首页 > 编程日记 > 正文

虚方法的调用是怎么实现的(单继承VS多继承)

我们知道通过一个指向之类的父类指针可以调用子类的虚方法,因为子类的方法会覆盖父类同样的方法,通过这个指针可以找到对象实例的地址,通过实例的地址可以找到指向对应方法表的指针,而通过这个方法的名字就可以确定这个方法在方法表中的位置,直接调用就行,在多继承的时候,一个类可能有多个方法表,也就有多个指向这些方法表的指针,一个类有多个父类,怎么通过其中一个父类的指针调用之类的虚方法?

其实前面几句话并没有真正说清楚,在单继承中,父类是怎么调用子类的虚方法的,还有多继承又是怎么实现这点的,想知道这些,请认真往下看。

我们先看单继承是怎么实现的。先上两个简单的类:

#include <iostream> 
using namespace std; class A
{
public:A():a(0){}virtual ~A(){}virtual void GetA(){cout<<"A::GetA"<<endl; }void SetA(int _a){a=_a; } int a;
};class B:public A
{
public:B():A(),b(0){}virtual ~B(){}virtual void GetA(){ cout<<"B::GetA"<<endl; }virtual void GetB(){ cout<<"B::GetB"<<endl; }
private:int b;
};typedef int (*Fun)(void);void TestA()
{Fun pFun;A a; cout<<"类A的虚方法(第0个是A的析构函数):"<<endl;int** pVtab0 = (int**)&a;for (int i=1; (Fun)pVtab0[0][i]!=NULL; i++){ pFun = (Fun)pVtab0[0][i]; cout << "    ["<<i<<"] "; pFun(); }cout<<endl;B b ;A* b1=&b;cout<<"类B的虚方法(第0个是B的析构函数)通过类B的实例:"<<endl;int** pVtab1 = (int**)&b;for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ pFun = (Fun)pVtab1[0][i]; cout << "    ["<<i<<"] "; pFun(); }cout<<endl;cout<<"类B的虚方法(第0个是B的析构函数)通过类A的指针:"<<endl;int** pVtab2 = (int**)&*b1;for (int i=1; (Fun)pVtab2[0][i]!=NULL; i++){ pFun = (Fun)pVtab2[0][i]; cout << "    ["<<i<<"] "; pFun(); }cout<<endl;cout<<"     b的地址:"<<&b<<endl;cout<<"b1指向的地址:"<<b1<<endl<<endl;
}

运行结果如下:

通过运行结果我们知道:通过父类指向子类的指针调用的是子类的虚方法。在单一继承中,虽然父类有父类的虚方法表,子类有子类的虚方法表,但是子类并没有指向父类虚方法的指针,在子类的实例中,子类和父类是公用一个虚方法表,当然只有一个指向方法表的指针,为什么可以公用一个虚方法表呢,虚方法表的第一个方法是析构函数,子类的方法会覆盖父类的同样的方法,子类新增的虚方法放在虚方法表的后面,也就是说子类的虚方法表完全覆盖父类的虚方法表,即子类的每个虚方法与父类对应的虚方法,在各种的方法表中的索引是一样的。

但是在多继承中就不是这样了,第一个被继承的类使用起来跟单继承是完全一样的,但是后面被继承的类就不是这样了,且仔细往下看。

还是先上3个简单的类

#include <iostream> 
using namespace std; class A
{
public:A():a(0){}virtual ~A(){}virtual void GetA(){cout<<"A::GetA"<<endl; }int a;
};class B 
{
public:B():b(0){}virtual ~B(){}virtual void SB(){ cout<<"B::SB"<<endl; } virtual void GetB(){  cout<<"B::GetB"<<endl; }private:int b;
};class C:public A,public B 
{
public:C():c(0){}virtual ~C(){}virtual void GetB()//覆盖类B的同名方法
    { cout<<"C::GetB"<<endl; }virtual void GetC(){ cout<<"C::GetC"<<endl; }virtual void JustC(){cout<<"C::JustC"<<endl; }
private:int c;
};typedef int (*Fun)(void);void testC()
{C* c=new C();A* a=c;B* b=c;Fun pFun;cout<<"sizeof(C)="<<sizeof(C)<<endl<<endl;cout<<"c的地址:"<<c<<endl;cout<<"a的地址:"<<a<<endl;cout<<"b的地址:"<<b<<endl<<endl<<endl;cout<<"类C的虚方法(第0个是C的析构函数)(通过C类型的指针):"<<endl;int** pVtab1 = (int**)&*c;for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ pFun = (Fun)pVtab1[0][i]; cout << "    ["<<i<<"] "<<&*pFun<<"    "; pFun(); }cout<<endl<<endl;cout<<"类C的虚方法(第0个是C的析构函数)(通过B类型的指针):"<<endl;pVtab1 = (int**)&*b;for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ pFun = (Fun)pVtab1[0][i]; cout << "    ["<<i<<"] "<<&*pFun<<"    "; pFun(); }
}

运行结果如下:

从结果说话:

Sizeof(C)=20,我们并不意外,在单继承的时候,父类和子类是公用一个指向虚方法表的指针,在多继承中,同样第一个父类和子类公用这个指针,而从第二个父类开始就有自己单独的指针,其实就是父类的实例在子类的内存中保持完整的结构,也就是说在多重继承中,之类的实例就是每一个父类的实例拼接而成的,当然可能因为继承的复杂性,会加一些辅助的指针。

指针a与指针c指向同一个地址,即c的首地址,而b所指的地址与a所指的地址相差8字节刚好就是类A实例的大小,也就是说在C的内存布局中,先存放了A的实例,在存放B的实例,sizeof(B)=8(字段int b和指向B虚方法表的指针),在家上C自己的字段int c刚好是20字节。

让我有点意外的是:方法B::SB,C::GetB并没有出现在类C的方法表中,而且C::GetB是C覆写B中的GetB方法,怎么没有出现在C的方法表中呢?在《深入探索C++对象模型》一书中讲到,这两个方法同时应该出现在C的方法表中,同样也会覆盖B的虚方法表。可能是不通的编译器有不同的实现,我用的是VS2010,那本书上讲的是编译器cfront

OK,我们不用管不同的编译器实现上的区别,这点小区别无伤大雅,虚方法的调用机制还是一样的。

先来分析几个小例子,看看虚方法的实现机制。

C* c=new C();

A* a=c;

a->GetA();

c->GetA();

c->GetC();

上面已经说了,a与c指向的是同一个地址,且公用同一个虚方法表,而方法GetA,GetC的地址就在这个方法表中,那么调用起来就简单多了,大致就是下面这个样子:

a->GetA()   ->   (a->vptr1[1])(a);   // GetA在方法表中的索引是1

c->GetA()  ->  (c->vptr1[1])(c);   // GetA在方法表中的索引是1

c->GetC()   ->   (a->vptr1[2])(c);   // GetC在方法表中的索引是2

vptr1表示指向类C第一个方法表的指针,这个指针实际的名字会复杂一些,暂且将指向类C的第一个方法表的指针命名为vptr2,下面会用到这个指针。

再来分析几行代码:

B* b=c;

c->GetB();

b->GetB();

指针b和指针c指向的不是同一个地址,那么B* b=c;到底是做了啥呢?大致是会转换成下面这个样子:

B* b=c+sizeof(A);

c所指的地址加上A的大小,刚好是b所指的地址。

c->GetB();同样需要转换,因为方法GetB根本不在c所指的那个方法表中,可能转换成这个样子(实际转换成啥样子我真不知道):

this=c+sizeof(A);

(this->vptr2[2])(c);

如果像编译器cfront所说的那样,方法GetB在vptr1所指的方法表中,那么就不用产生调整this指针了,如果在vptr1所指的方法表中,就让方法表变大了,且跟别的方法表是重复的。

b->GetB();就不需要做过多的转换了,因为b正好指向vptr2,可能转换成下面这个样子:

b->GetB()   ->   (b->vptr2[2])(b);   // GetB在方法表中的索引是2

总之指针所指的方法表如果没有要调用的方法,就要做调整,虚方法需要通过方法表调用,相对于非虚方法,性能就慢那么一点点,这也是别人常说的C++性能不如C的其中一点。

虚多继承就更麻烦了,不熟悉可能就会被坑。《深入探索C++对象模型》这本书是这样建议的:不要在一个virtual base class中声明nonstatic data members,如果这样做,你会距复杂的深渊越来越近,终不可拔。

virtual base class还是当做接口来用吧。

转载于:https://www.cnblogs.com/hlxs/p/3214062.html

相关文章:

asp.net 2.0防止同一用户同时登陆

要防止同一用户同时登陆,首页应该记录在线用户的信息(这里与用户名为例),然后判断正在登陆的用户里面是否已存在&#xff0e;在这里使用一个cache存放已经登陆的用户名&#xff0e;但是还有一个问题就是要知道用户是什么时候离开系统的呢&#xff1f;这就要定期清除cache中的内…

Python+Dash快速web应用开发——基础概念篇

作者&#xff1a;费弗里来源&#xff1a;Python大数据分析❝本文示例代码与数据已上传至https://github.com/CNFeffery/DataScienceStudyNotes❞1 简介这是我的新系列教程「PythonDash快速web应用开发」的第一期&#xff0c;我们都清楚学习一个新工具需要一定的动力&#xff0c…

POJ 1273 Drainage Ditches

网络流。题意非常easy。给出单向边&#xff0c;容量。找最大流。注意重边要加起来。g[u][v].cc; 第一次写网络流。也是第一个网络流的题。看了两天&#xff0c;理解了之后就唰唰唰的写出来了。 大概可能是EK吧。ORZ都不知道用的啥算法。仅仅是感觉要这样写。由于重边还WA了。改…

利用GridView显示主细表并一次编辑明细表所有数据的例子

全部代码如下&#xff1a; ASPX&#xff1a; <% Page Language"C#"ValidateRequest"false"AutoEventWireup"true"EnableViewState"false"CodeFile"Default2.aspx.cs"Inherits"Default2"%><!DOCTYPE ht…

TensorFlow搭建垃圾分类系统大师(免费领源码)

人工智能是一个多学科交叉融合的领域&#xff0c;其包含机器学习、计算机视觉、自然语言处理等多个子领域&#xff0c;其中计算机视觉是应用最广泛的领域之一。大多数人熟悉的手机和相机中的人脸识别功能&#xff0c;就是人工智能子领域——计算机视觉的体现。计算机视觉中的图…

for的循环遍体

以下讲解for的变体形式&#xff0c;对于一般的for语句常规这里不再赘述关于for变体 主要是用来实现一些特殊需求&#xff1a;//注意不要使for成为死循环 for(int i0;i!5;1){//DOLOOP }1&#xff09;假如&#xff0c;我们需要对循环变量i在循环外部使用&#xff0c;并调用循环变…

切版网上线,启用qieban.cn

2019独角兽企业重金招聘Python工程师标准>>> 近期&#xff0c;切版网收购并启用了qieban.cn域名&#xff0c;输入域名可以看到非常抢眼的黄底黑色的网站。复制国外psd2html模式&#xff0c;主要提供html5/css3前端外包。 可见切版网对域名的保护是非常的重视。据查询…

Microsoft .NET Pet Shop 4 架构与技术分析

1&#xff0e;项目概述与架构分析微软刚推出了基于ASP.NET 2.0下的Pet Shop 4, 该版本有了一个全新的用户界面。是研究ASP.NET 2.0的好范例啊&#xff0c;大家都知道&#xff0c;一直以来&#xff0c;在.NET和Java之间争论不休&#xff0c;到底使用哪个平台开发的企业级应用性能…

一学就会的 Python 时间转化总结(超全)

作者 | Peter来源 | Python编程时光在生活和工作中&#xff0c;我们每个人每天都在和时间打交道&#xff1a;早上什么时候起床&#xff1f;地铁几分钟来一趟&#xff1f;中午什么时候开始午休&#xff1f;明天是星期几&#xff1f;距离上次买衣服已经2个月呢&#xff1f;领导让…

ny20 吝啬的国度

吝啬的国度 时间限制&#xff1a;1000 ms | 内存限制&#xff1a;65535 KB难度&#xff1a;3描述在一个吝啬的国度里有N个城市&#xff0c;这N个城市间只有N-1条路把这个N个城市连接起来。现在&#xff0c;Tom在第S号城市&#xff0c;他有张该国地图&#xff0c;他想知道如果…

Linux常见命令(二)

随着Linux应用的扩展许多同学开始接触Linux&#xff0c;根据学习Windwos的经验往往有一些茫然的感觉&#xff1a;不知从何处开始学起。虽然Linux桌面应用发展很快&#xff0c;但是命令在Linux中依然有很强的生命力。Linux是一个命令行组成的操作系统,精髓在命令行&#xff0c;无…

谷歌编程语言年度榜NO.1:知识体系总结(2021版)

本文专注整理一些有关Python学习的知识体系。整理的Python知识体系主要包括基础知识&#xff0c;Python热门的应用方向&#xff0c;推荐书籍&#xff0c;FAQ以及一些常见面试题目&#xff0c;包含了作为一个Python全栈工程师以及数据分析工程师在开发工作和学习中需要用到或者可…

看看大网站到底是如何保障网络安全的

首先&#xff0c;服务器上用的是私有的操作系统和数据库&#xff0c;所谓私有&#xff0c;并不是完全自己写&#xff0c;而是说&#xff0c;全部都是进行私有化改造过的&#xff0c;一般使用开源的操作系统和数据库进行改造&#xff0c;比如说操作系统使用free bsd的改&#xf…

php 魔术方法 说明

1、__get、__set这两个方法是为在类和他们的父类中没有声明的属性而设计的。◆__get( $property ) 当调用一个未定义的属性时&#xff0c;此方法会被触发&#xff0c;传递的参数是被访问的属性名。◆__set( $property, $value ) 给一个未定义的属性赋值时&#xff0c;此方法会被…

小功能 - 收藏集 - 掘金

中国可以访问 Google Codelabs 网站啦&#xff01; - 掘金今天&#xff0c;Google 官方又宣布了一条信息「全球皆可访问的 Google Codelabs 网站」&#xff0c;说是全球&#xff0c;其实我们大家心里都明白&#xff0c;这是针对中国开发者而专门发布的一个网站&#xff0c;最近…

ASP.NET设置数据格式与String.Format使用总结

{0:d} YY-MM-DD{0:p} 百分比00.00%{0:N2} 12.68{0:N0} 13{0:c2} $12.68{0:d} 3/23/2003{0:T} 12:00:00 AM{0:男;;女} DataGrid-数据格式设置表达式 数据格式设置表达式 .NET Framework 格式设置表达式&#xff0c;它在数据显示在列中之前先应用于数据。此表达式由可选静态文本…

Android Display System --- Surface Flinger

SurfaceFlinger 是Android multimedia 的一个部分&#xff0c;在Android 的实现中它是一个service &#xff0c;提供系统范围内的surface composer 功能&#xff0c;它能够将各种[url]应用[/url]程序的2D 、3D surface 进行组合。在具体讲SurfaceFlinger 之前&#xff0c;我们先…

最新组合式模型量化方法,实现FPGA最高硬件利用率,准确率-推理速度达到SOTA...

作者 | 王言治来源 | AI科技大本营&#xff08;ID:rgznai100&#xff09;深度神经网络&#xff08;DNN&#xff09;在图像、语言处理等领域获得了巨大成功&#xff0c;而如何将这些网络部署在ASIC、FPGA等嵌入式设备仍是热门研究方向。结构搜索&#xff0c;以及传统的剪枝、量化…

消息服务发送短信,手机接收不到短信解决思路

阿里云使用消息服务&#xff0c;发送注册码给手机。测试几次发现手机都接收不到&#xff0c;后台也没报错&#xff01;今天我提交自己的工单&#xff0c;售后工程师已经帮我解决了&#xff0c;非常感谢他&#xff01;官方代码&#xff1a;https://help.aliyun.com/document_det…

Asp.net中具体的日期格式化用法

1.绑定时格式化日期方法: <ASP:BOUNDCOLUMN DATAFIELD "JoinTime " DATAFORMATSTRING "{0:yyyy-MM-dd} " > <ITEMSTYLE WIDTH "18% " > </ITEMSTYLE > </ASP:BOUNDCOLUMN > 2.数据控件如DataGrid/DataList等的件格式…

日本「AI 鱼脸识别」项目,每分钟识别 100 条

来源 | HyperAI超神经头图 | 视觉中国近日&#xff0c;日本的一个 AI 分拣鱼类项目进入实验阶段。这将有望改善日本渔业劳动力老龄化及短缺的社会现状。日本作为岛国&#xff0c;其独特的地理位置&#xff0c;让国民自古以来就跟鱼结下了不解之缘&#xff0c;甚至形成了其独特的…

使用Spring实现邮件发送

2019独角兽企业重金招聘Python工程师标准>>> 这两天写个小程序需要使用邮件发送的功能&#xff0c;在网上搜索了一帮子文章&#xff0c;感觉还是使用Spring的邮件发送功能比较方便&#xff0c;哈哈&#xff0c;懒人就这样子了&#xff0c;不想再动了。整好了&#x…

GZip压缩与解压缩

GZIP的压缩与解压缩代码&#xff1a; public static class CompressionHelper{/// <summary> /// Compress the byte[] /// </summary> /// <param name"input"></param> /// <returns></returns> public static byte[] Compres…

String.Format()方法

String.Format方法是我们在.Net应用开发时经常使用到的&#xff0c;它的灵活使用有时能够达到事半功倍的效果&#xff0c;下面我们就借用MSDN上的一个示例来向大家展示String.Format的各种用法。 该示例展示了Numeric、DateTime和Enumeration标准格式的使用&#xff0c;另外&am…

Brian 的 Perl 问题之万能指南

为什么80%的码农都做不了架构师&#xff1f;>>> PerlChina brians Guide to Solving Any Perl Problem Home Page | All Pages | Recently Revised | Authors | Feeds | Export | 原 名&#xff1a;Brian’s Guide to Solving Any Perl Problem 中 文: Brian 的 Pe…

联手小米,雀巢中国推出健康管家Nesfinity,满足个性化生活需求管理

1月20日&#xff0c;雀巢中国携手小米战略合作正式开启&#xff0c;双方共同打造的雀巢健康管家“Nesfinity”正式发布。这项综合性智能健康生活新生态的合作&#xff0c;不仅开启了雀巢中国在智能物联网和大数据应用的新历程&#xff0c;更是双方由品牌合作跨入战略合作的开端…

使用joda-time工具类 计算时间相差多少 天,小时,分钟,秒

下面程序使用了两种方法计算两个时间相差 天&#xff0c;小时&#xff0c;分钟&#xff0c;秒 package jodotest;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import org.joda.time.DateTime;import org.joda.time.Days;import …

C语言格式控制符和转义字符

1. 格式控制符 格式输出printf 作用是向终端输出若干个类型任意的数据。 格式&#xff1a;printf &#xff08;格式控制符&#xff0c;输出列表&#xff09; 1) 格式控制符 l &#xff05; 格式说明引导符。 l &#xff0d; 指定…

面试高频题:单链表的逆置操作/链表逆序

函数内对形参的操作并不能影响实参&#xff0c;函数内修改的是实参的副本。要想在函数内部修改输入参数&#xff0c;要么传入的是实参的引用&#xff0c;要么传入的是实参的地址。 #include <iostream> #include <cstdlib> #include <cstring>//strlen using…

猖狂!微软、思科源码惨遭黑客 100 万美元打包出售

【编者按】SolarWinds 黑客攻击事件又延伸出新的危害了&#xff1a;微软、思科、FireEye 等公司的源代码在一网站公开出售&#xff0c;明码标价&#xff0c;甚至打包价为一百万&#xff0c;究竟是什么情况&#xff1f;整理 | 郑丽媛出品 | CSDN&#xff08;ID&#xff1a;CSDNn…