CString工作原理和常见问题分析
關(guān)于Cstring 類
版權(quán)所有©
Stevencao@benq.com
2003-11-6
轉(zhuǎn)自:http://blog.csdn.net/laiyiling/archive/2004/10/05/125216.aspx
看了很多人寫的程序,包括我自己寫的一些代碼,發(fā)現(xiàn)很大的一部分bug是關(guān)于MFC類中的CString的錯誤用法的.出現(xiàn)這種錯誤的原因主要是對CString的實現(xiàn)機(jī)制不是太了解。
CString是對于原來標(biāo)準(zhǔn)c中字符串類型的一種的包裝。因為,通過很長時間的編程,我們發(fā)現(xiàn),很多程序的bug多和字符串有關(guān),典型的有:緩沖 溢出、內(nèi)存泄漏等。而且這些bug都是致命的,會造成系統(tǒng)的癱瘓。因此c++里就專門的做了一個類用來維護(hù)字符串指針。標(biāo)準(zhǔn)c++里的字符串類是 string,在microsoft MFC類庫中使用的是CString類。通過字符串類,可以大大的避免c中的關(guān)于字符串指針的那些問題。
這里我們簡單的看看Microsoft MFC中的CString是如何實現(xiàn)的。當(dāng)然,要看原理,直接把它的代碼拿過來分析是最好的。MFC里的關(guān)于CString的類的實現(xiàn)大部分在strcore.cpp中。
CString就是對一個用來存放字符串的緩沖區(qū)和對施加于這個字符串的操作封裝。也就是說,CString里需要有一個用來存放字符串的緩沖區(qū), 并且有一個指針指向該緩沖區(qū),該指針就是LPTSTR m_pchData。但是有些字符串操作會增建或減少字符串的長度,因此為了減少頻繁的申請內(nèi)存或者釋放內(nèi)存,CString會先申請一個大的內(nèi)存塊用來 存放字符串。這樣,以后當(dāng)字符串長度增長時,如果增加的總長度不超過預(yù)先申請的內(nèi)存塊的長度,就不用再申請內(nèi)存。當(dāng)增加后的字符串長度超過預(yù)先申請的內(nèi)存 時,CString先釋放原先的內(nèi)存,然后再重新申請一個更大的內(nèi)存塊。同樣的,當(dāng)字符串長度減少時,也不釋放多出來的內(nèi)存空間。而是等到積累到一定程度 時,才一次性將多余的內(nèi)存釋放。
還有,當(dāng)使用一個CString對象a來初始化另一個CString對象b時,為了節(jié)省空間,新對象b并不分配空間,它所要做的只是將自己的指針指 向?qū)ο骯的那塊內(nèi)存空間,只有當(dāng)需要修改對象a或者b中的字符串時,才會為新對象b申請內(nèi)存空間,這叫做寫入復(fù)制技術(shù) (CopyBeforeWrite)。
這樣,僅僅通過一個指針就不能完整的描述這塊內(nèi)存的具體情況,需要更多的信息來描述。
首先,需要有一個變量來描述當(dāng)前內(nèi)存塊的總的大小。
其次,需要一個變量來描述當(dāng)前內(nèi)存塊已經(jīng)使用的情況。也就是當(dāng)前字符串的長度
另外,還需要一個變量來描述該內(nèi)存塊被其他CString引用的情況。有一個對象引用該內(nèi)存塊,就將該數(shù)值加一。
CString中專門定義了一個結(jié)構(gòu)體來描述這些信息:
struct CStringData
{
?long nRefs;???????????? // reference count
?int nDataLength;??????? // length of data (including terminator)
?int nAllocLength;?????? // length of allocation
?// TCHAR data[nAllocLength]
?TCHAR* data()?????????? // TCHAR* to managed data
??{ return (TCHAR*)(this+1); }
};
實際使用時,該結(jié)構(gòu)體的所占用的內(nèi)存塊大小是不固定的,在CString內(nèi)部的內(nèi)存塊頭部,放置的是該結(jié)構(gòu)體。從該內(nèi)存塊頭部開始的sizeof(CstringData)個BYTE后才是真正的用于存放字符串的內(nèi)存空間。這種結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)的申請方法是這樣實現(xiàn)的:
pData = (CStringData*)?new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用于說明需要一次性申請的內(nèi)存空間的大小的。
從代碼中可以很容易的看出,如果想申請一個256個TCHAR的內(nèi)存塊用于存放字符串,實際申請的大小是:
sizeof(CStringData)個BYTE + (nLen+1)個TCHAR
其中前面sizeof(CstringData)個BYTE是用來存放CstringData信息的。后面的nLen+1個TCHAR才是真正用來存放字符串的,多出來的一個用來存放’\0’。
?CString中所有的operations的都是針對這個緩沖區(qū)的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的實現(xiàn)方法是:
首先通過CString::GetData()取得CStringData對象的指針。該指針是通過存放字符串的指針m_pchData先后偏移sizeof(CstringData),從而得到了CStringData的地址。
然后根據(jù)參數(shù)nMinBufLength給定的值重新實例化一個CStringData對象,使得新的對象里的字符串緩沖長度能夠滿足nMinBufLength。
然后在重新設(shè)置一下新的CstringData中的一些描述值。C
最后將新CStringData對象里的字符串緩沖直接返回給調(diào)用者。
這些過程用C++代碼描述就是:
?if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength)
?{
??// 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;
很多時候,我們經(jīng)常的對大批量的字符串進(jìn)行互相拷貝修改等,CString 使用了CopyBeforeWrite技術(shù)。使用這種方法,當(dāng)利用一個CString對象a實例化另一個對象b的時候,其實兩個對象的數(shù)值是完全相同的, 但是如果簡單的給兩個對象都申請內(nèi)存的話,對于只有幾個、幾十個字節(jié)的字符串還沒有什么,如果是一個幾K甚至幾M的數(shù)據(jù)量來說,是一個很大的浪費。
因此CString 在這個時候只是簡單的將新對象b的字符串地址m_pchData直接指向另一個對象a的字符串地址m_pchData。所做的額外工作是將對象a的內(nèi)存應(yīng)用CStringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
??m_pchData = stringSrc.m_pchData;
??InterlockedIncrement(&GetData()->nRefs);
}
這樣當(dāng)修改對象a或?qū)ο骲的字符串內(nèi)容時,首先檢查CStringData:: nRefs的值,如果大于一(等于一,說明只有自己一個應(yīng)用該內(nèi)存空間),說明該對象引用了別的對象內(nèi)存或者自己的內(nèi)存被別人應(yīng)用,該對象首先將該應(yīng)用值 減一,然后將該內(nèi)存交給其他的對象管理,自己重新申請一塊內(nèi)存,并將原來內(nèi)存的內(nèi)容拷貝過來。
其實現(xiàn)的簡單代碼是:
void CString::CopyBeforeWrite()
{
?if (GetData()->nRefs > 1)
?{
??CStringData* pData = GetData();
??Release();
??AllocBuffer(pData->nDataLength);
memcpy(m_pchData, pData->data(),
??(pData- >nDataLength+1)*sizeof(TCHAR));
?}
}
其中Release 就是用來判斷該內(nèi)存的被引用情況的。
void CString::Release()
{
?if (GetData() != _afxDataNil)
?{
??if (InterlockedDecrement(&GetData()->nRefs) <= 0)
???FreeData(GetData());
?}
}
當(dāng)多個對象共享同一塊內(nèi)存時,這塊內(nèi)存就屬于多個對象,而不在屬于原來的申請這塊內(nèi)存的那個對象了。但是,每個對象在其生命結(jié)束時,都首先將這塊內(nèi)存的引用減一,然后再判斷這個引用值,如果小于等于零時,就將其釋放,否則,將之交給另外的正在引用這塊內(nèi)存的對象控制。
CString使用這種數(shù)據(jù)結(jié)構(gòu),對于大數(shù)據(jù)量的字符串操作,可以節(jié)省很多頻繁申請釋放內(nèi)存的時間,有助于提升系統(tǒng)性能。
通過上面的分析,我們已經(jīng)對CString的內(nèi)部機(jī)制已經(jīng)有了一個大致的了解了。總的說來MFC中的CString是比較成功的。但是,由于數(shù)據(jù)結(jié) 構(gòu)比較復(fù)雜(使用CStringData),所以在使用的時候就出現(xiàn)了很多的問題,最典型的一個就是用來描述內(nèi)存塊屬性的屬性值和實際的值不一致。出現(xiàn)這 個問題的原因就是CString為了方便某些應(yīng)用,提供了一些operations,這些operation可以直接返回內(nèi)存塊中的字符串的地址值,用戶 可以通過對這個地址值指向的地址進(jìn)行修改,但是,修改后又沒有調(diào)用相應(yīng)的operations1使CStringData中的值來保持一致。比如,用戶可 以首先通過operations得到字符串地址,然后將一些新的字符增加到這個字符串中,使得字符串的長度增加,但是,由于是直接通過指針修改的,所以描 述該字符串長度的CStringData中的nDataLength卻還是原來的長度,因此當(dāng)通過GetLength獲取字符串長度時,返回的必然是不正 確的。
存在這些問題的operations下面一一介紹。
1.?GetBuffer
很多錯誤用法中最典型的一個就是CString:: GetBuffer ()了.查了MSDN,里面對這個operation的描述是:
?Returns a pointer to the internal character buffer for the CString object. The returned LPTSTR is not const and thus allows direct modification of CString contents。
這段很清楚的說明,對于這個operation返回的字符串指針,我們可以直接修改其中的值:
?CString str1("This is the string 1");――――――――――――――――1
?int nOldLen = str1.GetLength();―――――――――――――――――2
?char* pstr1 = str1.GetBuffer( nOldLen );――――――――――――――3
?strcpy( pstr1, "modified" );――――――――――――――――――――4
?int nNewLen = str1.GetLength();―――――――――――――――――5
通過設(shè)置斷點,我們來運(yùn)行并跟蹤這段代碼可以看出,當(dāng)運(yùn)行到三處時,str1的值是”This is the string 1”,并且nOldLen的值是20。當(dāng)運(yùn)行到5處時,發(fā)現(xiàn),str1的值變成了”modified”。也就是說,對GetBuffer返回的字符串指 針,我們將它做為參數(shù)傳遞給strcpy,試圖來修改這個字符串指針指向的地址,結(jié)果是修改成功,并且CString對象str1的值也響應(yīng)的變成了” modified”。但是,我們接著再調(diào)用str1.GetLength()時卻意外的發(fā)現(xiàn)其返回值仍然是20,但是實際上此時str1中的字符串已經(jīng)變 成了” modified”,也就是說這個時候返回的值應(yīng)該是字符串” modified”的長度8!而不是20。現(xiàn)在CString工作已經(jīng)不正常了!這是怎么回事?
很顯然,str1工作不正常是在對通過GetBuffer返回的指針進(jìn)行一個字符串拷貝之后的。
再看MSDN上的關(guān)于這個operation的說明,可以看到里面有這么一段話:
If you use the pointer returned by GetBuffer to change the string contents, you must call ReleaseBuffer before using any other CString member functions.
?原來在對GetBuffer返回的指針使用之后需要調(diào)用ReleaseBuffer,這樣才能使用其他CString的operations。上 面的代碼中,我們在4-5處增建一行代碼:str2.ReleaseBuffer(),然后再觀察nNewLen,發(fā)現(xiàn)這個時候已經(jīng)是我們想要的值8了。
從CString的機(jī)理上也可以看出:GetBuffer返回的是CStringData對象里的字符串緩沖的首地址。根據(jù)這個地址,我們對這個地 址里的值進(jìn)行的修改,改變的只是CStringData里的字符串緩沖中的值, CStringData中的其他用來描述字符串緩沖的屬性的值已經(jīng)不是正確的了。比如此時CStringData:: nDataLength很顯然還是原來的值20,但是現(xiàn)在實際上字符串的長度已經(jīng)是8了。也就是說我們還需要對CStringData中的其他值進(jìn)行修 改。這也就是需要調(diào)用ReleaseBuffer()的原因了。
正如我們所預(yù)料的,ReleaseBuffer源代碼中顯示的正是我們所猜想的:
?CopyBeforeWrite();? // just in case GetBuffer was not called
?if (nNewLength == -1)
??nNewLength = lstrlen(m_pchData); // zero terminated
?ASSERT(nNewLength <= GetData()->nAllocLength);
?GetData()->nDataLength = nNewLength;
?m_pchData[nNewLength] = '\0';
其中CopyBeforeWrite是實現(xiàn)寫拷貝技術(shù)的,這里不管它。
下面的代碼就是重新設(shè)置CStringData對象中描述字符串長度的那個屬性值的。首先取得當(dāng)前字符串的長度,然后通過GetData()取得CStringData的對象指針,并修改里面的nDataLength成員值。
但是,現(xiàn)在的問題是,我們雖然知道了錯誤的原因,知道了當(dāng)修改了GetBuffer返回的指針?biāo)赶虻闹抵笮枰{(diào)用ReleaseBuffer才 能使用CString的其他operations時,我們就能避免不在犯這個錯誤了。答案是否定的。這就像雖然每一個懂一點編程知識的人都知道通過new 申請的內(nèi)存在使用完以后需要通過delete來釋放一樣,道理雖然很簡單,但是,最后實際的結(jié)果還是有由于忘記調(diào)用delete而出現(xiàn)了內(nèi)存泄漏。
實 際工作中,常常是對GetBuffer返回的值進(jìn)行了修改,但是最后卻忘記調(diào)用ReleaseBuffer來釋放。而且,由于這個錯誤不象new和 delete人人都知道的并重視的,因此也沒有一個檢查機(jī)制來專門檢查,所以最終程序中由于忘記調(diào)用ReleaseBuffer而引起的錯誤被帶到了發(fā)行 版本中。
要避免這個錯誤,方法很多。但是最簡單也是最有效的就是避免這種用法。很多時候,我們并不需要這種用法,我們完全可以通過其他的安全方法來實現(xiàn)。
比如上面的代碼,我們完全可以這樣寫:
?CString str1("This is the string 1");
?int nOldLen = str1.GetLength();
?str1 = "modified";
?int nNewLen = str1.GetLength();
但是有時候確實需要,比如:
我們需要將一個CString對象中的字符串進(jìn)行一些轉(zhuǎn)換,這個轉(zhuǎn)換是通過調(diào)用一個dll里的函數(shù)Translate來完成的,但是要命的是,不知道什么原因,這個函數(shù)的參數(shù)使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
這個時候我們可能就需要這個方法了:
CString strDest;
Int nDestLen = 100;
DWORD dwRet = Translate( _strSrc.GetBuffer( _strSrc.GetLength() ),
?strDest.GetBuffer(nDestLen),
?_strSrc.GetLength(), nDestlen );
_strSrc.ReleaseBuffer();
strDest.ReleaseBuffer();
if ( SUCCESSCALL(dwRet)? )
{
}
if ( FAILEDCALL(dwRet) )
{
}
的確,這種情況是存在的,但是,我還是建議盡量避免這種用法,如果確實需要使用,請不要使用一個專門的指針來保存GetBuffer返回的值,因為 這樣常常會讓我們忘記調(diào)用ReleaseBuffer。就像上面的代碼,我們可以在調(diào)用GetBuffer之后馬上就調(diào)用ReleaseBuffer來調(diào) 整CString對象。
2.?LPCTSTR
關(guān)于LPCTSTR的錯誤常常發(fā)生在初學(xué)者身上。
例如在調(diào)用函數(shù)
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
時,初學(xué)者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
?(char*)(LPCTSTR)_strSrc),
?nLen,
?nLen);
if ( SUCCESSCALL(dwRet)? )
{
}
if ( FAILEDCALL(dwRet) )
{
}
他原本的初衷是將轉(zhuǎn)換后的字符串仍然放在_strSrc中,但是,當(dāng)調(diào)用完Translate以后之后再使用_strSrc時,卻發(fā)現(xiàn)_strSrc已經(jīng)工作不正常了。檢查代碼卻又找不到問題到底出在哪里。
其實這個問題和第一個問題是一樣的。CString類已經(jīng)將LPCTST重載了。在CString中LPCTST實際上已經(jīng)是一個 operation了。對LPCTST的調(diào)用實際上和GetBuffer是類似的,直接返回CStringData對象中的字符串緩沖的首地址。
其C++代碼實現(xiàn)是:
_AFX_INLINE CString::operator LPCTSTR() const
?{ return m_pchData; }
因此在使用完以后同樣需要調(diào)用ReleaseBuffer()。
但是,這個誰又能看出來呢?
其實這個問題的本質(zhì)原因出在類型轉(zhuǎn)換上。LPCTSTR返回的是一個const char*類型,因此使用這個指針來調(diào)用Translate編譯是不能通過的。對于一個初學(xué)者,或者一個有很長編程經(jīng)驗的人都會再通過強(qiáng)行類型轉(zhuǎn)換將 const char*轉(zhuǎn)換為char*。最終造成了CString工作不正常,并且這樣也很容易造成緩沖溢出。
通過上面對于CString機(jī)制和一些容易出現(xiàn)的使用錯誤的描述,可以使我們更好的使用CString。
轉(zhuǎn)載于:https://www.cnblogs.com/rockstone/archive/2010/01/15/1648802.html
總結(jié)
以上是生活随笔為你收集整理的CString工作原理和常见问题分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 现在一部全新的OPPOr9多少钱?
- 下一篇: 创建Silverlight自定义启动画面