自学内容网 自学内容网

(多继承+模板回传子类)的结构所遇到的问题

(一)问题初现

(1)问题代码

在调试一个项目时,发生崩溃,通过调用堆栈看是访问成员变量时发生的,代码结构如下

class BaseA
{
public:
BaseA() { cout << "new BaseA " << endl; }
virtual ~BaseA() {};
int m_aa;
};

template< typename T>
class BaseB
{
public:
BaseB(){cout << "new BasBe " << endl;}
virtual ~BaseB(){}

void Test()
{
Func(this);
}
private:
void Func(void* pObj)
{
T* pThis = (T*)(pObj);
pThis->Run();
}
int m_bb;
};

class Drive:  public BaseA, public BaseB<Drive>
{
public:
Drive()
{
m_cc = 5;
cout << "new Drive " << endl;
}
virtual ~Drive(){ }

void Run(bool bl=true)
{
cout << "m_cc = " << m_cc << endl;
}

int m_cc;
};

(2)调用方法

Drive* pDrive = new Drive();
pDrive->Test();

(二)问题研究

(1)尝试解决

通过尝试,发现几种方法都可以使代码结果正确

方法1) 调换Drive的继承顺序

class Drive:  public BaseB<Drive>,public BaseA

方法2)  去掉BaseA的虚析构函数

//virtual ~BaseA() {};

方法3)直接调用Func的代码

void Test()
{
T* pThis = (T*)(this);
pThis->Run();
}

(2)问题探究

虽然上面能解决问题了,但是没有了解其本质,上面三种方法都是不同的门,
最后通向的是同一个房间,我应该找到那个房间,才是解决问题的本质。
推断问题原因与C++多态机制有关,于是通过vs工具查看了Drive实例的内存布局
在vs命令行的添加命令:/d1reportSingleClassLayoutDrive Test.cpp 
编译时可看到输出的Drive类型的内存结构信息:
class eLane::Drive size(20):
1> +---
1> 0 | +--- (base class BaseA)
1> 0 | | {vfptr}
1> 4 | | m_aa
1> | +---
1> 8 | +--- (base class BaseB<class Drive>)
1> 8 | | {vfptr}
1>12 | | m_bb
1> | +---
1>16 | m_cc
1> +---

这里有两个知识点:

1) 多态类对象的内存布局

如果基类有虚函数,派生类对象内存中,会将该基类虚表指针及成员数据放到头部
如果是多继承,顺序以派生类定义时的顺序为准
没有虚函数的基类不会放在头部

2)基类与派生类的指针转换

派生类内存地址转为基类地址时,编译器会做对应的偏移处理,使其指向正确的内存起始地址通过上面的结构信息可以看到,最前面是BaseA的虚表指针和成员变量m_aa
接着是BaseB<class Drive>的虚表指针和成员变量m_bb
最后是Drive的成员变量m_cc

(3)问题明了

Drive* pDrive = new Drive(),pDrive指向完整的Drive内存的首地址
pDrive->Test()中的Func(this),this实际是指向Drive内存中BaseB的起始地址,编译器做了偏移
通过转为Func的void*参数传入其内部后执行 T* pThis = (T*)(pObj)
由于pObj的类型不明确,编译器无法还原为派生类型地址,所以pThis指向的仍是BaseB的起始地
再执行pThis->Run(),调用Drive的成员函数是无法访问到正确成员变量,
其实问题本质就是在一个BaseB的内存实例上,调用了Drive的成员函数,导致错误的内存访问。

(4)不同方法的相同原理

1)调换Drive的继承顺序:class Drive:  public BaseB<Drive>,public BaseA
Drive内存中最前面放置的是BaseB类,这样BaseB的起始地址与Drive的起始地址相同
两者指针在转换时偏移为0,所以即使错用也不会出错

2)注释掉BaseA的虚析构函数://virtual ~BaseA() {};
Drive内存最前面原来是BaseA,当BaseA没有虚函数时,不需要虚表实现多态
BaseA的成员数据放到了Drive内存的后面,这样BaseB到了最前面,效果同 1)

3)Test()中直接调用Func的代码
T* pThis = (T*)(this),这里this的类型是明确的BaseB,编译器能够做正确偏移
使得pThis指向Drive的起始地址,可正常调用Run

(5)更多的方法

在Func中使用以下两种方法也是有效的
1)告诉编译器内存地址真实类型

void Func(void* pObj)
{
T* pThis = (T*)((Base* )pObj);
pThis->Run();
}


2)避免使用void*

void Func()
{
T* pThis = (T*)(this);
pThis->Run();
}


原文地址:https://blog.csdn.net/shellching/article/details/142878085

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!