浅析VC.NET的CString类


对于使用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那么多花花肠子,所以分析起来比较方便,毕竟大多数都是操作函数~

, , , , ,

  1. No comments yet.
(will not be published)