• C++继承总结(下)——菱形继承


    一.什么是菱形继承

    菱形继承是多继承的一种特殊情况,一个类有多个父类,这些父类又有相同的父类或者祖先类,那么该类就会有多份重复的成员,从而造成调用二义性和数据冗余。

    1. class Person
    2. {
    3. public:
    4. Person()
    5. {
    6. cout << "Person构造" << endl;
    7. }
    8. public:
    9. int _name = 0;
    10. int _age = 0;
    11. };
    12. class Student : public Person
    13. {
    14. public:
    15. Student()
    16. {
    17. cout << "Student构造" << endl;
    18. }
    19. int _stuid = 0;
    20. };
    21. class Teacher : public Person
    22. {
    23. public:
    24. Teacher()
    25. {
    26. cout << "Teacher构造" << endl;
    27. }
    28. int _jobid = 0;
    29. };
    30. class Assistant : public Student, public Teacher
    31. {
    32. public:
    33. Assistant()
    34. {
    35. cout << "Assistant构造" << endl;
    36. }
    37. int _task = 0;
    38. };
    39. int main()
    40. {
    41. Assistant a;
    42. //a._name;//二义性:访问Student的_name还是Teacher的_name呢?
    43. //需要指定类域访问
    44. a.Student::_name = 1;
    45. a.Student::_age = 2;
    46. a._stuid = 3;
    47. a.Teacher::_name = 4;
    48. a.Teacher::_age = 5;
    49. a._jobid = 6;
    50. a._task = 7;
    51. return 0;
    52. }

    从a的内存布局可以看到,a中有两份_name和_age,它们是从Student和Teacher类继承下来的。二义性的问题可以通过指定类域访问解决,但数据冗余的问题是无法规避的,必须引入新的技术——虚继承 

    二.虚继承的用法

    只需在继承那个祖先类时加上关键字virtual即可

    1. class Person
    2. {
    3. public:
    4. Person()
    5. {
    6. cout << "Person构造" << endl;
    7. }
    8. public:
    9. int _name = 0;
    10. int _age = 0;
    11. };
    12. class Student : virtual public Person
    13. {
    14. public:
    15. Student()
    16. {
    17. cout << "Student构造" << endl;
    18. }
    19. int _stuid = 0;
    20. };
    21. class Teacher : virtual public Person
    22. {
    23. public:
    24. Teacher()
    25. {
    26. cout << "Teacher构造" << endl;
    27. }
    28. int _jobid = 0;
    29. };
    30. class Assistant : public Student, public Teacher
    31. {
    32. public:
    33. Assistant()
    34. {
    35. cout << "Assistant构造" << endl;
    36. }
    37. int _task = 0;
    38. };
    39. int main()
    40. {
    41. Assistant a;
    42. a.Student::_name = 1;
    43. a.Student::_age = 2;
    44. a._stuid = 3;
    45. a.Teacher::_name = 4;
    46. a.Teacher::_age = 5;
    47. a._jobid = 6;
    48. a._task = 7;
    49. return 0;
    50. }

    虚继承前:

    虚继承后: 

    可以看到,Person构造函数只调用了一次。

    再来看看虚继承后a的内存分布:

     虚继承后,重复的那部分成员被单独拎了出来,只有一份,此时就不存在二义性的问题了。a.Student::_name;a.Student::_name;a._name访问的是同一份数据。同时也解决了数据冗余的问题。 

    三.虚继承的原理

    Student和Teacher中多出的这两个东西是什么呢?这似乎是一个地址,那我们在内存中看一看(注意是小端存储,低字节存低位数据,高字节存高位数据,故地址应该为007e9b4c和007e9b54)

    注意这是16进制,故第一个数 是20,第二个数是12。

    在看看上面的内存分布,会发现:006ff8d0这个地址加上20,006ff8d8加上12,刚好是006ff8e4,也就是重复的Person那部分变量的起始地址。

    Assistant对象中将Person放到的了对象组成的最下面,这个Person同时属于Student和Teacher,给Student和Teacher都添加一个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存了偏移量,通过偏移量可以找到下面的Person。事实上,虚基表中存放了两个数据,第二个数是偏移量,第一个数与多态中的虚表有关,这里不作展开,后面的多态会讲到。

    四.例题

    1.多继承中指针偏移问题?下面说法正确的是( )

    1. class Base1 { public: int _b1; };
    2. class Base2 { public: int _b2; };
    3. class Derive : public Base1, public Base2 { public: int _d; };
    4. int main(){
    5. Derive d;
    6. Base1* p1 = &d;
    7. Base2* p2 = &d;
    8. Derive* p3 = &d;
    9. return 0;
    10. }

     A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

    d的内存分布如图,p1和p3相等,不同之处在于p3解引用能访问整个对象,p1解引用只能访问Base1哪部分 

            

     2.下面程序输出结果是什么? ()

    1. using namespace std;
    2. class A{
    3. public:
    4. A(const char *s) { cout<
    5. ~A(){}
    6. };
    7. class B:virtual public A
    8. {
    9. public:
    10. B(const char *s1,const char*s2):A(s1) { cout<
    11. };
    12. class C:virtual public A
    13. {
    14. public:
    15. C(const char *s1,const char*s2):A(s1) { cout<
    16. };
    17. class D:public B,public C
    18. {
    19. public:
    20. D(const char *s1,char *s2,const char *s3,const char *s4)
    21. :B(s1,s2)
    22. ,C(s1,s3)
    23. ,A(s1)
    24. { cout<
    25. };
    26. int main() {
    27. D *p=new D("class A","class B","class C","class D");
    28. delete p;
    29. return 0;
    30. }

    A:class A class B class C class D

    B:class D class B class C class A

    C:class D class C class B class A

    D:class A class C class B class D

    首先明确一点,A的构造函数会调一次还是三次?因为虚继承解决数据冗余的问题,A的成员在D中只有一份,故只会调用一次构造函数。那么调用时机在哪呢?应该是在调用B,C的构造函数之前,A构造完后,构造B,C时就不会再去调用A的构造函数了。而B,C的构造函数谁先调用?答案是按继承顺序来,B先继承,故B先调用。故答案为A 

    假如把两个virtual去掉,A的构造函数会调用几次?答案是编译错误,去掉virtual后这就是一个普通的多继承,B,C中都有A,D不能单独去初始化A。去掉初始化列表中的A(s1)就正确了,A会被构造两次。

  • 相关阅读:
    Hive工作原理
    10个python爬虫入门实例
    VM ware中Linux连网
    Linux提权方法总结
    Bias and Debias in Recommender System: A Survey and Future Directions学习笔记
    Eslint与Prettier配合解决代码格式问题
    想掌握vue模板语法?两篇就够了!(上)
    机器学习笔记--数学库
    子组件自定义事件$emit实现新页面弹窗关闭之后父界面刷新
    每天五分钟机器学习:从数学向量角度理解支持向量机为何是大间距分类器
  • 原文地址:https://blog.csdn.net/weixin_74113106/article/details/134068569