CString原理介紹

看了很多人寫的程序,包括我自己寫的一些代碼,發現很大的一部分bug是關于MFC類中的CString的錯誤用法的.出現這種錯誤的原因主要是對CString的實現機制不是太了解。

??? CString是對于原來標準c中字符串類型的一種的包裝。因為,通過很長時間的編程,我們發現,很多程序的bug多和字符串有關,典型的有:緩沖溢出、 內存泄漏等。而且這些bug都是致命的,會造成系統的癱瘓。因此c++里就專門的做了一個類用來維護字符串指針。標準c++里的字符串類是string, 在microsoft MFC類庫中使用的是CString類。通過字符串類,可以大大的避免c中的關于字符串指針的那些問題。

這里我們簡單的看看Microsoft MFC中的CString是如何實現的。當然,要看原理,直接把它的代碼拿過來分析是最好的。MFC里的關于CString的類的實現大部分在strcore.cpp中。

??? CString就是對一個用來存放字符串的緩沖區和對施加于這個字符串的操作封裝。也就是說,CString里需要有一個用來存放字符串的緩沖區,并且有 一個指針指向該緩沖區,該指針就是LPTSTR m_pchData。但是有些字符串操作會增建或減少字符串的長度,因此為了減少頻繁的申請內存或者釋放內存,CString會先申請一個大的內存塊用來 存放字符串。這樣,以后當字符串長度增長時,如果增加的總長度不超過預先申請的內存塊的長度,就不用再申請內存。當增加后的字符串長度超過預先申請的內存 時,CString先釋放原先的內存,然后再重新申請一個更大的內存塊。同樣的,當字符串長度減少時,也不釋放多出來的內存空間。而是等到積累到一定程度 時,才一次性將多余的內存釋放。

還有,當使用一個CString對象a來初始化另一個CString對象b時,為了節省空間,新對象b并不分配空間,它所要做的只是將自己的指針指 向對象a的那塊內存空間,只有當需要修改對象a或者b中的字符串時,才會為新對象b申請內存空間,這叫做寫入復制技術 (CopyBeforeWrite)。

這樣,僅僅通過一個指針就不能完整的描述這塊內存的具體情況,需要更多的信息來描述。

首先,需要有一個變量來描述當前內存塊的總的大小。
其次,需要一個變量來描述當前內存塊已經使用的情況。也就是當前字符串的長度
另外,還需要一個變量來描述該內存塊被其他CString引用的情況。有一個對象引用該內存塊,就將該數值加一。

CString中專門定義了一個結構體來描述這些信息:
struct CStringData
{
?long nRefs;???????????? // reference count
?int nDataLength;??????? // length of data (including terminator)
?int nAllocLength;?????? // length of allocation
?// TCHAR data[nAllocLength]

?TCHAR* data()?????????? // TCHAR* to managed data
? { return (TCHAR*)(this+1); }
};

實際使用時,該結構體的所占用的內存塊大小是不固定的,在CString內部的內存塊頭部,放置的是該結構體。從該內存塊頭部開始的sizeof(CStringData)個BYTE后才是真正的用于存放字符串的內存空間。這種結構的數據結構的申請方法是這樣實現的:
pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用于說明需要一次性申請的內存空間的大小的。

從代碼中可以很容易的看出,如果想申請一個256個TCHAR的內存塊用于存放字符串,實際申請的大小是: sizeof(CStringData)個BYTE + (nLen+1)個TCHAR

其中前面sizeof(CStringData)個BYTE是用來存放CStringData信息的。后面的nLen+1個TCHAR才是真正用來存放字符串的,多出來的一個用來存放’\0’。

?? CString中所有的operations的都是針對這個緩沖區的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的實現方法是:
首先通過CString::GetData()取得CStringData對象的指針。該指針是通過存放字符串的指針m_pchData先后偏移sizeof(CStringData),從而得到了CStringData的地址。
然后根據參數nMinBufLength給定的值重新實例化一個CStringData對象,使得新的對象里的字符串緩沖長度能夠滿足nMinBufLength。
然后在重新設置一下新的CStringData中的一些描述值。
最后將新CStringData對象里的字符串緩沖直接返回給調用者。

這些過程用C++代碼描述就是:
?if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength)
?{
? // we have to grow the buffer
? CStringData* pOldData = GetData();
? int nOldLen = GetData()->nDataLength;?? // AllocBuffer will tromp it
? if (nMinBufLength < nOldLen)
?? nMinBufLength = nOldLen;
? AllocBuffer(nMinBufLength);
? memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR));
? GetData()->nDataLength = nOldLen;
? CString::Release(pOldData);
?}
?ASSERT(GetData()->nRefs <= 1);

?// return a pointer to the character storage for this string
?ASSERT(m_pchData != NULL);
?return m_pchData;

很多時候,我們經常的對大批量的字符串進行互相拷貝修改等,CString 使用了CopyBeforeWrite技術。使用這種方法,當利用一個CString對象a實例化另一個對象b的時候,其實兩個對象的數值是完全相同的, 但是如果簡單的給兩個對象都申請內存的話,對于只有幾個、幾十個字節的字符串還沒有什么,如果是一個幾K甚至幾M的數據量來說,是一個很大的浪費。
因此CString 在這個時候只是簡單的將新對象b的字符串地址m_pchData直接指向另一個對象a的字符串地址m_pchData。所做的額外工作是將對象a的內存應用CStringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
? m_pchData = stringSrc.m_pchData;
? InterlockedIncrement(&GetData()->nRefs);
}

這樣當修改對象a或對象b的字符串內容時,首先檢查CStringData:: nRefs的值,如果大于一(等于一,說明只有自己一個應用該內存空間),說明該對象引用了別的對象內存或者自己的內存被別人應用,該對象首先將該應用值 減一,然后將該內存交給其他的對象管理,自己重新申請一塊內存,并將原來內存的內容拷貝過來。

其實現的簡單代碼是:
void CString::CopyBeforeWrite()
{
?if (GetData()->nRefs > 1)
?{
? CStringData* pData = GetData();
? Release();
? AllocBuffer(pData->nDataLength);
memcpy(m_pchData, pData->data(),
? (pData- >nDataLength+1)*sizeof(TCHAR));
?}
}
其中Release 就是用來判斷該內存的被引用情況的。
void CString::Release()
{
?if (GetData() != _afxDataNil)
?{
? if (InterlockedDecrement(&GetData()->nRefs) <= 0)
?? FreeData(GetData());
?}
}

當多個對象共享同一塊內存時,這塊內存就屬于多個對象,而不在屬于原來的申請這塊內存的那個對象了。但是,每個對象在其生命結束時,都首先將這塊內存的引用減一,然后再判斷這個引用值,如果小于等于零時,就將其釋放,否則,將之交給另外的正在引用這塊內存的對象控制。

CString使用這種數據結構,對于大數據量的字符串操作,可以節省很多頻繁申請釋放內存的時間,有助于提升系統性能。

通過上面的分析,我們已經對CString的內部機制已經有了一個大致的了解了。總的說來MFC中的CString是比較成功的。但是,由于數據結 構比較復雜(使用CStringData),所以在使用的時候就出現了很多的問題,最典型的一個就是用來描述內存塊屬性的屬性值和實際的值不一致。出現這 個問題的原因就是CString為了方便某些應用,提供了一些operations,這些operation可以直接返回內存塊中的字符串的地址值,用戶 可以通過對這個地址值指向的地址進行修改,但是,修改后又沒有調用相應的operations1使CStringData中的值來保持一致。比如,用戶可 以首先通過operations得到字符串地址,然后將一些新的字符增加到這個字符串中,使得字符串的長度增加,但是,由于是直接通過指針修改的,所以描 述該字符串長度的CStringData中的nDataLength卻還是原來的長度,因此當通過GetLength獲取字符串長度時,返回的必然是不正 確的。

存在這些問題的operations下面一一介紹。

1. GetBuffer

很多錯誤用法中最典型的一個就是CString:: GetBuffer ()了.查了MSDN,里面對這個operation的描述是:
?Returns a pointer to the internal character buffer for the CString object. The returned LPTSTR is not const and thus allows direct modification of CString contents。
這段很清楚的說明,對于這個operation返回的字符串指針,我們可以直接修改其中的值:
?CString str1("This is the string 1");――――――――――――――――1
?int nOldLen = str1.GetLength();―――――――――――――――――2
?char* pstr1 = str1.GetBuffer( nOldLen );――――――――――――――3
?strcpy( pstr1, "modified" );――――――――――――――――――――4
?int nNewLen = str1.GetLength();―――――――――――――――――5

通過設置斷點,我們來運行并跟蹤這段代碼可以看出,當運行到三處時,str1的值是”This is the string 1”,并且nOldLen的值是20。當運行到5處時,發現,str1的值變成了”modified”。也就是說,對GetBuffer返回的字符串指 針,我們將它做為參數傳遞給strcpy,試圖來修改這個字符串指針指向的地址,結果是修改成功,并且CString對象str1的值也響應的變成了” modified”。但是,我們接著再調用str1.GetLength()時卻意外的發現其返回值仍然是20,但是實際上此時str1中的字符串已經變 成了” modified”,也就是說這個時候返回的值應該是字符串” modified”的長度8!而不是20。現在CString工作已經不正常了!這是怎么回事?

很顯然,str1工作不正常是在對通過GetBuffer返回的指針進行一個字符串拷貝之后的。

再看MSDN上的關于這個operation的說明,可以看到里面有這么一段話:
If you use the pointer returned by GetBuffer to change the string contents, you must call ReleaseBuffer before using any other CString member functions.

?原來在對GetBuffer返回的指針使用之后需要調用ReleaseBuffer,這樣才能使用其他CString的operations。上 面的代碼中,我們在4-5處增建一行代碼:str2.ReleaseBuffer(),然后再觀察nNewLen,發現這個時候已經是我們想要的值8了。

從CString的機理上也可以看出:GetBuffer返回的是CStringData對象里的字符串緩沖的首地址。根據這個地址,我們對這個地 址里的值進行的修改,改變的只是CStringData里的字符串緩沖中的值, CStringData中的其他用來描述字符串緩沖的屬性的值已經不是正確的了。比如此時CStringData:: nDataLength很顯然還是原來的值20,但是現在實際上字符串的長度已經是8了。也就是說我們還需要對CStringData中的其他值進行修 改。這也就是需要調用ReleaseBuffer()的原因了。

正如我們所預料的,ReleaseBuffer源代碼中顯示的正是我們所猜想的:
?CopyBeforeWrite();? // just in case GetBuffer was not called

?if (nNewLength == -1)
? nNewLength = lstrlen(m_pchData); // zero terminated

?ASSERT(nNewLength <= GetData()->nAllocLength);
?GetData()->nDataLength = nNewLength;
?m_pchData[nNewLength] = ′\0′;
其中CopyBeforeWrite是實現寫拷貝技術的,這里不管它。

下面的代碼就是重新設置CStringData對象中描述字符串長度的那個屬性值的。首先取得當前字符串的長度,然后通過GetData()取得CStringData的對象指針,并修改里面的nDataLength成員值。

但是,現在的問題是,我們雖然知道了錯誤的原因,知道了當修改了GetBuffer返回的指針所指向的值之后需要調用ReleaseBuffer才 能使用CString的其他operations時,我們就能避免不在犯這個錯誤了。答案是否定的。這就像雖然每一個懂一點編程知識的人都知道通過new 申請的內存在使用完以后需要通過delete來釋放一樣,道理雖然很簡單,但是,最后實際的結果還是有由于忘記調用delete而出現了內存泄漏。
實 際工作中,常常是對GetBuffer返回的值進行了修改,但是最后卻忘記調用ReleaseBuffer來釋放。而且,由于這個錯誤不象new和 delete人人都知道的并重視的,因此也沒有一個檢查機制來專門檢查,所以最終程序中由于忘記調用ReleaseBuffer而引起的錯誤被帶到了發行 版本中。

要避免這個錯誤,方法很多。但是最簡單也是最有效的就是避免這種用法。很多時候,我們并不需要這種用法,我們完全可以通過其他的安全方法來實現。
比如上面的代碼,我們完全可以這樣寫:
?CString str1("This is the string 1");
?int nOldLen = str1.GetLength();
?str1 = "modified";
?int nNewLen = str1.GetLength();

但是有時候確實需要,比如:
我們需要將一個CString對象中的字符串進行一些轉換,這個轉換是通過調用一個dll里的函數Translate來完成的,但是要命的是,不知道什么原因,這個函數的參數使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
這個時候我們可能就需要這個方法了:
CString strDest;
Int nDestLen = 100;
DWORD dwRet = Translate( _strSrc.GetBuffer( _strSrc.GetLength() ),
?strDest.GetBuffer(nDestLen),
?_strSrc.GetLength(), nDestlen );
_strSrc.ReleaseBuffer();
strDest.ReleaseBuffer();
if ( SUCCESSCALL(dwRet)? )
{
}
if ( FAILEDCALL(dwRet) )
{
}

的確,這種情況是存在的,但是,我還是建議盡量避免這種用法,如果確實需要使用,請不要使用一個專門的指針來保存GetBuffer返回的值,因為 這樣常常會讓我們忘記調用ReleaseBuffer。就像上面的代碼,我們可以在調用GetBuffer之后馬上就調用ReleaseBuffer來調 整CString對象。


2. LPCTSTR

關于LPCTSTR的錯誤常常發生在初學者身上。
例如在調用函數
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
時,初學者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
?(char*)(LPCTSTR)_strSrc),
?nLen,
?nLen);
if ( SUCCESSCALL(dwRet)? )
{
}
if ( FAILEDCALL(dwRet) )
{
}

他原本的初衷是將轉換后的字符串仍然放在_strSrc中,但是,當調用完Translate以后之后再使用_strSrc時,卻發現_strSrc已經工作不正常了。檢查代碼卻又找不到問題到底出在哪里。

其實這個問題和第一個問題是一樣的。CString類已經將LPCTST重載了。在CString中LPCTST實際上已經是一個 operation了。對LPCTST的調用實際上和GetBuffer是類似的,直接返回CStringData對象中的字符串緩沖的首地址。
其C++代碼實現是:
_AFX_INLINE CString::operator LPCTSTR() const
?{ return m_pchData; }

因此在使用完以后同樣需要調用ReleaseBuffer()。
但是,這個誰又能看出來呢?

其實這個問題的本質原因出在類型轉換上。LPCTSTR返回的是一個const char*類型,因此使用這個指針來調用Translate編譯是不能通過的。對于一個初學者,或者一個有很長編程經驗的人都會再通過強行類型轉換將 const char*轉換為char*。最終造成了CString工作不正常,并且這樣也很容易造成緩沖溢出。

通過上面對于CString機制和一些容易出現的使用錯誤的描述,可以使我們更好的使用CString。

?

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/benny5609/archive/2007/12/09/1926126.aspx

轉載于:https://www.cnblogs.com/liwqiang/archive/2010/02/23/1672067.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/276181.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/276181.shtml
英文地址,請注明出處:http://en.pswp.cn/news/276181.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何從零開始開發一個 Chrome 插件?

什么是瀏覽器插件&#xff1f;簡單來說瀏覽器插件&#xff0c;是瀏覽器上的一種工具&#xff0c;可以提供一些瀏覽器沒有的功能&#xff0c;幫你做一些有趣的事情。開發者可以根據自己的喜歡&#xff0c;去實現一些功能。插件基于Web技術&#xff08;html、css、js&#xff09;…

mysql 重復字段查詢及排除重復值

轉載鏈接&#xff1a;http://blog.sina.com.cn/s/blog_3edc5e2e010131ys.html mysql 重復字段查詢及排除重復值 SELECT a.id,a.title FROM dede_archives a left join dede_taglist t on t.taga.title WHERE t.typeid$id and t.arcrank>-1 and a.typeid28 group by t.tag; …

swiper移入暫停_react中swiper注意事項及鼠標劃入停止輪播

首先是實例化swiper這里有一個注意點&#xff0c;就是實例化的時機如果你的swiper內容是寫死的&#xff0c;可以在componentDidMount中實例化&#xff0c;但是如果你的內容是通過接口異步請求過來的&#xff0c;就必須在componentDidUpdate里實例化&#xff0c;因為如果在 comp…

轉Excel的一種簡單方法

寫了這么久的程序﹐越來越喜歡那種簡單的解決方法﹐這段時間在做一個報表系統﹐其中有需要轉Excel﹐而且要求兼容openoffice﹐遂利用asp語法,asp.net的控件封裝特性以及excel 2003的xml試算清格式做了一個看起來比較"清爽"的excel轉檔方案。一.開始原理很簡單﹐excel…

詳解MySQL中EXPLAIN解釋命令

轉載鏈接&#xff1a;http://database.51cto.com/art/200912/168453.htm explain顯示了mysql如何使用索引來處理select語句以及連接表。可以幫助選擇更好的索引和寫出更優化的查詢語句。 使用方法&#xff0c;在select語句前加上explain就可以了&#xff1a; 如&#xff1a;…

Shell編程基礎

我們可以使用任意一種文字編輯器&#xff0c;比如gedit、kedit、emacs、vi等來編寫shell腳本&#xff0c;它必須以如下行開始&#xff08;必須放在文件的第一行&#xff09;&#xff1a; # !/bin/sh ...注意&#xff1a;最好使用“!/bin/bash”而不是“!/bin/sh”&#xff0c;…

總結:自學前端的高效學習路線

提到前端&#xff0c;大多數人都會想到薪資高。也正因為如此&#xff0c;很多人想要從事前端開發這個崗位&#xff0c;也由此衍生出來一個問題&#xff1a;為什么前端工程師供不應求&#xff0c;但還是有很多學前端的人找不到工作&#xff1f;其實行業不是缺前端工程師&#xf…

機器人出魔切還是三相_英雄聯盟:輔助也要去上單,機器人布里茨玩法介紹

英雄聯盟&#xff1a;輔助也要去上單&#xff0c;機器人布里茨玩法介紹出裝方面我們都知道他的被動是可以將法力值化為機的護盾的&#xff0c;而這樣的話裝備就可以選擇魔切&#xff0c;然后再出一個鞋子&#xff0c;為什么不先出三項呢&#xff1f;三項的性價比是比較高的&…

vmware創建虛擬機不識別網卡

今天在給虛擬機添加網卡的時候&#xff0c;出現了虛擬機不識別新加的網卡&#xff0c;很納悶&#xff0c;連的一樣的端口組&#xff0c;為什么新加的網卡識別不了呢 然后查看pci設備&#xff0c;發現網卡的驅動為 AMD 79C970 PCnet32- LANCE 然后都vc上查看&#xff0c;果真驅動…

轉:26個Jquery使用小技巧(jQuery tips, tricks solutions)

26個Jquery使用小技巧(jQuery tips, tricks & solutions) 前段時間發布了Jquery類庫1.4版本&#xff0c;使用者也越來越多&#xff0c;為了方便大家對Jquery的使用&#xff0c;下面列出了一些Jquery使用技巧。比如有禁止右鍵點擊、隱藏搜索文本框文字、在新窗口中打開鏈接…

周末包郵送書和小紅包中獎名單公布

大家好&#xff0c;我是若川。周末送福利&#xff0c;給大家送紅包、包郵送新書&#xff01;這篇文章中&#xff0c;準備了3本自選前端新書&#xff0c;10個2元小紅包&#xff0c;在看抽10人每人5元紅包&#xff0c;2月28日晚8點開獎。現將名單公布如下&#xff1a;在看抽獎&am…

Ubuntu 命令行修改網絡配置方法

轉載鏈接&#xff1a;http://www.jb51.net/article/15807.htm Ubuntu 命令行修改網絡配置方法 /etc/network/interfaces 打開后里面可設置DHCP或手動設置靜態ip。前面auto eth0&#xff0c;讓網卡開機自動掛載. 1. 以DHCP方式配置網卡 編輯文件/etc/network/interfaces: sudo v…

python treeview底部加個按鈕_Python爬取京東商品信息(GUI版本)

前言本文的文字及圖片來源于網絡,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理。作者&#xff1a;DYblog轉載&#xff1a;https://www.cnblogs.com/dy8888/p/13257918.htmlPS&#xff1a;如有需要Python學習資料的小伙伴可以加點擊…

Linux下編譯安裝Mysql簡單步驟

常規方式編譯安裝MySQL時&#xff0c;適合用第一條最正宗的MySQL產品線5.2及以前版本&#xff1a;所謂常規方式編譯安裝MySQL就是延續早期MySQL的3部曲安裝方式&#xff0c;即./configure;make;make install&#xff0c;下面是老男孩在早期的企業生產場景下操作過的具體命令及參…

Vue.js 3.0 響應式 API 比 2.x 好在哪兒?

Hello&#xff0c;各位小伙伴&#xff0c;接下來的一段時間里&#xff0c;我會把我的課程《Vue.js 3.0 核心源碼解析》中問題的答案陸續在我的公眾號發布&#xff0c;由于課程的問題大多數都是開放性的問題&#xff0c;所以我的答案也不一定是標準的&#xff0c;僅供你參考喔。…

招聘.NET程序員

人才難找啊&#xff0c;順便發個招聘啟事。 西安瀚博科技有限公司招聘.NET程序員&#xff0c;有工作經驗者優先 如有意向&#xff0c;請發郵件到 slzhanghiweb.cn 轉載于:https://www.cnblogs.com/shengli/archive/2010/03/08/1680861.html

xml解析類

轉載鏈接&#xff1a;http://zyan.cc/post/253 今天在PHP4環境下重新寫一個接口程序&#xff0c;需要大量分析解析XML&#xff0c;PHP的xml_parse_into_struct()函數不能直接生成便于使用的數組&#xff0c;而SimpleXML擴展在PHP5中才支持&#xff0c;于是逛逛搜索引擎&#x…

jmeter學習指南之聚合報告

jmeter視頻地址&#xff1a;https://edu.51cto.com/course/14305.html 上一篇文章中我們講了Jmeter結果分析最常用的一個Listener查看結果樹&#xff0c;今天接著講另一個最常用的listener--聚合報告Aggregate Report。我們先來看看聚合報告中的主要名稱的含意&#xff1a;Labe…

敏捷開發概述

敏捷方法強調適應性而非預見性。 目前列入敏捷方法的有&#xff1a; 軟件開發節奏&#xff0c;Software Development Rhythms 敏捷數據庫技術&#xff0c;AD/Agile Database Techniques 敏捷建模&#xff0c;AM/Agile Modeling 自適應軟件開發&#xff0c;ASD/Adaptive Softwar…

2021 整理的最全學習資源,送給每一個努力著的人

時間來到了 2021 年&#xff0c;新的一年有新的期待&#xff0c;而我亦有新的祝福如果說在過去的一年&#xff0c;經歷太多&#xff0c;心酸、迷茫、焦慮、幸福、喜悅那么在 2021 年&#xff0c;希望你可以去過一種遇見自己的生活&#xff0c;恬淡、熱情&#xff0c;喜歡自己而…