VC++中CString类的引用问题

VC++中CString类的引用问题

VC++中CString类的引用问题

在一dll中有函数
__declspec (dllexport) void A(CString &sParam)
{
  sParam = "XXXXXX";//如果是static连接,并且传入的str为空串的话,这里出错。
}

外部调用时
在debug版本下没有问题,在release版本无法通过

查到相关资料如下
=====================================
跨模块时的Cstring。即一个DLL的接口函数中的参数为CString&时,它会发生怎样的现象。

构造一个这样CString对象时,如CString str,你可知道此时的str所指向的引用内存块吗?也许你会认为它指向NULL。其实不对,如果这样的话,CString所采用的引用机制管理内存块就会有麻烦了,所以CString在构造一个空串的对象时,它会指向一个固定的初始化地址,这块数据的声明如下:

  AFX_STATIC_DATA int _afxInitData[] = {-1,0,0,0};

简要描述概括一下:当某个CString对象串置空的话,如Empty(),CString a等,它的成员变量m_pchData就会指向_afxInitData这个变量的地址。当这个CString对象生命周期结束时,正常情况下它会去对所指向的引用内存块计数减1,如果引用计数为0(即没有任何CString引用它时),则释放这块引用内存。而现在的情况是如果CString所指向的引用内存块是初始化内存块时,则不会释放任何内存。



说了这么多,这与我遇到的问题有什么关系呢?其实关系大着呢?其真正原因就是如果exe模块与dll模块有一个是static编译连接的话。那么这个CString初始化数据在exe模块与dll模块中有不同的地址,因为static连接则会在本模块中有一份源代码的拷贝。另外一种情况,如果两个模块都是share连接的,CString的实现代码则在另一个单独的dll中实现,而AFX_STATIC_DATA指定变量只装一次,所以两个模块中_afxInitData有相同的地址。


= ==================================
现在问题是明白了,可是怎么解决,接手别人的程序,这种使用非常多,改起来太麻烦了。

另外,程序内存泄漏非常严重,在程序正常结束后,会有很长的时间停顿,估计是在释放内存,也不想改了,只要能快点结束就行,各位大大有什么办法没有,程序太大,要是全改的话还不如杀了我。

 

 

找到一个解决方法,不是很完美
=======================================
WTL::CString中的一点缺陷及修改 ( http://www.cppn.net/bbs/dispbbs.asp?boardid=32&id=796)

--------------------------------------------------------------------------------

-- 作者:【龙族】地狱
-- 发布时间:2005-4-6 17:42:00

-- WTL::CString中的一点缺陷及修改
WTL是在ATL基础上搭建的一个Win32界面框架。相当的精致小巧,效率和直接用SDK写相差无几。除此也还提供了很多实用的工具类,比如:WTL::CString。WTL提供的是一堆.H文件,没有CPP文件,也就是说WTL没有单独的编译单元,而是附在包含它的用户编译单元上。

  WTL::CString的定义和实现全部在include\\Atlmisc.h文件中。
  首先我们来看看它的部分实现。我把WTL::CString的构造部分抽取出来。

_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof(CStringData));

inline CString::CString()
{
  Init();
}

inline void CString::Init()
{ m_pchData = _GetEmptyString().m_pchData; }

static const CString& __stdcall _GetEmptyString()
{
  return *(CString*)&_atltmpPchNil;
}

  当生成一个WTL::CString的实例(不论是在heap,stack或静态数据段中),并赋了值之后,WTL会在heap上申请一块内存,内存的结构如下:
  +----+
  |信息|
  +----+ <- m_pchData
  |数据|
  +----+
  “信息”块的内容为:

struct CStringData
{
  long nRefs;   // reference count
  int nDataLength;
  int nAllocLength;
};

  记录了这块内存被引用的次数,有效字符串的长度,字符缓冲区的总长度。整个内存块的长度为(nAllocLength + sizeof(CStringData))。
  “数据”块中保存的是真正有效的字符串。
  m_pchData是WTL::CString类中唯一的一个数据成员。指向具体的字符串数据,类型为LPTSTR。
  采用这种结构和实现方式可以提供很高的效率。
  在对字符串进行复制时,实际只是使新的WTL::CString对象中的m_pchData指向原来的数据(即只拷贝了m_pchData成员),并增加了nRefs的值。改变时再先拷贝整个字符串,改变相应的引用计数,再改写,即“写时复制”。对于取字符串大小这类函数也可以非常高效的只返回nDataLength的值。

  我们再来看看我所遇到的问题在哪里?
  从WTL::CString的默认构造函数我们可以很容易看出,对于空的字符串,它让m_pchData指向了在全局数据区中的一块内存rgInitData[2],即rgInitData的第三个元素。这个地址代表相应的WTL::CString对象是个空字符串。使用了_declspec(selectany)编译指令保证了rgInitData在模块范围内是唯一的。我想用全局数据段中的一个地址表示空字符串,而不是将这块内存new到heap中,通过CStringData中的一个特定量来表示空字符串,也是出于效率的考虑。这样可以在heap中省下很多“空”的内存块。但是这个实现和一般的用户假设不一样,一般用户总会认为new出的数据应该在heap上。而WTL::CString中其他若干的实现也依赖于这个特定的实现技术,而不是普通的用户假设。

  现在我们来看这个问题的具体表现。
  假设一个应用引用了两个DLL,A和B。当应用启动初始化完后,DLL A和B位于同一个进程地址空间。
  如果在DLL A中new了一个空的字符串对象,再传到DLL B中,再在DLL B中释放了这个对象。这时就会发生内存错误,而这本来应该是合法的,因为A和B共用了同一个heap。通过跟踪发现错误在析构函数中。

inline CString::~CString()
{
  if (GetData() != _atltmpDataNil)
  {
    if (InterlockedDecrement(&GetData()->nRefs) <= 0)
        delete[] (BYTE*)GetData();
  }
}

inline CStringData* CString::GetData() const
{
  return ((CStringData*)m_pchData) - 1;
}

  我们可以看到释放时通过比较WTL::CString对象的m_pchData是不是指在全局数据区的代表空字符串的地址上来判断字符串是否为空。如果不为空,是就表示数据是在heap上,如果递减引用为0的话,就delete这块内存。
  现在的问题是_atltmpDataNil实际也就是rgInitData,只是模块范围内是唯一的,在我们上面的例子中DLL A和DLL B是两个不同的模块,他们各含一个rgInitData。这样在DLL A中new出的空字符串在传到DLL B中,并直接被Delete时,就会出错,析构函数的(if (GetData() != _atltmpDataNil))这句本应为假,此时却为真,这样会在执行(delete[] (BYTE*)GetData();)这句时出错,试图delete全局数据区中内存当然会出错。

  我当时用的是WTL7.0版,如是我又去找到了最新的WTL7.1版,发现还是没有解决这个问题,这样就只能自己来改了。本来想将标志为空的CStringData实例new到heap中去,这样可以避免上述的问题。但这样一是要改比较多的代码,另外对于多个空的WTL:CString实例,要产生多个标志,浪费了内存。最后找到了一个比较简单的解决方法。

  _declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
  -1对应CStringData结构的nRefs字段,即引用计数器的初始值。我们可以通过对这个值进行比较,而不是对地址进行比较来确认是否是空的字符串。但是-1有特殊的意义,可以看看WTL::CString::LockBuffer()成员函数,当nRefs为-1时表示锁定缓冲区。因为我选用了一个比较大的负整数,-10001代表空的字符串。并将这个数做为WTL::CStringData::nRefs的初值。
  将原来的

_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof(CStringData));

修改为:

#define NULLSTRING -10001   //PK test 2004-03-08
_declspec(selectany) int rgInitData[] = { NULLSTRING, 0, 0, 0 };   //PK test 2004-03-08
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof(CStringData));

  然后再将原来根据地址是否相同来判断一个字符串是否为空的代码,全部修改为根据WTL::CStringData::nRefs的值是否为NULLSTRING来判断,即可。共有以下五处:(注意,注释掉的是原来的代码,我加上去的代码后面也用注释做了标记)

inline void CString::Release()
{
  //if (GetData() != _atltmpDataNil)
  if (GetData()->nRefs != NULLSTRING)   //PK test 2004-03-08
  {
    ATLASSERT(GetData()->nRefs != 0);
    if (InterlockedDecrement(&GetData()->nRefs) <= 0)
        delete[] (BYTE*)GetData();
    Init();
  }
}

inline void PASCAL CString::Release(CStringData* pData)
{
  //if (pData != _atltmpDataNil)
  if (pData->nRefs != NULLSTRING)
  {
    ATLASSERT(pData->nRefs != 0);
    if (InterlockedDecrement(&pData->nRefs) <= 0)
        delete[] (BYTE*)pData;
  }
}

inline CString::~CString()
// free any attached data
{
  //if (GetData() != _atltmpDataNil)
  if (GetData()->nRefs != NULLSTRING)
  {
    if (InterlockedDecrement(&GetData()->nRefs) <= 0)
        delete[] (BYTE*)GetData();
  }
}

inline const CString& CString::operator =(const CString& stringSrc)
{
  if (m_pchData != stringSrc.m_pchData)
  {
    //if ((GetData()->nRefs < 0 && GetData() != _atltmpDataNil) || stringSrc.GetData()->nRefs < 0)
    if ((GetData()->nRefs < 0 && GetData()->nRefs != NULLSTRING) || stringSrc.GetData()->nRefs < 0)   //PK test 2004-03-08
    {
        // actual copy necessary since one of the strings is locked
        AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);
    }
    else
    {
        // can just copy references around
        Release();
        ATLASSERT(stringSrc.GetData() != _atltmpDataNil);
        m_pchData = stringSrc.m_pchData;
        InterlockedIncrement(&GetData()->nRefs);
    }
  }
  return *this;
}

inline void CString::UnlockBuffer()
{
  ATLASSERT(GetData()->nRefs == -1);
  //if (GetData() != _atltmpDataNil)
  if (GetData()->nRefs != NULLSTRING)   //PK 2004-03-08
    GetData()->nRefs = 1;
}

  共五处,修改后使用到目前为止,一直没有发现内存泄漏。

  关于这个问题我曾经和我的同事争论过。到底我上面说的使用方法是一种非法的使用方法,还是这个问题是应该属于WTL::CString设计上的一个缺陷呢。我认为应该是一个缺陷,理由很简单,对于这类底层功能的封装,应该要给用户也就是开发人员以正确的引导,让他们不易进行错误的使用。而这些功能的内部实现也应该站在用户可能的假设上来进行,离开这个就很容易存在设计或实现上的缺陷。比如这个问题,用户在使用时,因为对于空的WTL::CString实例,是new出来的,所以很正常的会认为,它可以跨模块的边界传递不会出现问题。而WTL::CString的实现并没有尊重这一假设,所以我认为这应该是WTL::CString实现上的一个缺陷。

 

你可能感兴趣的:(VC++中CString类的引用问题)