虚基类的作用

article/2025/10/7 12:46:56


1 概念

    首先还是先给出虚继承和虚基类的定义。

虚继承:在继承定义中包含了virtual关键字的继承关系;
虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:
CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass的虚基类,而不是说CBase就是个虚基类,因为CBase还可以作为不是虚继承体系中的基类。

    Virtual 在C++中就是采用了这个词意,不可以在语言模型中直接调用或体现的,但是确实是存在可以被间接的方式进行调用或体现的。比如:虚函数必须要通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数,而虚继承也是必须在运行时才能够进行定位访问的一种体制。存在,但间接。其中关键就在于存在、间接和共享这三种特征。

    对于虚函数而言,这三个特征是很好理解的,间接性表明了他必须在运行时根据实际的对象来完成函数寻址,共享性表象在基类会共享被子类重载后的虚函数,其实指向相同的函数入口。

    对于虚继承而言,这三个特征如何理解呢?存在即表示虚继承体系和虚基类确实存在,间接性表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成(下面模型中会讲到),共享性表象在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。

    那现在可以解释语法小节中留下来的那个问题了,“ 为什么一旦出现了虚基类,就必须在每一个继承类中都必须包含虚基类的初始化语句”。由上面的分析可以知道,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的),这样一来既然是共享的那么每一个子类都不会独占,但是总还是必须要有一个类来完成基类的初始化过程(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化, 那到底谁应该负责完成初始化呢?C++标准中(也是很自然的) 选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),而在最下层继承子类中实际执行初始化过程。所以上面在每一个继承类中都要书写初始化语句,但是在创建对象时,而仅仅会在创建对象用的类构造函数中实际的执行初始化语句,其他的初始化语句都会被压制不调用。



2 模型

    为了实现上面所说的三种语义含义,在考虑对象的实现模型(也就是内存模型)时就很自然了。在C++中对象实际上就是一个连续的地址空间的语义代表,我们来分析虚继承下的内存模型。


(1) 存在

    也就是说在对象内存中必须要包含虚基类的完整子对象,以便能够完成通过地址完成对象的标识。那么至于虚基类的子对象会存放在对象的那个位置(头、中间、尾部)则由各个编译器选择,没有差别。(在VC8中无论虚基类被声明在什么位置,虚基类的子对象都会被放置在对象内存的尾部)。


(2) 间接

    间接性表明了在 直接虚继承子类中一定包含了某种指针(偏移或表格)来完成通过子类访问虚基类子对象(或成员)的间接手段(因为虚基类子对象是共享的,没有确定关系),至于采用何种手段由编译器选择。(在VC8中在子类中放置了一个虚基类指针vbc,该指针指向虚函数表中的一个slot,该slot中存放则虚基类子对象的偏移量的负值,实际上就是个以补码表示的int类型的值,在计算虚基类子对象首地址时,需要将该偏移量取绝对值相加,这个主要是为了和虚表中只能存放虚函数地址这一要求相区别,因为地址是原码表示的无符号int类型的值)。


(3) 共享

    共享表明了在对象的内存空间中仅仅能够包含一份虚基类的子对象,并且通过某种间接的机制来完成共享的引用关系。在介绍完整个内容后会附上测试代码,体现这些内容。



3 性能

    由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。


(1) 时间

    在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。(在VC8中通过打开汇编输出,可以查看*.cod文件中的内容,在访问虚基类对象成员时会形成三条mov间接寻址语句,而在访问一般继承类对象时仅仅只有一条mov常量直接寻址语句)


(2) 空间

由于共享所以不存在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。




实例1,无虚函数的虚基类

 当一个基类被声明为虚基类后,即使它成为了多继承链路上的公共基类,最后的派生类中也只有它的一个备份。例如:


class CBase { };
class CDerive1:virtual public CBase{ };
class CDerive2:virtual public CBase{ };
class CDerive12:public CDerive1,CDerive2{ };

则在类CDerive12的对象中,仅有类CBase的一个对象数据

虚基类的特点:

       虚基类构造函数的参数必须由最新派生出来的类负责初始化(即使不是直接继承);

       虚基类的构造函数先于非虚基类的构造函数执行。


#include <iostream> 
using namespace std;//基类    
class CBase
{
protected:int a;
public:CBase(int na){a = na;cout << "CBase constructor! \n";}~CBase(){cout << "CBase deconstructor! \n"; }
};//派生类1(声明CBase为虚基类)    
class CDerive1 :virtual public CBase{
public:CDerive1(int na) :CBase(na){cout << "CDerive1 constructor! \n";}~CDerive1(){cout << "CDerive1 deconstructor! \n"; }int GetA(){return a; }
};//派生类2(声明CBase为虚基类)    
class CDerive2 :virtual public CBase{
public:CDerive2(int na) :CBase(na){cout << "CDerive2 constructor! \n";}~CDerive2(){cout << "CDerive2 deconstructor! \n"; }int GetA(){return a; }
};//子派生类    
class CDerive12 :public CDerive1, public CDerive2{
public:CDerive12(int na1, int na2, int na3) :CDerive1(na1), CDerive2(na2), CBase(na3){cout << "CDerive12 constructor! \n";}~CDerive12(){cout << "CDerive12 deconstructor! \n"; }
};int main()
{{CDerive12 obj(100, 200, 300);//得到从CDerive1继承的值    cout << "from CDerive1 : a = " << obj.CDerive1::GetA() << endl;//得到从CDerive2继承的值    cout << "from CDerive2 : a = " << obj.CDerive2::GetA() << endl << endl;}system("pause");return 0;
}



(1) 子派生类对象的值:

  
     从上例可以看出,在类CDerived12的构造函数初始化表中,调用了间接基类CBase的构造函数,这对于非虚基类是非法的,但对于虚基类则是合法且必要的。

  对于派生类CDerived1和CDerived2,不论是其内部实现,还是实例化的对象,基类CBase是否是它们的虚基类是没有影响的。受到影响的是它们的派生类CDerived12,因为它从两条路径都能到达CBase。


(2) 内存布局



备注:

vbtable : 虚基类指针表

vftable : 虚函数指针表

         


(3) 运行结果:

    由此可知,其公共基类的构造函数只调用了一次,并且优先于非基类的构造函数调用;并且发现,子派生类的对象obj的成员变量的值只有一个,所以,当公共基类CBase被声明为虚基类后,虽然它成为CDerive1和CDerive2的公共基类,但子派生类CDerive12中也只有它的一个备份。可以仔细比较与例2的运行结果有什么不同。




5 实例2,有虚函数的虚基类


下述实例析构函数为虚函数


#include <iostream> 
using namespace std;//基类    
class CBase
{
protected:int a;
public:CBase(int na){a = na;cout << "CBase constructor! \n";}virtual ~CBase(){cout << "CBase deconstructor! \n"; }
};//派生类1(声明CBase为虚基类)    
class CDerive1 :virtual public CBase{
public:CDerive1(int na) :CBase(na){cout << "CDerive1 constructor! \n";}~CDerive1(){cout << "CDerive1 deconstructor! \n"; }int GetA(){return a; }
};//派生类2(声明CBase为虚基类)    
class CDerive2 :virtual public CBase{
public:CDerive2(int na) :CBase(na){cout << "CDerive2 constructor! \n";}~CDerive2(){cout << "CDerive2 deconstructor! \n"; }int GetA(){return a; }
};//子派生类    
class CDerive12 :public CDerive1, public CDerive2{
public:CDerive12(int na1, int na2, int na3) :CDerive1(na1), CDerive2(na2), CBase(na3){cout << "CDerive12 constructor! \n";}~CDerive12(){cout << "CDerive12 deconstructor! \n"; }
};int main()
{{CDerive12 obj(100, 200, 300);//得到从CDerive1继承的值    cout << "from CDerive1 : a = " << obj.CDerive1::GetA() << endl;//得到从CDerive2继承的值    cout << "from CDerive2 : a = " << obj.CDerive2::GetA() << endl << endl;}system("pause");return 0;
}


(1) 内存布局











6 实例3,非虚继承


#include <iostream>   
using namespace std;//基类      
class CBase
{
protected:int a;char b;
public:CBase(int na){a = na;cout << "CBase constructor! \n";}~CBase() { cout << "CBase deconstructor! \n"; }
};//派生类1      
class CDerive1 : public CBase
{
public:CDerive1(int na) :CBase(na){cout << "CDerive1 constructor! \n";}~CDerive1() { cout << "CDerive1 deconstructor! \n"; }int GetA() { return a; }private:char c;
};//派生类2      
class CDerive2 : public CDerive1
{
public:CDerive2(int na) :CDerive1(na){cout << "CDerive2 constructor! \n";}~CDerive2() { cout << "CDerive2 deconstructor! \n"; }int GetA() { return a; }
private:char d;
};int main()
{system("pause");return 0;
}


(1) 内存布局




CDerive1 直接从CBase搬移过来,再加上自己的




CDerive2 直接从CDerive1搬移过来,再加上自己的












本文转自:

http://blog.csdn.net/caomiao2006/article/details/4463664






http://chatgpt.dhexx.cn/article/DIxWcdX9.shtml

相关文章

系统调用:系统调用的实现

7.3.3 系统调用的实现 1. 中断和陷入硬件机构 (1) 什么是中断和陷入。 中断是指CPU对系统发生某事件时的这样一种响应&#xff1a;CPU暂停正在执行的程序&#xff0c;在保留现场后自动地转去执行该事件的中断处理程序&#xff1b;执行完后&#xff0c;再返回到原程序的断点处继…

系统调用、函数调用

1、系统调用 操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性&#xff0c;为了达到这个目的&#xff0c; 内核提供一系列具备预定功能的多内核函数&#xff0c;通过一组称为系统调用&#xff08;system call)的接口呈现给…

C++ 虚基类

C 虚基类 概述多重继承的问题虚基类初始化例子 总结 概述 虚基类 (virtual base class) 是用关键字 virtual 声明继承的父类. 多重继承的问题 N 类: class N { public:int a;void display(){cout << "A::a" << a <<endl;} };A 类: class A :…

虚基类

虚基类 意义:假设定义了一个公共基类A。类B和类C都由类A公有派生,类D由类B和类C公有派生。显然D包含类A的两个拷贝,不仅多占用内存,而且还造成多个拷贝的数据不一致。 不定义虚基类的效果如下: class A {public:int x;void SetX(int a) { x = a; }A(int a = 0) { x = a…

【操作系统】系统调用

文章目录 系统调用系统调用举例文件拷贝的系统调用过程应用程序接口系统调用接口C语言系统调用接口Windows和UNIX系统调用示例系统调用与的库函数区别补充说明系统调用 系统调用,System call,提供了操作系统的服务接口。这些系统调用通常以C或C++编写,对某些底层任务可能以…

C++之基类调用自己的虚函数

每个类中存在虚函数时&#xff0c;当构造一个对象时&#xff0c;系统会为对象分配相应的内存空间。但是虚函数表存放在程序的只读数据段中&#xff0c;在实例化对象时&#xff0c;编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable&#xff1b; 下面看一个小例子&#x…

如何调用基类私有的虚函数

直接看代码 #include <iostream> #include <stdio.h> using namespace std;class person { public:virtual void name(){cout<<"A::name"<<endl;}private:virtual void sex(){cout<<"A::sex"<<endl;} };class studen…

详解虚函数的实现过程之虚基类(4)

博客虚函数实现过程3 时提到过虚基类&#xff0c;这里呢&#xff0c;我们来详细讲述一下&#xff1a; 当我们在虚函数的声明结尾处添加“0”&#xff0c;这种虚函数就被称为纯虚函数。 它好似一个没有实现只有声明的函数&#xff0c;它的存在就是为了让类具有抽象类的功能&…

虚基类的基本概念

1.虚基类的作用 类B与类C都为类D的基类&#xff0c;而类A为类B与类C的基类&#xff0c;因此类D会继承类B与类C的多份同名成员。如下图所示&#xff1a; 在引用这些同名成员时&#xff0c;必须在派生类对象后增加直接基类名&#xff0c;以避免产生二义性&#xff0c;如&#x…

【虚基类、虚函数及应用】

虚基类 1.虚基类存在的意义 当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处&#xff0c;这个公共的基类就会产生多个实例(或多个副本)&#xff0c;若只想保存这个基类的一个实例&#xff0c;可以将这个公共基类说明为虚基类。 在继承中产生歧义的原因有可能是…

虚函数的调用机理

C中&#xff0c;必然会接触到虚函数这个概念&#xff0c;那么&#xff0c;会不会对虚函数的内部机理产生疑问呢&#xff1f;而这里&#xff0c;就是对其的简单的研究&#xff08;本人水平有限(&#xff61;•́︿•̀&#xff61;) &#xff09; 首先&#xff0c;先来简单介绍…

系统调用

程序接口是 OS 专门为用户程序设置的&#xff0c;也是用户程序取得 OS 服务的唯一途径。程序接口通常是由各种类型的系统调用所组成的&#xff0c;因而&#xff0c;也可以说&#xff0c;系统调用提供了用户程序和操作系统之间的接口&#xff0c;应用程序通过系统调用实现其与 O…

舵机内部结及工作原理浅析

一、舵机实物图 就像上面这张照片&#xff0c;相信大家都不会陌生&#xff0c;我们常见到的舵机就是这个模样&#xff0c;一般是塑料外壳&#xff0c;当然很少见的也有金属外壳的舵机&#xff0c;因为涉及到控制信号&#xff0c;所以一般有三条引出线。 像上图所示的样子&#…

舵机控制原理详解

控制信号由接收机的通道进入信号调制芯片&#xff0c;获得直流偏置电压。它内部有一个基准电路&#xff0c;产生周期为20ms&#xff0c;宽度为1.5ms的基准信号&#xff0c;将获得的直流偏置电压与电位器的电压比较&#xff0c;获得电压差输出。最后&#xff0c;电压差的正负输出…

PWM驱动舵机原理、SG90、舵机控制

舵机自控系统 自控制电路板接收来自信号线的控制信号&#xff0c;控制电机转动&#xff0c;电机带动一系列齿轮组&#xff0c;减速后传动至输出舵盘。舵机的输出轴和位置反馈电位计是相连的&#xff0c;舵盘转动的同时&#xff0c;带动位置反馈电位计&#xff0c;电位计将输出…

舵机的原理和控制

舵机的原理和控制控制信号由接收机的通道进入信号调制芯片&#xff0c;获得直流偏置电压。它内部有一个基准电路&#xff0c;产生周期为20ms&#xff0c;宽度为1.5ms的基准信号&#xff0c;将获得的直流偏置电压与电位器的电压比较&#xff0c;获得电压差输出。最后&#xff0c…

小白入门STM32(2)---控制SG90舵机---基础工作原理详解

文章目录 序言一、基础理论1.1 舵机控制原理--PWM习题 1.2 定时器1.2.1 基础定时器时钟装置循环计数器 1.2.2 比较定时器习题 二、实战上手2.1 设置定时器和单片机接线习题 2.2 代码 三、习题答案 序言 本人一枚软件编程人员&#xff0c;有一定C语言基础&#xff0c;目前自学S…

舵机的控制

舵机是一种位置「角度」伺服的驱动器&#xff0c;适用于那些需要角度不断变化并可以保持的控制系统。目前在高档遥控玩具、航模、机器人中已经得到普遍使用。舵机是一种俗称&#xff0c;其实是一种伺服马达。本篇通过官方驱动库来实现舵机控制。 一. 舵机介绍 1、结构组成 舵机…

51单片机SG90舵机控制原理

舵机三根线的接法:黄线接信号线&#xff0c;红线接vcc&#xff0c;褐色线接GND 舵机控制原理:通过控制PWM来控制舵机转动的角度&#xff0c;关于PWM的知识可以去智能小车专栏进行学习&#xff0c;转动周期设置为20ms&#xff0c;控制高电平的时间来进行舵机转动的角度。 对于1…

单片机PWM舵机控制原理

舵机的控制一般需要一个20ms的时基脉冲&#xff0c;该脉冲的高电平部分一般为0.5ms~2.5ms范围内的角度控制脉冲部分。以180度角度舵机为例&#xff0c;那么对应的控制关系是这样的&#xff1a; 0.5ms--------------0度&#xff1b; 1.0ms------------45度&#xff1b; 1.5ms---…