我现在做的系统有的时候会出现这样的断言失败: Debug Error! DAMAGE: after Normal block (#3289) at 0x182C30F0. 跟踪一下,发现问题竟出在CString的析构函数中,于是拿出了大半天的时间来研究这个问题,终于发现了原因所在。 问题的起因是我像下面这样调用无参的构造函数声明一个CString对象: CString strText; 然后把它以这样的方式传递给别的函数:(函数1) pVCG->GetRotDirection(WAVE_P, m_nWaveSide, strText.GetBuffer(0)); 而在这个函数里对于字符串指针进行了类似于如下的操作: sprintf(strDir, "%s", "CW"); 这样做的危险性在于当字符串没有被初始化的时候,CString内部指向缓冲区的指针指向的是一个随机的地址,在CString的无参构造函数调用 了如下函数: _AFX_INLINE void CString::Init() { m_pchData = afxEmptyString.m_pchData; } m_pdhData的定义:LPTSTR m_pchData; afxEmptyString的定义是: #define afxEmptyString AfxGetEmptyString() const CString& AFXAPI AfxGetEmptyString() { return *(CString*)&_afxPchNil; } _afxPchNil的来源如下: AFX_STATIC_DATA int _afxInitData[] = { -1, 0, 0, 0 }; AFX_STATIC_DATA CStringData* _afxDataNil = (CStringData*)&_afxInitData; AFX_COMDAT LPCTSTR _afxPchNil = (LPCTSTR)(((BYTE*)&_afxInitData)+sizeof(CStringData)); 从上面的代码可以看出,没有进行初始化操的CString对象它们的缓冲区指针都是指向一块相同的内存:和一个全局数组相关的地址。 而在函数1例调用sprintf修改CString对象的缓冲区的结果是修改所有未初始化CString内部缓冲区指针所指,这么做是非常危险的。但是这还不是出现断言错误的原因。 接下来的错误,更难被发现。接着我的程序又调用了两次类似于下面的函数(函数2) pVCG->GetCompressionGrade(WAVE_QRS, m_nWaveSide, 0, 60, 0, 0, strText); 在这个函数的内部有str.Format(IDS_COMPRESSION_LESS);这样的操作。 这是MFC里CString::Format的相关代码: void AFX_CDECL CString::Format(UINT nFormatID, ...) { CString strFormat;//没有直接修改自己,而是先对新声明的字符串进行操作 VERIFY(strFormat.LoadString(nFormatID) != 0); va_list argList; va_start(argList, nFormatID); FormatV(strFormat, argList); va_end(argList); } 而在void CString::FormatV(LPCTSTR lpszFormat, va_list argList)里最后作如下操作: GetBuffer(nMaxLen); VERIFY(_vstprintf(m_pchData, lpszFormat, argListSave) <= GetAllocLength());//将修改后的字符串拷贝到自己的缓冲区内 ReleaseBuffer(); 关键在GetBuffer: LPTSTR CString::GetBuffer(int nMinBufLength) { ASSERT(nMinBufLength >= 0); if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength) //如果指定的内存空间比已经分配的空间小的话,则重新分配,并释放掉原来的内存 { #ifdef _DEBUG // give a warning in case locked string becomes unlocked if (GetData() != _afxDataNil && GetData()->nRefs < 0) TRACE0("Warning: GetBuffer on locked CString creates unlocked CString!\n"); #endif // we have to grow the buffer CStringData* pOldData = GetData(); int nOldLen = GetData()->nDataLength; // AllocBuffer will tromp it if (nMinBufLength < nOldLen) nMinBufLength = nOldLen; AllocBuffer(nMinBufLength); memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR)); GetData()->nDataLength = nOldLen; CString::Release(pOldData); } ASSERT(GetData()->nRefs <= 1); // return a pointer to the character storage for this string ASSERT(m_pchData != NULL); return m_pchData; } 由于字符串没有被初始化,所以GetData()->nAllocLength=0,因此if语句块被执行,重新在堆上分配内存,销毁原来的内存,这才第一次给字 符串分配内存。 这时还不会出现问题,接下来还会执行类似函数1的操作。 最后问题之所以发生在CString被析构的时候,原因就在于,在执行函数2的时候,字符串有了能容纳4个字节的缓冲区.如果调试的时候打开Memory窗口,在Address:文本框里输入一个堆内存的地址,可以发现VC在调试版的程序里为每个在堆里分配的内存块的后面加了4个字节的内容,值全为FD,用于检查内存越界。CString析构的时候,调用了调试版的operator delete,它就以此为依据进行了内存检测: if (!CheckBytes(pbData(pHead) + pHead->nDataSize, _bNoMansLandFill, nNoMansLandSize)) _RPT3(_CRT_ERROR, "DAMAGE: after %hs block (#%d) at 0x%08X.\n", szBlockUseName[_BLOCK_TYPE(pHead->nBlockUse)], pHead->lRequest, (BYTE *) pbData(pHead)); 由于后来再次调用的函数1时它产生的长度有的时候会大于4 ,就破坏了后面的边界,所以会出现这样的问题。 出现这种问题时,在调试状态下会在输出窗口输出如下类似信息: memory check error at 0x182C7F22 = 0x57, should be 0xFD 结论: 1.所以str.GetBuffer(0)作为参数传递的时候适合于作为只读的参数; 2.如果非得要做可以修改的参数,那就得给GetBuffer传递一个保证足够安全的参数,也就是足够大; 2.如果调试版的程序出现类似 Debug Error! DAMAGE: after Normal block (#3289) at 0x182C30F0. 的错误,应想到内存冲突。 问题终于水落石出了。反思一下,这个问题一点也不难,都怪自己基础没有打好,考虑问题不周全。 
|