目标
我在使用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)
总结
- 将子类指针转换为基类指针,一定是正确的,也不需要强制转换。
- 需要将基类指针转换为子类指针时,首先要有强制转换的语法,其次需要程序员从逻辑上保证在发生转换时,这个指针一定确实指向子类类型的对象,否则即便通过了编译程序也会出错。
- 两个没有继承关系的类型的指针不能转换(哪怕他们都被同一个子类所继承)。如果两个类确实都是同一个子类的基类,且在特定环境下能保证一个基类指针确实指向一个子类对象,则可以先将其强制转换为子类指针,然后再转换为另一个基类的指针。
(虽然做了这么多实验,但最后发现,实际上这些结论在知道继承关系下内存排布的方式后,都能从理论上推导出来)
完整代码
#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;
}
}
}