WTL :: CString 包含在 WTL 標頭檔 atlmisc.h 中,並且在未定義宏 _WTL_NO_CSTRING 時可用。
WTL :: CString 是一個非常獨立的類,記憶體結構與 BSTR 類似,即有一個額外的字串資訊頭標識字串相關資訊,資訊頭後緊跟有效字串資料。此外它還借鑒 COM ,使用引用計數管理字串對象指標的複製和對象的析構,使得對字串對象的複製只是複製指標而引用相同記憶體區的有效字串資料。這樣提高了記憶體利用率和操作的高效性。引用計數也管理對象的生命期,使其超出生命期沒有被使用時自動釋放記憶體。更為關鍵的是,利用三個全域變數,WTL :: CString 讓所有初始化為空白的字串對象都指向同一個記憶體塊,這提供了相當多的便利。
WTL :: CString 相容 UNICODE/ANSI,提供了 7 個重載版本的建構函式接收不同類型的參數。重載的賦值操作符“=,+=”、拼接操作符“+”以及尋找替換,子串操作,使得 WTL :: CString 無與倫比。WTL::CString 所提供的全部操作可以查看 WTL Documentation。
下面具體分析 WTL :: CString 的記憶體結構和初始化。不過在這之前,先講講指標與記憶體塊的關係:
指標的真正含義,在於它用類型標識符指示編譯器如何解析它所指向的記憶體塊。一旦它指明了記憶體塊的具體類型,編譯器就安裝其指明的類型來訪問此記憶體塊。這裡“訪問”的含義需要引起注意,並且“訪問”的過程發生時,編譯器必須要知道相應類型的具體定義。因此,這就間接地決定了“訪問”是運行時行為。比如:
struct Data
{
char a;
char b;
int c;
int d;
}
int nTemp[4] = {10, 10, 0, 0};
(Data*)pdata = (Data*)&nTemp;
一個結構體和一個初始化了的整型數組,最後一句含義如下:
取得整型數組記憶體塊地址,轉化為 Data* 也就是說把整型數組的記憶體塊按照 Data 結構體類型的記憶體塊來解析。這意味著對此記憶體塊的訪問方式可以是 nTemp[0],nTemp[1]···也可以是 pdata.a,pdata.b等等。訪問可以是取值也可以是賦值。具體操作是根據記憶體塊按位取值或賦值,相當於是一個記憶體塊的拷貝過程。此過程遵循儲存規則,比如是高位位元組在前還是低位位元組在前。
對於諸如:
int * p = nTemp;
p += 1;
int * q = &nTemp;
q += 1;
涉及到記憶體塊的解析方式,並未涉及到記憶體塊的訪問。
進入正題:
#ifndef _WTL_NO_CSTRING
struct CStringData
{
long nRefs; // reference count
int nDataLength;
int nAllocLength;
// TCHAR data[nAllocLength]
TCHAR* data()
{ return (TCHAR*)(this + 1); }
};
// Globals
// For an empty string, m_pchData will point here
// (note: avoids special case of checking for NULL m_pchData)
// empty string data (and locked)
_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof(CStringData));
class CString
{
public:
···
protected:
LPTSTR m_pchData; // pointer to ref counted string data UNICODE/ANSI 均適用。
CStringData* GetData() const
{
ATLASSERT(m_pchData != NULL);
return ((CStringData*)m_pchData) - 1;
}
void Init()
{
m_pchData = _GetEmptyString().m_pchData;
}
static const CString& __stdcall _GetEmptyString()
{
return *(CString*)&_atltmpPchNil;
}
···
}
#endif // !_WTL_NO_CSTRING
我們來詳細分析一下上述代碼。
定義了一個結構體 CStringData ,三個資料成員表示 WTL::Cstring 的引用計數、資料區長度、緩衝區長度。一個成員函數
TCHAR* data()
{ return (TCHAR*)(this + 1); }
this代表的是當前字串資訊頭指標,(this + 1)使指標往後移動sizeof(CStringData)長度。即指向字串資訊頭的下一個位置,指標類型為 TCHAR* 。
接下來是三個全域變數,
第一行定義一個一維 int 型數組記憶體塊,含有4個元素並賦初始值。此記憶體塊儲存在全域資料區。我們將會看到,這一變數就是一個初始化為空白字串的 WTL::CString 對象記憶體指標,所有初始化後的Null 字元串 WTL::Cstring 類對象均指向此記憶體塊。具體實現後面分析。
第二行定義一個 CStringData 指標,指向轉換為 CStringData 結構的該數組記憶體塊。則此指標所指的 CstringData 對象的資料成員由此記憶體塊中的全域數組相應元素初始化,相當於:
nRefs = -1,nDataLength = 0, nAllocLength = 0;
因此這一全域變數就是一個初始化了的字串資訊頭 CStringData 對象記憶體指標。
第三行 定義一個 字串指標指向從數組開始地址跨越了sizeof(CStringData)長度的地址,此即指向數組最後一個元素的地址,並且此記憶體塊轉換為 LPCTSTR 類型,內容為0(NULL)。因此這一變數就是初始化為空白字串的字串指標。後面我們將看到,所有初始化為空白字串的 WTL::CString 對象的有效字串指標都等於此指標。
註:_declspec(selectany)使得可以在標頭檔中定義變數,多次包含而不會引起錯誤,即編譯器自動最佳化刪除多餘的重複定義始終保持只有一個定義,滿足文法要求。
好了,現在來看 Cstring 的定義。只有一個成員變數,
protected:
LPTSTR m_pchData;
指向有效字串記憶體區的指標。
多個重載的建構函式可以接受不同的參數進行初始化,更為重要的是每個建構函式中均調用了初始化函數 Init(),複製建構函式中若需要初始化時也調用此函數。那麼函數 Init()似乎很關鍵。它的作用是:
將第三個全域變數所在的記憶體塊轉換為 WTL::CString 對象記憶體塊;即此時該記憶體塊中的內容被解釋為一個 WTL::CString 對象。
擷取該記憶體塊中 WTL::CString 對象的資料成員(也就是上述第三個全域變數),並賦值給待初始化的 CString 對象。
這樣做的結果就是,所有經過調用 Init()初始化後的 WTL::CString 對象都指向相同的記憶體塊並且有效字串內容為空白。
如下所示為 WTL::CString 字串對象記憶體結構圖:
我們再來看WTL::CString中對記憶體的分配和釋放,受保護的成員函數
BOOL AllocBuffer(int nLen)
void Release()
負責 WTL::CString對象的記憶體配置與釋放。
我們看到如果請求分配的記憶體長度為0則調用 Init()函數,使其初始化後指向全域變數。否則,分配 nLen+1 長度記憶體,額外的一個位元組用以嵌入結束字元‘/0’(pData->data()[nLen] = _T('/0'))但不算在有效字串長度中(pData->nDataLength = nLen),並且設定引用計數為1(pData->nRefs = 1)。最後把分配的記憶體塊掛接到 CString對象上(m_pchData = pData->data())。
對於記憶體釋放,首先判斷不是Null 字元串,然後遞減引用計數,若引用計數減為0表示此對象不在被引用,因此釋放記憶體。調用 Init()函數的作用是令該對象的 m_pchData = NULL。避免漂浮指標。