CString物件的一種錯誤的使用方式
阿新 • • 發佈:2019-02-04
我現在做的系統有的時候會出現這樣的斷言失敗:
Debug Error!
DAMAGE: after Normal block (#328Array) 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 (#328Array) at 0x182C30F0.
的錯誤,應想到記憶體衝突。
問題終於水落石出了。反思一下,這個問題一點也不難,都怪自己基礎沒有打好,考慮問題不周全。