对于使用MFC的Coder来说,CString肯定是一个经常要用到的类,它封装了一个字符串应具有的大部分功能,使得MFC的字符串操作不像C中那么痛苦。
而文本旨在通过对CString类源代码的一些分析,使得大家对于这个类具有更深入的了解,避免在以后使用过程中出错。
在进入正题前,要说点题外话:为什么是VC.NET的CString类?
答案很简单,在M$将MFC从4.X(VC6)升级到7.X(VC.NET)开始,原来的CString被重新设计以便与ATL共用,并且支持了template特性。而由于VC6对于C++标准支持差得令人发指,成为众矢之的,且逐渐式微,将成明日黄花,故这里分析VC.NET的CString类
1. 亲子疑团之谁是我爹
在新的MFC体系中,CString并不是一个实际存在的类,实际上存在的是CSimpleStringT和CStringT,而我们常用的CString只是一个typedef-class。
这种变动,无疑让CString的身世显得扑朔迷离。
CString童鞋内牛满面,不禁高呼:你们到底谁TM是俺爹?
CString是谁呢?呃,在经过一番从CString到它祖宗十八代的调查后,我们发现:CString的爹,其实是CSimpleStringT。而CStringT,则是CString的前世
2.亲子疑团之混乱的关系
CSimpleString是CString体系变化的一个代表。
在过去,CString实际上是个独立的类,据说连万物之祖——CObject和它都没有半毛钱关系。CString一个人负责内存管理、字符串操作.etc
而到了新体系下,AFX小组为了更好地与ATL重用,将CString功能分拆成两块:基本的内存管理和缓存操作以及高级的字符串处理。CSimpleStringT便是负责前者。
至于CStringT,便继承自CSimpleStringT,然后封装了高级的字符串处理。
那么,我们为什么说CStringT是CString的前世呢?因为下面一行代码
typedef ATL::CStringT< TCHAR, StrTraitMFC< TCHAR > > CString;
对于采用了模板的类来说,这种typedef并不少见,std::string也是一个typedef-class
PS1:CStringT最初在ATL中设计,并有了MFC支持版(二者应该区别不大),于是编译器分别提供了cstringt.h和atlstr.h
PS2:即使是现在,CSimpleStringT也是独立的类,与CObject仍然没有半毛钱关系
3.神秘的老爸——CSimpleStringT
CSimpleStringT在atlsimplestr.h中,而且是一个比较大的类。CSimpleStringT大多数函数都有文档说明了,我也没必要做无用功,所以我们来看一些没有文档说明的东西
CSimpleStringT为一些常用的字符/字符串类型定义了别名,便于使用
public: typedef typename ChTraitsBase< BaseType >::XCHAR XCHAR; typedef typename ChTraitsBase< BaseType >::PXSTR PXSTR; typedef typename ChTraitsBase< BaseType >::PCXSTR PCXSTR; typedef typename ChTraitsBase< BaseType >::YCHAR YCHAR; typedef typename ChTraitsBase< BaseType >::PYSTR PYSTR; typedef typename ChTraitsBase< BaseType >::PCYSTR PCYSTR;
ChTraitsBase类的信息如下:
template< typename BaseType = char >
class ChTraitsBase
{
public:
typedef char XCHAR;
typedef LPSTR PXSTR;
typedef LPCSTR PCXSTR;
typedef wchar_t YCHAR;
typedef LPWSTR PYSTR;
typedef LPCWSTR PCYSTR;
};
template<>
class ChTraitsBase< wchar_t >
{
public:
typedef wchar_t XCHAR;
typedef LPWSTR PXSTR;
typedef LPCWSTR PCXSTR;
typedef char YCHAR;
typedef LPSTR PYSTR;
typedef LPCSTR PCYSTR;
};
其实偌大一个的CSimpleStringT只有一个成员变量,你猜得没错,就是指向字封装的符串的指针
PXSTR m_pszData;
到这里你可能会有一个很大的疑问:既然CString是带引用计数的,那么这个东西被放到哪里了呢?
答案在CStringData这个结构上,CSimpleStringT并不直接管理字符串内存和引用计数这些,而是利用CStringData来管理
CStringData是一个struct,这意味着无论是member-varialbes还是member-functions,都是public的
struct CStringData
{
IAtlStringMgr* pStringMgr; // String manager for this CStringData
int nDataLength; // Length of currently used data in XCHARs (not including terminating null)
int nAllocLength; // Length of allocated data in XCHARs (not including terminating null)
long nRefs; // Reference count: negative == locked
// XCHAR data[nAllocLength+1] // A CStringData is always followed in memory by the actual array of character data
void* data() throw()
{
return (this+1);
}
void AddRef() throw()
{
ATLASSERT(nRefs > 0);
_AtlInterlockedIncrement(&nRefs);
}
bool IsLocked() const throw()
{
return nRefs < 0;
}
bool IsShared() const throw()
{
return( nRefs > 1 );
}
void Lock() throw()
{
ATLASSERT( nRefs <= 1 );
nRefs--; // Locked buffers can't be shared, so no interlocked operation necessary
if( nRefs == 0 )
{
nRefs = -1;
}
}
void Release() throw()
{
ATLASSERT( nRefs != 0 );
if( _AtlInterlockedDecrement( &nRefs ) <= 0 )
{
pStringMgr->Free( this );
}
}
void Unlock() throw()
{
ATLASSERT( IsLocked() );
if(IsLocked())
{
nRefs++; // Locked buffers can't be shared, so no interlocked operation necessary
if( nRefs == 0 )
{
nRefs = 1;
}
}
}
};
我们见到了熟悉的引用计数,而且CStringData并不直接操作nRef,而是通过几个API。这么做是基于多线程情况下,保证计数是真正正确的操作。
Lock和Unlock用于在不需要计数的情况下锁定和解锁计数
我们现在来看一个大头:Data
代码很简单,只有一行,return (this + 1)
但是你确定你自己明白这背后的意义么?
this+1指的不是this地址加上一个地址偏移量,而是this地址加上一个对象类型大小的偏移量
也就是说,这个返回的是紧跟CStringData首地址后面的首地址,其实就是CSimpleStringT的m_pszData的地址。
为什么?这得从CString在内存中的存储说起,简言之如下图所示
在CStringData里,还有一个IAtlStringMgr的接口指针
IAtlStringMgr是ATL接口(接口只定义名字,不定义行为,和.NET中类似)
__interface IAtlStringMgr
{
public:
// Allocate a new CStringData
CStringData* Allocate( int nAllocLength, int nCharSize ) throw();
// Free an existing CStringData
void Free( CStringData* pData ) throw();
// Change the size of an existing CStringData
CStringData* Reallocate( CStringData* pData, int nAllocLength, int nCharSize ) throw();
// Get the CStringData for a Nil string
CStringData* GetNilString() throw();
IAtlStringMgr* Clone() throw();
};
这个东西定义了字符串缓冲区的管理函数,在atlstr.h中有一个类CAtlStringMgr继承并实现了这个接口
// IAtlStringMgr
public:
_Ret_opt_bytecap_x_(sizeof(CStringData) + nChars*nCharSize) virtual CStringData* Allocate( _In_ int nChars, _In_ int nCharSize ) throw()
{
size_t nTotalSize;
CStringData* pData;
size_t nDataBytes;
nChars = AtlAlignUp( nChars + 1, 8 ); // Prevent excessive reallocation. The heap will usually round up anyway.
if( FAILED(::ATL::AtlMultiply(&nDataBytes, static_cast<size_t>(nChars), static_cast<size_t>(nCharSize))) ||
FAILED(::ATL::AtlAdd(&nTotalSize, static_cast<size_t>(sizeof( CStringData )), nDataBytes)))
{
return NULL;
}
pData = static_cast< CStringData* >( m_pMemMgr->Allocate( nTotalSize ) );
if( pData == NULL )
{
return( NULL );
}
pData->pStringMgr = this;
pData->nRefs = 1;
pData->nAllocLength = nChars - 1;
pData->nDataLength = 0;
return( pData );
}
virtual void Free( _Inout_ CStringData* pData ) throw()
{
ATLASSERT( pData->pStringMgr == this );
m_pMemMgr->Free( pData );
}
_Ret_opt_bytecap_x_(sizeof(CStringData) + nChars*nCharSize) virtual CStringData* Reallocate( _Inout_ CStringData* pData, _In_ int nChars, _In_ int nCharSize ) throw()
{
CStringData* pNewData;
ULONG nTotalSize;
ULONG nDataBytes;
ATLASSERT( pData->pStringMgr == this );
nChars = AtlAlignUp( nChars+1, 8 ); // Prevent excessive reallocation. The heap will usually round up anyway.
if( FAILED(::ATL::AtlMultiply(&nDataBytes, static_cast<ULONG>(nChars), static_cast<ULONG>(nCharSize))) ||
FAILED(::ATL::AtlAdd(&nTotalSize, static_cast<ULONG>(sizeof( CStringData )), nDataBytes)))
{
return NULL;
}
pNewData = static_cast< CStringData* >( m_pMemMgr->Reallocate( pData, nTotalSize ) );
if( pNewData == NULL )
{
return NULL;
}
pNewData->nAllocLength = nChars - 1;
return pNewData;
}
virtual CStringData* GetNilString() throw()
{
m_nil.AddRef();
return &m_nil;
}
virtual IAtlStringMgr* Clone() throw()
{
return this;
}
protected:
IAtlMemMgr* m_pMemMgr;
CNilStringData m_nil;
很多,我们主要就看一个Allocate吧,其他的其实类似的
Allocate其实就两部分:计算实际需要的长度;分配内存
nChars = AtlAlignUp( nChars + 1, 8 ); // Prevent excessive reallocation. The heap will usually round up anyway.
这里是内存字节按对齐(其实就是申请的大小是8的倍数),AtlAlignUp代码如下
template< typename N >
inline N WINAPI AtlAlignUp( N n, ULONG nAlign ) throw()
{
return( N( (n+(nAlign-1))&~(N( nAlign )-1) ) );
}
位运算呵呵~
内存分配的代码是
pNewData = static_cast< CStringData* >( m_pMemMgr->Reallocate( pData, nTotalSize ) );
IAtlMemMgr是ATL的内存管理接口,实现如下(在atlmem.h中)
virtual void* Allocate( size_t nBytes ) throw()
{
return( malloc( nBytes ) );
}
看到了啥?malloc!多熟悉啊,直接就是CRT了~
其他的诸如Reallocate、free什么的都大同小异
4.几个无文档的成员函数
我们来看几个CSimpleStringT的protected-functions。这些函数显然都是不会有文档说明的
void Attach( _In_ CStringData* pData ) throw()
{
m_pszData = static_cast< PXSTR >( pData->data() );
}
Attach用来设置引用字符串内存时的指针
CStringData* GetData() const throw()
{
return( reinterpret_cast< CStringData* >( m_pszData )-1 );
}
GetData返回这个CSimpleStringT对象的CStringData数据。至于指针问题,还记得CString内存分布么?
ATL_NOINLINE void Fork( _In_ int nLength )
{
CStringData* pOldData = GetData();
int nOldLength = pOldData->nDataLength;
CStringData* pNewData = pOldData->pStringMgr->Clone()->Allocate( nLength, sizeof( XCHAR ) );
if( pNewData == NULL )
{
ThrowMemoryException();
}
int nCharsToCopy = ((nOldLength < nLength) ? nOldLength : nLength)+1; // Copy '\0'
CopyChars( PXSTR( pNewData->data() ), nCharsToCopy,
PCXSTR( pOldData->data() ), nCharsToCopy );
pNewData->nDataLength = nOldLength;
pOldData->Release();
Attach( pNewData );
}
Fork的意图很明显:当引用的CString对象发生改变时,需要有自己的独立空间,并从原来的共享中分离。
其他的都大同小异,大家自己去分析即可
5.扑朔迷离的前世——CStringT
其实CStringT是一个比较简单的类,说穿了就是一句话:
CStringT是CSimpleStringT加上一层封装了大多数CRT字符串操作函数的类
CStringT的SRC在cstringt.h里
一个最大的特点是:构造函数非常多,这个适应了各种情况下CString的产生
还有一个特点是,CStringT并不直接在各种成员函数中使用CRT函数,而是在中间加了一层——ChTraitsCRT。通过这个类来访问CRT
ChTraitsCRT封装了几乎所有的CRT字符串操作函数,当然,都是static的,可以独立于实际对象存在。
M$这么做的原因估计是,后期维护方便,因为ChTraitsCRT相当于一个CRT字符串函数的接口,如果需要什么变动,可以保持调用接口代码不变而去修改接口代码
CStringT还自动帮你处理了MBCS和UNICODE问题,通过类模板的类型制定和两中字符类型间的转换API来保证结果的正确性
因为CStringT没有CSimpleStringT那么多花花肠子,所以分析起来比较方便,毕竟大多数都是操作函数~

COMMENTS