先看一个代码示例:
#include <iostream>
#include <string>
class Nlp {
public:
virtual void DoNlp() {
std::cout << "DoNlp\n";
}
virtual void DoTermNlp() {
std::cout << "DoTermNlp\n";
}
};
class BaseNlp {
public:
virtual void DoNlpTask() = 0;
};
class DerivedNlp: public Nlp, public BaseNlp {
public:
void DoNlpTask() override {
std::cout << "DoNlpTask\n";
}
};
int main() {
void* ptr = new DerivedNlp();
BaseNlp* base_ptr = (BaseNlp*)(ptr);
base_ptr->DoNlpTask();
return 0;
}
DeriveNlp先继承Nlp,然后继承BaseNlp。我们在main函数中,先用void*存储DerivedNlp的地址,然后强制类型转换到BaseNlp类型。从逻辑上看,父类指针可以兼容子类指针,并且也是可以正常执行的,但是该代码却是如下输出:
DoNlp
这和我们的认知不符合。
但是,如果把main函数改成如下的形式:
int main() {
DerivedNlp* ptr = new DerivedNlp();
BaseNlp* base_ptr = (BaseNlp*)(ptr);
base_ptr->DoNlpTask();
return 0;
}
会正常输出
DoNlpTask
为了解释这个现象,我们需要理解C++继承的虚表机制,具体可以参看这篇文章:zhuanlan.zhihu.com/p/75172640
总结如下:
C++的每个class会把virtual函数放到一个虚表中,继承父class的子类,会把父类的虚表按照继承顺序排列。对于上文提到的正常输出的情况,C++在编译期间知道具体的类型转换情况,因此会正常处理虚表的偏移位置等,所以输出也是正常的。而void*的情况下,编译器只能强制使用BaseNlp的方式解释虚表,这会直接映射到Nlp的虚表上,导致输出差异。
因此,多重继承的情况,坚决避免使用void*强制转换,这会带来不确定性,而且可能会导致coredump。
上面第二种正确的情况也不建议这么使用,正确的方式应该是利用dynamic_cast进行类型安全的强制转换。
但是,作者最近却遇到了必须使用void*的情况,这种情况下,我们能做的是把多继承改成单继承,这么做虽然逻辑理解上不太合理,但是能解决上述的问题。
更改后的代码:
#include <iostream>
#include <string>
class Nlp {
public:
void DoNlp() {}
void DoTermNlp() {}
};
class BaseNlp: private Nlp {
public:
virtual void DoNlpTask() = 0;
};
class DerivedNlp: public BaseNlp {
public:
void DoNlpTask() override {
std::cout << "DoNlpTask\n";
}
};
int main() {
void* ptr = new DerivedNlp();
BaseNlp* base_ptr = (BaseNlp*)(ptr);
base_ptr->DoNlpTask();
return 0;
}