CString的GetBuffer用法,GetBuffer本質,GetBuffer常見問題解決方法
CString::GetBuffer
LPTSTR GetBuffer( int nMinBufLength );
throw( CMemoryException );
Return Value
AnLPTSTRpointer to the object’s (null-terminated) character buffer.
Parameters
nMinBufLength
The minimum size of the character buffer in characters. This value does not include space for a null terminator.
Remarks
Returns a pointer to the internal character buffer for theCStringobject. The returnedLPTSTRis notconstand thus allows direct modification ofCStringcontents.
If you use the pointer returned byGetBufferto change the string contents, you must callReleaseBufferbefore using any otherCStringmember functions.
二.函式作用及使用範圍
對一個CString變數,你可以使用的唯一合法轉換符是LPCTSTR,直接轉換成非常量指標(LPTSTR-[const] char*)是錯誤的。正確的得到一個指向緩衝區的非常量指標的方法是呼叫GetBuffer()方法。
GetBuffer()主要作用是將字串的緩衝區長度鎖定,releaseBuffer則是解除鎖定,使得CString物件在以後的程式碼中繼續可以實現長度自適應增長的功能。
CString::GetBuffer有兩個過載版本:
LPTSTR GetBuffer( );LPTSTR GetBuffer(int nMinBufferLength);
在第二個版本中,當設定的長度小於原字串長度時,nMinBufLength = nOldLen
略,不分配記憶體,指向原CString;當設定的長度大於原字串本身的長度時就要重新分配(reallocate)一塊比較大的空間出來。而呼叫第一個版本時,應如通過傳入0來呼叫第二個版本一樣。
是否需要在GetBufer後面呼叫ReleaseBuffer(),是根據你的後面的程式是否需要繼續使用該字串變數,並且是否動態改變其長度而定的。如果你GetBuffer以後程式自函式就退出,區域性變數都不存在了,呼叫不呼叫ReleaseBuffer沒什麼意義了。
最典型的應用就是讀取檔案:
CFile file;
// FILE_NAME 為實現定義好的檔名稱
if(file.Open(FILE_NAME,CFile::modeRead))
{
CString szContent;
int nFileLength = file.GetLength();
file.Read(szContent.GetBuffer(nFileLength),nFileLength);
szContent.ReleaseBuffer();
// 取得檔案內容放在szContent中,我們之後可以對其操作
}
三.測試
以下就CString::GetBuffer,做簡單測試:
測試1:
// example for CString::GetBuffer
#include <stdio.h>
#include <afx.h>
void main(void)
{
CString s( "abcd" );
printf("(1)before GetBuffer:\n");
printf("CString s.length=%d\n",s.GetLength());
printf("CString s=%s\n",s);
LPTSTR p = s.GetBuffer( 2 );
printf("(2)after GetBuffer and before ReleaseBuffer:\n");
printf("LPTSTR p=%s\n",p);
printf("p.length=%d\n",strlen(p));
printf("CString s=%s\n",s);
printf("CString s.length=%d\n",s.GetLength());
s.ReleaseBuffer( );
printf("(3)after ReleaseBuffer:\n");
printf("LPTSTR p=%s\n",p);
printf("p.length=%d\n",strlen(p));
printf("CString s=%s\n",s);
printf("CString s.length=%d\n",s.GetLength());
}
測試結果1:
(1)before GetBuffer:
CString s.length=4
CString s=abcd
(2)after GetBuffer and before ReleaseBuffer:
LPTSTR p=abcd
p.length=4
CString s=abcd
CString s.length=4
(3)after ReleaseBuffer:
LPTSTR p=abcd
p.length=4
CString s=abcd
CString s.length=4
Press any key to continue
測試2:
將LPTSTR p = s.GetBuffer( 2 ); 修改為:LPTSTR p = s.GetBuffer( 10 );
測試結果同1。
測試3:
在測試二的LPTSTR p = s.GetBuffer( 10 );後新增 p[5]='f';
測試結果同1。
測試4:
將測試三的p[5]='f';修改為p[4]='e';
測試結果4:
(1)before GetBuffer:
CString s.length=4
CString s=abcd
(2)after GetBuffer and before ReleaseBuffer:
LPTSTR p=abcde屯屯?
p.length=10
CString s=abcde屯屯?
CString s.length=4
(3)after ReleaseBuffer:
LPTSTR p=abcde屯屯?
p.length=10
CString s=abcde屯屯?
CString s.length=10
Press any key to continue
很顯然(2)after GetBuffer and before ReleaseBuffer:中CString s.length=4結果有問題。
注意:以上測試是在_MBCS環境下,如果換成_UNICODE則結果有可能不同。
參考:
《CString GetBuffer()》
《CString的GetBuffer》
《CString GetBuffer() and ReleaseBuffer()》
《CString::GetBuffer()與CString::ReleaseBuffer到底有什麼用?》
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;
}
void CString::ReleaseBuffer(int nNewLength)
{
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 ';
}
=============
看了很多人寫的程式,包括我自己寫的一些程式碼,發現很大的一部分bug是關於MFC類中的CString的錯誤用法的.出現這種錯誤的原因主要是對CString的實現機制不是太瞭解。
CString是對於原來標準c中字串型別的一種的包裝。因為,通過很長時間的程式設計,我們發現,很多程式的bug多和字串有關,典型的有:緩衝溢位、記憶體洩漏等。而且這些bug都是致命的,會造成系統的癱瘓。因此c++裡就專門的做了一個類用來維護字串指標。標準c++裡的字串類是string,在microsoft MFC類庫中使用的是CString類。通過字串類,可以大大的避免c中的關於字串指標的那些問題。
這裡我們簡單的看看Microsoft MFC中的CString是如何實現的。當然,要看原理,直接把它的程式碼拿過來分析是最好的。MFC裡的關於CString的類的實現大部分在strcore.cpp中。
CString就是對一個用來存放字串的緩衝區和對施加於這個字串的操作封裝。也就是說,CString裡需要有一個用來存放字串的緩衝區,並且有一個指標指向該緩衝區,該指標就是LPTSTR m_pchData。但是有些字串操作會增建或減少字串的長度,因此為了減少頻繁的申請記憶體或者釋放記憶體,CString會先申請一個大的記憶體塊用來存放字串。這樣,以後當字串長度增長時,如果增加的總長度不超過預先申請的記憶體塊的長度,就不用再申請記憶體。當增加後的字串長度超過預先申請的記憶體時,CString先釋放原先的記憶體,然後再重新申請一個更大的記憶體塊。同樣的,當字串長度減少時,也不釋放多出來的記憶體空間。而是等到積累到一定程度時,才一次性將多餘的記憶體釋放。
還有,當使用一個CString物件a來初始化另一個CString物件b時,為了節省空間,新物件b並不分配空間,它所要做的只是將自己的指標指向物件a的那塊記憶體空間,只有當需要修改物件a或者b中的字串時,才會為新物件b申請記憶體空間,這叫做寫入複製技術(CopyBeforeWrite)。
這樣,僅僅通過一個指標就不能完整的描述這塊記憶體的具體情況,需要更多的資訊來描述。
首先,需要有一個變數來描述當前記憶體塊的總的大小。
其次,需要一個變數來描述當前記憶體塊已經使用的情況。也就是當前字串的長度
另外,還需要一個變數來描述該記憶體塊被其他CString引用的情況。有一個物件引用該記憶體塊,就將該數值加一。
CString中專門定義了一個結構體來描述這些資訊:
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); }
};
實際使用時,該結構體的所佔用的記憶體塊大小是不固定的,在CString內部的記憶體塊頭部,放置的是該結構體。從該記憶體塊頭部開始的sizeof(CstringData)個BYTE後才是真正的用於存放字串的記憶體空間。這種結構的資料結構的申請方法是這樣實現的:
pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用於說明需要一次性申請的記憶體空間的大小的。
從程式碼中可以很容易的看出,如果想申請一個256個TCHAR的記憶體塊用於存放字串,實際申請的大小是:
sizeof(CStringData)個BYTE + (nLen+1)個TCHAR
其中前面sizeof(CstringData)個BYTE是用來存放CstringData資訊的。後面的nLen+1個TCHAR才是真正用來存放字串的,多出來的一個用來存放’/0’。
CString中所有的operations的都是針對這個緩衝區的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的實現方法是:
首先通過CString::GetData()取得CStringData物件的指標。該指標是通過存放字串的指標m_pchData先後偏移sizeof(CstringData),從而得到了CStringData的地址。
然後根據引數nMinBufLength給定的值重新例項化一個CStringData物件,使得新的物件裡的字串緩衝長度能夠滿足nMinBufLength。
然後在重新設定一下新的CstringData中的一些描述值。C
最後將新CStringData物件裡的字串緩衝直接返回給呼叫者。
這些過程用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;
很多時候,我們經常的對大批量的字串進行互相拷貝修改等,CString 使用了CopyBeforeWrite技術。使用這種方法,當利用一個CString物件a例項化另一個物件b的時候,其實兩個物件的數值是完全相同的,但是如果簡單的給兩個物件都申請記憶體的話,對於只有幾個、幾十個位元組的字串還沒有什麼,如果是一個幾K甚至幾M的資料量來說,是一個很大的浪費。
因此CString 在這個時候只是簡單的將新物件b的字串地址m_pchData直接指向另一個物件a的字串地址m_pchData。所做的額外工作是將物件a的記憶體應用CStringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
m_pchData = stringSrc.m_pchData;
InterlockedIncrement(&GetData()->nRefs);
}
這樣當修改物件a或物件b的字串內容時,首先檢查CStringData:: nRefs的值,如果大於一(等於一,說明只有自己一個應用該記憶體空間),說明該物件引用了別的物件記憶體或者自己的記憶體被別人應用,該物件首先將該應用值減一,然後將該記憶體交給其他的物件管理,自己重新申請一塊記憶體,並將原來記憶體的內容拷貝過來。
其實現的簡單程式碼是:
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 就是用來判斷該記憶體的被引用情況的。
void CString::Release()
{
if (GetData() != _afxDataNil)
{
if (InterlockedDecrement(&GetData()->nRefs) <= 0)
FreeData(GetData());
}
}
當多個物件共享同一塊記憶體時,這塊記憶體就屬於多個物件,而不在屬於原來的申請這塊記憶體的那個物件了。但是,每個物件在其生命結束時,都首先將這塊記憶體的引用減一,然後再判斷這個引用值,如果小於等於零時,就將其釋放,否則,將之交給另外的正在引用這塊記憶體的物件控制。
CString使用這種資料結構,對於大資料量的字串操作,可以節省很多頻繁申請釋放記憶體的時間,有助於提升系統性能。
通過上面的分析,我們已經對CString的內部機制已經有了一個大致的瞭解了。總的說來MFC中的CString是比較成功的。但是,由於資料結構比較複雜(使用CStringData),所以在使用的時候就出現了很多的問題,最典型的一個就是用來描述記憶體塊屬性的屬性值和實際的值不一致。出現這個問題的原因就是CString為了方便某些應用,提供了一些operations,這些operation可以直接返回記憶體塊中的字串的地址值,使用者可以通過對這個地址值指向的地址進行修改,但是,修改後又沒有呼叫相應的operations1使CStringData中的值來保持一致。比如,使用者可以首先通過operations得到字串地址,然後將一些新的字元增加到這個字串中,使得字串的長度增加,但是,由於是直接通過指標修改的,所以描述該字串長度的CStringData中的nDataLength卻還是原來的長度,因此當通過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
通過設定斷點,我們來執行並跟蹤這段程式碼可以看出,當執行到三處時,str1的值是”This is the string 1”,並且nOldLen的值是20。當執行到5處時,發現,str1的值變成了”modified”。也就是說,對GetBuffer返回的字串指標,我們將它做為引數傳遞給strcpy,試圖來修改這個字串指標指向的地址,結果是修改成功,並且CString物件str1的值也響應的變成了” modified”。但是,我們接著再呼叫str1.GetLength()時卻意外的發現其返回值仍然是20,但是實際上此時str1中的字串已經變成了” modified”,也就是說這個時候返回的值應該是字串” modified”的長度8!而不是20。現在CString工作已經不正常了!這是怎麼回事?
很顯然,str1工作不正常是在對通過GetBuffer返回的指標進行一個字串拷貝之後的。
再看MSDN上的關於這個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返回的指標使用之後需要呼叫ReleaseBuffer,這樣才能使用其他CString的operations。上面的程式碼中,我們在4-5處增建一行程式碼:str2.ReleaseBuffer(),然後再觀察nNewLen,發現這個時候已經是我們想要的值8了。
從CString的機理上也可以看出:GetBuffer返回的是CStringData物件裡的字串緩衝的首地址。根據這個地址,我們對這個地址裡的值進行的修改,改變的只是CStringData裡的字串緩衝中的值, CStringData中的其他用來描述字串緩衝的屬性的值已經不是正確的了。比如此時CStringData:: nDataLength很顯然還是原來的值20,但是現在實際上字串的長度已經是8了。也就是說我們還需要對CStringData中的其他值進行修改。這也就是需要呼叫ReleaseBuffer()的原因了。
正如我們所預料的,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是實現寫拷貝技術的,這裡不管它。
下面的程式碼就是重新設定CStringData物件中描述字串長度的那個屬性值的。首先取得當前字串的長度,然後通過GetData()取得CStringData的物件指標,並修改裡面的nDataLength成員值。
但是,現在的問題是,我們雖然知道了錯誤的原因,知道了當修改了GetBuffer返回的指標所指向的值之後需要呼叫ReleaseBuffer才能使用CString的其他operations時,我們就能避免不在犯這個錯誤了。答案是否定的。這就像雖然每一個懂一點程式設計知識的人都知道通過new申請的記憶體在使用完以後需要通過delete來釋放一樣,道理雖然很簡單,但是,最後實際的結果還是有由於忘記呼叫delete而出現了記憶體洩漏。
實際工作中,常常是對GetBuffer返回的值進行了修改,但是最後卻忘記呼叫ReleaseBuffer來釋放。而且,由於這個錯誤不象new和delete人人都知道的並重視的,因此也沒有一個檢查機制來專門檢查,所以最終程式中由於忘記呼叫ReleaseBuffer而引起的錯誤被帶到了發行版本中。
要避免這個錯誤,方法很多。但是最簡單也是最有效的就是避免這種用法。很多時候,我們並不需要這種用法,我們完全可以通過其他的安全方法來實現。
比如上面的程式碼,我們完全可以這樣寫:
CString str1("This is the string 1");
int nOldLen = str1.GetLength();
str1 = "modified";
int nNewLen = str1.GetLength();
但是有時候確實需要,比如:
我們需要將一個CString物件中的字串進行一些轉換,這個轉換是通過呼叫一個dll裡的函式Translate來完成的,但是要命的是,不知道什麼原因,這個函式的引數使用的是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返回的值,因為這樣常常會讓我們忘記呼叫ReleaseBuffer。就像上面的程式碼,我們可以在呼叫GetBuffer之後馬上就呼叫ReleaseBuffer來調整CString物件。
2. LPCTSTR
關於LPCTSTR的錯誤常常發生在初學者身上。
例如在呼叫函式
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
時,初學者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
(char*)(LPCTSTR)_strSrc),
nLen,
nLen);
if ( SUCCESSCALL(dwRet) )
{
}
if ( FAILEDCALL(dwRet) )
{
}
他原本的初衷是將轉換後的字串仍然放在_strSrc中,但是,當呼叫完Translate以後之後再使用_strSrc時,卻發現_strSrc已經工作不正常了。檢查程式碼卻又找不到問題到底出在哪裡。
其實這個問題和第一個問題是一樣的。CString類已經將LPCTST過載了。在CString中LPCTST實際上已經是一個operation了。對LPCTST的呼叫實際上和GetBuffer是類似的,直接返回CStringData物件中的字串緩衝的首地址。
其C++程式碼實現是:
_AFX_INLINE CString::operator LPCTSTR() const
{ return m_pchData; }
因此在使用完以後同樣需要呼叫ReleaseBuffer()。
但是,這個誰又能看出來呢?
其實這個問題的本質原因出在型別轉換上。LPCTSTR返回的是一個const char*型別,因此使用這個指標來呼叫Translate編譯是不能通過的。對於一個初學者,或者一個有很長程式設計經驗的人都會再通過強行型別轉換將const char*轉換為char*。最終造成了CString工作不正常,並且這樣也很容易造成緩衝溢位。
通過上面對於CString機制和一些容易出現的使用錯誤的描述,可以使我們更好的使用CString。
=======================
CString str = "abcde\0cde";
輸出字串的值為: abcde
而字串的長度為 s.GetLength() 的值為: 5
這是因為CString物件在賦值時只檢查到'\0',後面的忽略了, 也就是說實際物件str內容為"abcde".
而str真正的儲存空間為6(字串以'\0'結尾).
所以說在字元長度和實際的空間是不一樣的. 好!別跑!
請看下面有趣的程式:
CString str = "hello";
LPSTR pf = (LPSTR)(LPCSTR)s;
LPSTR pa = s.GetBuffer(0);
你可以測得 pf == pa;
LPSTR pb = s.GetBuffer(10);
你可以測得 pf != pb;
為什麼:
我們都知道(LPSTR)(LPCSTR)s 實際指向物件str的實際字串的記憶體地址, GetBuffer() 函式中的引數(其實就是重新申請的字串的長度)如果小於等於先前的字串長度, 則不會重新分配記憶體使用原來的記憶體所以 pf == pa, 如果大於先前的字串長度, 則重新追加記憶體(也就是要複製原來的內容),
所以pf != pb.
注意GetBuffer()函式中的引數為重新申請的字串的長度, 實際記憶體的大小應再加1.
CString s = "hello";
LPSTR pf = s.GetBuffer(0);
strcpy(pf,"hi");
這時物件str 的內容為 "hi"
但是s.GetLength()的值為5, 如果加上一條語句:
s.ReleaseBuffer();
則s.GetLength()的值為2
解釋:
CString物件在記憶體中用一個計數器來維持可用緩衝區的大小
void ReleaseBuffer( int nNewLength = -1 )
{
if( nNewLength == -1 )
{
nNewLength = StringLength( m_pszData );
}
SetLength( nNewLength );
}
很明顯ReleaseBuffer的作用就是更新字串的長度。 CString內,GetLength獲取字串長度並不是動態計算的,而是在賦值操作後計算並儲存在一個int變數內的,當通過GetBuffer直接修改CString時,那個int變數並不可能自動更新,於是便有了ReleaseBuffer.
CString s = "hello";
LPSTR pf = s.GetBuffer(0);
strcpy(pf,"hi");
LPSTR ps = (LPSTR)(LPCSTR)s; 字串緩衝區的首地址
*(ps+2) = 'x';
則字串的實際內容為: "hixlo"
*(ps+6) = 'a'; 出錯, 因為物件s的實際空間為 6
而
CString s = "hello";
LPSTR pf = s.GetBuffer(10);
strcpy(pf,"hi");
LPSTR ps = (LPSTR)(LPCSTR)s; 字串緩衝區的首地址
*(ps+2) = 'x';
*(ps+5)= '\0';
則字串的實際內容還是為: "hixlo"
*(ps+6) = 'a'; 可以因為s物件的實際空間為11
說白了 ReleaseBuffer就是更新賦值之後的字串的長度, 而實際空間沒有根本的變化, GetBuffer才是使記憶體空間大小變化的罪魁禍首.