在使用CString的GetBufferSetLength方法时,遇到了一个问题,代码如下:
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);
那么输出变成了\SubDir,GetCurrentDirectory函数返回的结果倒不见了。
查阅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\SubDir,Append方法终于见效了。
那么,GetBuffer和GetBufferSetLength有什么不同?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’字符。
现在可以看出GetBuffer和GetBufferSetLength的区别了:除了改变缓冲区的大小之外,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方法确实是通过字符串的长度来操作的。
下面再来看看ReleaseBuffer和ReleaseBufferSetLength的实现:
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的话,ReleaseBuffer和ReleaseBufferSetLength一样只是简单地设置字符串的长度。如果对ReleaseBuffer传入-1或者不传参数的话,它将调用StringLengthN获取字符串的长度,然后通过SetLength设置字符串长度。
现在可以证实猜想是正确的了。通过分析CString的源代码,我对其工作方式有了更深的了解,于是写下本文,希望对大家有所帮助。