以前知道,在一个模块中 new 一块内存,然后在其它模块中释放,会导致异常。 但这次出现的问题比较古怪,刚开始根本没想到是内存的原因。
查找原因比较曲折,但后来用虚函数的方法解决了问题,没有修改代码逻辑,感觉还是比较巧妙的^_^
执行环境简单介绍如下:
一个静态库工程
MyShare.lib ,关键代码如下:
// CMyObject 类部分代码
class CMyObject
{
public:
CMyClient *m_pParent;
void FinalObject();
};
void CMyObject::FinalObject()
{
m_pParent->UnBand(this);
}
// CMyClient 类部分代码
class CMyClient
{
std::map<CMyObject *,int> m_mapObjects;
public:
void Band(CMyObject *pObject);
void UnBand(CMyObject *pObject);
};
void CMyClient::Band(CMyObject *pObject)
{
m_mapObjects[pObject] = 1;
}
void CMyClient::UnBand(CMyObject *pObject)
{
m_mapObjects.erase(pObject); // 这里出现异常!
}
一个引用了 MyShare.lib 的 dll 工程:
MyCommon.dll,关键代码如下
// 一个从 CMyObject 派生的类
class CMyCommonObject : public CMyObject
{
};
// 创建一个 CMyCommonObject
CMyObject * CreateObject()
{
return static_cast<CMyObject *>(new CMyCommonObject);
}
// 释放对象
void ReleaseObject(CMyObject * pObject)
{
pObject->FinalObject();
delete pObject;
}
一个引用了以上两个模块 exe工程:
MyTest.exe,关键代码如下:
int main(int argc, TCHAR* argv[])
{
CMyClient client;
// 调用 MyCommon.dll 中的代码创建一个对象
CMyObject * pObject = CreateObject();
// 初始化对象
client.Band(pObject);
pObject->m_pParent = &client;
// 使用完毕,调用 MyCommon.dll 中的代码释放对象
ReleaseObject(pObject);
}
以上代码,在运行时,会在 m_mapObjects.erase(pObject); 处出现异常;如果单纯看类的每个函数,很难看出问题,另外,工程本身比较复杂,所以一直没有怀疑是因为不同模块之间分配和释放内存导致的问题。
值得注意的是,这里的 MyTest.exe 和 MyCommon.dll 中包含了同一个静态库,也就是说,他们之中都有 CMyObject 和 CMyClient 的二进制代码!很容易向导的是,问题应该出在 Band 和 UnBand 。毫无疑问,以上代码中的 client.Band 执行的是
MyTest.exe 中的代码。那么,m_pParent->UnBand 执行的是哪里的代码呢?之前我想当然的以为,既然 m_pParent 指针都是从 MyTest.exe 中传递来的,那肯定是执行的 MyTest.exe 中的代码。后来在VC中调试时偶然发现,执行UnBand 的代码在 MyCommon.dll 中,才突然想到,调用类的成员函数不就相当于普通函数加一个 this 参数吗?而普通函数编译时就确定了地址,那肯定是指向自己模块中的二进制代码了。 那么,m_pParent->UnBand 肯定执行的 MyCommon.dll 中的代码!这样问题就真相大白了:在 MyTest.exe 中向 map 加入元素,而在 MyCommon.dll 中释放,肯定会出错!因为加入或删除元素极可能造成堆内存分配的变化!
现在问题找到了,怎么解决呢?如果修改代码逻辑,则会造成其它关联代码的修改,想起来都有些头痛。问题主要是函数地址,什么函数是延迟绑定地址的呢?突然想到了虚函数!从 C++ 机制我们知道,调用虚函数其实是调用虚函数表中的函数指针,而虚函数表的内容是对象分配的时候填写的!那么,这样就能保证,无论在哪里调用虚函数,都是调用分配该对象的模块中的代码!
马上将 CMyClient 中的 Band 和 UnBand 改成虚函数,再试,问题果然消失了,而且再次用 VC 调试,发现从 MyCommon.dll 调用 UnBand 时 ,也是在MyTest.exe 中执行 !^_^