CString實現原理簡單介紹

來源:互聯網
上載者:User
 

很多的程式,發現很大的一部分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。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.