一、前言
在日常工作中踩了一个坑,现象是:从一个dll里获取导出类A,然后调用类A的FunA,结果FunA没有被调用,相关代码简化如下:
class AImpl : public OtherClass, public A
{
// OhterClass 不包含类A的FunA
...
// 用户通过该函数获取A的指针,结果用A的指针调用FunA失败
void Query(void ** ptr)
{
*ptr = this;
}
}
最后定位到的原因是:多重继承下,直接赋值指针是没有进行类型转换的,其仅仅只是单纯赋值,这导致this指针没有类型转换为A类,所以A类的FunA不会被调用
踩到这个坑说明我对C++的多重继承下的类型转换完全不了解,所以就搞个文章梳理下~
二、情景模拟复现
原工程项目比较庞大,不利于学习研究,所以把它抽成下面这个demo:
class ChildA
{
public:
ChildA(){}
virtual ~ChildA(){}
virtual void FunA() = 0;
virtual void Fun() {printf("childa fun\n");}
};
class ChildB
{
public:
ChildB(){}
virtual~ChildB() {}
virtual void FunB(){};
virtual void Fun() {printf("childb fun\n");}
};
class Child : public ChildA, public ChildB
{
public:
Child(){}
virtual ~Child(){}
virtual void FunA(){printf("funa\n");}
virtual void FunB(){printf("funb\n");}
virtual void Fun() {printf("child fun\n");}
void Query(void ** ptr)
{
*ptr = this;
}
void QueryB(void ** ptr)
{
*ptr = dynamic_cast<ChildB *>(this);
}
};
class ChildBase : public ChildB
{
public:
virtual void FunB(){printf("ChildBase funb\n");}
virtual void Fun() {printf("ChildBase fun\n");}
};
int _tmain(int argc, _TCHAR* argv[])
{
Child A;
// 情况1:直接将指针赋值
printf("情况1:\n");
ChildB * ptr = NULL;
A.Query((void**)&ptr);
ptr->FunB();
// 情况2:获取直接赋值的指针后,再调用dynamic_cast进行类型转换
printf("\n情况2:\n");
ChildB * ptra = dynamic_cast<ChildB *>(ptr);
ptra->FunB();
// 情况3:获取类型转换后的指针
printf("\n情况3:\n");
ChildB * ptrb = NULL;
A.QueryB((void**)&ptrb);
ptrb->FunB();
// 情况4:多态
printf("\n情况4:\n");
ChildB * ptrDrive = new ChildBase();
ptrDrive->Fun();
ptr->Fun();
ptrb->Fun();
printf("\nA ptr is 08x%08x, \nptr is 08x%08x, \nptrb is 08x%08x, \nptrDrive is 08x%08x\n", &A, ptr, ptrb, ptrDrive);
return 0;
}
看到这里,不妨可以先思考下这几种情况下输出结果是啥~
三、开始分析
关于多重继承下的类型转换,先瞅瞅这个相关知识:
因为C++编译器没有足够的知识来把Child*类型转换为ChildB*类型,只能按照传统的C指针强制转换处理,也就是指针位置不变。(此处可通过打印指针的值验证)
详情可见:https://blog.csdn.net/smstong/article/details/24455371
接下来就对每个情况进行分析:
1. 情况1
该情况是将子类指针直接赋值给父类指针,即Child*类型赋值给ChildB*类型。该情况下就是强制类型转换,其结果为ptr指向的内存表位置和A的位置一样,类似下图情况:
在编译器看来,ptr->FunB();就是ptr指针+4的结果,所以结合上图,最后展示的结果就是FunA被调用了。
ps:这时可能有小伙伴会提问,如果ptr指针+4地方没有函数咋整?我理解这就是不可知行为了,相当于访问一个不是你程序分配的内存。所以我们编码时要避免出现情况1。
2. 情况2
该情况和情况1一样,但是这里多了一个步骤:在获取指针后再调用dynamic_cast进行类型转换。但实际上这种情况的指针ptra指向内存位置依然和情况1一致,所以最后也是FunA被调用。没找到相应解释,欢迎大家在评论区提出你的见解,帮忙解惑~
3. 情况3
该情况是在Child类里面进行了动态转换(dynamic_cast),所以通过QueryB方法获取的指针类型就是ChildB类型,所以该指针指向的内存表位置是ChildB类的位置,所以FunB被调用。
以上三种情况是多重继承下类型转换的情况,情况4是探讨多态是否受多重继承影响的问题
4. 情况4
该情况是比较以上3个情况的多态结果。首先要搞明白啥是多态:
多态是一种晚绑定,当父子类有相同的函数名时(该函数被virtual修饰&&父子类之间是通过指针或引用赋值),运行时期才知道调用的父类还是子类的函数。一般都是调用对象类型的函数,即
父类* Father = new 子类; --》调用的是子类方法
父类 & Father = 子类; --》调用的是子类的方法
注:不能子类指针指向父类,例如这样:子类* son = new 父类;编译会出错,除非用类型转换,但也不推荐,认真思考下就晓得啦~
ptrDrive是没有多重继承情况的多态,它明显的就是调用了ChildBase方法而不是ChildB方法。
ptr其实指向的对象是Child类型,所以调用的是Child方法。
ptrb和ptr一样,指向的对象也是Child类型,所以调用的是Child方法。
PS:个人觉得一篇博客讲的关于多态的某个总结挺不错的,摘抄于此:
程序一般运行时,找到类,如果它有基类,再找到它的基类,最后运行的是基类中的函数,这时,它在基类中找到的是virtual标识的函数,它就会再回到子类中找同名函数,派生类也叫子类,基类也叫父类,这就是虚函数的产生,和类的多态性的体现。
详情可见:https://www.cnblogs.com/alinh/p/9636352.html
四、运行结果
经过上一部分的分析,再看这个结果,应该很好理解了。
五、总结
多重继承情况下不要强制类型转换,否则结果奇奇怪怪。
为了能得到预期结果,记得用动态转换dynamic_cast