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

article/2025/10/7 13:10:26

虚基类

在这里插入图片描述

1.虚基类存在的意义

当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
在继承中产生歧义的原因有可能是继承类继承了基类多次,如概述图所示,子类C最后会接受分别来自A和B的同一个或多个相同拷贝,从而产生了多个拷贝,即不止一次的通过多个路径继承类在内存中创建了基类成员的多份拷贝。而这些是A和B从父类继承而来,所以C类该继承A还是B传下来的还是都接受呢?
2.虚基类的定义方式:

虚基类(virtual base class)定义方式如下:
class 派生类名:virtual 访问限定符 基类类名{…};
class 派生类名:访问限定符 virtual 基类类名{…};

不用虚基类时产生的数据冗余和二义性问题:
如下代码属于菱形继承方式:
blog.csdnimg.cn/ded0abe6d56444a2b8c57862639a9a33.png)
下面是不用虚基类处理此继承关系时的代码:

class person
{public:string _pname;string _sex;person(){cout << "Create person " << endl;}person(string& pname ):_pname(pname){cout << "Create person " << endl;}virtual ~person() { cout << "Destroy person" << endl; }void fun(){cout << "person::fun()" << endl;}
};
class student  :public   person
{
public:string _id;student() { cout << "Create student" << endl; }student(string &sname,string &id) : _id(id){person::_sex = "男";cout << "Create student" << endl;}~student() { cout << "Destroy student" << endl; }virtual void fun(){cout << "student::fun" << endl;}
};
class employee :public   person
{
protected:string _ename;
public:employee(string name):_ename(name){person::_sex = "女";cout << "Create employee" << endl;}~employee() { cout << "Destroy employee" << endl; }
};
class estudent :public student,public employee 
{
protected:string _esname;
public:estudent(string esname,string &id):student(esname,id),_esname(esname){cout << "Create estudent" << endl;}~estudent() {}virtual void fun(){cout << "estudent::fun" << endl;}
};int main()
{string esname{ "李华" };string sex{ "女" };string id{ "202201" };person s;estudent es1(esname,  id);student es2(esname,  id);es1._sex = { "女" };//errores2._sex= { "女" };//ok !return 0;
}

我们可以发现想调动estudent类的成员时,编译器不知道你到底想调用哪个类中的person 属性。在这里插入图片描述

我们访问人类型中的_sex 属性,如果不明确是哪个类中的_sex则无法编译通过。如果我们只想要一个person类的实例,那就要用到虚基类

虚基类的处理代码:

class person
{public:string _pname;string _sex;person(){cout << "Create person " << endl;_sex={"男"};}person(string& pname ):_pname(pname){cout << "Create person " << endl;}virtual ~person() { cout << "Destroy person" << endl; }void fun(){cout << "person::fun()" << endl;}
};
class student  :public  virtual person
{
public:string _id;student() { cout << "Create student" << endl; }student(string &sname,string &id) : _id(id){cout << "Create student" << endl;}~student() { cout << "Destroy student" << endl; }virtual void fun(){cout << "student::fun" << endl;}
};
class employee :public virtual  person
{
protected:string _ename;
public:employee(string name):_ename(name){cout << "Create employee" << endl;}~employee() { cout << "Destroy employee" << endl; }
};
class estudent :public student,public employee 
{
protected:string _esname;
public:estudent(string esname,string &id):student(esname,id),employee(esname),_esname(esname){cout << "Create estudent" << endl;}~estudent() {}virtual void fun(){cout << "estudent::fun" << endl;cout<<"person::_sex"<<person::_sex<<endl;}
};int main()
{string esname{ "李华" };string id{ "202201" };person s;estudent es1(esname,  id);esl._sex={"中性"};es1.fun();return 0;

在这里插入图片描述

注意,一旦使用虚基类,那么派生类中构造函数中的基类的属性就不用传参了,因为没有必要。

编译器不同,虚表指针指向的处理方式就会不同。

在这里插入图片描述
可以看到 person 只有单独一份。

在这里插入图片描述
接下来是一个测试,看能不能成功打印_sex;

int main()
{string esname{ "李华" };string id{ "202201" };//person s;estudent es1(esname,  id);person*ps=&es1;cout<<ps->_sex<<endl;return 0;

在这里插入图片描述
结果是可以打印的,也就是说会指针自动偏移到person类。但是这个结果放在不同的编译器可能会不一样,有的可能不会自动偏移。

静态转化和动态转换

int main()
{
person s;//cout<<s._sex << endl;estudent es1(esname, id);string s1;person* ps = &es1;estudent* pest = static_cast<estudent*>(ps);//如果没有虚基类,则此静态转换可以成功。}

虚析构函数
一个重要的作用:重置虚表指针(析构时设置虚表指针指向自己类型的虚表)
在这里插入图片描述

class object
{int value;
public:object(int x=0):value(x){ add(); }~object(){ add(); }virtual void add(int x = 10) { cout << "object ::add x " << x << endl; }};
class base :public object
{
public:int num;public:base(int x = 0) :object(x + 10), num(x) { add(100); }~base() { add(200); }virtual void add(int x ){cout << "base::add  x: " << x << endl;}
};
int main()
{base base;return 0;
}

在这里插入图片描述
可以看到 ,在构造函数和析构函数中调动的函数,查虚表与否的结果都是一样的。编译器在优化时选择不再查虚表。

在这里插入图片描述
在obj的构造函数中调动add函数前已经完成了虚表和虚表指针的设置,obj的构造函数中调动add函数时,将会调动的是它自己的虚表。(虚表指针的指向的是obj的虚表)


在进行指针指向对象的操作时对象的析构有时会出现内存的泄漏:
1)虚构函数前没有加vitual 关键字的情况:

class person
{public:string _pname;string _sex;~person() { cout << "Destroy person" << endl; }
};
class student  :public   person
{
public:string _id;~student() { cout << "Destroy student" << endl; }}
};class estudent :public student,public employee 
{
protected:string _esname;
public:~estudent() {}
};
int main(){
string esname{ "李华" };string sex{ "女" };string id{ "202201" };student* s1 = new estudent();delete s1;}

在这里插入图片描述
可以看到泄漏了estudent对像。

2)虚析构函数前加了vitual 关键字的情况:

class person
{
public:virtual ~person() { cout << "Destroy person" << endl; }
};class estudent :public student,public employee 
{
public:~estudent() {}
};

在这里插入图片描述
在虚析构函数析构时会查虚表。

运行时的多态

类型名+ 点的形式属于编译时的多态。

在这里插入图片描述
运行时的多态:

总结:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。

int main()
{
person s;estudent es1(esname, id);person* ps = &es1;}

在这里插入图片描述

多态的原理

虚函数表的示例:运行时多态的原理

虚函数指针表简称虚表, 虚表就是虚函数指针的集合,虚函数指针表本质是一个存储虚函数指针的指针数组,这个数组的首元素之上存储RTTI(运行时类型识别信息的指针),从数组下标0开始依次存储虚函数地址, 最后面放了一个nullptr。

虚函数指针表存储在只读数据段(.rodata)

静态联编和动态联编:

静态联编(static binding)早期绑定: 静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。

动态联编(dynamic binding)亦称滞后联编(late binding)或晚期绑定: 动态联编是指在程序执行的时候才将函数实现和函数调用关联起来。
C++语言中,使用类类型的引用或指针调用虚函数(成员选择符“->”),则程序在运行时选择虚函数的过程,称为动态联编

关于动态联编的几个例子

1.当派生类的对象属性不是公有,属性值有初始值的情况:

class object
{
public://virtual void fun(object *const this,int a = 10)constvirtual void fun(int a = 10)const{cout << "object ::fun::a :" << a << endl;}
};
class base :public object
{
private:virtual void fun(int x = 20)const{cout << "base::fun  x: " << x << endl;}
};
int main()
{base base1;object* op =&base1;base1.fun()//base(&base1);op->fun();return 0;
}

在这里插入图片描述
先看流程
在这里插入图片描述
我们把下图右侧的es1对象当作base1对象,当拿obj类型的指针指向base1对象时,op就指向了此对象的首地址,也就是把this指针给给op,op指向了base1的虚表。所以就算op是obj类型,调动的fun()函数也是base的虚表。

虽然op是
示例2:

class Object {int value;
public:Object(int x = 0) :value(x) {}void print() { cout << "object::print<<" << endl; add(10); }virtual void add(int x) { cout << "object::add :x" << x << endl; }};
class Base :public Object
{int num;
public:Base(int x = 0) :Object(x + 10), num(x) {}void show(){cout << "Base ::show" << endl;print();}virtual void add(int x) { cout << "base ::Add x" << x << endl; }
};
int main()
{Base base;base.show();//show(&base);
}

流程如图:
在这里插入图片描述
思考:

int main()
{Base base;Object &ob = base;ob.print(); 是什么结果?//Object ob = base;//ob.print();  又是什么结果?
}

Object &ob = base;
以引用的方式调动虚函数将会查引用于Base的虚表。

Object ob = base;
以值传递,调动的obj的虚表。

示例3:memset 与虚表指针

class object
{int val;int data[10];
public:object() { memset(this, 0, sizeof(object)); }virtual void fun(){cout << "fun " << endl;}
};int main(){object obj;object* op = &obj;obj.fun();op->fun();}

在这里插入图片描述

memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。

当使用memset初始化obj对象时,对象内存中的虚表指针连同整个空间全部被置为空。所以此时用此函数时比较危险的。

示例4 :this 指针与 虚表指针

using namespace std;
class object
{int val;
public:object(int x=0):val(x) { }void show() { cout << "object::show "  << endl; }void print(){cout << "object::val " << val << endl;}
};int main(){object *op=nullptr;op->show();op->print();}

打印结果??

show可以打印,因为打印的值不用this操作。而print要打印属性值,没有this指针,编译器报错。
在这里插入图片描述


class object
{int val;
public:object(int x=0):val(x) { }void show() { cout << "object::show "  << endl; }void print(){cout << "object::val " << hex<<val << endl;}
};
int main(){object* op = (object*)malloc(sizeof(object));op->show();op->print();}

打印结果?在这里插入图片描述
编译器认为op指向了一个对象,op指向了val,但这个对象没有初始化,所以是个随机值、(cdcdcdcdc是未初始化的数据值)。

class object
{int val;
public:object(int x=0):val(x) { }void show() { cout << "object::show "  << endl; }virtual void print(){cout << "object::val " << hex<<val << endl;}
};
int main(){object* op = (object*)malloc(sizeof(object));op->show();op->print();}

print函数加了个虚,那么打印结果?
在这里插入图片描述
show可以正常打印、
因为只是开辟空间,并没有实例化对象,没有调用构造函数,不能设置虚表指针。


示例5:

int main(){object* op = (object*)malloc(sizeof(object));object ob;*op=ob;op->show();op->print();}

打印结果?
在这里插入图片描述

在这里插入图片描述
蓝色是ob,黑色是*op
op->指向的对象被ob赋值(编译器认为,其实没有对象只有空间),缺醒的赋值重载只赋值数据域不赋值指针域。所以show()正常,print失败。

为什么只赋值数据域?

这里是引用

在这里插入图片描述

假如base给给obj的切片现象不仅赋值派生类中基类的数据域,也赋值派生类指向的虚表指针,那么op在指向base的虚表时要打印派生类的属性成员的值,但op指向的是基类object的对象,没有value成员(obj没有被赋值子对象的空间),所以这在逻辑上是行不通的。


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

相关文章

虚函数的调用机理

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---…

STM32 PWM控制舵机——原理、接线、源程序

STM32——PWM 控制舵机 通用定时输出PWM PWM的工作原理PWM的模式 TIM_OCMode_PWM1 &#xff08;边沿对齐模式&#xff09;TIM_OCMode_PWM2&#xff08;中央对齐模式&#xff09; 占空比 舵机 实物图接线舵机工作原理周期TPWM占空比 TIM3 PWM输出 驱动SG90电机 配置过程&#xf…

pwm波控制舵机原理(转)

文章转自&#xff1a;http://www.geek-workshop.com/thread-70-1-1.html 一、关于舵机&#xff1a; 舵机&#xff08;英文叫Servo&#xff09;&#xff1a;它由直流电机、减速齿轮组、传感器和控制电路组成的一套自动控制系统。通过发送信号&#xff0c;指定输出轴旋转角度。…

pwm信号控制舵机的简单原理

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

SG90舵机的原理和控制方式

前言 做过机器人、智能车或者玩航模的朋友应该对舵机不会陌生&#xff0c;这种舵机也是很常用的。 舵机只是我们通俗的叫法&#xff0c;它的本质是一个伺服电机&#xff0c;也可以叫做位置(角度)伺服驱动器。一般被应用在那些需要控制角度变化的系统中&#xff0c;可以方便的…

舵机控制原理和结构

原文&#xff1a;https://blog.zeruns.tech/index.php/archives/358/ 什么是PWM信号 PWM&#xff0c;英文名Pulse Width Modulation&#xff0c;是脉冲宽度调制缩写&#xff0c;它是通过对一系列脉冲的宽度进行调制&#xff0c;等效出所需要的波形&#xff08;包含形状以及幅…

舵机控制原理/舵机内部电路原理

舵机结构 舵机是我们常用到的末端执行器&#xff0c;但是在应用时我们只知道利用一定频率和周期的PWM来控制其摆动一定角度。及控制器给舵机一个周期为20ms的PWM波&#xff0c;高电平时间为0.5ms时对应0&#xff0c;高电平时间为2.5ms时对应180。以上规律适用于大多数180摆角的…

矩阵谱半径与矩阵范数的关系

摘自 程云鹏. 矩阵论(第二版)[M]// 矩阵论&#xff08;第二版&#xff09;. 西北工业大学出版社, 2000. p135~p137

谱半径

矩阵的谱或叫矩阵的谱半径&#xff0c;在特征值估计、广义逆矩阵、数值分析以及数值代数等理论的建树中&#xff0c;都占有极其重要的地位&#xff1b; 矩阵的谱半径为矩阵的特征值的模的最大值。 关于矩阵的谱&#xff08;半径&#xff09;的一个重要性质即是&#xff1a;任意…

【矩阵论笔记】谱半径

定义 例子 谱半径比他的诱导范数都小。 证明 例子 hemite对称矩阵 谱半径什么时候跟诱导范数相等&#xff1f;答曰&#xff1a;Hermite矩阵。

python求矩阵的谱半径

在学习计算方法的时候&#xff0c;线性方程组的迭代法中的雅可比(Jacobi)迭代法和高斯-塞德尔(Gauss-Seidel)迭代法的收敛条件中需要求矩阵的谱半径&#xff0c;而经过查阅资料&#xff0c;python numpy库中没有直接求谱半径的函数。 谱半径的定义为&#xff1a; 设A是n n矩阵…