CString 的部分實現(xiàn)剖析

1、CString初探:

在CString的實現(xiàn)中,其最基礎(chǔ)的類結(jié)構(gòu)如下:

基礎(chǔ)類結(jié)構(gòu)

CString 其實只有一個數(shù)據(jù)成員 m_pszData,這個成員指向了字符串的首地址。但在 MFC 的具體實現(xiàn)中, m_pszData 指向的其實是 CStringData 后面的一塊數(shù)據(jù)的首地址。比如執(zhí)行:

CString strHello = _T("hello");

這樣一條語句之后,m_pszData的指向其實是下面這個樣子:

                 m_pszData
                    ↓
    +---------------+--+--+--+--+--+---+
    |  CStringData  | h  |  e |   l |  l |   o |  \0 |
    +---------------+--+--+--+--+--+---+

我們知道,CStringData 里面的信息如下:

IAtlStringMgr* pStringMgr;       --> 執(zhí)行 Allocate、Reallocate、Free 等操作;重要的一點,提供 GetNilString 方法的實現(xiàn)(下文會講到);  
int            nDataLength;      --> 字符串的實際長度(通過 SetLength 等函數(shù)可操作這個大?。? 
int            nAllocLength;     --> 實際分配的空間大小(除非重新分配,否則這個大小不可變);  
int            nRefs;            --> 明顯為了支持 CopyOnWrite 機制,為引用計數(shù)  

我們可以看出,CStringData 里面有字符串的長度信息,但在 CAfxStringMgr::Allocate 的時候確實又為 '\0' 分配了空間。

CAfxStringMgr::Allocate

也就是說,每當字符串發(fā)生更改或者觸發(fā)了 CopyOnWrite 的機制時,就會調(diào)用 CAfxStringMgr 的 Allocate/Reallocate 函數(shù)進行分配空間,分配的大小為:

      (nChars + 1) * nCharSize + sizeof(CStringData)

2、CStringData 和 m_pszData 的關(guān)聯(lián)

當執(zhí)行 CString 的默認構(gòu)造函數(shù)時,會調(diào)用前面我們提到的 CAfxStringMgr::GetNilString 返回一個 CStringData 的指針,這個指針指向全局的一個 CNilStringData。CNilStringData 如下:

CNilStringData

CNilStringData 派生自 CStringData,額外擁有一個 achNil 的數(shù)組成員,這個數(shù)組初始化為空字符串。通過這個 achNil,保證了一個經(jīng)過調(diào)用默認構(gòu)造函數(shù)初始化的 CString,其指向的真正的字符串是一個空串。CSimpleStringT 的構(gòu)造函數(shù)如下:

CSimpleStringT

注意,這里為什么是一個長度為 2 的數(shù)組?原來,有時候我們需要兩個 '\0' 結(jié)尾的字符串——比如用 GetOpenFileName 打開一個文件的時候,需要在 OPENFILENAME 的 lpstrFilter 填入一個兩個 '\0' 結(jié)尾的字符串,這樣,萬一我們用一個默認的 CString 空串來傳值的時候,不會造成 Crash。

重要的是接下來的 Attach 操作,通過 Attach 操作,將這個 CStringData* 與 CSimpleStringT::m_pszData 執(zhí)行了關(guān)聯(lián):

Attach

pData->data() 具體做了哪些操作呢?

pData->data()

可以看出,data() 是 CStringData 類里的一個成員函數(shù),它返回 this 指針加 1 之后的一個指針。我們知道,對于一個類型為 T* 的指針,對它取偏移,得到的實際地址是:ptr + sizeof(T) * offset。所以,針對一個 CStringData* 的指針作偏移,得到的地址是緊挨在 CStringData 之后的那塊數(shù)據(jù)塊的地址。

這樣,就順理成章的將字符串的真正的指針 m_pszData 和描述字符串信息的 CStringData 關(guān)聯(lián)了起來。那么,我們也可以很容易的通過 m_pszData 反推出 CStringData 的指針,CSimpleStringT::GetData 這個成員方法就提供了這么一個操作:

GetData

先把 m_pszData 強轉(zhuǎn)為 CStringData* 的類型,再在這個基礎(chǔ)上做 -1 的偏移,得到的就是真正的 CStringData 的地址。

3、CopyOnWrite機制的觸發(fā)

CopyOnWrite——寫時復制機制,這個機制也算非常常見了。我第一次接觸這個機制,是 DLL 的寫時復制,當要手動 Hook 一個 DLL 中的 API 時,會在 API 開頭手動寫入跳轉(zhuǎn)匯編,這時候,系統(tǒng)會復制一份 DLL 鏡像給我們,不會影響到加載該 DLL 的其他進程。

CopyOnWrite,說白了:就是大家先共享一份數(shù)據(jù),可以進行共享只讀操作,事情順利進行;突然有個家伙想修改這份數(shù)據(jù)里的某一個地方,如果發(fā)現(xiàn)這塊數(shù)據(jù)是由多個人共享的,那好,你自己把這份數(shù)據(jù)復制一份,然后把共享的引用計數(shù)減一,然后你自己去玩吧。

CString 也是提供了這樣一個 CopyOnWrite 機制的,其中,CSimpleStringT::Fork 函數(shù)就提供了這樣一個操作,具體分為下面幾步:

  1. 它根據(jù)傳入的一個長度分配一段新的空間;—— Allocate(nLength, ...)
  2. 把舊數(shù)據(jù)拷貝到新的空間里面;—— CopyChars(...)
  3. 舊數(shù)據(jù)塊的引用技術(shù)減1; —— pOldData->Release()
  4. 把 m_pszData 和新的數(shù)據(jù)塊關(guān)聯(lián)起來。—— Attach(pNewData)
CSimpleStringT::Fork

那么,什么時候會觸發(fā) CopyOnWrite 機制呢?一般來說,對 CString 進行寫操作的所有方法,都會觸發(fā)該機制,Write 操作都會進行,但只有該字符串的數(shù)據(jù)塊被共享的時候,或者舊的 CStringData::nAllocLength 不足以存放新的字符串的時候,才會執(zhí)行 Copy 操作。這些對 CString 進行寫操作的方法,大家通過使用經(jīng)驗和肉眼,很容易就可以分辨出來。

4、operator LPCTSTR 及 GetBuffer 的故事

4.1、operator LPCTSTR:

OK,有些 API 接受的入?yún)⒖赡懿皇?CString,而是一個 char* 或者 wchar_t* 的字符串指針,這時候,我們往往會用到 LPCTSTR 的一個隱式轉(zhuǎn)換函數(shù) —— operator LPCTSTR,如你所想,它干了你想讓它干的,就是返回 m_pszData:

operator PCXSTR

呃,PCXSTR,說好的 LPCTSTR 呢?原來,對 wchar_t 類型的字符串,PCXSTR 的定義是這樣的,還是 LPCWSTR,這里夾雜的大寫 “C”,保留了 const 屬性:

ChTraitsBase

這里我們要注意了:當我們執(zhí)行 (LPCTSTR)str 這樣一個強轉(zhuǎn)操作,就會調(diào)用到 operator PCXSTR 這個轉(zhuǎn)換函數(shù),返回的是帶 const 屬性的字符串指針,所以,我們不應該對這個指針做任何的寫操作。比如:

CString str1 = _T("hello");  
CString str2 = str1;                                 // 這時候 str1 和 str2 共享字符串 "hello" 的數(shù)據(jù)塊  
  
LPCTSTR pcszAddr = (LPCTSTR)str1;  
LPTSTR  pszEvil  = const_cast<LPTSTR>(pcszAddr);     // 我們邪惡一下  
pszEvil[0] = _T('H');                                // 強制改一下,這時候 str1 和 str2 都變成了 "Hello" 了!  

所以,當我們要對字符串只讀的時候,應該使用這個隱式轉(zhuǎn)換符,或者調(diào)用 CSimpleStringT::GetString 方法,這兩個操作完全等價:

CSimpleStringT::GetString

4.2、GetBuffer:

比起GetString或者operator PCXSTR,GetBuffer 函數(shù)就有趣多了。

CSimpleStringT::GetBuffer

這里我們注意到,返回的是 PXSTR 而不是PXCSTR,也就是說,GetBuffer 返回的字符串,是不帶 const 屬性的,我們可以進行寫操作——那么,為了不影響其他共享的字符串,這里觸發(fā)了 CopyOnWrite 機制!——當然,如果 pData->IsShared 返回 FALSE 的話,說明沒有共享,是不會 Copy 的。我們再嘗試邪惡一把:

CString str1 = _T("hello");  
CString str2 = str1;                        // 這時候 str1 和 str2 共享字符串 "hello" 的數(shù)據(jù)塊  
  
LPTSTR pszEvil = str1.GetBuffer();  
pszEvil[0] = _T('H');                       // 強制改一下,這時候 str1 變成了 "Hello",str2 依然為 "hello"!  

可以看出,我們通過 GetBuffer 得到的字符串指針,是可以寫的,不會影響到其他字符串。很遺憾,這里,我們沒有邪惡成功。

4.3、GetBuffer的重載版本:

What!還有重載版本?對的,CString 還有一個重載了的 GetBuffer 函數(shù),這個重載版本接收一個 int 的長度作為入?yún)ⅲ?/p>

CSimpleStringT::GetBuffer(int)

繼續(xù)調(diào)用了 PrePareWrite2,繼續(xù)往下跟:

CSimpleStringT::PrePareWrite2

發(fā)現(xiàn)新需求的長度比已經(jīng)分配的小,或者字符串數(shù)據(jù)塊被共享,就調(diào)用 PrepareWrite2,否則,直接返回 m_pszData,我們繼續(xù)往下跟:

CSimpleStringT::PrePareWrite2

這里,第二個 if 分支,發(fā)現(xiàn)數(shù)據(jù)被共享,直接執(zhí)行 Fork 進行 Copy 操作,接下來的 elseif 分支,如果沒被共享,但已分配的最大長度小于用戶請求的長度,則進行擴容,然后調(diào)用 Reallocate 進行重新分配。

Reallocate 的執(zhí)行,大家可以參見源代碼,這里就不貼了,其實現(xiàn),大概可以想到個八九分吧。Fork 和 Reallocate 最后都執(zhí)行了 Attach 操作,將新數(shù)據(jù)塊和 m_pszData 關(guān)聯(lián)起來。

5、“到底要不要 ReleaseBuffer,This is a Question!”

那么,大家的疑問一直糾結(jié)在這里,GetBuffer 之后,到底要不要 ReleaseBuffer?

5.1、ReleaseBuffer干了什么?

我們要判斷一個函數(shù)該不該調(diào)用的時候,如果一直找不到想要的結(jié)果,參考源代碼,不失為一個好選擇:

ReleaseBuffer

ReleaseBuffer 如果你不傳任何參數(shù)進去,它會取字符串的真實長度(這里通過調(diào)用 wcslen 獲?。?,然后進行 SetLength 操作。但如果你傳了一個長度,它會直接用這個長度進行 SetLength 操作。

SetLength 干了什么?只是把新的長度賦到 CStringData 里面,并且把字符串按新長度,在對應的位置塞入 '\0':

SetLength

“哦,哦,怎么感覺滿世界都是坑吶!”——你這樣埋怨道!我們發(fā)現(xiàn),ReleaseBuffer 干了一件與它的名字完全不符的一件事,你這是鬧哪樣?結(jié)合 ReleaseBuffer 做的操作,我們完全有理由相信:UpdateBuffer 這個函數(shù)名,更適合這么一個操作!

5.3、什么情況下需要調(diào)用 ReleaseBuffer:

那么什么情況下需要調(diào)用 ReleaseBuffer 呢?我們看到,GetBuffer 返回的是可寫的指針,也就是說,我們得到這個字符串指針的時候,如果發(fā)生了一些寫操作,那么,CString 是不知道我們干了什么的,因為我們沒通過 CString 提供的接口去操作。所以,我們需要 ReleaseBuffer(UpdateBuffer什么時候能被扶正?)來把字符串的新長度更新到 CString 里面——具體點,更新到 CStringData 里面,因為我們調(diào)用 CString::GetLength 的時候,需要用到這個長度:

GetLength

舉個具體的例子:

CString str = _T("Hello World!");  
LPSTR pszAddr = str.GetBuffer();                // pszAddr 為 "Hello World!"  
int nStrLength = str.GetLength();               // nStrLength 為12  
  
pszAddr[6] = 0;                                 // pszAddr 變成了 "Hello",但str這個對象并不知道,它的m_pszData已經(jīng)不是從前的那個它了  
int nStrAfterChangeLength = str.GetLength();    // str依然相信,nStrAfterChangeLength 依然是 12  
  
str.ReleaseBuffer();                            // 我們讓第三方悄悄告訴str,你的m_pszData已經(jīng)變了,你最好重新審視一下它  
int nStrAfterUpdateLength = str.GetLength();    // nStrAfterUpdateLength 變成了 5,雖然變短了,但str不得不接受這個現(xiàn)實  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,692評論 0 4
  • 史上最全的iOS面試題及答案 迷途的羔羊--專為路癡量身打造的品牌。史上最精準的定位。想迷路都難!閃電更新中......
    南虞閱讀 1,647評論 0 8
  • 指針是C語言中廣泛使用的一種數(shù)據(jù)類型。 運用指針編程是C語言最主要的風格之一。利用指針變量可以表示各種數(shù)據(jù)結(jié)構(gòu); ...
    朱森閱讀 3,624評論 3 44
  • 椎間盤的相關(guān)知識 負重狀態(tài)下的椎間盤 坐姿,背部與地面垂直,此時頭部,軀干與上肢的重量施加在脊柱上。當身體下降的時...
    厚_德_載_物閱讀 336評論 0 2
  • 一、任何時候都要堅持原則 今天科一考試,上午匆忙上完班午飯都沒來得及吃便出門等教練的車。說好的十二點到單位門口接的...
    脁登閱讀 210評論 2 3

友情鏈接更多精彩內(nèi)容