關於Cstring 類
著作權
Stevencao@benq.com
2003-11-6
看了很多人寫的程式,包括我自己寫的一些代碼,發現很大的一部分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中的一些描述值。
最後將新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());
}
}
當多個對象共用同一塊記憶體時,這塊記憶體就屬於多個對象,而不在屬於原來的申請這塊記憶體的那個對象了。但是,每個對象在其生命結束時,都首先將這塊記憶體的引用減一,然後再判斷這個引用值,如果小於等於零時,就將其釋放,否則,將之交給另外的正在引用這塊記憶體的對象控制。