代码改变世界

ATL2.1版的CString分析

2011-06-12 07:35  menggucaoyuan  阅读(1364)  评论(0编辑  收藏  举报
       经常使用MFC做界面开发的人,可能会经常使用CString这个字符串类。个人也经常使用这个类,前几天在翻阅ATL2.1的源码时,发现了CString类的源码,就顺便把它解剖下,虽然它比起现在高级版本的CString类,还比较原始,但是原理应该是一致的。

介绍CString类之前先介绍下结构体CStringData和几个全局变量。

结构体CStringData保存了CString类的空间大小和引用信息,相当于信息头,而CStringm_pchData指向了实际数据区域。如果多个字符串的数据一样,可以让他们的对象引用一个数据空间,CString类就是这样实现的,其中的信息头就保存了引用次数。当然使用CString类的人不看源码的话,不会知道这些事实,他们会感觉使用两个不同的数据空间。

用户每次访问CString的某段数据时,程序就需要根据CStringData保存的信息头内容先确定这段数据的存在性。其源码如下:

struct CStringData

{

    long nRefs;     // reference count   //被引用次数,实际上多份CString的指针可能指向同一个内存空间

    int nDataLength;                  //已占用的内存空间大小

    int nAllocLength;                 //实际分配的内存空间大小

    // TCHAR data[nAllocLength]

    TCHAR* data()                  //信息头后面的就是数据

        { return (TCHAR*)(this+1); }   //等效于(BYTE*)rgInitData + 12

};

全局数据、全局变量和全局函数如下:

// afxChNil is left for backward compatibility

_declspec(selectany) TCHAR afxChNil = '\0';      //代表CStringNULL时的情形

                //_declspec(selectany)

// For an empty string, m_pchData will point here

// (note: avoids special case of checking for NULL m_pchData)

// empty string data (and locked)

_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 }; 

_declspec(selectany) CStringData* afxDataNil = (CStringData*)&rgInitData;

_declspec(selectany) LPCTSTR afxPchNil = (LPCTSTR)(((BYTE*)&rgInitData)+sizeof(CStringData));

inline const CString& __stdcall AfxGetEmptyString()

    { return *(CString*)&afxPchNil; }

#define afxEmptyString AfxGetEmptyString()

以上代码中,最重要的是数组rgInitData。当一个CString的实例初始化时没有分配内存空间时,它会引用这段内存空间,或者一个CString实例的内存空间被释放时,即调用Release函数后,它也会再次引用这段内存空间。所以一个CString实例引用这段内存空间的话,基本上可以断定它是一个空的实例。rgInitData的前三个元素保存了CString的内存空间区域信息,这三个元素占用的内存空间大小与CStringData的大小一致,所以它们实际上构成了结构体CStringData的一个对象, rgInitData[0]等效于nRefs,而rgInitData[1]相当于nDataLength,则rgInitData[2]就是nAllocLength了。


下面开始逐步介绍CString一些有用的函数的源码:

1 CString实例的初始化时的内存空间的分配以及被删除时其内存空间的释放

下面介绍CString类的初始过程。如果CString初始化时,被指定的长度为零,则它的指针就会指向rgInitData[3],以作为初始空间。这个功能是有函数Init完成,它会被AllocBuffer调用。Init代码如下:

inline void CString::Init()

    { m_pchData = afxEmptyString.m_pchData; }

inline CString::CString()

{

    Init();

}

CString在第一次初始化的时候,一般要先分配一定大小的空间,这个功能是由函数AllocBuffer完成的,代码如下:

inline BOOL CString::AllocBuffer(int nLen)

// always allocate one extra character for '\0' termination

// assumes [optimistically] that data length will equal allocation length

{

    ATLASSERT(nLen >= 0);

    ATLASSERT(nLen <= INT_MAX-1);    // max size (enough room for 1 extra)

    if (nLen == 0)

        Init();

    else

    {

        UINT SizeToAlloc = nLen;

        CStringData* pData = NULL;

        if (SizeToAlloc > SizeToAlloc+1) {//类似的无用代码很多,不明白M$写这种代码是干嘛用

            return FALSE;

        }

        SizeToAlloc++;  //加一,最后一个空间分配给结尾符号'\0'

        if (SizeToAlloc > (SizeToAlloc * sizeof(TCHAR))) {

            return FALSE;

        }

        SizeToAlloc *= sizeof(TCHAR);  //实际要分配的内存空间的大小

        //最后加上自己的信息头空间

        if (SizeToAlloc > (SizeToAlloc + sizeof(CStringData))) {

            return FALSE;

        }

        SizeToAlloc += sizeof(CStringData);

        ATLTRY(pData = (CStringData*)new BYTE[SizeToAlloc]);

        if(pData == NULL)

            return FALSE;

        //注意以下四行代码,CString有自己的数据空间,就要有自己的信息头,不再使用全局的rgInitData

        pData->nRefs = 1;          

        pData->data()[nLen] = '\0';

        pData->nDataLength = nLen;

        pData->nAllocLength = nLen;

        m_pchData = pData->data();

    }

    return TRUE;

}

CString的一个实例的空间分配完成后,如果别的实例要拷贝这个实例的数据,CString类会让下一个实例引用这个实例。

inline CString::CString(const CString& stringSrc)

{

    ATLASSERT(stringSrc.GetData()->nRefs != 0);

    if (stringSrc.GetData()->nRefs >= 0)

    {

       //stringSrc引用this

        ATLASSERT(stringSrc.GetData() != afxDataNil);

        m_pchData = stringSrc.m_pchData;

        InterlockedIncrement(&GetData()->nRefs);

    }

    else

    {

        Init();

        *this = stringSrc.m_pchData;

    }

}

inline const CString& CString::operator=(const CString& stringSrc)

{

    if (m_pchData != stringSrc.m_pchData) //防止自拷贝

    {

        //如果目标对象的数据空间已经分配,而且没有别的CString实例指向它,则把stringSrc的数据拷贝过来;

        //或者,如果stringSrc的数据的引用次数小于零,则把它的数据拷贝过来

        if ((GetData()->nRefs < 0 && GetData() != afxDataNil) ||

            stringSrc.GetData()->nRefs < 0)

        {

            // actual copy necessary since one of the strings is locked

            AssignCopy(stringSrc.GetData()->nDataLength, stringSrc.m_pchData);

        }

        else

        {

            //其他情况,就先自己的数据空间释放掉,然后再指向stringSrc的数据空间

            // can just copy references around

            Release();

            ATLASSERT(stringSrc.GetData() != afxDataNil);

            m_pchData = stringSrc.m_pchData;

            InterlockedIncrement(&GetData()->nRefs);  //引用次数增1,这里为了防止多线程情况下出错,使用了原子性自增

        }

    }

    return *this;

}

inline void CString::AssignCopy(int nSrcLen, LPCTSTR lpszSrcData)

{

    if(AllocBeforeWrite(nSrcLen))

    {

        memcpy(m_pchData, lpszSrcData, nSrcLen*sizeof(TCHAR));

        GetData()->nDataLength = nSrcLen;

        m_pchData[nSrcLen] = '\0';

    }

}

上面分析了CString数据空间的分配,下面的Release()函数则完成了对这些数据空间的释放。

inline void CString::Release()

{

    if (GetData() != afxDataNil)  //确保不会释放全局的rgInitData

    {  //释放空间的条件是没有CString实例指向这个数据空间

        ATLASSERT(GetData()->nRefs != 0);

        if (InterlockedDecrement(&GetData()->nRefs) <= 0)  //注意,在此执行了引用次数减1操作

            delete[] (BYTE*)GetData();

        Init();   //数据释放后,让CString对象重新指向全局的数据空间rgInitData

    }

}

一般释放CString实例的内存空间的时候,可以使用函数Empty()释放内存空间,不要调用Release()函数,因为Empty()本身会调用Release()函数释放内存空间,然后会做一些"善后处理"工作。

inline void CString::Empty()

{

    if (GetData()->nDataLength == 0)

        return;

    if (GetData()->nRefs >= 0)

        Release();

    else

        *this = &afxChNil;

    ATLASSERT(GetData()->nDataLength == 0);

    ATLASSERT(GetData()->nRefs < 0 || GetData()->nAllocLength == 0);

}

inline void PASCAL CString::Release(CStringData* pData)

{

    if (pData != afxDataNil)   //确保不会释放全局的内存空间

    {

        ATLASSERT(pData->nRefs != 0);

        if (InterlockedDecrement(&pData->nRefs) <= 0)  //没有实例引用这段内存,则释放之

            delete[] (BYTE*)pData;

    }

}

还记得CStingData的两个成员nAllocLengthnDataLength两个成员变量吗?nDataLength是已使用的存放了数据的内存空间的大小,而nAllocLength则存放了为这个实例预分配的内存空间的大小。如果nAllocLength大于nDataLength,而且你想释放这点额外的空间,则可以通过函数FreeExtra实现。

inline void CString::FreeExtra()

{

    ATLASSERT(GetData()->nDataLength <= GetData()->nAllocLength);

    if (GetData()->nDataLength != GetData()->nAllocLength)

    {

        CStringData* pOldData = GetData();

        if(AllocBuffer(GetData()->nDataLength))

        {

            memcpy(m_pchData, pOldData->data(), pOldData->nDataLength*sizeof(TCHAR));

            ATLASSERT(m_pchData[GetData()->nDataLength] == '\0');

            CString::Release(pOldData);

        }

    }

    ATLASSERT(GetData() != NULL);

}


2 CString实例的内存数据的调用

在使用CString类时,常用的函数之一是GetBuffer,以获取CString的数据。经常会建议你在使用这个函数后,紧跟着调用ReleaseBuffer函数,以免造成内存泄露。下面看下这两个函数的源码:

inline LPTSTR CString::GetBuffer(int nMinBufLength)

{

    ATLASSERT(nMinBufLength >= 0);

    //从以下if条件来看,如果以GetBuffer()形式调用,则不会引发新的内存空间的申请与数据拷贝

    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;  //保证存储空间的最小值,保证数据不丢失

        if(AllocBuffer(nMinBufLength))  //分配新的空间

        {

            memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR)); //数据拷贝

            GetData()->nDataLength = nOldLen;

            CString::Release(pOldData);  //释放旧有的数据空间

        }

    }

    ATLASSERT(GetData()->nRefs <= 1);

    // return a pointer to the character storage for this string

    ATLASSERT(m_pchData != NULL);

    return m_pchData;

}

如果用户是以GetBuffer(nLen)形式调用而且nLen 小于CString实例的原有字符串的长度,下面的这个函数就显得很有必要了。

inline void CString::ReleaseBuffer(int nNewLength)

{

    CopyBeforeWrite();  // just in case GetBuffer was not called

    if (nNewLength == -1)

        nNewLength = lstrlen(m_pchData); // zero terminated

    ATLASSERT(nNewLength <= GetData()->nAllocLength);

    GetData()->nDataLength = nNewLength;

    m_pchData[nNewLength] = '\0';  //指定新的正确的结尾

}

另一个比较重要的函数是GetData(), 它的返回值是CString的信息头,即存储CStringData的那一部分。

inline CStringData* CString::GetData() const

{ ATLASSERT(m_pchData != NULL); return ((CStringData*)m_pchData)-1; }


3 CString实例的数据保护

由于CString允许多个实例的数据存储在同一个内存空间,所以必定存在多个实例同时访问一段内存空间的情况,为了防止“读时写”发生写出了""数据的情况,可以使用写前拷贝技术防止这种情况发生。CString是通过CopyBeforeWrite()函数实现的,一般在CString的一个实例要改变其内存空间的数据时,它会检查已有的内存空间的引用次数是否大于1,如果大于1则先把已有的数据通过复制保护起来,然后再为这个CString的实例分配新的内存空间。

inline void CString::CopyBeforeWrite()

{

    if (GetData()->nRefs > 1)

    {

        CStringData* pData = GetData();

        Release();  //引用次数减1,即这个CString实例(this)不再引用这段内存空间的数据

        if(AllocBuffer(pData->nDataLength))

            memcpy(m_pchData, pData->data(), (pData->nDataLength+1)*sizeof(TCHAR));  //把旧有的数据拷贝过来

    }

    ATLASSERT(GetData()->nRefs <= 1);

}

类似的还有AllocBeforeWrite,比较类似于CopyBeforeWrite,这个函数为Cstring的实例分配空间后,不会复制已有的内存的数据。

inline BOOL CString::AllocBeforeWrite(int nLen)

{

    BOOL bRet = TRUE;

    if (GetData()->nRefs > 1 || nLen > GetData()->nAllocLength)

    {

        Release();

        bRet = AllocBuffer(nLen);

    }

    ATLASSERT(GetData()->nRefs <= 1);

    return bRet;

}

CString还提供了一对加锁函数LockBuffer/ReleaseBuffer,它们分别可以把对数据的引用次数设置为-1和设置为1

inline LPTSTR CString::LockBuffer()

{

    LPTSTR lpsz = GetBuffer(0);

    GetData()->nRefs = -1;

    return lpsz;

}

inline void CString::UnlockBuffer()

{

    ATLASSERT(GetData()->nRefs == -1);

    if (GetData() != afxDataNil)

        GetData()->nRefs = 1;

}

如果出现以下代码:

CString str1(_T("hello"));

CString str2();

str1.LockBuffer();

str2 = str1;  //此时str2不会引用str1的内存空间,operator =()函数为str2开辟新的内存空间,然后拷贝str1的数据