菱形继承->菱形虚拟继承(继承系列问题)

//待解决:用C语言模拟实现继承

继承(继承权限public->protected->private)

继承是面向对象复用的重要手段,通过继承定义一个类,继承是关系类型之间的建模, 共享共有的东西,实现各自本质不同的东西。

在继承关系当中,派生类继承父类的成员由此达到复用手段。

public继承是一个接口继承,保持is-a原则,每个父类可用的成员,对于子类也都可用。 因为每个子类对象也都是一个父类成员

protected/private继承是一个实现继承,是has-a的原则,基类的部分成员函数并未完全的成为子类接口的一部分。
与其有差异的是组合(包含),组合应用的也是has-a原则,只不过组合与protected/private继承的应用场合不同
这里写图片描述

通俗的来讲:is-a表示了一种是的关系;例如 白马是马,香蕉是水果的这种关系。
has-a 表示了有的关系,例如:午餐有香蕉,但是香蕉不是午餐,而是午餐的一种。

赋值兼容(前提:public继承权限)

1.派生类对象可以直接赋值给基类对象。
2.基类对象的指针或引用可以指向派生类对象

同名隐藏

在继承体系当中基类和派生类都有自己独立的作用域。
派生类和基类中有同名成员,派生类成员将屏蔽对基类成员的直接访问,
(在子类当中,可以使用基类::基类成员 访问),隐藏重定义。
在实际中在继承体系当中最好不要定义同名成员。
与同名隐藏有点相像的就是钻石继承问题,要区分开,同名隐藏是在基类和派生类里有相同的成员变量或者是成员函数。而钻石继承是相对于最后一个派生类而言,他的两个基类里有相同的成员对象或成员函数。

派生类的构造函数

1.构造函数
1》派生类的数据成员包含了基类中说明的数据成员和派生类中说明的数据成员。

2》构造函数不能被继承,因此,派生类的构造函数必须通过调用基类的构造函数来初始化基类的数据成员。

3》如果派生类中还有子对象时,还应包含对子对象初始化的构造函数。

2.派生类构造函数的调用顺序:

1》基类的构造函数(按照继承列表中的顺序调用)

2》派生类中对象的构造函数(按照在派生类中成员对象声明顺序调用)

3》派生类构造函数

注意:

1》基类没有缺省构造函数,派生类必须要在初始化列表中显示给出基类名和参数列表。

2》基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。

3》基类定义了带有形参列表的构造函数,派生类就一定要定义构造函数。

3.合成缺省构造函数的4种场景

1》类中有类类型成员对象,该成员对象它有自己的缺省构造函数,这时编译器也会在该类中合成一个缺省的构造函数(不一定是继承和派生的关系)

2》继承层次,基类中有缺省构造函数,而派生类中没有构造函数,这时编译器就会在派生类中合成一个缺省的构造函数

3》虚拟继承时,在派生类中不管有没有构造函数,编译器都会重新合成缺省构造函数,填写偏移量表格地址

4》基类含有虚函数时,在派生类中会合成缺省构造函数。

——默认构造函数是编译器默认合成的

——缺省构造函数是里面带缺省值的

派生类的析构函数

1.派生类的析构函数:由于析构函数也不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。

执行顺序是:

1》先执行派生类的析构函数,

2》派生类包含成员对象的析构函数(调用顺序和成员对象在类中的声明顺序相反)

3》基类析构函数(调用顺序和基类在派生列表中声明的顺序相反)

单继承&多继承

  1. 单继承–一个子类只有一个直接父类时称这个继承关系为单继承
#pragma once
#include<iostream>
using namespace std;
class A
{
public:
    int _a;
};

class B : public A
{
public:
    int _b;
};


void Test()
{
    B b;
    b._a = 1;
    b._b = 2;
}

从内存中我们可以看到单继承派生类的对象模型

这里写图片描述

画的通俗一点就是:

这里写图片描述

  1. 多继承– 一个子类有两个或以上直接父类时称这个继承关系为多重继承
#pragma once
#include<iostream>
using namespace std;
class A1
{
public:
    int _a1;
};

class A2
{
public:
    int _a2;
};

class B : public A1, public A2
{
public:
    int _b;
};


void Test()
{
    B b;
    b._a1 = 1;
    b._a2 = 2;
    b._b = 3;
}

从内存中我们可以看到多继承派生类的对象模型
这里写图片描述

同样,画的通俗点:

这里写图片描述

菱形继承是典型的多继承
菱形继承:两个子类同时继承一个父类,而又有子类同时继承这两个子类

#pragma once
#include<iostream>
using namespace std;
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;  
};


void Test()
{
    D d;
    d.B::_a = 1;//二义性问题的第一种解决方法(域限定符),先不管它,讨论对象模型
    d.C::_a = 2;//后面会告诉大家不采取这种方法
    d._b = 3;
    d._c = 4;
    d._d = 5;
}

程序运行结果(方便后面在菱形虚拟继承中虚拟继承的原理,可以先不看)

这里写图片描述

从内存中我们可以看到菱形继承派生类的对象模型

这里写图片描述

这里写图片描述

重新理解钻石继承

钻石继承也就是我们上面所述的菱形继承,假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。下图的形状类似于钻石(或者菱形),因此这个问题被形象地称为钻石问题(菱形继承问题)。
这里写图片描述
现在,我们将上面的图表翻译成具体的代码:

#pragma once
#include<iostream>
using namespace std;
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;  
};


void Test()
{
    D d;
    d._a = 1;
}

测试部分

#include<iostream>
using namespace std;
#include"Test.h"
int main()
{
    Test();
    system("pause");
}

程序结果:

这里写图片描述

继承结构中,B类和C类都继承自A基类。所以问题是:因为D类多重继承了B类和C类,因此D类会有两份A基类的成员(数据和方法),D类对象”d”会包含A基类的两个子对象。将会导致编译错误。这是因为编译器并不知道是调用B类的成员变量_ a还是调用C类的成员变量_ a。所以,调用成员变量_a方法是不明确的,因此不能通过编译。这就是所谓的菱形继承存在的二义性和数据冗余的问题。

解决钻石继承二义性问题

#pragma once
#include<iostream>
using namespace std;
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;  
};


void Test()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
}

这里写图片描述

读者细心的话,在前面刚写菱形继承代码的时候,在代码里我备注过这是第一种解决方法,还说并不采取这种方法
我们可以在给_a赋值时,使用域访问限定符,来解决编译器无法识别是对BB对象还是对CC对象中的 _a赋值。这样虽然解决了二义性问题,但是没有解决数据冗余的问题.

同时解决钻石继承二义性和数据冗余问题

这时候就要用到虚拟继承,虚拟继承是一种机制,类通过虚拟继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。虚基类用virtual声明继承关系就行了。这样一来,D就只有A的一份拷贝。如下:
如果B类和C类在分别继承A类时都用virtual来标注,对于D类对象,C++会保证只有一个A类的子对象会被创建。看看下面的代码:

#pragma once
#include<iostream>
using namespace std;
class A
{
public:
    int _a;
};

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;  
};


void Test()
{
    D d;
    d._a = 1;
    d._a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    cout<<sizeof(D)<<endl;
}

这里写图片描述
在这里我们通过监视窗口可以明显的看到对_a赋值,B类和C类中的 _a都会改变,说明在D类的对象模型中只存在一个 _a,这样既解决了二义性问题又解决了数据冗余的问题。

菱形虚拟继承的实现原理

紧接着我们来看看虚拟继承后D派生类的大小,在之前开始讲解菱形继承时,我们看过菱形继承的对象模型,同时还求出了D派生类的大小为20,那么我们先看看菱形虚拟继承的大小:
上述代码的运行结果是:

这里写图片描述

为什么比之前增大了呢?到这里带着疑问我们看看内存:

这里写图片描述

从内存中我们可以得出D派生类的对象模型:

这里写图片描述

和之前的对比一下:
这里写图片描述

会发现内存里多了偏移量表格地址,打开这个地址,可以看到:

这里写图片描述

so,到这里还不明白的话,我们看看反汇编,他的底层实现原理:

D d;
00104508 6A 01                push        1  
0010450A 8D 4D E0             lea         ecx,[d]  
0010450D E8 85 CF FF FF       call        D::D (0101497h) //d创建
    d._a = 1;
00104512 8B 45 E0             mov         eax,dword ptr [d]  
00104515 8B 48 04             mov         ecx,dword ptr [eax+4]  
00104518 C7 44 0D E0 01 00 00 00 mov         dword ptr d[ecx],1//写入1
    d._a = 2;
00104520 8B 45 E0             mov         eax,dword ptr [d]  
00104523 8B 48 04             mov         ecx,dword ptr [eax+4]  
00104526 C7 44 0D E0 02 00 00 00 mov         dword ptr d[ecx],2//写入2  
    d._b = 3;
0010452E C7 45 E4 03 00 00 00 mov         dword ptr [ebp-1Ch],3  
    d._c = 4;
00104535 C7 45 EC 04 00 00 00 mov         dword ptr [ebp-14h],4  
    d._d = 5;
0010453C C7 45 F0 05 00 00 00 mov         dword ptr [ebp-10h],5  

2.进入d的创建

D::D:


01143197 74 13                je          D::D+3Ch (011431ACh)  
01143199 8B 45 F8             mov         eax,dword ptr [this]//将d首地址送往eax  
0114319C C7 00 60 D1 14 01    mov         dword ptr [eax],114D160h  //将B距离A的偏移地址送到eax 

011431A2 8B 45 F8             mov         eax,dword ptr [this]  
011431A5 C7 40 08 B0 D2 14 01 mov         dword ptr [eax+8],114D2B0h  //将C距离A的偏移地址送到eax 


011431AC 6A 00                push        0  
011431AE 8B 4D F8             mov         ecx,dword ptr [this]  
011431B1 E8 DC E2 FF FF       call        B::B (01141492h)//B的构造  
011431B6 6A 00                push        0  
011431B8 8B 4D F8             mov         ecx,dword ptr [this]  
011431BB 83 C1 08             add         ecx,8  
011431BE E8 D9 E2 FF FF       call        C::C (0114149Ch) //C的构造 
011431C3 8B 45 F8             mov         eax,dword ptr [this] 

3.进入B的构造

B::B:

01142D59 8B 45 F8             mov         eax,dword ptr [this]  
01142D5C C7 00 70 CC 14 01    mov         dword ptr [eax],114CC70h//存放B的大小 
01142D62 8B 45 F8             mov         eax,dword ptr [this]  

4.进入C的构造

C::C:

01143869 8B 45 F8             mov         eax,dword ptr [this]  
0114386C C7 00 D4 CB 14 01    mov         dword ptr [eax],114CBD4h//存放C的大小
01143872 8B 45 F8             mov         eax,dword ptr [this]

总结:

d对象在创建的过程中,调用D的构造,然后对B和C进构造,在B构造时,在其起始位置(d位置)放置一指向B的父类(A)的指针的偏移地址,偏移地址+4便是距离A的偏移量,同时在创建一偏移地址,保存B的对象的大小。然后d加上B的对象的空间大小,在之后创建B的对象。B和A的对象的首地址指向同一空间(父类A)。

1.虚继承体系看起来好复杂,在实际应用中我们通常不会定义如此复杂的继承体系,⼀般不到万不得已都不要用。
2.定义菱形结构的虚继承体系结构,因为虚继承体系解决数据冗余问题也带来了性能上的损耗。

一个类如何才能不被继承

1.一个类不想被继承,也就是说它的子类不能构造父类,我们可以将一个类的构造函数声明为私有,使得这个类的构造函数对子类不可见,那么这个类也就不能被继承了。

#pragma once
#include<iostream>
using namespace std;
class A
{
private:
    A(int a)
    {}
    ~A()
    {}
public:
    int _a;
};

class B : public A
{
public:
    int _b;
};
void Test()
{
    B b;
    b._a = 1;

}

这里写图片描述
看到没,目的达成了!!
但是这样有一个缺点:就是假如现在只有A类,我们在A类外也不能实例化对象了,这个类已经没法正常使用了!!!
比如:

#pragma once
#include<iostream>
using namespace std;
class A
{
private:
    A(int a)
    {}
    ~A()
    {}
public:
    int _a;
};

void Test()
{
    A a;
    a._a = 1;

}

2.为了能够让A类不被继承后还能够正常的使用,我们有第二种方法:

将A类虚继承E类,但是E类的构造函数是带private属性的,A类还是E类的友元。

分析:

如果我们让A类虚继承E类,根据虚继承的特性,虚基类的构造函数由最终的子类负责构造,此时E类的构造函数虽然是私有的,但是A类是E类的友元,所以可以调用E类的构造函数完成初始化。

B类如果要想继承A类,它必须能够调用E虚基类的构造函数来完成初始化,这是不可能的,因为它不是E类的友元!因此,我们的A类也就终于成为了一个无法继承的类,并且我们还可以在A类外实例化对象,可以正常使用。

代码如下:

#pragma once
#include<iostream>
using namespace std;

class E
{
private:
    friend class A;
    E()
    {}
};

class A : virtual public E
{
public:
    A()
    {}

};

//class B : public A
//{
//public:
//  B()
//  {
//
//  }
//};

void Test()
{
    //B a;
      A a;

}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值