-
2016-03-17 12:00:08
1、什么是虚继承?
虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。
2、虚继承解决了什么问题?
解决了二义性问题,也节省了内存,避免了数据不一致的问题。
3、虚继承的语法是?
在派生类继承基类时(即在声明派生类的时候),加上一个virtual关键词则为虚拟继承.
class 派生类名:virtual 继承方式 基类名
{
。。。。。。
}
class 派生类: virtual 基类1,virtual 基类2,...,virtual 基类n
{
...//派生类成员声明
};
说明:在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
4、执行顺序及优先级
首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;
执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;
执行派生类自己的构造函数;
析构以与构造相反的顺序执行;
从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
优先级:
在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
代码下载地址:http://download.csdn.net/detail/duan19920101/9464127
执行结果:
当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
用上述实例来说明这段话就是:类Mid1和类Mid2都继承自公共的基类Base,这个公共的基类会产生多个实例(或多个副本),即在使用类d调用它的print()方法时会产生多义性,导致程序出错,因此我们考虑到使用“虚继承”,将公共基类说明为虚基类,这样在调用时就只保存这个基类的一个实例。
在调用Mid1和Mid2两个类的print()方法时,需要加上类名,即这样做:
d.Mid1::print(); d.Mid2::print();
5、通过输出的比较
1.在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
2.声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
3.观察类构造函数的构造顺序,拷贝也只有一份。
6、与虚函数关系
虚拟继承与虚函数有一定相似的地方,但他们之间是绝对没有任何联系的。
原文参考:http://blog.csdn.net/crystal_avast/article/details/7678704
更多相关内容 -
C++继承机制(三)——多继承、菱形继承、虚继承原理
2020-05-23 15:10:43虚继承 C++中的多继承: 1)基本语法:class SubClass : public SuperClass1, public SuperClass2 2)多继承可能出现的问题: 当父类与父类直接出现同名情况 此时访问这些父类间的同名成员需要加上父类作用域用于...目录:
本篇涉及内容:
- 多继承
- 菱形继承
- 虚继承
C++中的多继承:
1)基本语法:
class SubClass : public SuperClass1, public SuperClass2
2)多继承可能出现的问题:
- 当父类与父类之间出现成员同名的情况
此时访问这些父类间的同名成员需要加上父类作用域用于区分是来自哪个父类的成员
菱形继承:
1)基本概念
有两个子类同时继承了同一个父类,然后又有一个类多继承了这两个子类2)菱形继承可能会出现数据杂冗问题
比如动物都有sex这个属性
所以羊和驼这两个类都继承了这个属性
当羊驼类多继承羊和驼这两个类时,则会重复继承分别来自羊类和驼类的sex属性
羊驼类中要两份这样的数据没有有意义
这样在羊驼类中就出现了数据杂冗的问题
如何解决菱形继承中出现的数据杂冗问题?
- 利用虚继承
-
什么是虚继承?
在继承方式前加上关键字
virtual
即可,此时Animal类称为虚基类 -
虚继承的底层原理
每一个继承了虚基类的子类中都会有:
vbptr——虚基类表指针(virtual base table pointer)占用4个字节
vbtable——虚基类表(virtual table)不占用内存
子类中的虚基类表指针指向了虚基类表
虚基类表中记录了虚基类与本类的偏移地址
通过这个偏移地址就可以找到虚基类中的成员
这样一来羊类和驼类就共同维护同一份虚基类的成员数据
同样虚基类表指针也会继承给下一个子类需要注意的是:虚基类中的成员依旧会继承到子类中,但是其所有子类还有间接子类(Animal和SheepTuo),都只会存在其一份成员数据,下面我们用代码来验证.
这里的SheepTuo类的大小为12字节,其继承过来了Sheep类和Tuo类的虚基类表指针,和一份Animal类中的age成员变量,虚继承解决了之前普通继承出现的数据杂冗的情况.
-
- 有关虚继承和虚基类更详细的介绍:
介绍了有关虚基类的构造
详细介绍了其底层原理
-
虚继承基本原理
2022-02-16 10:03:56二、从内存布局看虚继承原理 1、普通类的菱形继承:虚基类的成员会被拷贝两份,一模一样的,导致了空间的浪费; class A { public: int age=10; }; class B: public A { public: int b; }; class C : public A { ...一、可以先看看这个链接
二、从内存布局看虚继承原理
1、普通类的菱形继承:虚基类的成员会被拷贝两份,一模一样的,导致了空间的浪费;
class A { public: int age=10; }; class B: public A { public: int b; }; class C : public A { public: int c; }; class D : public B, public C { public: int d; };
2、基于虚基类的菱形继承
class A { public: int age=10; }; class B:virtual public A { public: int b; }; class C :virtual public A { public: int c; }; class D : public B, public C { public: int d; };
类A的结构:和其正常的类一样,只拥有自己的成员变量
类B的结构:由于对A的虚继承,所以除了B本身的成员外,还有一个指针变量(虚基类指针),指向一个整数,该整数是类B中继承A的成员变量的位置偏移量,最后还继承了A的成员变量(指针,B的成员变量b,A的成员变量age)
类C的结构:由于对A的虚继承,所以除了C本身的成员外,还有一个指针变量(虚基类指针),指向一个整数,该整数是类C中继承A的成员变量的位置偏移量,最后还继承了A的成员变量(指针,C的成员变量c,A的成员变量age)
类D的结构:B 和C都是D的父类,所以会继承B、C中的指针(虚基类指针)和成员变量b、c,同时因为BC都是虚继承于A,所以D中的来自A的成本变量age只有一份,再加上自身的d,一共是6*4=24字节;
**- 如何查看类的结构(限于windows平台的VS:)
**
cl /d1 reportSingleClassLayout类名 \cpp路径\test.cpp
- 虚继承之所以产生虚基类指针和虚基类表,都是为了保证不管多少层的继承,虚基类的数据只有一份,从而避免二义性和不浪费类对象内存空间;(虚基类表不占用类对象的空间);
- vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual
table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
三、虚继承和虚函数
1、相似之处:都使用了虚指针(均占用类对象存储空间)和虚表(均不占用类对象的存储空间)
2、虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
3、虚基类表存储的是虚基类相对***直接继承类***的偏移;而虚函数表存储的是虚函数地址。
**
- 暂时记录到这里,以后有新体会再加上!
**
-
继承详解(虚继承实现原理)
2022-04-01 22:11:19继承的概念及定义 概念: 继承机制是面向对象程序设计为了提高代码复用率的一种手段,它可以保持原类特性的基础上进行拓展,简单来说继承是类层次的复用。 接下来我们来看一个简单的继承 class Person { ...继承的概念及定义
概念:
继承机制是面向对象程序设计为了提高代码复用率的一种手段,它可以保持原类特性的基础上进行拓展,简单来说继承是类层次的复用。
接下来我们来看一个简单的继承
class Person { public: void Print() { cout<<"name:"<<_name<<endl; cout<<"age:"<<_age<<endl; } protected: string _name="zhao"; int _age=18; }; class Student : public Person { protected: int _stuid; }; class Teacher :public Person { protected: int _jobid; };
在上面这个类中继承后父类(Person)的成员都会变成子类的一部分。
定义:
格式:class 子类:public 父类{ };
继承关系和访问限定符
继承基类成员访问方式的变化
类成员/继承方式 public继承 protected继承 private继承 基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员 基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员 基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见 总结
-
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
-
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象/指针/引用。这里有一个形象的书法叫做切片或切割
基类对象不能赋值给派生类对象
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全地。
继承中的作用域
在继承体系中基类和派生类都有独立的作用域。
子类和父类中有同名成员;子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫作重定义。如果要访问父类的成员可以使用域作用限定符进行访问。
注意函数构成隐藏的话只需要函数名相同。
实际在继承体系里面最好不要定义同名的成员。
派生类的默认成员函数
在这里又把类与对象中学的六个默认成员函数拉出来了,那么在继承体系中这几个成员函数是如何生成的呢?
- 构造函数:派生类的构造函数必须基类的构造函数初始化基类的那部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 拷贝构造函数:派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 赋值重载:派生类operator=必须要调用基类的operator=完成基类的赋值。
- 析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
派生类对象初始化先调用基类构造再调派生类构造
派生类对象析构清理先调用派生类析构再调基类的析构。
简单的运用:
class Person { public: Person(const char* name="zhao") :_name(name) { cout<<"父构造"<<endl; } Person(const Person& p) :_name(p.name) { cout<<"父拷贝构造"<<endl; } Person& operator=(const Person& p) { cout<<"父赋值重载"<<endl; if(this!=&p) _name=p.name; return *this; } ~Person() { cout<<"父析构"<<endl; } protected: string _name; }; class Student:public Person { public: Student(const char* name,int num) :Person(name) ,_num(num) { cout<<"子构造"<<endl; } Student(const Student& s) :Person(s) ,_num(num) { cout<<"子拷贝构造"<<endl; } Student& operator=(const Student& s) { cout<<"子赋值重载"<<endl; if(this!=&s) { //小心这里是隐藏 Person::operator=(s); _num=s._num; } return *this; } //需要注意在这块~Student()和~Person()构成隐藏,这是由于多态的一些原因,任何类析构函数名都会被统一处理为destructor() ~Student() { cout<<"子析构"<<endl; //为了保证析构时,保持先子再父的后进先出的析构顺序,子类析构函数完成后,会自动去调用父类的析构函数。 } protected: int _num; };
继承与友元
友元关系不能继承,也就是说基类友元不是子类的友元。
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例。
class Person { public : Person () {++ _count ;} protected : string _name ; // 姓名 public : static int _count; // 统计人的个数。 }; int Person :: _count = 0; class Student : public Person { protected : int _stuNum ; // 学号 }; class Graduate : public Student { protected : string _seminarCourse ; // 研究科目 }; void TestPerson() { Student s1 ; Student s2 ; Student s3 ; Graduate s4 ; cout <<" 人数 :"<< Person ::_count << endl; Student ::_count = 0; cout <<" 人数 :"<< Person ::_count << endl; }
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用
虚拟继承解决数据冗余和二义性的原理为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
虚继承原理
虚继承实现:
在腰部两个继承之前加上关键字vittul,实现虚继承。
class Animal{ public: int _a; } class Tuo:virtual public Animal { public: int _b; } class Sheep:virtual public Animal { public: int _c; } class SheepTuo:public B ,public C { public: int _b; }
要探究虚继承如何实现,需要借用VS的开发人员命令提示工具,在VS2019的工具->命令行->开发者命令提示中。cd到当前项目的目录,输入cl /d1reportSingleClassLayout"要查看的类名" “文件名”,在这里就是cl/d1reportSingleClassLayoutSheepTuo diamond_Inherit.cpp。可以看到当前类内存的结构。(编译后才能查看到内存分布)
这个图就是内存结构,可以看到,SheepTuo类中分别继承了来自Sheep类的vbptr(虚基类指针)和Tuo类的vbptr(虚基类指针)。这个虚基类指针指向的是一个虚基类表,可以在图中看到虚基类表中第一项存储的是vbptr与本类的偏移地址,也就是继承过来的Sheep类中初始位置就是存放Sheep类的的vbptr,在这里为0;第二项是本类的vbptr与虚基类的公有成员之间的偏移量,也就是Sheep的vbptr和Animal类的age之间偏移为8,Tuo的vbptr和age之间偏移量为4。对于虚基类的派生类,虚基类的偏移量由实际类型决定,因此在运行时才可以确定虚基类的地址。
指的注意的是,Sheep类中也是存放了一份age,在这里还可以看到,Sheep和Tuo的Size都是8,因为除了继承的age以外,还有Size为4的虚函数指针
因为class SheepTuo :public Sheep, public Tuo继承的时候,把Sheep和Tuo的vbptr都继承了,然后通过他们类距离虚基类中的公共成员age的偏移量发现他们指向的是同一个age,所以就不会拷贝两份,SheepTuo只保留一份age。至于虚继承底层实现原理则与编译器相关
继承的总结
-
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java
-
继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
-
-
虚继承实现原理
2021-04-24 18:12:37虚继承的作用 两个父类中有祖类中的数据,然后子类会继承两个父类的数据,会产生二义性问题 class Animal { public: Animal() { mA = 100; } public: int mA; }; class Sheep :virtual public Animal{ }; ... -
C++ 虚继承内部工作原理
2021-12-18 21:38:39//虚基类 class Animal { public: int age; protected: private: }; //绵羊类 //class Sheep : public Animal {}; class Sheep : virtual public Animal {}; //骆驼类 //class Camel : public Animal {}; class ... -
[C++基础]虚继承实现原理
2019-04-12 12:54:00在C++中,我们会遇到virtual这个关键字,但是它有两种含义:虚函数和虚继承,它们两个是完全无相关的两个概念。 什么是虚继承 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中... -
【C++11】 从内存布局看C++虚继承的实现原理
2019-05-08 20:06:55既然我们今天讲的是虚基类和虚继承,我们就先用上面介绍的命令提示工具查看一下普通多继承子类的内存布局,可以跟后文虚继承子类的内存布局情况加以比较。 我们新建一个名叫NormalInheritance的cpp文件,输入一下... -
C++ 虚继承详解
2021-02-08 13:51:24多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一... -
C++中虚继承的作用及底层实现原理
2017-08-30 22:26:11虚继承和虚函数是完全无相关的两个概念。 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将... -
C++普通继承和虚继承详解
2020-06-07 22:18:46继承 继承概念 所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。 继承特点 子类拥有父类的所有属性和方法(除了构造... -
【C++】浅谈虚继承
2019-10-16 15:02:16C++——浅谈虚继承 问题1:什么是虚继承?为什么要虚继承? 虚继承 :是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给从这个基类型直接或间接派生的其它类。虚拟继承是... -
详解虚继承
2012-12-21 09:58:09详解C++中虚继承虚函数 的要点和继承关系c++开发人员细细阅读哦 -
图文详解虚继承的实现原理(内存布局分析)
2020-04-03 22:21:181.为什么需要虚继承? 虚继承是为了解决多重继承中存在的一些问题而出现的。在多重继承中,可能会存在多个派生类继承同一个基类的情况,这个时候,不仅会浪费存储空间,而且还会导致二义性。二义性解释如下: //... -
C++中多态实现的原理分析、虚继承的原理
2018-05-12 21:43:35多态的原理分析 在面向对象的语言中,封装、继承、多态三大特性。我们今天说说C++中多态的实现原理。 多态往往是用来在继承中,子类中的某些行为与父类中的不同,但是为了降低...1)必须在继承中,存在虚函数,... -
为什么需要虚继承,虚继承的实现原理
2019-05-12 21:07:54虚继承和虚函数是完全无相关的两个概念。 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以... -
C++虚继承实现原理——解决菱形继承问题
2018-11-11 14:36:14首先给出以下继承关系,以便描述虚继承原理: class AAA { public: int age; }; class BBB:virtual public AAA//变为虚继承,A变为虚基类 { }; class CCC:virtual public AAA { }; class DDD:public BBB, ... -
C++基础——虚继承以及实现原理
2020-07-17 00:17:33多继承 多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可... -
虚继承的实现原理
2017-05-02 16:44:04类B和类C通过虚继承的方式派生自类A,这两个对象的内存布局中,编译器在对象中添加了一个vbptr(virtual base pointer)指针,vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。类D派生与类... -
虚基类(虚继承) 原理
2016-03-17 15:15:30在《深度探索C++对象模型》里,有一个问题,也是去公司面试的时候那些技术人员常问的问题:在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,...(1)、obj 是一个虚拟继承的派生类的对象 -
C++ 虚继承实现原理(虚基类表指针与虚基类表)
2018-06-03 18:45:20虚继承和虚函数是完全无相关的两个概念。虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将... -
C++虚继承详解
2022-04-21 15:57:14C++虚继承详解 看侯捷老师的C++内存模型时,讲到了虚继承。虚继承算是C++特有的知识了,特此记录下。 什么是虚继承 由于C++支持多继承,可能会出现菱形继承,代码如下: #include <iostream> // std::cout ... -
C++虚继承中的虚基类表
2021-07-19 02:03:28这篇文章主要探究虚继承的原理。文章中多处给出了类实例对象的内存布局,查看其内存布局时,使用 VS 工具 /d1 reportAllClassLayout 进行查看,关于这个工具的详细介绍,请点击这里。 虚继承的实现原理 虚继承的底层... -
C/C++:从内存布局理解C++虚继承的实现原理
2018-09-13 21:08:29在这个例子中,类B中的vbptr指向了虚表D::$vbtable@B@,虚表表明公共基类A的成员变量dataA距离类B开始处的位移为20,这样就找到了成员变量dataA,而虚继承也不用像普通多继承那样维持着公共基类的两份同样的拷贝,... -
C++-Record37—虚继承简单原理剖析
2020-01-29 11:43:05虚继承简单原理剖析 -
浅谈C++多态实现原理(虚继承的奥秘)
2018-12-24 17:47:24根据我的已有知识,如果要实现C++的多态,那么,基类中相应的函数必须被声明为虚函数(或纯虚函数)。举个例子: class Point { public: Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) { } virtual ...