实验:C++中有继承关系类型间指针强制转换行为的正确性

目标

我在使用C++时经常有“将一个子类指针转换为基类指针,再在需要时转换为子类指针”的行为。大多时候没发现问题,以至于让我产生错觉“觉得只要确实是一个子类对象,那么不管其指针类型如何在基类与子类间转换,都不会出问题” 。而这一错觉让我在最近一个牵扯到多重继承的环境下,指针转换出现了问题。

因此我想专门对这一问题做些实验,本篇的目标是:

  • 实验在各种情况下指针强制转换行为是否能保证正确性。

在实验与思考的过程中,我从《C++多重继承下的指针类型转换 - 敲代码的小阿狸 - 博客园》中得到些提示。

实验1. 子类指针转换为基类

#include<iostream>
using namespace std;

//基类
class BaseA
{
public:
	const char* BaseA_data;
};

//子类
class ChildB : public BaseA
{
public:
	const char* ChildB_data;
};

int main()
{
	//ChildB类型的对象
	ChildB* ptB = new ChildB();

	//数据:
	ptB->BaseA_data = "Able";
	ptB->ChildB_data = "Bill";

	{
		cout << "实验1. 子类指针转换为基类:" << endl;

		//子类指针转换为基类
		BaseA* ptA = ptB;
		//调试打印数据
		cout << ptA->BaseA_data << endl;
	}
}

输出:

实验1. 子类指针转换为基类:
Able

可以看到没有问题。


另外注意,在将子类的指针转换为基类时,我是没有使用强制转换的:
在这里插入图片描述
现在通过了编译,而且没有发出warning。这说明:这一行为在理论上就是正确的。

实验2. 基类指针转换为子类

(接上例继续试验)

首先,基类指针转换为子类必须要使用强制转换,否则通过不了编译:
在这里插入图片描述
在使用强制转换后,编译能通过:

{
	cout << "实验2. 基类指针转换为子类:" << endl;

	//先将子类指针转换为基类
	BaseA* ptA = ptB;
	//在这里实验基类指针转换为子类(必须使用强制转换)
	ChildB* ptB2 = (ChildB*)ptA;
	//调试打印数据
	cout << ptB2->ChildB_data << endl;
}

可以输出正确的值:

实验2. 基类指针转换为子类:
Bill

然而,C++编译器并不能保证这一行为的正确性。
构造出一个能通过编译但实际有错误的情况也很容易:

下面是另一个继承自BaseA的子类:

//继承BaseA的第二个子类
class ChildC : public BaseA
{
public:
	int ChildC_data2;
	const char* ChildC;
};

创建一个ChildC类型的对象:

//ChildC类型的对象
ChildC* ptC = new ChildC();
//数据:
ptC->ChildC_data = "Car";

然后将一个ChildC类型的对象转换为基类指针,再将其转变为一个ChildB类型:

//将子类指针转换为基类
BaseA* ptA2 = ptC;
//在这里实验基类指针转换为子类,实际上这个指针所指向的对象并非是ChildB类型
ChildB* ptB3 = (ChildB*)ptA2;
//调试打印数据
cout << ptB3->ChildB_data << endl;

则会出现问题:
在这里插入图片描述


也就是说:
“基类指针转换为子类”这一行为在理论上可能出错,一般情况下也被编译器禁止,除非使用强制转换。当使用强制转换时虽然能通过编译,但其正确性得不到编译器的保证,而是由程序员自己去判断是否正确。

实验3. 多重继承下的指针转换

(接上例继续试验)

增加一个ChildB继承的基类BaseD

//ChildB继承的第二个基类
class BaseD
{
public:
	const char* BaseD_data;
};

//子类
class ChildB : public BaseA, public BaseD
{
public:
	const char* ChildB_data;
};

并为其数据赋值:

ptB->BaseD_data = "Door";

然后向刚才那样转换为基类,再转回子类:

{
	cout << "实验3. 多重继承下的指针转换:" << endl;

	//子类指针转换为两个基类
	BaseA* ptA = ptB;
	BaseD* ptD = ptB;
	//调试打印数据
	cout << ptA->BaseA_data << endl;
	cout << ptD->BaseD_data << endl;

	//两个基类的指针再转换回来:
	ChildB* ptB2 = (ChildB*)ptA;
	ChildB* ptB3 = (ChildB*)ptD;
	//调试打印数据
	cout << ptB2->ChildB_data << endl;
	cout << ptB3->ChildB_data << endl;
}

发现仍旧没问题:

实验3. 多重继承下的指针转换:
Able
Door
Bill
Bill

但是有一种行为会造成问题:

//由于ptA确实指向ChildB类型的数据,因此将其转换为BaseD类型在理想上“合乎逻辑”
BaseD* ptD2 = (BaseD*)ptA;
//调试打印数据
cout << ptD2->BaseD_data << "(预期应打印Door)" << endl;

“理想”上,虽然ptA的类型是BaseA,但其实际指向的对象是ChildB类型的,而ChildB继承自BaseD,所以我获得这个对象的BaseD类型的指针在“理想”上是没问题的。但实际这样做是错误的,上述程序将输出:

Able(预期应打印Door)

究其原因,和其内存排布有关,在《C++多重继承下的指针类型转换 - 敲代码的小阿狸 - 博客园》有所讨论。


但并不是说“当有一个BaseA类型的指针时,哪怕知道其实际指向的对象是ChildB类型,也不能获取BaseD的接口”。要想正确地转换,需要在之中多做一层转换:

//将其转换为ChildB,它一定正确,因为我知道ptA实际指向一个ChildB对象
ChildB* ptB4 = (ChildB*)ptA;
//再将其转换为BaseD,这在理论上就是正确的
BaseD* ptD2 = ptB;
//调试打印数据
cout << ptD2->BaseD_data << "(预期应打印Door)" << endl;

即:

  • 先将其转换为ChildB,它一定正确,因为我知道ptA实际指向一个ChildB对象。
  • 再将其转换为BaseD,这在理论上就是正确的

这样程序输出一个符合预期的结果:

Door(预期应打印Door)

总结

  1. 将子类指针转换为基类指针,一定是正确的,也不需要强制转换。
  2. 需要将基类指针转换为子类指针时,首先要有强制转换的语法,其次需要程序员从逻辑上保证在发生转换时,这个指针一定确实指向子类类型的对象,否则即便通过了编译程序也会出错。
  3. 两个没有继承关系的类型的指针不能转换(哪怕他们都被同一个子类所继承)。如果两个类确实都是同一个子类的基类,且在特定环境下能保证一个基类指针确实指向一个子类对象,则可以先将其强制转换为子类指针,然后再转换为另一个基类的指针。

(虽然做了这么多实验,但最后发现,实际上这些结论在知道继承关系下内存排布的方式后,都能从理论上推导出来)

完整代码

#include<iostream>
using namespace std;

//基类
class BaseA
{
public:
	const char* BaseA_data;
};

//ChildB继承的第二个基类
class BaseD
{
public:
	const char* BaseD_data;
};

//子类
class ChildB : public BaseA, public BaseD
{
public:
	const char* ChildB_data;
};

//继承BaseA的第二个子类
class ChildC : public BaseA
{
public:
	int ChildC_data2;
	const char* ChildC_data;
};

int main()
{
	//ChildB类型的对象
	ChildB* ptB = new ChildB();
	//数据:
	ptB->BaseA_data = "Able";
	ptB->ChildB_data = "Bill";
	ptB->BaseD_data = "Door";

	//ChildC类型的对象
	ChildC* ptC = new ChildC();
	//数据:
	ptC->ChildC_data = "Car";

	{
		cout << "实验1. 子类指针转换为基类:" << endl;

		//子类指针转换为基类
		BaseA* ptA = ptB;
		//调试打印数据
		cout << ptA->BaseA_data << endl;
	}

	{
		cout << "实验2. 基类指针转换为子类:" << endl;

		//先将子类指针转换为基类
		BaseA* ptA = ptB;
		//在这里实验基类指针转换为子类(必须使用强制转换)
		ChildB* ptB2 = (ChildB*)ptA;
		//调试打印数据
		cout << ptB2->ChildB_data << endl;

		/*
		//将子类指针转换为基类
		BaseA* ptA2 = ptC;
		//在这里实验基类指针转换为子类,实际上这个指针所指向的对象并非是ChildB类型
		ChildB* ptB3 = (ChildB*)ptA2;
		//调试打印数据
		cout << ptB3->ChildB_data << endl;
		*/
	}

	{
		cout << "实验3. 多重继承下的指针转换:" << endl;

		//子类指针转换为两个基类
		BaseA* ptA = ptB;
		BaseD* ptD = ptB;
		//调试打印数据
		cout << ptA->BaseA_data << endl;
		cout << ptD->BaseD_data << endl;

		//两个基类的指针再转换回来:
		ChildB* ptB2 = (ChildB*)ptA;
		ChildB* ptB3 = (ChildB*)ptD;
		//调试打印数据
		cout << ptB2->ChildB_data << endl;
		cout << ptB3->ChildB_data << endl;

		//会造成问题的行为:
		{
			//由于ptA确实指向ChildB类型的数据,因此将其转换为BaseD类型在理想上“合乎逻辑”
			BaseD* ptD2 = (BaseD*)ptA;
			//调试打印数据
			cout << ptD2->BaseD_data << "(预期应打印Door)" << endl;
		}
		//不会造成问题的行为:
		{
			//将其转换为ChildB,它一定正确,因为我知道ptA实际指向一个ChildB对象
			ChildB* ptB4 = (ChildB*)ptA;
			//再将其转换为BaseD,这在理论上就是正确的
			BaseD* ptD2 = ptB;
			//调试打印数据
			cout << ptD2->BaseD_data << "(预期应打印Door)" << endl;
		}
	}
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值