C++的学习心得和知识总结 第七章(完)

本章节的重点:多重继承

文章目录

  • 本章节的重点:多重继承
  • 第一节:虚基类、虚继承
  • 第二节:菱形继承
  • 第三节:C++的四种类型强转

第一节:虚基类、虚继承

多重继承
最直接的好处就是:代码的复用程度更高。一个派生类可以有多个基类
在这里插入图片描述
如上C类 直接可以把类AB的成员都继承过来,直接进行复用。
但是多重继承的缺点也很明显:菱形继承问题。

抽象类:拥有纯虚函数的类,不可以和虚基类的概念搞混。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
C++的学习心得和知识总结 第七章(完)_第1张图片
但是现在在 继承方式的限定符前面 加上virtual
C++的学习心得和知识总结 第七章(完)_第2张图片
此时的类A 被B 虚继承,则 A成了 虚基类

接下来 就是分析 多重继承与虚基类的关系,以及虚基类用来解决什么问题?
C++的学习心得和知识总结 第七章(完)_第3张图片
此时的类A 被虚继承了,成了虚基类 但是其定义的对象的内存布局还是 只有一个成员变量 4字节。
C++的学习心得和知识总结 第七章(完)_第4张图片
类B生成对象的内存布局如上:内存大小占了 12字节。最上面原来是ma的地方,现在放的是 vbptr ,可原来从类A继承来的成员变量ma到派生类的最下面了。因为从A虚继承来的 。 vbptr 指向的是vbtable(一个类对应一个虚基类表)。
vbtable(一个类对应一个虚基类表)也是在编译时期生成,在运行时候放在只读数据段。一个类型定义的多个对象的vbptr指向的同一个类型的vbtable,只是内容存的不太一样而已。
vbtable第一行是0 ,向上的偏移量。因为现在vbptr在派生类内存的最上面,所以向上的偏移量为0 。
第二行是8 表示vbptr 虚基类指针从类A继承来的成员变量ma:即虚基类的数据 的向下偏移量是8字节。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

看到类B 虚继承类A,在分析B的内存布局的时候 可以先如下做法:
(1)先不考虑 虚继承。普通继承派生类内存布局如下:
在这里插入图片描述
(2)类B在虚继承之后,基类A成了虚基类。虚基类A的数据要移动到派生类内存的最后面。然后在刚才的那个地方添加 vbptr 。如下:
C++的学习心得和知识总结 第七章(完)_第5张图片
C++的学习心得和知识总结 第七章(完)_第6张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
C++的学习心得和知识总结 第七章(完)_第7张图片
C++的学习心得和知识总结 第七章(完)_第8张图片
当一个类里面有虚函数,则这个类生成的对象 内存里面有vfptr,指向的是vftable(里面有RTTI指针:运行时类型信息和虚函数地址)

派生类从基类虚继承来了之后,则这个派生类生成的对象 内存里面有vbptr,指向的是vbtable(第二行 放的是vbptr到 到 虚基类数据内存的向下偏移量)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
当vbptr和vfptr 一起出现的时候,并不影响多态的使用。
C++的学习心得和知识总结 第七章(完)_第9张图片
此时基类指针指向派生类对象,A*p 调用show方法,发现是虚函数。同名覆盖重写方法,发生动态绑定。最终访问的是B的vfptr,进而访问B::show().

运行之后 发生了错误:
C++的学习心得和知识总结 第七章(完)_第10张图片
不影响调用B::show()。但是堆操作有问题(delete时候出错了)。
vfptr vbptr vftable vbtable在一起结合的时候,出了问题。
原因分析:
分析B的内存布局如下:
在这里插入图片描述
但是现在A被虚继承了。虚基类里面所有的东西都要移动到派生类的 最后面去。在原来的地方,补上一个vbptr(指向了vbtable)。虚基类里面所有的东西里面的vfptr指向了vftable 。
但是在new完B对象 的时候,返回地址给p。他返回的是vfptr(基类指针指向派生类对象,永远指向的是派生类对象的基类部分数据的起始地址)普通情况下,派生类的内存布局就是先是基类再是派生类,基类指针指向派生类对象的时候,基类指针指向的就是派生类对象内存的起始地址。

但是虚继承,基类成了虚基类 ,虚基类里面所有的东西都要移动到派生类的 最后面去。在原来的地方,补上一个vbptr(指向了vbtable)。现在再用基类指针指向派生类对象,基类指针永远指向的是派生类对象的基类部分数据的起始地址。如下:
C++的学习心得和知识总结 第七章(完)_第11张图片
这样虚基类一开始,就是vfptr 这也就是p->show(); 可以用p指向的对象访问vfptr,进而访问vftable 最终可以调用派生类的覆盖重写函数的原因。但是释放内存的时候,应该从对象的起始地址开始,但是delete p; p指向的是派生类对象的基类部分数据(虚基类)的起始地址。
C++的学习心得和知识总结 第七章(完)_第12张图片
如果用基类指针指向栈上或者全局的对象,在出作用域的时候,内存会自动进行回收。
C++的学习心得和知识总结 第七章(完)_第13张图片
如上:开辟内存的地址 和 最后释放内存的地址不一样。
开辟内存的地址是最上面的那个,但是给用户返回的地址是(基类指针指向派生类对象)指向派生类对象的基类部分数据(虚基类)的起始地址(p)。两者相差的是8字节,是(vbptr mb)两个。
C++的学习心得和知识总结 第七章(完)_第14张图片
基类指针指向堆上的对象,堆上的对象需要delete手动释放。这样就会出错,在delete的时候,应该人为的 释放正确的地址。
C++的学习心得和知识总结 第七章(完)_第15张图片
基类指针指向栈上的对象 都是对的,出了作用域 自己会释放。
C++的学习心得和知识总结 第七章(完)_第16张图片
为什么派生类B的内存布局 不是左上角那一部分?(vfptr是派生类的,怎么画在A::下)?

答:这种情况是 基类没有虚函数,而派生类自己有虚函数。则vfptr是派生类的,即:B::。但是 现在是基类有虚函数,则vfptr相当于是从基类里面继承来的。基类里面已经有一个虚函数指针了,就没必要自己再写一个。vfptr最终可以指向派生类的虚函数表就行了。如下图所示: 派生类的虚函数表里面放的就是 如下:。如果派生类的虚函数是对基类的覆盖重写,则是要覆盖掉 从基类继承来的虚函数的地址。
C++的学习心得和知识总结 第七章(完)_第17张图片
C++的学习心得和知识总结 第七章(完)_第18张图片
vbtable里面的第二行 放的是vbptr到 到 虚基类数据内存的向下偏移量。是8
vftable里面的第一行 就是vfptr到派生类对象内存起始的偏移量是-8,第二行是 B::func()。

第二节:菱形继承

但是多重继承的缺点也很明显:菱形继承问题。

A 是D的间接基类,B C是D的直接基类。
问题在于:D有间接基类的多份数据(这种数据属性相同,这在软件设计上 是不行的)。
C++的学习心得和知识总结 第七章(完)_第19张图片
D的内存布局如下:

C++的学习心得和知识总结 第七章(完)_第20张图片
D可以看见 B、C、md。所以需要在其构造函数里面 负责调用一下B C的构造函数,以及md的初始化。ma的初始化并不是D的,相当于是从 B C继承来的。如上图所示:ma的初始化也将是在B C的构造函数里面进行的。D的内存是4*5=20字节
C++的学习心得和知识总结 第七章(完)_第21张图片
在D的这个派生类里面,调用了两次A的构造。内存占用上存储了重复意义相同的属性,且这还是软件设计上应该杜绝的问题。

怎么解决呢?虚继承
所有从A继承的地方,都采用virtual 继承。 A就成了虚基类。注意是所有 不然的话,没有解决问题。

首先B从A虚继承而来,A就成了虚基类。把虚基类移动到派生类的最后面,并在原处补上一个vbptr。同理:C从A虚继承而来,A就成了虚基类。把虚基类移动到派生类的最后面,但是发现最后面已经有一份虚基类的数据了,于是就舍弃这一份数据(保留一份即可),并在原处补上一个vbptr。
C++的学习心得和知识总结 第七章(完)_第22张图片
现在派生类里面只有一份 基类ma的数据。虚继承就是解决多重继承(尤其是菱形继承或者半圆形继承)派生类D有多份间接基类的属性。
C++的学习心得和知识总结 第七章(完)_第23张图片
每次访问ma的时候,依旧在两个 “原处” 访问。但是被移动,由偏移量进行访问。 无论是B:: 还是C:: 下的ma都是同一份数据。

但是此时A::ma ,在D中是可以看到的。A的初始化不再需要B C进行,而是由D进行初始化。
否则的话,会出现以下问题:
C++的学习心得和知识总结 第七章(完)_第24张图片
现在的虚基类数据是需要D进行负责初始化的。不属于B C,而且由于D中未指定ma的初始化方式,所以报错:没有合适的默认构造函数。
手动添加A的构造方式,在派生类D的构造函数当中,给间接基类A指定合适的构造方式。
C++的学习心得和知识总结 第七章(完)_第25张图片
C++的学习心得和知识总结 第七章(完)_第26张图片
C++的学习心得和知识总结 第七章(完)_第27张图片
就算是在参数列表里面修改了 类的构造先后出现顺序,但实际上是没有影响的。最后构造上,依旧是先A B C D。(哪怕是B C交换顺序 也是如此)

第三节:C++的四种类型强转

在C中,提供类型强转的方式:
在这里插入图片描述
把右边的类型强转成左边的类型。
C++的学习心得和知识总结 第七章(完)_第28张图片
第二种是比C语言的更安全一些。第三种谈不上安全,如果非要在C++里面做编译器认为不安全的类型强转的时候,static_cast通过不了可以考虑第三种。
C++的学习心得和知识总结 第七章(完)_第29张图片
const_cast 和 普通的C的类型强转在指令上 是一样的。
但是这两个在编译阶段有所不同:
C++的学习心得和知识总结 第七章(完)_第30张图片
只能做到const int* ——>int是可以的。(防止不安全,C的那种不安全)
C++的学习心得和知识总结 第七章(完)_第31张图片
不可能做到 把一个常量值本身放在这里。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
static_cast 用的最多。
C++的学习心得和知识总结 第七章(完)_第32张图片
有关联的 类型强转 是可以的(如上图)
C++的学习心得和知识总结 第七章(完)_第33张图片
short * 和 int * 之间是没什么联系的。double
和 int * 之间也是没什么联系的。
C++的学习心得和知识总结 第七章(完)_第34张图片
在这里插入图片描述
基类类型和派生类类型 是可以的。因为结构上属于从上到下的继承结构,类型是有关系的。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
C++的学习心得和知识总结 第七章(完)_第35张图片
指针一解引用,8个字节 但是实质上是(int* 只是个int四个字节有效)。这个 是类似于C语言的那种不安全的那种。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
第四种:
动态类型转换:
C++的学习心得和知识总结 第七章(完)_第36张图片

int main()
{
	Derive1 d1;
	Derive2 d2;
	showFunc(&d1);
	showFunc(&d2);
}

在这里插入图片描述
此时 这两处分别调用了Derive1 Derive2的func()。虽然是基类指针,但是是动态绑定。访问了它所访问对象的vfptr,进而访问了相应类型的虚函数表。一切OK。

但是随之项目需求增加:
C++的学习心得和知识总结 第七章(完)_第37张图片
C++的学习心得和知识总结 第七章(完)_第38张图片
当指针p指向的是 Derive2对象的时候,调用其另一个方法。而不再是其func()方法。不是指向的是 Derive2对象的时候,都是通过动态绑定,调用各自的覆盖重写func()方法。

这里需要识别一下 p的类型,看指针p指向的是不是Derive2对象。
C++的学习心得和知识总结 第七章(完)_第39张图片
如上图:p的类型是Base
但是Base里面有虚函数 这里识别的是运行时的类型。指针指向哪个对象,进而访问其虚函数表 取的是RTTI类型。 但是实际上这么low的字符串比较的方式来完成RTTI类型的转化。

Dynamic_cast方式。(可以看做是运行时期的类型强转,支持RTTI类型识别)
C++的学习心得和知识总结 第七章(完)_第40张图片
p类型的指针,转化成Derive2 *类型。
如下:
C++的学习心得和知识总结 第七章(完)_第41张图片
运行结果如下:
C++的学习心得和知识总结 第七章(完)_第42张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
C++的学习心得和知识总结 第七章(完)_第43张图片
static_cast (可以看做是编译时期的类型强转) 也可以强转成功,毕竟是有联系的(继承)。但是只有成功一种情况。无法识别 对象是不是Derive2对象。

#include 
#include 
using namespace std;
class Base
{
public:
	virtual void func() = 0;
};
class Derive1 : public Base
{
public:
	void func() { cout << "call Derive1::func" << endl; }
};
class Derive2 : public Base
{
public:
	void func() { cout << "call Derive2::func" << endl; }
	// Derive2实现新功能的API接口函数
	void derive02func()
	{
		cout << "call Derive2::derive02func" << endl;
	}
};
/*
typeid(*p).name() == "Derive"
*/
void showFunc(Base* p)
{
	// dynamic_cast会检查p指针是否指向的是一个Derive2类型的对象?
	// p->vfptr->vftable RTTI信息 如果是,dynamic_cast转换类型成功,
	// 返回Derive2对象的地址,给pd2;否则返回nullptr
	// static_cast编译时期的类型转换  dynamic_cast运行时期的类型转换 支持RTTI信息识别
	Derive2* pd2 = dynamic_cast<Derive2*>(p);
	if (pd2 != nullptr)
	{
		pd2->derive02func();
	}
	else
	{
		p->func(); // 动态绑定  *p的类型 Derive2  derive02func
	}
}
int main()
{
	Derive1 d1;
	Derive2 d2;
	showFunc(&d1);
	showFunc(&d2);

	//static_cast 基类类型 《=》 派生类类型  能不能用static_cast?当然可以!
	//int *p = nullptr;
	//double* b = reinterpret_cast(p);

	//const int a = 10;
	//int *p1 = (int*)&a;
	//int *p2 = const_cast(&a);
	// const_cast<这里面必须是指针或者引用类型 int* int&>
	//int b = const_cast(a);

	return 0;
}

你可能感兴趣的:(C++的学习心得和知识总结)