我现在做的系统有的时候会出现这样的断言失败:
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.
的错误,应想到内存冲突。
问题终于水落石出了。反思一下,这个问题一点也不难,都怪自己基础没有打好,考虑问题不周全。