虚基类

1.虚基类存在的意义

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

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

不用虚基类时产生的数据冗余和二义性问题:
如下代码属于菱形继承方式:

下面是不用虚基类处理此继承关系时的代码:

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的虚表。


示例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没有被赋值子对象的空间),所以这在逻辑上是行不通的。

【虚基类、虚函数及应用】相关推荐

  1. 虚继承c语言例子,C/C++ 多继承{虚基类,虚继承,构造顺序,析构顺序}

    C/C++:一个基类继承和多个基类继承的区别 1.对多个基类继承会出现类之间嵌套时出现的同名问题,如果同名变量或者函数出现不在同一层次,则底层派生隐藏外层比如继承基类的同名变量和函数,不会出现二义性, ...

  2. php 虚基类,1.9 多态

    1.9 多态 返回目录 1 面向对象技术 上一节 1.8 继承 多态(Polymorphism)按字面的意思就是"多种状态".在面向对象语言中,接口的多种不同的实现方式即为多态. ...

  3. 构造函数怎么在主函数调用_C++ 虚基类及其派生类构造函数(学习笔记:第7章 12)...

    虚基类及其派生类构造函数[1] 建立对象时所指定的类称为最远派生类. 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的. 在整个继承结构中,直接或间接继承虚基类的所有派生类,都 ...

  4. C++ 多继承类 虚基类

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/jzj_c_love/article/d ...

  5. 继承与派生(Part Ⅲ)——多重继承 虚基类

    虚基类 虚基类的作用 如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员.在引用这些同名的成员时,必须在派生类对象名后增加直接 ...

  6. 【C++】继承和派生、虚继承和虚基类、虚基类表和虚基类指针

    继承和派生.虚继承和虚基类.虚基类表和虚基类指针 继承和派生 继承概述 继承基本概念 派生类中的成员 继承的内容 派生类定义 派生类访问控制 对象构造和析构 对象构造和析构的调用顺序 继承中的构造和析 ...

  7. 虚继承,虚基类以及虚继承的构造函数

    目录 一.什么是虚继承,虚基类 二.定义虚继承的基本格式 三.虚继承的构造函数 一.什么是虚继承,虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系 虚基类:在虚继承体系中的通过virt ...

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

    博客虚函数实现过程3 时提到过虚基类,这里呢,我们来详细讲述一下: 当我们在虚函数的声明结尾处添加"=0",这种虚函数就被称为纯虚函数. 它好似一个没有实现只有声明的函数,它的存在 ...

  9. 使用“override”声明的成员函数不能重写基类成员_C++日志(三十五)虚基类与其子类的构造函数...

    第三十四篇日志中提到,子类对象调用父类同名的函数时需要使用作用域标识符进行限定,这是在任何继承情况下都可以使用的普适方法. 问题引入: 考虑一种特殊的情况:子类全部或部分的基类(称为中间基类)同是继承 ...

  10. c++ map 析构函数_C++|类继承关系中的虚函数、虚析构函数、虚基类

    在继承关系中,虚函数.虚析构函数.虚基类中使用的关键字virtual都是在告诉编译器,此处要进行特殊处理: 虚函数:函数重写时的要求编译器动态绑定来实现多多态 : 虚析构函数:当基类指针指向在堆内实现 ...

最新文章

  1. 异类框架BigDL,TensorFlow的潜在杀器!
  2. shell 下的运算表达
  3. oracle 高水位线回收,回收高水位线
  4. linux多线程信号总结
  5. 随便聊一些编程开发工具
  6. 怎样搭建Android开发平台(转)
  7. 分割范围Codeforces Round #181 (Div. 2)
  8. SpringCloud创建Eureka Client服务注册
  9. pthread 立即停止线程_iOS多线程:『pthread、NSThread』详尽总结
  10. MD5加密概述,原理及实现
  11. 豆瓣电影评论情感分析(含代码+数据)
  12. Mathematica图像处理
  13. windows清理C盘
  14. mac怎么无线打印机连接到服务器,Mac电脑怎么连接打印机
  15. php7.4 ffi,PHP7.4中FFI的介绍(代码示例)
  16. JavaScript对象的取值
  17. 网络协议-ARP协议与攻击模拟
  18. matlab 预定义变量和变量的管理
  19. SpringAOP原理+使用
  20. 基于jmeter的接口自动化测试平台

热门文章

  1. 华为手机怎么关闭下拉智慧搜索
  2. TC2.0库函数清单
  3. mysql和ES对比
  4. 单片机c语言报错_asm啥意思,SPMC75系列单片机地C和ASM( - 控制/MCU - 电子发烧友网...
  5. Object.assign 原理及其实现
  6. Gradient_patch_recovery, Z_Z posteriori error estimator
  7. 扔掉U盘:两步教你如何使用硬盘装系统(适用gpt磁盘)
  8. 写给自己,学习如逆水行舟,不进则退
  9. 模型评价 - 回归模型的常用评价指标
  10. oracle中查看pga大小,关于oracle pga大小限制