1、单继承与多继承

  单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承。这种关系比较简单是一对一的关系:

  多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承。这种继承方式使一个子类可以继承多个父类的特性。多继承可以看作是单继承的扩展。派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。在子类的内存中它们是按照声明定义的顺序存放的,下面的截图将清晰看到。

菱形继承也叫钻石继承

但是多继承存在一个问题,要想研究这个问题,我们先从单继承讲起。来看内存空间:

 class Base
{
public:
Base() {
cout << "B()" << endl;
}
int b1;
};
class Derive : public Base
{
public:
Derive() {
cout << "D()" << endl;
}
int d1;
};
int main()
{
Test();
getchar();
return ;
}

多继承的内存空间:

 class Base
{
public:
Base() {
cout << "B()" << endl;
}
int b1;
};
class C
{
public:
C() {
cout << "C()" << endl;
}
int c;
};
class Derive : public Base, public C
{
public:
Derive() {
cout << "D()" << endl;
}
int d1;
};

菱形继承内存中数据分布:

 class A
{
public:
A() {
cout << "A()" << endl;
}
int a;
};
class Base:public A
{
public:
Base() {
cout << "B()" << endl;
}
int b1;
};
class C: public A
{
public:
C() {
cout << "C()" << endl;
}
int c;
};
class Derive : public Base, public C
{
public:
Derive() {
cout << "D()" << endl;
}
int d1;
};

在A类中初始化int a=4则可清楚的看到菱形继承中内存分布

所以子类Derive中有两份A类中的数据成员,这造成了访问二义性和数据冗余的问题

这就是我前面说的多继承存在的问题。可以这样访问

 tmp.C::a=;
tmp.Base::a=;

什么是对象模型

有两个概念可以解释C++对象模型:

1、语言中直接支持面向对象程序设计的部分。
2、对于各种支持的底层实现机制。

菱形继承对象模型如下:

2、虚继承

还有另外一个方法解决这个问题,我们要用到一种新的继承方法:虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类,它可共享的特性,避免了拷贝多份相同的数据,从而解决菱形继承的二义性和数据冗余的问题。看下面这段代码:

 class Base
{
public:
Base() {
cout << "B()" << endl;
}
int b1;
};
class Derive : virtual public Base
{
public:
Derive() {
cout << "D()" << endl;
}
int d1;
};
void Test()
{
Derive tmp;
tmp.d1 = ;
tmp.b1 = ;
}
int main()
{
Test();
getchar();
return ;
}

虚拟继承的关键字---virtual

下图为单继承的内存分布:

图中的偏移量地址其实为一个指向基类偏移量表的指针。

虚拟继承是虽然不是多重继承中特有的概念。但虚拟基类是为解决多重继承而出现的。
下图可以看出虚基类和非虚基类在多重继承中的区别

  虚继承的提出就是为了解决多重继承时可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?待我慢慢道来

1.类中无其它数据成员时

 class B //基类
{
public:
B()
{
cout << "B" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class C1 :virtual public B
{
public:
C1()
{
cout << "C1()" << endl;
}
~C1()
{
cout << "~C1()" << endl;
}
};
class C2 :virtual public B
{
public:
C2()
{
cout << "C2()" << endl;
}
~C2()
{
cout << "~C2()" << endl;
}
};
class D :public C1, public C2
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
}; int main()
{
cout << sizeof(B) << endl;
cout << sizeof(C1) << endl;
cout << sizeof(C2) << endl;
cout << sizeof(D) << endl;
return ;
}

输出结果为:

结果分析:首先,基类中除了构造函数和析构函数没有其他成员了,所以 sizeof(B) = 1;这里再提一个问题有的初学者可能会问为什么为1呢?首先类在内存中的存储是这样的:

如果有一个类Base定义如下例:

 class Base
{
public:
void fun();
int b;
};
int Test()
{
Base b1,b2,b3;
}

那么在内存中的对象模型如下图:

成员函数是单独存储的,并且所有为类对象公用。
  类的实例化要求每个实例对象在有独立无二的地址空间,而空类也可以实例化。编译器要区分开所有的类对象,就要给对象一个地址,只是一个占位符,表示这个对象存在,并且让编译器给这个对象分配地址。至于占多少位,由编译器决定,这里空类的大小为1,是在VS2015中,其他编译器可能不同。

  由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向偏移量表的指针。所以C1和C2大小为4,这就是指针的大小了。D的大小就是继承的两个指针的大小了。这里再详细解释一下偏移量表,是什么的偏移量呐?

我们在main函数中生成一个C1类对象c1:

 int main()
{
C1 c1;
return ;
}

内存布局如下:

  由图可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是 c1 的 vbptr 指向的偏移量表的地址。偏移量表有八个字节,分别存的为0和4。 那么0和4代表的都是什么呢? 虚基类表存放的为两个偏移地址,分别为0和4。其中0表示c1对象地址相对于存放vbptr指针的地址的偏移量。(因为vbptr指针是属于c1对象的,c1对象地址相对于vbptr指针的地址偏移量为0。这里我把它这个表叫做偏移量表,避免与后面多态中的虚表混淆。)

而4表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量,可以用 &c1(B)-&vbpt 表示,其中&c1(B)表示对象c1中基类B部分的地址。

c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有独自的数据成员。

  总结:C1,C2是虚继承自基类B,所以编译器会给C1,C2中生成一个指针vbptr指向一个偏移量表,即指针vbptr的值是偏移量表的地址。表中存放对象相对于偏移量表指针的偏移量。表中分两部分,第一部分存储的是对象相对于存放vptr指针的偏移量,可以用&(对象名)->vbptr_(对象名)来表示。对c1对象来说,可以用&c1->vbprt_c1来表示。表的第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量,我们知道在本例中基类对象与指针偏移量就是指针的大小。

下面再来看看D的内存结构:

如图所示,d中存放了两个虚基类指针,每个虚基类表中存储了偏移量。形象的内存布局如下图:

2.类中加数据成员

下面看一下拥有独立数据成员的类的虚继承,可以更清晰的理解内存布局:

 #include <iostream>
using namespace std; class B
{
public:
B()
{
cout << "B" << endl;
}
~B()
{
cout << "~B()" << endl;
}
int b;
};
class C1 :virtual public B
{
public:
C1()
{
cout << "C1()" << endl;
}
~C1()
{
cout << "~C1()" << endl;
}
int c1;
};
class C2 :virtual public B
{
public:
C2()
{
cout << "C2()" << endl;
}
~C2()
{
cout << "~C2()" << endl;
}
int c2;
};
class D :public C1, public C2
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
void fun()
{
b = ;
c1 = ;
c2 = ;
d = ;
}
int d;
}; int main()
{
cout << sizeof(B) << endl;
cout << sizeof(C1) << endl;
cout << sizeof(C2) << endl;
cout << sizeof(D) << endl;
D d;
d.fun();
return ;
}

输出结果为:

B占四个字节没有问题,因为B类中有int b数据成员,所以B类占四个字节。 C1,C2是虚继承自B类的,所以C1,C2的内存布局是相似的,在这里我只分析一下C1。 我在C1类中加一个Fun成员函数,为了更清楚的看到内存布局:

 class C1 :virtual public B
{
public:
C1()
{
cout << "C1()" << endl;
}
~C1()
{
cout << "~C1()" << endl;
}
void Fun()
{
b = ;
c1 = ;
}
int c1;
};
int main()
{
C1 c1;
c1.Fun();
return ;
}

在main函数中生成对象c1,C1=int+int+指针=4+4+4=12,再来看一看内存布局:

现在来看看D类的内存布局:

 class D :public C1, public C2
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
void fun()//fun()函数主要帮助我们看D类的内存布局
{
b = ;//基类数据成员
c1 = ;//C1类数据成员
c2 = ;//C2类数据成员
d = ;//D类自己的数据成员
}
int d;
};

内存布局如下:

下面再看看多重虚拟继承

 class A
{
public:
A() {
cout << "A()" << endl;
}
int a ;
};
class Base : virtual public A
{
public:
Base() {
cout << "B()" << endl;
}
int b1;
};
class C:virtual public A
{
public:
C() {
cout << "C()" << endl;
}
int c;
};
class Derive : virtual public Base, virtual public C
{
public:
Derive() {
cout << "D()" << endl;
}
int d1;
};
void Test()
{
Derive tmp;
tmp.d1 = ;
tmp.b1 = ;
tmp.c = ;
tmp.a = ;
}
int main()
{
Test();
getchar();
return ;
}

现在我们直接看内存布局:

C++中的类继承(4)继承种类之单继承&多继承&菱形继承的更多相关文章

  1. C++学习之路(九):从菱形继承引入的对象模型

    一.单继承 class A {int a;}; class B : public A {int b;}; 普通的单继承关系,类的大小是由其虚表指针和非静态成员函数大小决定.故上述sizeof(A)的大 ...

  2. c++继承汇总(单继承、多继承、虚继承、菱形继承)

    多重继承中,一个基类可以在派生层次中出现多次,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多分同名成员.C++提供虚基类的方法使得在 ...

  3. JavaScript中的类式继承和原型式继承

    最近在看<JavaScript设计模式>这本书,虽然内容比较晦涩,但是细品才发现此书内容的强大.刚看完第四章--继承,来做下笔记. 书中介绍了三种继承方式,类式继承.原型式继承和掺元类继承 ...

  4. Python中新式类和经典类的区别,钻石继承

    1)首先,写法不一样: class A: pass class B(object): 2)在多继承中,新式类采用广度优先搜索,而旧式类是采用深度优先搜索. 3)新式类更符合OOP编程思想,统一了pyt ...

  5. Python中的类、对象、继承

    类 Python中,类的命名使用帕斯卡命名方式,即首字母大写. Python中定义类的方式如下: class 类名([父类名[,父类名[,...]]]): pass 省略父类名表示该类直接继承自obj ...

  6. lua中基类和“继承机制”

    基类:基类定义了所有对于派生类来说普通的属性和方法,派生类从基类继承所需的属性和方法,且在派生类中增加新的属性和方法. 继承:继承是C++语言的一种重要机制,它允许在已定义的类的基础上产生新类. lu ...

  7. java中关于类的封装与继承,this、super关键字的使用

    原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/5454849.html. this关键字: this代表当前对象,它有以下几种用途: 1.本类 ...

  8. C++中的类继承(1) 三种继承方式

    继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一.简单的说,继承是指一个对象直接使用另一对象的属性和方法.继承呈现了 面向对象程序设 计的层次结构, 体现了 由简单到复杂的认知过程. ...

  9. C++中的类继承(2)派生类的默认成员函数

    在继承关系里面, 在派生类中如果没有显示定义这六个成员 函数, 编译系统则会默认合成这六个默认的成员函数. 构造函数. 调用关系先看一段代码: class Base { public : Base() ...

随机推荐

  1. for循环的表达规则,for循环的嵌套,跳转语句;穷举;迭代;异常处理

    for循环的基本格式 for(表达式1:表达式2:表达式3) { 循环体: } for循环的四要素 表达式1就是变量初始化:表达式2就是循环条件:表达式3是状态改变 static void Main( ...

  2. Django之Cookie

    Cookie 在浏览器端(客户端)保存的键值对,特性:每次http请求都会携带.           举个例子:{"name":身份证号} 1丶获取cookie request.C ...

  3. Java原生API操作XML

    使用Java操作XML的开源框架比较多,如著名的Dom4J.JDOM等,但个人认为不管你用那个框架都要对JDK原生的API有所了解才能更得心应手的应用.本篇就来简单了解下原生的XML API. JAV ...

  4. Spring IOC以及三种注入方式

    IOC是spring的最基础部分,也是核心模块,Spring的其他组件模块和应用开发都是以它为基础的.IOC把spring的面向接口编程和松耦合的思想体现的淋漓尽致. IOC概念 IOC(Invers ...

  5. ER图是啥?

    文章转载自「开发者圆桌」一个关于开发者入门.进阶.踩坑的微信公众号 E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型.属性和联系的方法,用来描述现 ...

  6. 2017,科学使用strace神器(附代码,举栗子)

    我感到惊讶,都2017年了,几乎没有人知道他们可以使用strace的了解所有事情.它总是我拔出的第一个调试工具之一,因为它通常在我运行的Linux系统上可用,并且它可以用于解决各种各样的问题. 什么是 ...

  7. 如何理解jQuery中的ajaxSubmit方法

    版权声明:本文为博主原创文章,转载请标注:www.cnblogs.com/gdsblog 刚刚学习中,使用到了ajaxSubmit,犹豫以前没有接触㢧这个,所以刚开始是一脸懵逼状态,最后通过查找资料的 ...

  8. java深拷贝和浅拷贝

    1.概念 java里的clone分为: A:浅复制(浅克隆): 浅复制仅仅复制所考虑的对象,而不复制它所引用的对象. b:深复制(深克隆):深复制把要复制的对象所引用的对象都复制了一遍. Java中对 ...

  9. kali linux 忘记root密码重置办法

    有段时间没用kali linux 的,加上最近装的系统有比较多,系统root的密码忘掉了,真是麻烦啊.之前在网上看到的一些方法尝试后没进的去,可能是因为不同的linux 不一样吧. 如果因为忘记密码而 ...

  10. Python的字典dictionary

    创建: dict = {'Name': 'Zara', 'Age': 7, 'Class': 'First'};删除: del dict['Name']; # 删除键是'Name'的条目 dict.c ...