C++继承详解三 ----菱形继承、虚继承

转载:http://blog.csdn.net/pg_dog/article/details/70175488

今天呢,我们来讲讲菱形继承虚继承。这两者的讲解是分不开的,要想深入了解菱形继承,你是绕不开虚继承这一点的。它俩有着什么关系呢?值得我们来剖析。 
菱形继承也叫钻石继承,它是多继承的一种特殊实例吧,它的基本架构如下图: 
这里写图片描述

在我们的设想中,D所对应的对象模型应该如下图所示:

这里写图片描述 
下面我们来用一段代码验证一下:

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}char a;
};class B  :public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}char b;
};class C :public A
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}int c;
};
class D :public B, public C
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}int d;};int main()
{cout << sizeof(A)<< endl;  //1cout << sizeof(B)<< endl;  //2cout << sizeof(C)<< endl;  //8cout << sizeof(D)<< endl;  //16system("pause");return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

这里写图片描述

上面显示的大小似乎证实了我们的猜想,但实际上对象模型不是这样的,如下图所示 
这里写图片描述 
但是你会发现,这里面存在一个问题,对象D中有两个‘a’,存在数据冗余的问题,如果对象B,C中有两个同名的函数或同名成员变量(本例中的变量‘a’),那么对象D在调用该函数或该成员变量时,该选择调用哪个呢?这也就可以看出还存有二义性问题。那么该如何处理呢? 
解决二义性问题很简单,你在调用函数时加上作用域运算符(::),但是数据冗余问题还是没有解决。那么编译器是如何处理这两个问题的呢? 
为了解决二义性问题和数据冗余问题,C++引入了虚继承这一概念。下面重点来看虚继承。

虚继承 
虚继承又称共享继承,是面向对象编程的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类直接或间接派生的其他类。虚拟继承是多重继承中特有的概念,虚拟继承就是为了解决多重继承而出现的。 
这里我想引入《C++ Primer》这本书中对虚继承的有关描述。

在C++语言中我们通过虚继承的机制来解决共享问题。虚继承的目的是令某个类作出声明,承诺共享它的基类。其中,共享的基类子对象称其为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只含有唯一一个共享的虚基类子对象。

这里还有一个概念,虚基类。虚基类是通过virtual继承而来的派生类的基类。例如:B虚继承了A,所以A是B的虚基类,而不是说A是虚基类。 
看下图了解普通基类与虚基类的区别:

这里写图片描述

按照上面的说法,在对象D中应该只含有一个共享的虚基类子对象,也就是例子中的_a。确实,这样就解决了数据冗余与二义性问题。我们来验证上面的的说法。(为了计算简单,我将上例中每个类成员变量变为整形int)

下面我们来看一段代码:

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}void print(){printf("A");}int _a;
};class B  :virtual public A   //B虚继承A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};class C :virtual public A    //C虚继承A
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}int _c;
};
class D :public B, public C
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}int _d;};int main()
{cout << sizeof(A)<< endl;cout << sizeof(B)<< endl;cout << sizeof(C)<< endl;cout << sizeof(D)<< endl;B bb;C cc;D dd;dd.B::_a = 1;dd.C::_a = 2;dd._b = 3;dd._c = 4;dd._d = 5;system("pause");return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

B和C都是虚拟继承, 
按照我们之前的推理,对象D的结构应该如图所示: 
这里写图片描述 
我们来通过vs2013调试中的内存窗口来验证一下: 
这里写图片描述 
看到这个结果是不是吓坏宝宝了?和我们预测的完全不一样,对象A和B中的_a跑到了最底部,这种结构明显没有了数据冗余和二义性问题了,这是怎么实现的呢?这就要引入一新的概念——虚基类表。 
虚基类表:又称虚基表,编译器会给虚继承而来的派生类生成一个指针vbptr指向一个虚基表,而虚基表中存放的是偏移量。 
我们来看对象D中的对象B,它的第一部分(第一行)就是虚基类表指针vbptr,它存的是虚基表的地址,虚基表中存的是共享基类成员变量_a的相对此位置的偏移量,我们来看看,“01259b60”是个地址,利用内存窗口我们可以发现里面存着两部分第一行“00 00 00 00”和第二行“00 00 00 14”,虚基表中分两部分:第一部分存储的是对象相对于存放vptr指针的偏移量(在这就是“00 00 00 00”,偏移量为0),第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量(在这就是“00 00 00 14”), 
即20(十六进制下14就是十进制的20),也就是说偏移量是20个字节,你可以用他们的地址相减验证一番。你可以看图数一下,而对象D中的C的第一部分也是一样,是个虚基表,存的一样也是偏移量,它存的地址“00369b68”,里面是“00 00 00 0c”即十进制的12,即偏移量为12字节。可以看下图:

这里写图片描述

下面我再讲一个概念——虚函数,这会在下篇文章多态中重点讲解,但是这里有必要了解一下。 
虚函数——类的成员函数前面加上virtual关键字,则这个函数被称为虚函数。

虚函数:用于定义类型特定行为的成员函数。通过引用和指针对虚函数的调用直到运行时才被解析,依据是引用或指针所绑定对象的类型。(《C++ Primer》中定义)

虚函数重写(覆盖):当在子类定义了一个和父类完全相同的虚函数时,则称这个这个子类的函数重写了(覆盖了)父类的虚函数。 
既然说到这,就有必要区分一下几个概念: 
重载:在同一作用域内,函数名相同,参数不同,返回值可不同的一对函数被称为重载。 
隐藏(重定义):在不同作用域(一般指基类和派生类),函数名相同,参数列表也相同,但不需要virtual关键字的一组函数称为隐藏。 
覆盖:不在同一作用域(一般指派生类和基类),完全相同(协变除外)基类中函数必须有virtual关键字的一对函数被称为重定义。

注: 
1,基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。 
2,只有类的成员函数才能定义为虚函数。 
3,静态成员函数不能定义为虚函数。 
4,如果在类外定义虚函数,只能在声明处加virtual关键字,类外定义函数时不能加virtual关键字。 
5,构造函数不能为虚函数。 
6,最好不要将赋值运算符重载定义为虚函数,因为使用容易混淆。 
7,不要在构造函数和析构函数调用虚函数,在构造函数和析构函数中对象是不完整的,可能会发生未定义的行为。 
8,最好将基类的析构函数定义为虚函数。(注:虽然基类的析构函数和派生类的析构函数名称不一样,但构成覆盖,因为编译器做了特殊处理) 
9,虚继承只对虚继承子类后面派生出的子类有影响,对虚继承自雷本身没有影响。

纯虚函数 
纯虚函数——在成员函数的后面加上=0,则成员函数为纯虚函数。一个纯虚函数无需定义,但也可以定义,但是必须在类外,也就是说我们不能在类内部为一个带有=0的函数提供函数体。包含纯虚函数的类被称为抽象类,也叫接口类。抽象类不能实例化出对象。他只是作为基类服务于派生类,如果派生类不对基类的虚函数进行覆盖,那他仍将是抽象基类。

class Father    //抽象类(接口类)
{
public:virtual void fun() = 0;  //定义纯虚函数
protected:int _a;
};class Child
{
public:virtual void fun() = 0; //覆盖,否则Child也是抽象类(接口类)
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

继承和友元 
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

继承和静态成员 
基类中定义了静态成员,则整个继承体系中只有一个这样的成员。无论派生出多少的子类,都只有一个静态成员实例。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384357.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux :IO多路复用模型

转载&#xff1a;http://blog.csdn.net/mr253727942/article/details/50827127 一、IO多路复用定义 IO多路复用允许应用在多个文件描述符上阻塞&#xff0c;并在某一个可以读写时通知&#xff0c; 一般遵循下面的设计原则&#xff1a;、 IO多路复用&#xff1a;任何文件描述符…

leetcode(一)刷题两数之和

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 示例 1&#xff1a; 输入&#xff1a;nums [2,7,11,15], target 9 输出&#xff1a;[0,1] 解释&#xff1a;因为 nums[…

Linux并发服务器编程之多线程并发服务器

转载&#xff1a;http://blog.csdn.net/qq_29227939/article/details/53782198 上一篇文章使用fork函数实现了多进程并发服务器&#xff0c;但是也提到了一些问题&#xff1a; fork是昂贵的。fork时需要复制父进程的所有资源&#xff0c;包括内存映象、描述字等&#xff1b;目…

leetcode(二)二分法查找算法

给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target 9 输出: 4 解释: 9 出现在…

leetcode(977)有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff1a;[0,1,9,16,100] 解释&#xff1a;平方后&#xff0c;数组变为 […

IO多路复用之select全面总结(必看篇)

转载&#xff1a;http://www.jb51.net/article/101057.htm 1、基本概念 IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取&#xff0c;它就通知该进程。IO多路复用适用如下场合&#xff1a; &#xff08;1&#xff09;当客户处理多个描述字时&#xff08;一般…

leetcode(189) 旋转数组

**给定一个数组&#xff0c;将数组中的元素向右移动 k 个位置&#xff0c;其中 k 是非负数。 进阶&#xff1a; 尽可能想出更多的解决方案&#xff0c;至少有三种不同的方法可以解决这个问题。 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗&#xff1f; 示例 1: …

I/O 多路复用之select

转载&#xff1a;http://blog.csdn.net/u012432778/article/details/47347133 概述 Linux提供了三种 I/O 多路复用方案&#xff1a;select&#xff0c;poll和epoll。在这一篇博客里先讨论select, poll 在将下一篇中介绍&#xff0c;epoll是Linux特有的高级解决方案&#xff0c;…

leetcode(283)移动零

283. 移动零 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 示例: 输入: [0,1,0,3,12] 输出: [1,3,12,0,0] 说明: 必须在原数组上操作&#xff0c;不能拷贝额外的数组。 尽量减少操作次数。 方法一&#xff1…

exec函数族实例解析

转载&#xff1a;http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程(子进程是父进程的副本&#xff0c;它将获得父进程数据空间、堆、栈等资源的副本。注意&#xff0c;子进程持有的是上述…

leetcode(167)两数之和 II - 输入有序数组

两数之和 II - 输入有序数组 给定一个已按照 升序排列 的整数数组 numbers &#xff0c;请你从数组中找出两个数满足相加之和等于目标数 target 。 函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 &#xff0c;所以答案数组应当满足 …

常量指针与指针常量的区别(转帖)

转载&#xff1a;http://www.cnblogs.com/witty/archive/2012/04/06/2435311.html 三个名词虽然非常绕嘴&#xff0c;不过说的非常准确。用中国话的语义分析就可以很方便地把三个概念区分开。 一) 常量指针。 常量是形容词&#xff0c;指针是名词&#xff0c;以指针为中心的一个…

c/c++错题总结

1.类名 对象名 默认调用“对象名()”这个构造函数&#xff0c;在栈内存中存在对象名&#xff0c;在堆内存中存在实际对象&#xff1b; 2.类名 对象名(一个或以上个参数) 默认调用相应的构造函数&#xff0c;在栈内存中存在对象名&#xff0c;在堆内存中也是存在实际对象的&a…

智能指针学习笔记

转载&#xff1a;http://www.cnblogs.com/wuchanming/p/4411878.html 1. 介绍 本文介绍智能指针的使用。智能指针是c 中管理资源的一种方式&#xff0c;用智能指针管理资源&#xff0c;不必担心资源泄露&#xff0c;将c 程序员 从指针和内存管理中解脱出来&#xff0c;再者&…

c++程序编译过程

c程序编译分成四个过程&#xff1a;编译预处理&#xff0c;编译&#xff0c;汇编&#xff0c;链接 编译预处理&#xff1a;处理以#为开头 编译&#xff1a;将.cpp文件翻译成.s汇编文件 汇编&#xff1a;将.s汇编文件翻译成机器指令.o文件 链接&#xff1a;汇编生产的目标文件.o…

仿函数(函数对象)

转载&#xff1a;http://www.cnblogs.com/wuchanming/p/4411867.html 本文乃作者学习《C标准程序库》的学习笔记&#xff0c;首先介绍了仿函数&#xff08;函数对象&#xff09;和函数适配器&#xff08;配接器&#xff09;的概念&#xff0c;然后列出STL中所有的仿函数&#x…

C++ template —— 动多态与静多态(六)

转载&#xff1a;http://www.cnblogs.com/yyxt/p/5157517.html 前面的几篇博文介绍了模板的基础知识&#xff0c;并且也深入的讲解了模板的特性。接下来的博文中&#xff0c;将会针对模板与设计进行相关的介绍。 ------------------------------------------------------------…

变量之间的区别

全局变量、局部变量、静态全局变量、静态局部变量的区别 c变量根据定义具有不同的生命周期&#xff0c;会有不同的作用域&#xff0c;主要有六个作用域&#xff1a;全局作用域&#xff0c;局部作用域&#xff0c;文件作用域&#xff0c;类作用域&#xff0c;语句作用域&#xf…

计算机的网络体系以及参考模型

计算机的网络体系以及参考模型一、OSI七层模型二、TCP/IP参考模型三、TCP/IP 五层参考模型四、OSI 模型和 TCP/IP 模型异同比较五、OSI 和 TCP/IP 协议之间的对应关系六、为什么 TCP/IP 去除了表示层和会话层&#xff1f;七、数据如何在各层之间传输&#xff08;数据的封装过程…

C++ 模板详解(二)

转载&#xff1a;http://www.cnblogs.com/gw811/archive/2012/10/25/2736224.html 四、类模板的默认模板类型形参 1、可以为类模板的类型形参提供默认值&#xff0c;但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。 2、类模板的类…