關於CString

Trackback: http://tb.donews.net/TrackBack.aspx?PostId=1201980


1. CString實現的機制.

CString 是通過“引用”來管理串的,“引用”這個詞我相信大家並不陌生,象Window內核對象、COM對象等都是通過引用來實現的。而CString也是通過這 樣的機制來管理分配的內存塊。實際上CString對象只有一個指針成員變量,所以任何CString實例的長度只有4字節.

即: int len = sizeof(CString);//len等於4

這個指針指向一個相關的引用內存塊,如圖: CString str("abcd");

 

 

 

 

 

‘A’

‘B’

‘C’

‘D’

0

 

 

0x04040404 head部,爲引用內存塊相關信息

 

str 0x40404040

 

 

 

 

 

正因爲如此,一個這樣的內存塊可被多個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對象生命結束,這些指針也相應無效。

 

3.拷貝 & 賦值 & "引用內存塊" 什麼時候釋放?

 

下面演示一段代碼執行過程

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,這樣也許會比我講的好多了。我只側重那些可能會出錯的情況。當然,如我上面敘述中有錯誤,敬請高手指點,不勝感謝!



Trackback: http://tb.donews.net/TrackBack.aspx?PostId=1201980

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章