CString 是一種很有用的數據類型。它們很大程度上簡化了MFC中的許多操作,使得MFC在做字符串操作的時候方便了很多。 不管怎樣,使用CString有很多特殊的技巧,特別是對於純C背景下走出來的程序員來說有點難以學習。
1、CString 轉化 成 char*(1) —— 強制類型轉換為 LPCTSTR
這是一種略微硬性的轉換,我們首先要了解 CString 是一種很特殊的 C++ 對象,它裡面包含了三個值:一個指向某個數據緩沖區的指針、一個是該緩沖中有效的字符記數以及一個緩沖區長度。 有 效字符數的大小可以是從0到該緩沖最大長度值減1之間的任何數(因為字符串結尾有一個NULL字符)。字符記數和緩沖區長度被 巧妙隱藏。
除非你做一些特殊的操作,否則你不可能知道給CString對象分配的緩沖區的長度。這樣,即使你獲得了該緩沖的 地址,你也無法更改其中的內容,不能截短字符串,也 絕對沒有辦法加長它的內容,否則第一時間就會看到溢出。
LPCTSTR 操作符(或者更明確地說就是 TCHAR * 操作符)在 CString 類中被重載了,該操作符的定義是返回緩沖區的地址,因此,如果 你需要一個指向 CString 的 字符串指針的話,可以這樣做:
CString s("GrayCat");
LPCTSTR p = s;
它可 以正確地運行。這是由C語言的強制類型轉化規則實現的。當需要強制類型轉化時,C++規測容許這種選擇。比如,你可以將(浮 點數)定義為將某個復數 (有一對浮點數)進行強制類型轉換後只返回該復數的第一個浮點數(也就是其實部)。可以象下面 這樣:
Complex c(1.2f, 4.8f);
float realpart = c;
如果(float)操作符定義正確的話,那麼實部的的值應該是 1.2。
這種強制轉化適合所有這種情況,例如,任何帶有 LPCTSTR 類型參數的函數都會強制執行這種轉換。 於是,你可能有 這樣一個函數(也許在某個你買來的DLL中):
BOOL DoSomethingCool(LPCTSTR s);
你象下面這樣調用它:
CString file("c:\\myfiles\\coolstuff")
BOOL result = DoSomethingCool(file);
它能正確運行。因為 DoSomethingCool 函數已經說明了需要一個 LPCTSTR 類型 的參數,因此 LPCTSTR 被應用於該參數,在 MFC 中就是返回的串地址。
如果你要格式化字符串怎麼辦呢?
CString graycat("GrayCat");
CString s;
s.Format("Mew! I love %s", graycat);
注 意由於在可變參數列表中的值(在函數說明中是以“...”表示的)並沒有隱含一個強制類型轉換操作符。你會得到什麼結果呢 ?
一個令人驚訝的結果,我們得到的實際結果串是:
"Mew! I love GrayCat"。
因為 MFC 的設計者們 在設計 CString 數據類型時非常小心, CString 類型表達式求值後指向了字符串,所以這裡看不到任何象 Format 或 sprintf 中的強制類型轉換,你仍然可以得到正確的行為。描述 CString 的附加數據實際上在 CString 名義地址之後。
有一件事情 你是不能做的,那就是修改字符串。比如,你可能會嘗試用“,”代替“.”(不要做這樣的,如果你在乎國際化問題,你應該使 用十進制轉換的 National Language Support 特性)下面是個簡單的例子:
CString v("1.00"); // 貨幣金 額,兩位小數
LPCTSTR p = v;
p[lstrlen(p) - 3] = '','';
這時編譯器會報錯,因為你賦值了一個 常量串。如果你做如下嘗試,編譯器也會錯:
strcat(p, "each");
因為 strcat 的第一個參數應該是 LPTSTR 類型的數據,而你卻給了一個 LPCTSTR。
不要試圖鑽這個錯誤消息的牛角尖,這只會使你自己陷入麻煩!
原因是緩沖有一個計數,它是不可存取的(它位於 CString 地址之下的一個隱藏區域),如果你改變這個串,緩沖中的 字符計數不會反映所做的修改。此外,如果字符串長度恰好是該字符串物理限制的長度,那麼擴展該字符串將改寫緩沖以外的任 何數據,那是你無權進行寫操作的內存,你會毀換壞不屬於你的內存。這是應用程序真正的死亡處方。
2、CString轉化 成char* (2)—— 使用 CString 對象的 GetBuffer 方法
如果你需要修改 CString 中的內容,它有一個特殊的方法可 以使用,那就是 GetBuffer,它的作用是返回一個可寫的緩沖指針。 如果你只是打算修改字符或者截短字符串,你完全可以這 樣做:
CString s(_T("File.ext"));
LPTSTR p = s.GetBuffer();
LPTSTR dot = strchr(p, ''.''); // OK, should have used s.Find...
if(p != NULL)
*p = _T(''\0'');
s.ReleaseBuffer();
這是 GetBuffer 的第一種用法,也是最簡單的一種,不用給它傳遞參數,它使用默認值 0,意思是 :“給我這個字符串的指針,我保證不加長它”。當你調用 ReleaseBuffer 時,字符串的實際長度會被重新計算,然後存入 CString 對象中。
必須強調一點,在 GetBuffer 和 ReleaseBuffer 之間這個范圍,一定不能使用你要操作的這個緩沖的 CString 對象的任何方法。因為 ReleaseBuffer 被調用之前,該 CString 對象的完整性得不到保障。研究以下代碼:
CString s(...);
LPTSTR p = s.GetBuffer();
//... 這個指針 p 發生了很多事情
int n = s.GetLength(); // 有可能給出錯誤的答案
s.TrimRight(); // 不能保證能正常工作
s.ReleaseBuffer(); // 現 在應該 OK
int m = s.GetLength(); // 這個結果可以保證是正確的。
s.TrimRight(); // 將正常工作。
假設你想增加字符串的長度,你首先要知道這個字符串可能會有多長,好比是聲明字符串數組的時候用:
char buffer [1024];
表示 1024 個字符空間足以讓你做任何想做得事情。在 CString 中與之意義相等的表示法:
LPTSTR p = s.GetBuffer(1024);
調用這個函數後,你不僅獲得了字符串緩沖區的指針,而且同時還獲得了長度至少為 1024 個字符的空 間(注意,我說的是“字符”,而不是“字節”,因為 CString 是以隱含方式感知 Unicode 的)。
同時,還應該注意的是 ,如果你有一個常量串指針,這個串本身的值被存儲在只讀內存中,如果試圖存儲它,即使你已經調用了 GetBuffer ,並獲得 一個只讀內存的指針,存入操作會失敗,並報告存取錯誤。我沒有在 CString 上證明這一點,但我看到過大把的 C 程序員經常 犯這個錯誤。
C 程序員有一個通病是分配一個固定長度的緩沖,對它進行 sprintf 操作,然後將它賦值給一個 CString:
char buffer[256];
sprintf(buffer, "%......", args, ...); // ... 部分省略許多細節
CString s = buffer;
雖然更好的形式可以這麼做:
CString s;
s.Format(_T("%...."), args, ...);
如果你的 字符串長度萬一超過 256 個字符的時候,不會破壞堆棧。
另外一個常見的錯誤是:既然固定大小的內存不工作,那麼就 采用動態分配字節,這種做法弊端更大:
int len = lstrlen(parm1) + 13 lstrlen(parm2) + 10 + 100;
char * buffer = new char[len];
sprintf(buffer, "%s is equal to %s, valid data", parm1, parm2);
CString s = buffer;
......
delete [] buffer;
它可以能被簡單地寫成:
CString s;
s.Format(_T("%s is equal to %s, valid data"), parm1, parm2);
需要注意 sprintf 例子都不是 Unicode 就緒的,盡管你可以使用 tsprintf 以及用 _T() 來包圍格式化字符串,但是基本思路仍然是在 走彎路,這這樣很容易出錯。
3、CString 和臨時對象
這是出現在 microsoft.public.vc.mfc 新聞組中的一個小 問題,我簡單的提一下,這個問題是有個程序員需要往注冊表中寫入一個字符串,他寫道:
我試著用 RegSetValueEx() 設置 一個注冊表鍵的值,但是它的結果總是令我困惑。當我用char[]聲明一個變量時它能正常工作,但是當我用 CString 的時候, 總是得到一些垃圾: "ÝÝÝÝ...ÝÝÝÝ&Ya cute;Ý"為了確認是不是我的 CString 數據出了問題,我試著用 GetBuffer,然後強制轉化成 char*,LPCSTR 。GetBuffer 返回的值是正確的,但是當我把它賦值給 char* 時,它就變成垃圾了。以下是我的程序段:
char* szName = GetName().GetBuffer(20);
RegSetValueEx(hKey, "Name", 0, REG_SZ,
(CONST BYTE *) szName,
strlen (szName + 1));
這個 Name 字符串的長度小於 20,所以我不認為是 GetBuffer 的參數的問題。
真讓人困惑,請幫幫 我。
親愛的 Frustrated,
你犯了一個相當微妙的錯誤,聰明反被聰明誤,正確的代碼應該象下面這樣:
CString Name = GetName();
RegSetValueEx(hKey, _T("Name"), 0, REG_SZ,
(CONST BYTE *) (LPCTSTR)Name,
(Name.GetLength() + 1) * sizeof(TCHAR));
為什麼我寫的代碼能行而你寫的就有問題呢?主要是因 為當你調用 GetName 時返回的 CString 對象是一個臨時對象。參見:《C++ Reference manual》§12.2
在一些環境中,編 譯器有必要創建一個臨時對象,這樣引入臨時對象是依賴於實現的。如果編譯器引入的這個臨時對象所屬的類有構造函數的話, 編譯器要確保這個類的構造函數被調用。同樣的,如果這個類聲明有析構函數的話,也要保證這個臨時對象的析構函數被調用。
編譯器必須保證這個臨時對象被銷毀了。被銷毀的確切地點依賴於實現.....這個析構函數必須在退出創建該臨時對象的范圍 之前被調用。
大部分的編譯器是這樣設計的:在臨時對象被創建的代碼的下一個執行步驟處隱含調用這個臨時對象的析構函 數,實現起來,一般都是在下一個分號處。因此,這個 CString 對象在 GetBuffer 調用之後就被析構了(順便提一句,你沒有 理由給 GetBuffer 函數傳遞一個參數,而且沒有使用ReleaseBuffer 也是不對的)。所以 GetBuffer 本來返回的是指向這個臨 時對象中字符串的地址的指針,但是當這個臨時對象被析構後,這塊內存就被釋放了。然後 MFC 的調試內存分配器會重新為這 塊內存全部填上 0xDD,顯示出來剛好就是“Ý”符號。在這個時候你向注冊表中寫數據,字符串的內容當然全被破 壞了。
我們不應該立即把這個臨時對象轉化成 char* 類型,應該先把它保存到一個 CString 對象中,這意味著把臨時對象 復制了一份,所以當臨時的 CString 對象被析構了之後,這個 CString 對象中的值依然保存著。這個時候再向注冊表中寫數據 就沒有問題了。
此外,我的代碼是具有 Unicode 意識的。那個操作注冊表的函數需要一個字節大小,使用lstrlen(Name+1) 得到的實際結果對於 Unicode 字符來說比 ANSI 字符要小一半,而且它也不能從這個字符串的第二個字符起開始計算,也許你 的本意是 lstrlen(Name) + 1(OK,我承認,我也犯了同樣的錯誤!)。不論如何,在 Unicode 模式下,所有的字符都是2個字 節大小,我們需要處理這個問題。微軟的文檔令人驚訝地對此保持緘默:REG_SZ 的值究竟是以字節計算還是以字符計算呢?我 們假設它指的是以字節為單位計算,你需要對你的代碼做一些修改來計算這個字符串所含有的字節大小。