-
2021-02-02 11:06:30
菱形继承
概念:
两个派生类继承同一个基类,又有某个类同时继承这两个派生类。这种继承被称为菱形继承,或者钻石继承
菱形继承的问题:
1.羊继承了动物的数据,骆驼也继承了动物的数据,当羊驼使用数据时,就会产生二异性
2.羊驼继承动物的数据继承了两份,但是这份数据我们只需要一份
虚继承前:#include<iostream> using namespace std; class Animal { public: int age; }; class Sheep : public Animal {}; class Camel : public Animal {}; class SheepTuo : public Sheep, public Camel {}; int main() { SheepTuo st; st.Sheep::age = 18; st.Camel::age = 19; //当我们出现菱形继承的时候,有两个父类拥有相同数据,需要作用域加以区分 cout << "绵羊的年龄为:" << st.Sheep::age << endl; //18 cout << "骆驼的年龄为:" << st.Camel::age << endl; //19 cout << "羊驼的年龄为:" << st.age << endl; //会因为数据不明确报错 //这份数据我们知道只需要有一份就可以了,菱形继承导致数据有两份,造成资源浪费 system("pause"); return 0; }
虚继承后:
#include<iostream> using namespace std; class Animal { public: int age; }; //利用虚继承,解决菱形继承的问题 //继承之前加上关键字virtual变为虚继承 //Aniamal类称为虚基类 class Sheep :virtual public Animal {}; class Camel :virtual public Animal {}; class SheepTuo :virtual public Sheep, public Camel {}; int main() { SheepTuo st; st.Sheep::age = 18; st.Camel::age = 19; //当我们出现菱形继承的时候,有两个父类拥有相同数据,需要作用域加以区分 cout << "绵羊的年龄为:" << st.Sheep::age << endl; //19 cout << "骆驼的年龄为:" << st.Camel::age << endl; //19 cout << "羊驼的年龄为:" << st.age << endl; //虚继承后,数据只有一份,现在打印年龄,三个都为19 //这份数据我们知道只需要有一份就可以了,菱形继承导致数据有两份,造成资源浪费 system("pause"); return 0; }
虚继承前,羊驼类中的内部结构:
Sheep类里面存放的是自己m_age;
Tuo类里面存放的是自己的m_age;
两个age并不相同;
SheepTuo会在继承时,继承两个名字同为age,但所存放数据内容不同,因此编译器不知道如何处理,会产生二异性;虚继承后,羊驼类中的内部结构:
sheep类里面所存放的变为了一个vbptr的指针
vbptr指针解释:v----virtual b----base ptr-------pointer,虚基类指针
vbptr虚基类指针指向一个虚基类表
虚函数表相当于一个数组里面存储的是虚函数表指针,偏移量相当于访问数组中第几个元素得到带元素的值,这里数组中的值代表着sheep和tuo类里面的指针需要移动几个字节才能访问到animal里面的age
因此在发生虚继承后,羊和驼中原先继承下来的age变成了vbptr指针都指向各自的虚函数表,通过各自虚函数表中指针的偏移量找到animal基类中age,避免了菱形继承的二义性
可以利用羊驼类的内部结构分布,通过指针的来进行操作注意:指针无论类型,所占空间都为4字节
更多相关内容 -
关于C++中菱形继承和虚继承的问题总结
2021-01-01 14:36:29菱形继承是多重继承中跑不掉的,Java拿掉了多重继承,辅之以接口。C++中虽然没有明确说明接口这种东西,但是只有纯虚函数的类可以看作Java中的接口。在多重继承中建议使用“接口”,来避免多重继承中可能出现的各种... -
Python多重继承之菱形继承的实例详解
2020-09-17 23:26:39继承是面向对象编程的一个重要的方式,通过继承,子类就可以扩展父类的功能。这篇文章主要介绍了Python多重继承之菱形继承,需要的朋友可以参考下 -
C++中的菱形继承深入分析
2020-08-30 02:14:18主要介绍了C++中的菱形继承深入分析的相关资料,需要的朋友可以参考下 -
C++ 菱形继承
2022-04-18 11:02:03菱形继承的介绍,以及虚继承和菱形继承虚函数表问题模板继承
template<class T> class Object { T value; public: Object(T x = T()) : value(x) {} static int num; }; template<class T> int Object<T>::num = 0; class Base :public Object<int> { public: Base() { num += 1; } void Print() const { cout << "Base:" << num << endl; } }; class Test :public Object<int> { public: Test() { num += 1; } void Print() const { cout << "Test:" << num << endl; } }; int main() { Base b1, b2; Test t1, t2; b1.Print(); t1.Print(); return 0; }
我们可以很明确的看出应该输出4,4,但是如果我们将继承Object的类型进行修改template<class T> class Object { T value; public: Object(T x = T()) : value(x) {} static int num; }; template<class T> int Object<T>::num = 0; class Base :public Object<int> { public: Base() { num += 1; } void Print() const { cout << "Base:" << num << endl; } }; class Test :public Object<double> // 这里由int 修改为 double { public: Test() { num += 1; } void Print() const { cout << "Test:" << num << endl; } }; int main() { Base b1, b2; Test t1, t2; b1.Print(); t1.Print(); return 0; }
可以看出两个类型的Object拥有各自的num,那么打印出来的结果就是2,2
菱形继承
这里就可以看到,Stud_Elem类对象,内存结构中出现了两次Person类,继而出现了信息冗余的问题class Person { private: string p_id; string p_name; string p_sex; int p_age; public: Person(const string& id, const string& name, const string& sex, int age) :p_id(id), p_name(name), p_sex(sex), p_age(age) {} void print()const { cout << "id:" << p_id << endl; cout << "name:" << p_name << endl; cout << "sex:" << p_sex << endl; cout << "p_age:" << p_age << endl; } }; class Student :public Person { private: string s_id; public: Student(const string& id, const string& name, const string& sex, int age, string& d) :Person(id,name,"man",age),s_id(d) {} }; class Staff : public Person { private: string s_id; public: Staff(const string& id, const string& name, const string& sex, int age, const string& d) :Person(id, name, "woman", age), s_id(d) {} }; class Stud_Staff : public Student, public Staff { private: public: Stud_Staff(const string& id, const string& name, int age, const string& dd, string ss) :Student(id, name, "nan", age, ss), Staff(id, name, "woman", age, dd) {} }; int main() { Stud_Staff ss("123456789", "zyq", 20, "2020", "3030"); ss.Student::print(); ss.Staff::print(); Student s1 = ss; Staff s2 = ss; //Person p = ss; error!!! 数据不一致性 }
虚继承
解决数据冗余与数据不一致性的方案就是进行虚继承
当 Student 类与 Staff 类虚继承于Person类
class Person { private: string p_id; string p_name; string p_sex; int p_age; public: Person(const string& id, const string& name, const string& sex, int age) :p_id(id), p_name(name), p_sex(sex), p_age(age) {} void print()const { cout << "id:" << p_id << endl; cout << "name:" << p_name << endl; cout << "sex:" << p_sex << endl; cout << "p_age:" << p_age << endl; } }; class Student :virtual public Person { private: string s_id; public: Student(const string& id, const string& name, const string& sex, int age, string& d) :Person(id, name, "man", age), s_id(d) {} }; class Staff :virtual public Person { private: string s_id; public: Staff(const string& id, const string& name, const string& sex, int age, const string& d) :Person(id, name, "woman", age), s_id(d) {} }; class Stud_Staff : public Student, public Staff { private: int num = 10; public: Stud_Staff(const string& id, const string& name, int age, const string& dd, string ss) :Student(id, name, "nan", age, ss), Staff(id, name, "woman", age, dd), Person("60101", "zyq", "man", 20) {} }; int main() { Stud_Staff ss("123456789", "zyq", 20, "2020", "3030"); ss.Student::print(); ss.Staff::print(); Student s1 = ss; Staff s2 = ss; Person p = ss; }
class Object { int value; public: Object(int x = 0) :value(x) {} }; class Base :virtual public Object { int num; public: Base(int x = 0) :num(x), Object(x + 10) {} }; class Test :virtual public Object { int sum; public: Test(int x = 0) :sum(x), Object(x + 10) {} }; class Det :public Base, public Test { private: int total; public: Det(int x = 0) :total(x), Base(x + 10), Test(x + 20), Object(x + 30) {} }; int main() { Det d(0); return 0; }
由于每次程序编译链接时,每一次运行时,对象的地址不是固定的,若我们使用指针去指向基类,那么每次一次运行都需要进行一次填写地址
菱形继承中的虚函数
class Object { int value; public: Object(int x = 0) :value(x) {} virtual void fun() { cout << "Object" << endl; } virtual void add() { cout << "Object::add" << endl; } }; class Base : public Object { int num; public: Base(int x = 0) :num(x), Object(x + 10) {} virtual void fun() { cout << "Base" << endl; } }; class Test : public Object { int sum; public: Test(int x = 0) :sum(x), Object(x + 10) {} virtual void fun() { cout << "Test" << endl; } virtual void add() { cout << "Test::add" << endl; } }; class Det :public Base, public Test { private: int total; public: Det(int x = 0) :total(x), Base(x + 10), Test(x + 20), Object(x + 30) {} virtual void fun() { cout << "Det" << endl; } virtual void add() { cout << "Det::add" << endl; } }; int main() { Det d(0); Object* opa = (Base*)&d; //这两处的d地址是不同的 Object* opb = (Test*)&d; //Object* op = &d; error //Object* op = (Object*)&d; error //d中包含两个Object地址,并不清楚指向哪一个 opa->add(); opa->fun(); opb->add(); opb->fun(); return 0; }
当我们菱形继承,需要对两个类的两个虚函数表进行继承Object* opa = (Base*)&d; Object* opb = (Test*)&d; //Object* op = &d; error //Object* op = (Object*)&d; error
在这里由于两个类的虚函数表不同,其中opa与opb分别获得的地址也不同,并且直接对Object 地址获取是错误的,因为子对象中有两个Object
那么是如何明确到底是继承来自哪一个对象的虚表
可以看出,opa指的是Base里面的Object对象,而opb指的是Test中的Object对象,我们Base对象指的是由Base对象继承而来的虚表,Test对象指的是由Test对象继承而来的虚表,我们会以某一个虚函数表为主,虚函数表2去调用为主的虚函数表1,通过虚函数表1去调动真实的函数地址
也就是说虚函数表1指向真实的地址,而虚函数表2指向的实际是调节器
-
C++菱形继承对象内存布局实战讲解和分析
2021-02-27 21:20:44本文主要讲解C++对象模型中的菱形继承的对象模型,分别讨论基类对象变量和函数的继承问题。 何为菱形继承: 菱形继承是指一个基类(Base)派生出两个派生类(Derived1,Derived2),然后这两个派生类(Derived1,...本文主要讲解C++对象模型中的菱形继承的对象模型,分别讨论基类对象变量和函数的继承问题。
何为菱形继承:
菱形继承是指一个基类(Base)派生出两个派生类(Derived1,Derived2),然后这两个派生类(Derived1,Derived2)派生出一个最终的派生类,如1.1的下图所示。一、菱形继承之非虚继承
1.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图
1.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义
NonVirtualDerivedDiamondClass.cpp
#include <iostream> using namespace std; class Base { public: Base(int x) : x(x) {} protected: int x; }; class Derived1 : public Base { public: Derived1(int y1) : Base(1), y1(y1) {} protected: int y1; }; class Derived2 : public Base { public: Derived2(int y2) : Base(1), y2(y2) {} protected: int y2; }; class DDerived : public Derived1, public Derived2 { public: DDerived(int z) : Derived1(11), Derived2(22), z(z) {} void callX() { cout << this->x << endl; } protected: int z; };
1.3最终派生类DDerived对象模型
由于最终的派生类包含了基类Base、派生类Derived1,Derived2的对象模型,因此只分析最终派生类DDerived对象模型即可。
VS2017开发者模式查看C++对象模型方法可以参考这篇博客:C++单继承类对象内存布局实战讲解和分析- DDerived在内存中的布局
由上图可知,菱形继承派生类Derived1和Derived2对象的内存中各自继承和保存基类Base的成员变量int x;。当我们在最终派生类DDerived上调用成员变量x时,会出现歧义,DDerived不知道调用那个类对象的x。此时编译会报错,成员变量x调用歧义,如下图所示。
如果我们要在最终派生类DDerived调用继承而来的x,那么就要显示指定调用的作用域(“::”)限定符,指明是调用哪个基类继承过来的x,即this->Derived2::x,如下代码所示:
class DDerived : public Derived1, public Derived2 { public: DDerived(int z) : Derived1(11), Derived2(22), z(z) {} void callX() { cout << this->Derived2::x << endl; // 显示指定作用域Derived2::x,调用Derived2的成员变量x } protected: int z; };
从DDerived内存布局中可以看出,派生类Derived1和Derived2的类对象都各自保存了一份从基类Base继承而来的成员变量int x;这样不但会造成最终派生类DDerived获取变量x出现歧义,同时也会造成内存浪费。那么,是否有办法解决这些问题呢?答案是肯定的,那就是采用虚继承。
二、菱形继承之虚继承
2.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图
由上图可知,只有派生类Derived1和派生类Derived2继承类Base时采用虚继承,而最终派生类DDerived继承Derived1和Derived2时采用普通继承。即Derived1 : public virtual Base { ... }; Derived2 : public virtual Base { ... }; DDerived : public Derived1, public Derived2 { ... };
2.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义
VirtualDerivedDiamondClass.cpp
#include <iostream> using namespace std; class Base { public: Base() = default; Base(int x) : x(x) {} protected: int x; }; class Derived1 : public virtual Base { public: Derived1(int y1) : Base(1), y1(y1) {} protected: int y1; }; class Derived2 : public virtual Base { public: Derived2(int y2) : Base(1), y2(y2) {} protected: int y2; }; class DDerived : public Derived1, public Derived2 { public: DDerived(int z) : Derived1(11), Derived2(22), z(z) {} void callX() { cout << this->Derived2::x << endl; } protected: int z; };
2.3最终派生类DDerived对象模型
由于最终的派生类包含了基类Base、派生类Derived1,Derived2的对象模型,因此只分析最终派生类DDerived对象模型即可。
VS2017开发者模式查看C++对象模型方法可以参考这篇博客:C++单继承类对象内存布局实战讲解和分析- DDerived在内存中的布局
由上图可知,最终派生类DDerived的对象模型中,派生类Derived1和派生类Derived2都没有产生一份基类Base的成员变量int x;的内存,而是多了一个虚指针。该虚指针分别指向各自的虚函数表。虚函数表中存放了变量x的偏移地址。通过该偏移地址派生类Derived1和派生类Derived2就可以获取变量x。此时最终派生类可以直接用this指针调用变量x而不会产生歧义,如下图所示。
因此,虚拟继承主要是继承基类成员变量的偏移地址,该偏移地址是保存在虚指针指向的虚函数表上,排列顺序为按照变量的声明顺序进依次排列。如下图所示:
该虚继承的类都没有虚函数,那么假如基类存在虚函数,那么虚继承后的菱形继承最终派生类的类对象模型是怎么样的呢?接下来继续分析和讨论。
三、基类有虚函数的菱形继承之虚继承
3.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图
由上图可知,基类Base和派生类Derived1、Derived2都有虚析构函数和一个虚函数vfun1();,说明这是一个继承中有虚函数的类,即非POD类型的类,内存对象不可逐字节拷贝memcpy(…)。3.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义
#include <iostream> using namespace std; class Base { public: Base() = default; virtual ~Base() {} Base(int x) : x(x) {} protected: int x; private: virtual void vfun1() = 0; }; class Derived1 : public virtual Base { public: Derived1(int y1) : Base(1), y1(y1) {} virtual ~Derived1() {} virtual void vfun1() override { cout << "virtual Derived1::vfun1()" << endl; } protected: int y1; }; class Derived2 : public virtual Base { public: Derived2(int y2) : Base(1), y2(y2) {} virtual ~Derived2() {} virtual void vfun1() override { cout << "virtual Derived2::vfun1()" << endl; } protected: int y2; }; class DDerived : public Derived1, public Derived2 { public: DDerived(int z) : Derived1(11), Derived2(22), z(z) {} virtual void vfun1() override { cout << "virtual DDerived::vfun1()" << endl; } void callX() { cout << this->x << endl; } protected: int z; };
3.3最终派生类DDerived对象模型
图3-1 有虚函数的菱形继承之虚继承图
图3-2 没有虚函数的菱形继承之虚继承图
由上图3-1和对比图3-2可知,有虚函数的菱形继承之虚继承的最终派生类DDerived对象模型跟没有虚函数的菱形继承之虚继承的最终派生类DDerived基本一样,差别只有一个,那就是基类Base多了一个虚指针,该虚指针指向DDerived自身的虚函数表。这个虚函数表跟单继承的虚函数表一样,里面存放的都是DDerived自身的虚函数或者继承而来的虚函数。虚函数表的定义规则是,先将基类虚函数表内容拷贝一份到DDerived自身虚函数表中,然后用DDerived自身的虚函数覆盖虚函数表中同名的虚函数。
同理,当有静态成员函数和静态成员变量、普通成员函数时,DDerived的类内存模型也同样不受影响,具体代码博主就不贴出来了,留一个小作业各位读者自己验证。四、总结
- 菱形虚继承后基类的成员变量只有一份内存,不会在派生类中拷贝一份同样的成员变量占内存;
- 虚继承后派生类不会拷贝基类成员变量,而是产生一个虚指针指向自身的虚函数表,该虚函数表存放获取基类成员变量的偏移地址;
- 虚继承中的类存在虚函数,跟没有虚函数的虚继承只有一个差别,那就是产生当前类的虚指针,该虚指针指向最终的派生类的虚函数表,该虚函数表存放最终派生类的所有替换后的虚函数或者继承而来的虚函数地址;
- 只有非静态成员才占对象模型的内存;
- 类对象的静态变量和静态函数都不占用对象模型的内存,存放在静态储存区;
- 类对象的普通成员函数也不占用对象模型的内存,存放在普通数据区
五、参考内容
c++之菱形继承问题
C++对象模型和布局(三种经典类对象内存布局)
C++中菱形继承的基本概念及内存占用问题
C++之继承(多重继承+多继承+虚继承+虚析构函数+重定义)
《深度探索C++对象模型》 侯捷 page:83-134 - DDerived在内存中的布局
-
C++继承以及菱形继承
2022-03-27 13:33:35详细介绍C++中的继承机制,以及C++的多继承导致的虚拟继承及其解决方案C++面向对象——继承
问题的引出
假如我们需要给某个高校制作一款人员信息管理系统,学习过C++之后我们知道可以给每个职业设置一个class,到每个个体的时候再具体实例化出一个对象就行了,假如该高校的人员信息管理系统中只需要给学生、老师、保安人员三类人群进行设计。我们将三个类放置在下面:
class Student{ string _name;//姓名 int _age;//年龄 int _stdID;//学生卡ID }; class Teacher{ string _name; int _age; int _thID;//教师卡ID }; class Worker{ string _name; int _age; int _wkID;//工卡卡号 }
我们明显发现,这三个类中大部分的成员变量其实是重复的,真正每个人不同的地方只有证件编号不同。那如何解决大量重复字段的冗余问题呢?面向对象编程语言为我们实现了解决的方式:继承(inheritance)机制。
继承是面向对象程序设计语言中使得代码得以复用的重要手段,它允许程序员在保持原有类特征的基础上进行扩展,增加功能。原有的类可以叫父类/基类,而扩展后产生的新的类叫子类/派生类。继承向我们展现了面向对象程序设计的层次结构。
因此我们可以位上面的例子,定义出一个基础类Person,该类有两个成员变量,分别是姓名、年龄,当派生类继承了基类后,派生类中就已经拥有了基类的成员(成员变量、成员函数)。我们就不用再把这两个成员变量写入派生类当中了。
class Person{ public: //公共的成员方法,可以打印一下该对象的基本信息 void printInfo(){ std::cout <<"name: " << _name << std::endl; std::cout <<"age: " << _age << std::endl; } protected: string _name = "张三";//缺省值 int _age = 18; }; class Student : public Person{ protected: int _stuID; }; class Teacher : public Person{ protected: int _thID; }; class Worker : public Person{ int _wkID; };
继承定义
定义格式:
下面我们看到的Person是父类,也称作基类。Student是子类,也可以叫做派生类
class Sudent : public Person
先写上class关键字,后面跟上派生类的名称,然后写上冒号‘ : ’,冒号后面的public是继承方式,最后面的是基类的类名。
继承方式与访问方式一样,有三种权限限定符:public、protected、private
继承方式与访问限定符不同导致成员访问方式的变化
类成员/继承方式 public继承 protected继承 private继承 基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员 基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员 基类的private成员 派生类不可见 派生类不可见 派生类不可见 学习过C++的class后,我们都知道访问限定符的访问权限是public>protected>private,所以上面的表格,只需要取横纵上面限定符权限最小的那个就可以了。而且我们注意到,基类的private成员不管是什么继承方式,在派生类中都是不可见的。同时,C++中class的默认继承方式是private,而C++兼容的C语言中的结构体struct的默认继承方式是public。实际使用中,绝大多数情况下使用的是public继承,很少使用到protected/private继承,因为使用了protected和private继承的成员都只能在派生类的类内部使用。实际中的扩展性很弱,也难以后期维护。建议大家在写继承格式的时候显式地写出继承方式。
特别需要说明的一点是,基类的private成员在继承过程中,并不是没有继承给派生类才导致它不可见;在继承过程中,基类部分会作为一个整体一起继承给派生类,因此基类的private成员是占据着派生类对象的物理空间的,只是这部分物理空间派生类无法访问罢了。所以一个类作为基类的时候,尽量不要使用private修饰符修饰自己的成员,因为这样在派生类中是不可见的,尽量使用protected。
基类和派生类对象之间的赋值转换
派生类对象可以将自己赋值给基类的对象/基类的指针/基类的引用。我们可以把这种行为叫做切片,即将派生类中属于基类的那部分切下来赋值过去。
但是基类对象不可以将自己赋值给派生类的对象
基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须是基类的指针指向了派生类的对象时才是安全的。如果基类是多态类型,可以使用dynamic cast识别后再进行安全转换。
继承中的作用域问题
- 在继承体系中,基类与派生类之间各自有各自独立的作用域。
- 基类和派生类如果有同名成员,派生类的成员会将基类的同名成员屏蔽掉,这种情况被称为隐藏,也可以叫做重定义。如果想要访问基类的同名成员,需要在该成员名称前面加上基类的类域名即可。
- 如果是该同名成员是成员函数,那么只要函数名相同就会构成隐藏关系,这里要着重说明一下,函数隐藏与函数重载是不一样的!函数重载的前提是两个函数在同一作用域下,而我们刚刚提过隐藏发生在基类和子类两个不同的类作用域之中,其次是函数重载要求返回值不相同或者参数不同,但是隐藏关系没有这些要求,只要两个函数名相同,不管返回值、参数相同或是不同,就直接构成了隐藏关系。
- 因此,建议大家在使用C++的继承机制时,尽量不要在基类和子类中定义相同名称的成员。
派生类的默认成员函数
C++会为每个类默认生成六个成员函数,默认成员函数是程序员不写,编译器自动帮我们生成的成员函数。接下来挨个分析一遍。
默认构造:
首先是构造函数,派生类的构造函数必须去调用基类的构造函数来初始化它继承自基类的那部分成员。如果基类没有默认的构造函数(只要是不用传参的都是默认构造函数),则必须在派生类的构造函数的初始化列表阶段显式调用。现在我们分析一下当我们不写,编译器默认生成的那个默认构造函数,它的处理方式是:①将从基类那里继承来的基类成员作为一个整体,调用基类的默认构造函数初始化;②自己的自定义类型成员,调用它的默认构造函数;③自己的内置类型成员不会做处理(除非在声明时给了缺省值)
class Person{ public: string _name; int _age; Person(string name, int age) :_name(name) ,_age(age){}//提供了一个带参数的构造,这样编译器就无法给我们生成默认构造函数了 }; class Student{ private: string _address; int _stuNO; }; int main(){ Student s1;//这里就会报错 return 0; }
为什么会报错呢,因为我们根据上面的①可以知道,我们没有给派生类Student写构造函数,编译器会帮我们生成一个默认的构造函数,它会去调用基类Person的默认构造函数来把从基类继承过来的这部分进行初始化,但是我们给基类Person写了一个带参的构造函数,基类没有默认构造函数可以使用了,这样就会报错了,VS2022中报错如下:
所以推荐大家使用初始化列表来显式地给派生类初始化,使用一个基类的匿名对象,来给子类继承到的基类部分初始化:
Student(const char* name, int age, const char* adderss, int stuNO) :Person(name, age)//基类的匿名对象,并且可以使用参数显示赋值初始化 , _address(address) , _stuNO(stuNO){} int main(){ Student s1("张三", 18, "北京", 111); return 0 }
上面这种初始化方式不管基类有自己的默认构造函数,还是有带参数的构造函数,都可以完成对派生类的初始化了。
拷贝构造:
派生类的拷贝构造,我们不写,编译器会默认生成一个,其处理顺序与构造函数类似:①继承的基类的成员作为一个整体,调用基类的拷贝构造;②自己的自定义类型成员,调用它的拷贝构造;③自己的内置类型成员,调用值拷贝。正常来说编译器为我们默认生成的拷贝构造就可以使用不用再自己去写了,除非派生类自己内部有指针,指向了动态开辟的空间,这时需要我们手动将其改为深拷贝。
//手动写出派生类的拷贝构造 Student(const Student& s) :Person(s)//相当于将派生类对象直接赋给父类的匿名对象,通过上面提到的切片行为完成 ,_address(s._address) ,_stuNO(s._stuNO){}
注意在显式写出派生类的拷贝构造时,一定要调用基类的拷贝构造,即上面完成切片操作的
Person(s)
,如果没有调用基类的拷贝构造,则编译器会去调用基类的默认构造函数,就会仅仅只是将派生类继承自基类的部分进行构造初始化,而并没有完成拷贝的作用!//父类的默认构造函数 Person(){ _name = "xxx"; _age = 10; } Student(const Student& s) //:Person(s) :_address(s._address) ,_stuNO(s._stuNO){} int main(){ Student s1("张三", 18, "西安", 111); Student s2(s1);//拷贝构造,用s1拷贝构造s2 return 0 }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d04aK1uw-1648359386517)(C:\Users\83883\Desktop\没完成拷贝功能.png)]
我们可以看到,因为调用到的是父类的默认构造没有调用到父类的拷贝构造,所以s2明显只完成了子类自己的成员的拷贝,而继承下来的父类成员并没有实现拷贝,而是使用了父类默认构造函数中的缺省值。
赋值重载:
赋值重载在很多情况下与拷贝构造相似,它们的处理方式也很相似,这里就不再过多赘述。只要知道子类的赋值重载需要调用父类的赋值重载完成父类部分的成员的操作即可,
析构函数:
子类析构函数我们如果不写的话,编译器会帮助生成一个默认的析构函数,处理方式也与上面的一致:①继承的父类成员作为一个整体,调用父类的析构函数;②子类自己的自定义类型成员去调用它的析构函数;③子类自己的内置类型成员不作处理
如果我们要自己实现的话,要注意:子类的析构函数与父类的析构函数构成了隐藏关系
~Student(){ //释放掉子类自己的一些资源 ~Person(); } //按正常逻辑来说应该是这样,先子类调用自己的析构函数,将父类的部分调用它的析构函数。但是实际上这样编译器会报错
子类的析构函数和父类的析构函数,编译器都会进行特殊处理,所有类的析构函数名会被被编译器修改为
destructor()
,所以子类析构函数和父类的析构函数会构成隐藏关系。编译器这样处理的原因是析构函数要构成多态重写,重写的一个要求就是函数名必须相同。所以我们想要在子类的析构函数中调用父类的析构函数,需要添加域名:
~Student(){ Person::~Person(); }//这样编译器就不会再报错了
但是我们这样操作后,会重复调用父类的析构函数,因为子类的析构函数在执行结束后会自动去调用父类的析构函数,因为C++中规定先定义的后析构,后定义的先析构。所以我们不用在子类的析构函数里显式调用父类的析构函数,编译器会自动去调用的。
继承和友元
先说结论:友元关系不能继承。如果想要子类也实现友元,没有别的方法只能在子类中再次声明一下友元关系即可。
class Person{ public: friend void printInfo(const Person& p, const Student& s); protected: string _name; }; class Student : public Person{ protected: int _stuNO; }; void printInfo(const Person& p, const Student& s){ std::cout << p._name << std::endl;//正确,因为该函数是Person类的友元函数 std::cout << s._name <<std::endl;//错误,Student类不会将友元关系继承下来,所以不可以访问被保护的成员_name }
继承与静态成员
基类定义的static静态成员,则整个继承体系里只会有一个这样的成员,不论继承了多少层,都只会有一个static成员实例。
class Person{ public: static int _count;//人数 Person(){ ++ _count;//每调用一次Person类的构造函数就把_count加1 } protected: string _name; }; int Person::_count = 0; class Student : public Person{ protected: int _stuNO; }; class Pupils : public Student{ protected: int _age; } int main(){ Person p1; Person p2; Student s1; Student s2; Pupils pu1; std::cout << Person::_count << std::endl; std::cout << Student::_count << std::endl; std::cout << Pupils::_count << std::endl; //这三个输出结果是一样的,都是5 return 0 }
菱形继承和菱形虚拟继承
之前我们举出的所有例子都是单继承,即一个子类有且只有一个直接的父类,多代继承也是单继承,上面这个例子就是多代继承,即Pupils继承自Student,Student继承自Person,他们都是单继承。
如果有一个类,它有两个或两个以上的直接父类,这种继承关系叫做多继承,多继承是存在一定风险的,因此晚于C++的面向对象编程语言Java就直接取消了多继承,只允许单继承。原因在于多继承可能会导致菱形继承。
这样的多继承并没有什么问题,现实生活中也会有这种的例子,真正产生问题的是下面这种菱形继承:
class B、class C均继承于class A,这里是没有任何问题的,因为到这里只是两个单继承,但是一旦class D发生多继承,继承了B、C,那么根据我们之前说过的继承方面的知识,我们可以知道,D会获得两份A的成员,分别来自B和C,那么这两个A的成员不仅会产生数据冗余,还会造成二义性,如果要给D中继承下来的A类的成员赋值,是给从B继承过来的部分赋值还是给从C中继承过来的部分赋值呢?此时编译器就会报“访问不明确”的错误。
C++为了解决菱形继承的问题,就引入了”virtual“关键字,这里与多态部分的虚函数使用了同一个关键字。语法为
class B : virtual public class A
和class C : virtual public class A
注意virtual关键字是加在B、C两个类的地方,而不是加在D的位置。那菱形虚拟继承是如何改善菱形继承所造成的数据冗余和二义性问题呢?我们下面使用四个类来模拟出菱形继承,并在内存窗口中观察一下菱形继承的数据是如何存储的。
class A{ public: int _a; }; class B : public A{ public: int _b; }; class C : public A{ public: int _c; }; class D : public B, public C{ public: int _d; }; int main(){ D d; d.B::_a = 1;//从B继承而来的A的成员_a设置为1 d.C::_a = 2;//从C继承而来的A的成员_a设置为1 d._b = 3; d._c = 4; d._d = 5; return 0; }
在VS2022中打开内存窗口,并输入"&d",获取到d的地址:
那我们加上虚拟继承,即
class B : virtual public A | class C : virtual public A
之后,再来观察一下内存的情况:明显发现,使用了虚拟继承后,在原来没有使用虚拟继承的地方,本该存放从B、C中继承而来的A的成员_a值的地方, 却变成了两个像是地址的东西,我们再打开一个新的内存监视窗口,将这个地址输入进去,看一下具体是什么东西。
输入后发现该地址所存储的值都是0,但是下面一行却都存储着一个整数,分别是十六进制的’14’,即20;16进制的’0c’,即12。20和12都是偏移量,它们分别标记的是在菱形虚拟继承中产生冗余的那一块数据,即将这块数据作为公共部分,只保留一份,原来应该存放B、C中继承A而来的成语_a地方变成了获取偏移量的地址,所以我们想要访问B::_a还是C::_a,都只会拿到偏移量的地址,再通过偏移量访问到唯一一份的_a。
菱形虚拟继承虽然解决了数据冗余和二义性问题,但是明显数据访问变得更加繁琐,因此会造成部分的性能损失。因此我们应该尽量避免设计多继承,如果实在没办法,一定要使用多继承,那么就一定不要设计出菱形继承,上面我们看到了为了解决菱形继承问题的虚拟继承对复杂度和性能都有很大的影响。
-
再聊C++多重继承之菱形继承
2022-05-14 15:49:28菱形继承的问题和内存布局 class Base { public: Base(int){ std::cout << "Base()" << std::endl; }; ~Base(){ std::cout << "~Base()" << std::endl; }; protected: int n; }; class ... -
菱形继承(C++)
2022-05-15 11:05:12菱形继承 菱形继承:指有一个基类被两个不同的类所继承,且存在一个类继承于这两个类而形成的一种菱形关系,故称菱形继承。如下图所示: 如上图,Person类由Student类和Staff类继承,且有一个Stu_Staf类继承这两个... -
【C++】菱形继承和虚继承
2022-03-05 12:55:37文章目录一、菱形继承二、虚继承 一、菱形继承 二、虚继承 -
C++-继承-菱形继承-菱形虚拟继承-虚函数表
2021-05-22 22:28:04文章目录继承的概念和定义继承的概念继承的定义代码基类和派生类对象的赋值转换(赋值兼容原则) 继承的概念和定义 继承的概念 继承机制是对象对象程序设计使代码可以复用的重要手段,允许程序员在保持原有类特性的... -
C++菱形继承及解决方法
2022-07-22 11:35:37C++菱形继承 -
从内存的角度窥探C++多继承中的菱形继承问题
2022-04-06 20:26:00什么是菱形继承2. 菱形继承会带来什么问题3. 虚继承的方式解决菱形继承数据冗余和二义性的问题4. 通过内存窥探菱形继承的对象模型 1. 什么是菱形继承 菱形继承就是继承的方式长得像个菱形而已,它是多继承中一种... -
C++菱形继承分析
2018-04-04 22:25:43C++菱形继承对象构造部分分析和虚表内存布局,附件包括了源码和二进制文件 -
C++菱形继承与虚继承
2022-01-04 08:12:36继承菱形继承虚继承 菱形继承 虚继承 -
【C++】什么是菱形继承?菱形继承存在的问题?什么是虚拟继承?
2021-07-18 15:50:50目录 什么是菱形继承? 菱形继承存在的问题? 什么是虚拟继承? 什么是菱形继承? 单继承:一个子类只有一个直接父类时称这个继承关系为单继承 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承 菱形... -
C++_继承(菱形继承与虚基表)
2021-05-06 14:55:03什么是继承 继承是什么: 继承是面向对象程序设计、使得代码可以复用的重要手段,它允许程序员在保持原有特性的基础上进行扩展,增加功能,这样产生的新类,称之为派生类 继承的目的: 让子类继承和复用父类定义的... -
C++菱形继承原理分析
2021-01-20 03:34:41菱形继承在C++继承中是一种复杂的继承关系,存在着二义性和数据冗余的问题,而菱形虚拟继承则是为了解决菱形继承所存在的问题。为了探讨菱形虚拟继承如何解决该问题,我先建立个一个较为简单的菱形继承模型。下面... -
C++:再谈菱形继承问题 | 菱形继承时构造过程,内存模型是怎样的?
2022-04-07 16:26:51C++:菱形继承时的内存模型 -
单继承、多继承、菱形继承的虚函数表
2019-12-29 20:19:49最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个... -
C++学习之路-菱形继承与虚继承
2022-06-03 20:21:25菱形继承的问题 我们知道Student 继承于Person,那么就相当于把Person里所有的东西都拿到Student里。Person的成员变量也会存于Student里 同理,Worker类也是如此,会将Person类的成员变量存于Worker类。 由于,... -
菱形继承(及其深度探讨)
2022-03-08 21:29:12菱形继承(及其深度探讨) 普通的多继承 代码示例 #include<iostream> using namespace std; class object{ int value; public: object(int x = 0):value(x){} } class base:public object{ int num; ... -
C++的重大特性之一:继承、菱形继承
2022-06-30 10:43:03继承 -
C++--菱形继承
2021-04-25 11:12:58菱形继承1. 菱形继承2. 菱形虚拟继承 1. 菱形继承 单继承:一个子类只有一个直接父类时; 多继承:一个子类有两个或以上直接父类时; 菱形继承:是多继承的一种特殊情况; 菱形继承有数据冗余和二义性的问题。 2. ... -
继承的方式、多继承、菱形继承
2021-12-09 18:52:05虚继承解决了菱形继承中子类继承两份相同数据的问题; 实际开发中避免使用多继承; 继承中的对象模型: 静态成员访问方式:和非静态成员一样,只是静态成员有两种处理方式; 静态属性的访问 静态...