前言:串操做是編程中最經常使用也最基本的操做之一. 作爲VC程序員,不管是菜鳥或高手都曾用過CString.並且好像實際編程中很難離得開它(雖然它不是標準C++中的庫).由於MFC中提供的這個類對 咱們操做字串實在太方便了,CString不只提供各類豐富的操做函數、操做符重載,使咱們使用起串起來更象basic中那樣直觀;並且它還提供了動態內 存分配,使咱們減小了多少字符串數組越界的隱患。可是,咱們在使用過程當中也體會到CString簡直太容易出錯了,並且有的不可捉摸。因此有許多高人站過 來,建議拋棄它。
在此,我我的認爲:CString封裝得確實很完美,它有許多優勢,如「容易使用 ,功能強,動態分配內存,大量進行拷貝時它很能節省內存資源而且執行效率高,與標準C徹底兼容,同時支持多字節與寬字節,因爲有異常機制因此使用它安全方 便」 其實,使用過程當中之因此容易出錯,那是由於咱們對它瞭解得還不夠,特別是它的實現機制。由於咱們中的大多數人,在工做中並不愛那麼深刻地去看關於它的文 檔,況且它仍是英文的。
因爲前幾天我在工做中遇到了一個本不是問題但卻特別棘手、特別難解決並且莫名驚詫的問題。最後發現是因爲CString引起的,後來,沒辦法,我把整個 CString的實現所有看了一遍,才慌然大悟,並完全弄清了問題的緣由(這個問題,我已在csdn上開貼)。在此,我想把個人一些關於CString的 知識總結一番,以供他(她)人借鑑,也許其中有我理解上的錯誤,望發現者能通知我,不勝感謝。
1 CString實現的機制.
CString是經過「引用」來管理串的,「引用」這個詞我相信你們並不陌生,象Window內核對象、COM對象等都是經過引用來實現的。而 CString也是經過這樣的機制來管理分配的內存塊。實際上CString對象只有一個指針成員變量,因此任何CString實例的長度只有4字節.
即: int len = sizeof(CString);//len等於4
這個指針指向一個相關的引用內存塊,如圖: CString str("abcd");
___
____________ | |
| | | |
| 0x04040404 | | | head部,爲引用內存塊相關信息
|____________| | |
str |___|
|'a'| 0x40404040
|'b'|
|'c'|
|'d'|
| 0 |
正由於如此,一個這樣的內存塊可被多個CString所引用,例以下列代碼:
CString str("abcd");
CString a = str;
CString b(str);
CString c;
c = b;
上面代碼的結果是:上面四個對象(str,a,b,c)中的成員變量指針有相同的值,都爲0x40404040.而這塊內存塊怎麼知道有多少個CString引用它呢?一樣,它也會記錄一些信息。如被引用數,串長度,分配內存長度。
這塊引用內存塊的結構定義以下:
struct CStringData
{
long nRefs; //表示有多少個CString 引用它. 4
int nDataLength; //串實際長度. 4
int nAllocLength; //總共分配的內存長度(不計這頭部的12字節). 4
};
因爲有了這些信息,CString就能正確地分配、管理、釋放引用內存塊。
若是你想在調試程序的時候得到這些信息。能夠在Watch窗口鍵入下列表達式:
(CStringData*)((CStringData*)(this->m_pchData)-1)或
(CStringData*)((CStringData*)(str.m_pchData)-1)//str爲指CString實例
正由於採用了這樣的好機制,使得CString在大量拷貝時,不只效率高,並且分配內存少。
續
2 LPCTSTR 與 GetBuffer(int nMinBufLength)
這兩個函數提供了與標準C的兼容轉換。在實際中使用頻率很高,但倒是最容易出錯的地方。這兩個函數實際上返回的都是指針,但它們有何區別呢?以及調用它們後,幕後是作了怎樣的處理過程呢?
(1) LPCTSTR 它的執行過程其實很簡單,只是返回引用內存塊的串地址。 它是做爲操做符重載提供的,
因此在代碼中有時能夠隱式轉換,而有時卻需強制轉制。如:
CString str;
const char* p = (LPCTSTR)str;
//假設有這樣的一個函數,Test(const char* p); 你就能夠這樣調用
Test(str);//這裏會隱式轉換爲LPCTSTR
(2) GetBuffer(int nMinBufLength) 它相似,也會返回一個指針,不過它有點差異,返回的是LPTSTR
(3) 這二者到底有何不一樣呢?我想告訴你們,其本質上徹底不同,通常說LPCTSTR轉換後只應該當常量使用,或者作函數的入參;而GetBuffer (...)取出指針後,能夠經過這個指針來修改裏面的內容,或者作函數的入參。爲何呢?也許常常有這樣的代碼:
CString str("abcd");
char* p = (char*)(const char*)str;
p[2] = 'z';
其實,也許有這樣的代碼後,你的程序並無錯,並且程序也運行得挺好。但它倒是很是危險的。再看
CString str("abcd");
CString test = str;
....
char* p = (char*)(const char*)str;
p[2] = 'z';
strcpy(p, "akfjaksjfakfakfakj");//這下完蛋了
你知道此時,test中的值是多少嗎?答案是"abzd".它也跟着改變了,這不是你所指望發生的。但爲何會這樣呢?你稍微想一想就會明白,前面說過,因 爲CString是指向引用塊的,str與test指向同一塊地方,當你p[2]='z'後,固然test也會隨着改變。因此用它作LPCTSTR作轉換 後,你只能去讀這塊數據,千萬別去改變它的內容。
假如我想直接經過指針去修改數據的話,那怎樣辦呢?就是用GetBuffer(...).看下述代碼:
CString str("abcd");
CString test = str;
....
char* p = str.GetBuffer(20);
p[2] = 'z'; // 執行到此,如今test中值卻還是"abcd"
strcpy(p, "akfjaksjfakfakfakj"); // 執行到此,如今test中值仍是"abcd"
爲何會這樣?其實GetBuffer(20)調用時,它實際上另外創建了一塊新內塊存,並分配20字節長度的buffer,而原來的內存塊引用計數也相 應減1. 因此執行代碼後str與test是指向了兩塊不一樣的地方,因此相安無事。
續
(4) 不過這裏還有一點注意事項:就是str.GetBuffer(20)後,str的分配長度爲20,即指針p它所指向的buffer只有20字節長,給它賦 值時,切不可超過,不然災難離你不遠了;若是指定長度小於原來串長度,如GetBuffer(1),實際上它會分配4個字節長度(即原來串長度);另外, 當調用GetBuffer(...)後並改變其內容,必定要記得調用ReleaseBuffer(),這個函數會根據串內容來更新引用內存塊的頭部信息。
(5) 最後還有一注意事項,看下述代碼:
char* p = NULL;
const char* q = NULL;
{
CString str = "abcd";
q = (LPCTSTR)str;
p = str.GetBuffer(20);
AfxMessageBox(q);// 合法的
strcpy(p, "this is test");//合法的,
}
AfxMessageBox(q);// 非法的,可能完蛋
strcpy(p, "this is test");//非法的,可能完蛋
這裏要說的就是,當返回這些指針後, 若是CString對象生命結束,這些指針也相應無效。
下面演示一段代碼執行過程
void Test()
{
CString str("abcd");//str指向一引用內存塊(引用內存塊的引用計數爲1,
長度爲4,分配長度爲4)
CString a;//a指向一初始數據狀態,
a = str; //a與str指向同一引用內存塊(引用內存塊的引用計數爲2,
長度爲4,分配長度爲4)
CString b(a);//a、b與str指向同一引用內存塊(引用內存塊的引用
計數爲3,長度爲4,分配長度爲4)
{
LPCTSTR temp = (LPCTSTR)a;//temp指向引用內存塊的串首地址。
(引用內存塊的引用計數爲3,長度爲4,分配長度爲4)
CString d = a; //a、b、d與str指向同一引用內存塊(引用內存塊的引用計數爲4, 長度爲4,分配長度爲4)
b = "testa"; //這條語句實際是調用CString::operator=(CString&)函數。
b指向一新分配的引用內存塊。(新分配的引用內存塊的
引用計數爲1,長度爲5,分配長度爲5)
//同時原引用內存塊引用計數減1. a、d與str仍指向原
引用內存塊(引用內存塊的引用計數爲3,長度爲4,分配長度爲4)
}//因爲d生命結束,調用析構函數,導至引用計數減1(引用內存
塊的引用計數爲2,長度爲4,分配長度爲4)
LPTSTR temp = a.GetBuffer(10);//此語句也會致使從新分配新內存塊。
temp指向新分配引用內存塊的串首地址(新
分配的引用內存塊的引用計數爲1,長度
爲0,分配長度爲10)
//同時原引用內存塊引用計數減1. 只有str仍
指向原引用內存塊(引用內存塊的引用計數爲1,
長度爲4,分配長度爲4)
strcpy(temp, "temp"); //a指向的引用內存塊的引用計數爲1,長度爲0,分配長度爲10
a.ReleaseBuffer();//注意:a指向的引用內存塊的引用計數爲1,長度爲4,分配長度爲10
}
//執行到此,全部的局部變量生命週期都已結束。對象str a b 各自調用本身的析構構
//函數,所指向的引用內存塊也相應減1
//注意,str a b 所分別指向的引用內存塊的計數均爲0,這致使所分配的內存塊釋放
經過觀察上面執行過程,咱們會發現CString雖然能夠多個對象指向同一引用內塊存,可是它們在進行各類拷貝、賦值及改變串內容時,它的處理是很智能並 且很是安全的,徹底作到了互不干涉、互不影響。固然必需要求你的代碼使用正確恰當,特別是實際使用中會有更復雜的狀況,如作函數參數、引用、及有時需保存 到CStringList當中,若是哪怕有一小塊地方使用不當,其結果也會致使發生不可預知的錯誤
5 FreeExtra()的做用
看這段代碼
(1) CString str("test");
(2) LPTSTR temp = str.GetBuffer(50);
(3) strcpy(temp, "there are 22 character");
(4) str.ReleaseBuffer();
(5) str.FreeExtra();
上面代碼執行到第(4)行時,你們都知道str指向的引用內存塊計數爲1,長度爲22,分配長度爲50. 那麼執行str.FreeExtra()時,它會釋放所分配的多餘的內存。(引用內存塊計數爲1,長度爲22,分配長度爲22)
6 Format(...) 與 FormatV(...)
這條語句在使用中是最容易出錯的。由於它最富有技巧性,也至關靈活。在這裏,我沒打算對它細細分析,實際上sprintf(...)怎麼用,它就怎麼用。 我只提醒使用時需注意一點:就是它的參數的特殊性,因爲編譯器在編譯時並不能去校驗格式串參數與對應的變元的類型及長度。因此你必需要注意,二者必定要對 應上,
不然就會出錯。如:
CString str;
int a = 12;
str.Format("first:%l, second: %s", a, "error");//result?試試
續
7 LockBuffer() 與 UnlockBuffer()
顧名思議,這兩個函數的做用就是對引用內存塊進行加鎖及解鎖。
但使用它有什麼做用及執行過它後對CString串有什麼實質上的影響。其實挺簡單,看下面代碼:
(1) CString str("test");
(2) str.LockBuffer();
(3) CString temp = str;
(4) str.UnlockBuffer();
(5) str.LockBuffer();
(6) str = "error";
(7) str.ReleaseBuffer();
執行完(3)後,與一般狀況下不一樣,temp與str並不指向同一引用內存塊。你能夠在watch窗口用這個表達式(CStringData*)((CStringData*)(str.m_pchData)-1)看看。
其實在msdn中有說明:
While in a locked state, the string is protected in two ways:
No other string can get a reference to the data in the locked string, even if that string is assigned to the locked string.
The locked string will never reference another string, even if that other string is copied to the locked string.
8 CString 只是處理串嗎?
不對,CString不僅是能操做串,並且還能處理內存塊數據。功能完善吧!看這段代碼
char p[20];
for(int loop=0; loop<sizeof(p); loop++)
{
p[loop] = 10-loop;
}
CString str((LPCTSTR)p, 20);
char temp[20];
memcpy(temp, str, str.GetLength());
str徹底可以轉載內存塊p到內存塊temp中。因此能用CString來處理二進制數據
8 AllocSysString()與SetSysString(BSTR*)
這兩個函數提供了串與BSTR的轉換。使用時須注意一點:當調用AllocSysString()後,須調用它SysFreeString(...)
9 參數的安全檢驗
在MFC中提供了多個宏來進行參數的安全檢查,如:ASSERT. 其中在CString中也不例外,有許多這樣的參數檢驗,其實這也說明了代碼的安全性高,可有時咱們會發現這很煩,也致使Debug與Release版本 不同,若有時程序Debug通正常,而Release則程序崩潰;而有時恰相反,Debug不行,Release行。其實我我的認爲,咱們對 CString的使用過程當中,應力求代碼質量高,不能在Debug版本中出現任何斷言框,哪怕release運行彷佛
看起來一切正常。但很不安全。以下代碼:
(1) CString str("test");
(2) str.LockBuffer();
(3) LPTSTR temp = str.GetBuffer(10);
(4) strcpy(temp, "error");
(5) str.ReleaseBuffer();
(6) str.ReleaseBuffer();//執行到此時,Debug版本會彈出錯框
10 CString的異常處理
我只想強調一點:只有分配內存時,纔有可能致使拋出CMemoryException.
一樣,在msdn中的函數聲明中,注有throw( CMemoryException)的函數都有從新分配或調整內存的可能。
11 跨模塊時的CString.即一個DLL的接口函數中的參數爲CString&時,它會發生怎樣的現象。解答我遇到的
問題。個人問題原來已經發貼,地址爲: http://www.csdn.net/expert/topic/741/741921.xml?temp=.2283136
構造一個這樣CString對象時,如CString str,你可知道此時的str所指向的引用內存塊嗎?也許你會認爲它指向NULL。其實不對,若是這樣的話,CString所採用的引用機制管理內存塊就 會有麻煩了,因此CString在構造一個空串的對象時,它會指向一個固定的初始化地址,這塊數據的聲明以下:
AFX_STATIC_DATA int _afxInitData[] = {-1,0,0,0};
簡要描述歸納一下:當某個CString對象串置空的話,如Empty(),CString a等,它的成員變量m_pchData就會指向_afxInitData這個變量的地址。當這個CString對象生命週期結束時,正常狀況下它會去對所 指向的引用內存塊計數減1,若是引用計數爲0(即沒有任何CString引用時),則釋放這塊引用內存。而如今的狀況是若是CString所指向的引用內 存塊是初始化內存塊時,則不會釋聽任何內存。
說了這麼多,這與我遇到的問題有什麼關係呢?其實關係大着呢?其真正緣由就是若是exe模塊與dll模塊有一
個是static編譯鏈接的話。那麼這個CString初始化數據在exe模塊與dll模塊中有不一樣的地址,由於static鏈接則會在本模塊中有一份源 代碼的拷貝。另一種狀況,若是兩個模塊都是share鏈接的,CString的實現代碼則在另外一個單獨的dll實現,而AFX_STATIC_DATA 指定變量只裝一次,因此兩個模塊中_afxInitData有相同的地址。
如今問題徹底明白了吧!你能夠本身去演示一下。
__declspec (dllexport) void test(CString& str)
{
str = "abdefakdfj";//若是是static鏈接,而且傳入的str爲空串的話,這裏出錯。
}
最後一點想法:寫得這裏,其實CString中還有許多技巧性的好東東,我並沒去解釋。如不少重載的操做符、查找等。我認爲仍是詳細看看msdn,這樣會比我講好多了。我只側重那些狀況下會可能出錯。固然,我敘述若有錯誤,敬請高手指點,不勝感謝!程序員