C++继承与多态—多重继承的那些坑该怎么填


课程总目录



一、虚基类和虚继承

虚基类:被虚继承的类,就称为虚基类

virtual作用:

  1. virtual修饰成员方法是虚函数
  2. 可以修饰继承方式,是虚继承,被虚继承的类就称为虚基类

注意与抽象类(有纯虚函数的类)区分开来

来看这段代码:

class A
{
public:
private:
	int ma;
};

class B : public A
{
public:
private:
	int mb;
};
//A a; 4个字节
//B b; 8个字节

使用指令cl xxx.cpp -d1reportSingleClassLayoutAcl xxx.cpp -d1reportSingleClassLayoutB看一下

在这里插入图片描述 在这里插入图片描述
如果采用虚继承

class B : virtual public A

再来看一下,B从8字节变为了12字节了

在这里插入图片描述

分析:当我们遇到虚继承时候,要考虑派生类B的内存布局时,首先我们先不考虑虚继承。类B继承了基类A的ma,还有自己的mb;当我们基类A被虚继承后,基类A变为虚基类,虚基类的数据一定要被挪到派生类数据的最后面,再在最前面添加一个vbptr

在这里插入图片描述
来看一些例题

class A {};
sizeof(A)=1 //空类大小是1

class B : public A {};
sizeof(B) = 1
class A
{
	virtual void fun() {}
};
sizeof(A)=1

class B : public A {};
sizeof(B) = 4 //B的内存里有vfptr
class A
{
	virtual void fun() {}
};
sizeof(A)=1

class B : virtual public A {};
sizeof(B) = 8 //B的内存里有vfptr和vbptr

总结:

  • vfptr:一个类有虚函数,这个类生成的对象就有vfptr,指向vftable
  • vbptr:派生类中虚继承基类,会有vbptr
  • vftable:存放RTTI指针(指向运行时RTTI信息)、虚函数地址。
  • vbtable:第一行为向上偏移量,第二行为vbptr离虚基类数据在派生类内存中的偏移量。

接下来再来看,当虚基类指针与虚函数指针在一起出现的时候会发生什么呢?

class A
{
public:
	virtual void func() { cout << "call A::func" << endl; }
private:
	int ma;
};

class B : virtual public A
{
public:
	void func() { cout << "call B::func()" << endl; }
private:
	int mb;
};

int main()
{
	// 基类指针指向派生类对象,永远指向的是派生类中基类部分数据的起始地址
	A* p = new B();
	p->func();
	delete p;
	return 0;
}

在这里插入图片描述
可以看到,调用是没有被影响到的,但是delete会出错

分析
B的内存布局:B首先从A中获取vfptrma,B中还有自己的mb

此时A被虚继承,从A中继承来的所有的东西都移动到派生类的最后面,然后在最前面补一个vbptrvbptr指向vbtablevfptr指向vftable
在这里插入图片描述

基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址

普通情况下,派生类内存布局先是基类数据,再是派生类自己的数据,基类指针指向派生类对象时,基类指针指向的就是派生类内存的起始部分。

但是在虚继承下,基类为虚基类,虚基类的数据被挪到派生类最后面,最前面补上vbptr,此时再用基类指针指向派生类对象时候,基类指针还是指向派生类基类部分数据的起始地址,也即指向vfptr,这也是能正常调用p->func();的原因

那么在释放内存的时候呢?现在p指向的是vfptr,从vfptr开始释放内存,,而对象内存现在是从vbptr开始,这就出错了

验证一下:

class A
{
public:
	virtual void func() { cout << "call A::func" << endl; }
	void operator delete(void* p)
	{
		cout << "operator delete p:" << p << endl;
		free(p);
	}
private:
	int ma;
};

class B : virtual public A
{
public:
	void func() { cout << "call B::func()" << endl; }
	void* operator new(size_t size)
	{
		void* p = malloc(size);
		cout << "operator new p:" << p << endl;
		return p;
	}
private:
	int mb;
};

int main()
{
	// 基类指针指向派生类对象,永远指向的是派生类中基类部分数据的起始地址。
	A* p = new B();
	cout << "main p:" << p << endl;
	p->func();
	delete p;
	return 0;
}
operator new p:00D316A0
main p:00D316A8
call B::func()
operator delete p:00D316A8

可以看到,从A0开始new的,返回给p的是A8delete的时候也是A8,也就是从vfptr开始释放的,这是不对的

但是,这段代码也能说是错的,这和编译器有关,在Windowsvs中,是从vfptr开始释放的,但是在linuxg++下,会自动偏移到new出来的内存的起始部分来进行释放

如果在栈上开辟内存,基类指针指向派生类对象,出了作用域自己进行析构,不涉及内存的释放,这样是没有问题的,正常运行不会报错

B b;
A *p = &b;
cout << "main p:" << p << endl;
p->func();

运行结果:

main p:010FFE04
call B::func()

使用命令cl xxx.cpp -d1reportSingleClassLayoutB查看一下
在这里插入图片描述

再来看,这时有人会问了,派生类为啥不像下面这样画呢?

在这里插入图片描述

如果是这样画,也就是vfptr属于B的作用域,这是不对的,因为A中有虚函数,vfptr是从A中继承而来的

如果真的这样画的话,那就是基类中没有虚函数,从派生类中才有的虚函数

二、菱形继承的问题

多重继承:可以复用多个基类的代码到派生类中

但是多重继承中也会出现问题:菱形继承、半圆形继承等

在这里插入图片描述
这些都会导致派生类有多份间接基类的数据,此时可以采用虚继承来解决

菱形继承代码:

class A
{
public:
	A(int data) : ma(data) { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
protected:
	int ma;
};
//==========================================================
class B : public A
{
public:
	B(int data) : A(data), mb(data) { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
protected:
	int mb;
};

class C : public A
{
public:
	C(int data) : A(data), mc(data) { cout << "C()" << endl; }
	~C() { cout << "~C()" << endl; }
protected:
	int mc;
};
//==========================================================
class D : public B, public C
{
public:
	D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
	~D() { cout << "~D()" << endl; }
protected:
	int md;
};

int main()
{
	D d(10);
	return 0;
}

运行结果:

A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

来看一下D的内存布局
在这里插入图片描述

用指令cl xxx.cpp -d1reportSingleClassLayoutD看看

在这里插入图片描述
可以看到调用了两次A的构造,同时数据重复了

怎么解决呢?虚继承

class A { ... };
//==========================================================
class B : virtual public A { ... };
class C : virtual public A { ... };
//==========================================================
class D : public B, public C { ... };

此时内存布局变了,解决了多份数据的问题
在这里插入图片描述

用指令cl xxx.cpp -d1reportSingleClassLayoutD看看

在这里插入图片描述
但是注意,此时编译会报错,因为现在A::ma靠在了D的作用域上面,我们要在D里面给A初始化

class D : public B, public C
{
public:
	D(int data) : A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
	~D() { cout << "~D()" << endl; }
protected:
	int md;
};

再运行看一看结果:

A()
B()
C()
D()
~D()
~C()
~B()
~A()

多重继承的好处:可以做更多代码的复用,比如上面的例子,D继承自B和C,那么就可以B* p = new D();C* p = new D();,有两个基类,两个基类指针都可以指向派生类对象

三、C++语言级别提供的四种类型转换方式

1.常量类型转换 const_cast

去掉(指针或引用)常量属性的一个类型转换,const_cast<>里面必须是指针或引用类型

const int a = 10;
int* p1 = (int*)&a;	// C中类型转换
int* p2 = const_cast<int*>(&a);	// C++中类型转换const_cast

反汇编看看

在这里插入图片描述

可以看到,转换为相同类型的时候,C中的类型强转与C++中const_cast所生成的汇编指令底层是一模一样的

但是注意,在转换成汇编指令之前(编译阶段),他俩有所不同

const int a = 10;
char* p1 = (char*)&a;// C,可以
char* p2 = const_cast<char*>(&a);// C++,不可以,报错:无法从 const int* 转换为 char*

const int a = 10;
double* p1 = (double*)&a;// C,可以
double* p2 = const_cast<double*>(&a);// C++,不可以,报错:无法从 const int* 转换为 double* 

C++中的const_cast比C中的强转安全,如果真的用double指针(8字节)指向了int类型的数据(4字节),那么内存访问就会越界

同时要注意,const_cast<>里面必须是指针或引用类型,否则出错,const int a = 10; int b = const_cast<int>(a);这样就是不对的,会报错无法从 const int 转换为 int

2.静态类型转换 static_cast

编译时期的类型转换,提供编译器认为安全的(一般两个类型有关联,比如基类派生类)类型转换,没有任何联系的类型之间的转换就会被否定,一般这个用的最多

int a = 65;
char b = static_cast<int>(a);

int *p = nullptr;
//double* b = (double*)p; // C可以转换
double* b = static_cast<double*>(p); // 不行,没有任何联系的类型之间的转换会被否定

这就又遇到我们刚才的问题了,如果真转换成功了,解引用会造成访问越界

基类类型与派生类类型进行转换,可以用static_cast,它们类型之间有关系,static_cast只能保证能转换,但不一定安全,应该由我们开发者来保证代码安全

3.重新解释类型转换 reinterpret_cast

类似于C风格的强制类型转换,是C++里的强制类型转换符,不安全,这里就不介绍了,C中的强转怎么用,替换成reinterpret_cast强转就行了

4.动态类型转换 dynamic_cast

运行时期的类型转换,主要用在继承结构中,可以支持RTTI类型识别的上下转换

将一个基类对象指针或引用,转换到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理

示例:

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

void showFunc(Base* p)
{
	p->func();//动态绑定
}

int main()
{
	Derive1 d1;
	Derive2 d2;
	showFunc(&d1);	// call Derive1::func
	showFunc(&d2);	// call Derive2::func
	return 0;
}

现在程序运行,那我们如果增加需求了呢?比如Derive2实现了一个新功能的API接口函数

class Derive2 : public Base
{
public:
	void func() { cout << "call Derive2::func" << endl; }
	void new_func() { cout << "call Derive2::new_func" << endl; }
};

我们现在还是想用showFunc来进行不同类的访问,怎么做呢?

void showFunc(Base* p)
{
	// dynamic会检查p指针是否指向的是一个Derive2类型的对象
	// p->vfptr->vftable 找到RTTI信息,如果是Derive2类型
	// 那么转换类型成功,返回Derive2对象地址给pd2;否则,返回nullptr
	Derive2* pd2 = dynamic_cast<Derive2*>(p);
	if (pd2 != nullptr)
		pd2->new_func();
	else
		p->func();//动态绑定
}

此时的运行结果:

call Derive1::func
call Derive2::new_func
  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GeniusAng丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值