[MFC]对CString::GetBufferSetLength方法的探究

在使用CStringGetBufferSetLength方法时,遇到了一个问题,代码如下:

CString path;

::GetCurrentDirectory(MAX_PATH, path.GetBufferSetLength(MAX_PATH));

path.Append(TEXT("\\SubDir"));

wprintf(TEXT("%s"), path);

这段代码的输出始终是E:\Projects\Tests\Win32Console,也就是GetCurrentDirectory函数返回的结果,而通过Append方法添加的字符串不见踪影,换用+=操作符也是一样。

 

如果将GetBufferSetLength方法换成GetBuffer方法:

CString path;

::GetCurrentDirectory(MAX_PATH, path.GetBuffer (MAX_PATH));

path.Append(TEXT("\\SubDir"));

wprintf(TEXT("%s"), path);

那么输出变成了\SubDirGetCurrentDirectory函数返回的结果倒不见了。

 

查阅MSDN,发现原来调用了GetBuffer或者GetBufferSetLength方法并且修改了缓冲区里的内容之后,如果要调用其它的方法,就要先调用ReleaseBuffer或者ReleaseBufferSetLength方法。那么就在上面的代码中添加对ReleaseBuffer的调用:

CString path;

::GetCurrentDirectory(MAX_PATH, path.GetBufferSetLength(MAX_PATH));

path.ReleaseBuffer();

path.Append(TEXT("\\SubDir"));

wprintf(TEXT("%s"), path);

 

CString path;

::GetCurrentDirectory(MAX_PATH, path.GetBuffer (MAX_PATH));

path.ReleaseBuffer();

path.Append(TEXT("\\SubDir"));

wprintf(TEXT("%s"), path);

修改后的输出为E:\Projects\Tests\Win32Console\SubDirAppend方法终于见效了。

 

那么,GetBufferGetBufferSetLength有什么不同?ReleaseBuffer方法又做了什么事情呢?不妨作以下的猜想:

1.CString对象中,字符串的长度信息保存在一个成员变量中(假设为m_length),需要获取字符串长度的时候直接读取这个值。

2.GetBuffer分配了新的内存,但不改变m_length的值;GetBufferSetLength 也分配新的内存,同时将m_length的值设置为参数中指定的值。

3.GetCurrentDirectory方法将工作目录的路径直接写入path对象的内存,m_length的值没有改变。这时,使用GetBuffer的版本m_length值为0,使用GetBufferSetLength的版本m_length值为MAX_PATH

4.Append方法根据m_length的值将参数中的字符串复制到path对象的内存中。GetBuffer版本中,由于m_length值为0,所以“\SubDir”被复制到字符串的开头,覆盖了原来的内容;GetBufferSetLength版本中,m_length的值为MAX_PATH,“\SubDir”被复制到从第MAX_PATH个字符开始的位置,所以在输出中看不到。

5.不带参数的ReleaseBuffer方法查找字符串中的’\0’字符,确定字符串的长度,并修改m_length的值,此后m_length的值是正确的,因此Append方法可以正常调用。

 

为了证明上面的猜想是正确的,我对上面的两段代码进行了跟踪调试。为了突出重点,下面的代码都进行了裁剪,省略了不是很重要的部分。首先是GetBuffer方法的实现:

PXSTR GetBuffer(int nMinBufferLength) {

    return( PrepareWrite( nMinBufferLength ) );

}

 

看到GetBuffer方法调用了PrepareWrite方法,再看PrepareWrite方法的实现:

PXSTR PrepareWrite(int nLength) {

    CStringData* pOldData = GetData();

    int nShared = 1-pOldData->nRefs;

    int nTooShort = pOldData->nAllocLength-nLength;

    if( (nShared|nTooShort) < 0 ) {

       PrepareWrite2( nLength );

    }

    return( m_pszData );

}

 

在进行了检查之后调用了PrepareWrite2方法,继续进入该方法:

void PrepareWrite2(int nLength) {

    CStringData* pOldData = GetData();

    if( pOldData->nDataLength > nLength ) {

       nLength = pOldData->nDataLength;

    }

    if( pOldData->IsShared() ) {

       Fork( nLength );

    }

    else if( pOldData->nAllocLength < nLength )   {

        // 其它代码

    }

}

 

PrepareWrite2 方法又调用了Fork方法,再深入:

void Fork(int nLength)   {

    CStringData* pOldData = GetData();

    int nOldLength = pOldData->nDataLength;

    //分配新的缓冲区

    CStringData* pNewData = pOldData->pStringMgr->Clone()->Allocate( nLength, sizeof( XCHAR ) );

    //复制字符

    int nCharsToCopy = ((nOldLength < nLength) ? nOldLength : nLength)+1;

    CopyChars( PXSTR( pNewData->data() ), nCharsToCopy,

       PCXSTR( pOldData->data() ), nCharsToCopy );

//设置字符串长度

    pNewData->nDataLength = nOldLength;

    //释放原来的缓冲区

    pOldData->Release();

    //保存新的缓冲区指针

    Attach( pNewData );

}

 

可以看到在Fork方法中分配了新的缓冲区,并将原来缓冲区的内容复制到新缓冲区中,以此实现了扩大缓冲区。通过观察Fork的代码,可以证实猜想的第一点是正确的,CString对象确实是通过一个成员变量保存字符串长度。要注意的是Fork方法没有改变字符串的长度。总结GetBuffer方法的调用路径:GetBuffer->PreWrite->PreWrite2->Fork

 

接下来再看GetBufferSetLength方法的实现:

PXSTR GetBufferSetLength(int nLength) {

    PXSTR pszBuffer = GetBuffer( nLength );

    SetLength( nLength );

    return( pszBuffer );

}

 

该方法调用了GetBuffer,然后调用SetLength方法。进入该方法:

void SetLength(int nLength) {

    GetData()->nDataLength = nLength;

    m_pszData[nLength] = 0;

}

除去一些检查语句,SetLength方法实质上只有两条语句,第一条语句设置字符串的长度,第二条语句在字符串的末尾添加字符串结束标记,也就是’\0’字符。

 

现在可以看出GetBufferGetBufferSetLength的区别了:除了改变缓冲区的大小之外,GetBuffer不改变字符串的长度,而GetBuffefrSetLength会将字符串的长度设置为缓冲区的大小。

 

接下来再看看Append方法的实现:

void Append(PCXSTR pszSrc) {

    Append( pszSrc, StringLength( pszSrc ) );

}

 

void Append(PCXSTR pszSrc, int nLength) {

    //获取原字符串的长度

    UINT_PTR nOffset = pszSrc-GetString();

    UINT nOldLength = GetLength();

    //获取待添加字符串的长度

    nLength = StringLengthN(pszSrc, nLength);

    //计算新字符串的长度

    int nNewLength = nOldLength+nLength;

    //分配新的缓冲区

    PXSTR pszBuffer = GetBuffer( nNewLength );

    //复制字符串

    CopyChars( pszBuffer+nOldLength, nLength, pszSrc, nLength );

    //设置字符串长度

    ReleaseBufferSetLength( nNewLength );

}

 

可以看到Append方法确实是通过字符串的长度来操作的。

 

下面再来看看ReleaseBufferReleaseBufferSetLength的实现:

void ReleaseBuffer(int nNewLength = -1) {

    if( nNewLength == -1 ) {

       int nAlloc = GetData()->nAllocLength;

       nNewLength = StringLengthN( m_pszData, nAlloc);

    }

    SetLength( nNewLength );

}

 

void ReleaseBufferSetLength(int nNewLength) {

    SetLength( nNewLength );

}

 

如果参数值不为-1的话,ReleaseBufferReleaseBufferSetLength一样只是简单地设置字符串的长度。如果对ReleaseBuffer传入-1或者不传参数的话,它将调用StringLengthN获取字符串的长度,然后通过SetLength设置字符串长度。

 

现在可以证实猜想是正确的了。通过分析CString的源代码,我对其工作方式有了更深的了解,于是写下本文,希望对大家有所帮助。

你可能感兴趣的:(String)