详述字符串之 MFC库的CString

 

写在前面

     可以说CString在所有Visual C++ 程序员中占有很重要的地位,可以说是MFC Library里面最风光的一个类。据说当年还有很多程序员因为MFC中拥有好用的CString类,从其他开发工具跳槽到Visual C++来。我对CString的认识是从99年的Visual C++ 6.0中带的MFC 4.2版本中的CString类,那时候是小菜鸟,就是觉得CString好用。等变成老菜鸟的时候,发觉CString也不是那么完美的……

 今天说的CStringMFC 9.0 中的CString类。照比以前MFC4.2版本的CString,功能上、性能上都改进了许多。最大的特点是自MFC7.0CString类已经变成了一个模板类。

 获取源代码

    需要安装Microsoft Visual Studio 2005或者2008 ,并安装了Visual C++ 的组件。以Visual Studio2008为例,其源代码就在:\Microsoft Visual Studio 9.0\VC\atlmfc\include\ 路径下。其主要代码在cstringt.h文件和cstringt.inl 两个文件中。cstringt.h的内容是CString类的声明和实现,cstringt.inl文件包含了几个CString类常用的方法的实现,为了提高性能,这些方法被声明成了内联函数(inline)

 为什么把代码写在了cstringt.h文件中,而不是写在cstringt.cpp文件中呢?这还是要追溯到Visual C++ 6.0。在Visual C++ 6.0 以前(包括VC6),其自带的编译器MS C++(也就是cl.exe)是不支持将模板类的声明和实现分开写在.h.cpp文件里的。所以整个的ATL库源代码都是写在.h的头文件里的。这个习俗就一直保留了下来。

 不得不说的CStringData 结构

      CStringData结构体是用来保存CString对象中字符串数据的结构体。也就是说,这个CStringData才是真正管理字符串缓冲区的对象。源代码在atlsimpstr.h文件中。其声明如下:

struct CStringData

    如果您奇怪结构体中为什么还有方法?这个…… 简单的说一下,其实class struct 的区别不大,只是默认情况下class的访问和继承权限都是私有的(private)struct的访问和集成权限都是共有的(public)。能不能包含函数,并不是classstruct的重要区别之一。他们两个关键字都是允许同时包含变量和方法的。

CStringData对象中首先包含了一个指向IAtlStringMgr对象的指针,这个指针所指向对象的主要作用是对字符串缓冲区进行内存管理。而后跟随着两个变量nDataLength nAllocLength 一个表示实际字符串的长度,另一个表示申请内存空间的长度。

再粗略的扫视一下CStringData的代码,有新发现了吗? 反正我看到了int nRef; AddRef()Release() 这是啥?了解COM技术的人都知道,这是COM技术最最根本最最基础的接口IUnknown 的成员。虽然这里CStringData没有显式的继承IUnknown接口,但实际上也是采用了引用计数的方式进行管理的。nRef的在AddRef()进行递增和Release()进行递减操作是通过_AtlInterlockedIncrement  _AtlInterlockedDecrement 这对儿进行的。在互斥区中访问nRef 目的是为了保证CStringData是线程安全的。不会致使nRef产生脏数据。Lock/UnLock这对儿方法,可以在单一使用者(nRef = 1)的情况下,标记字符串缓冲区是否被锁定。以便使用者可以通过独占的方式访问字符串。不过很遗憾,这两个方法并不是线程安全的。有点儿靠不住的说。

 Data() 方法可以返回实际的字符串缓冲区首地址。return (this+1); this代表了指向当前CStringData对象内存首地址的指针,this + 1 就是指向在内存中紧跟CStringData对象的首地址。这里的1不是代表数值1,而是代表了1CStringData对象。啊?为什么?答案请参照谭浩强的《C语言程序设计》有关指针操作的部分。

字符串缓冲区的管理

上面提到,在CStringData类中,包含了一个指向IAtlStringMgr接口的指针,这个IAtlStringMgr接口定义了字符串缓冲区管理的基本方法,成员如下:
    Allocate                      申请新的内存缓冲区
   
Clone                         拷贝一个当前的字符串缓冲区内存管理对象。当然,所管理的内存缓冲区也被同时复制了。
    Free                          释放已申请的字符串缓冲区
   
GetNilString                  清空字符串缓冲区,用’\0’填充
    
Reallocate                    重新申请字符串缓冲区 

对于这个接口,ATL库带有默认的实现,实现代码在atlstr.h文件中。有一个类CAtlStringMgr继承了IAtlStringMgr接口。代码如下:

class CAtlStringMgr

 

我们先从Allocate() 说起。nChars = AtlAlignUp( nChars+1, 8 ); 这行代码调用了ATL内存管理的一个AtlAlignUp函数。这个函数的原型在atlmem.h文件中,其作用是将给定的长度(N n)按照规定的布局(ULONG nAlign)凑齐对应的字节,请看:
   
template< typename N >
    
inline N WINAPI AtlAlignUp( N n, ULONG nAlign ) throw()
   
{
         return( N( (n+(nAlign-1))&~(N( nAlign )-1) ) );
    
}

 这行代码要是换我写,我就直接让n+nAlign 再除以nAlign取商了。但是MS的工程师没有那么做,而是通过位运算实现。这无疑提高了这个模板函数的运算性能。快,就一个字。

 而后,函数中使用了AtlMultplyAtlAdd方法进行了要申请的总字节数的计算。使用这两个方法进行乘、加运算,是为了避免运算结果超界带来不必要的麻烦。把AtlAdd摆出来看看:

 

AtlAdd

  AtlMultply的代码也大同小异,如想更深入了解,可在atlalloc.h文件中看到函数的原型。有意思的是这个AtlMultply函数还曾经有一个bug,请参考http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=119531 意思是说,原始版本中如果tLeft参数传入0,为了避免出现除零的情况,直接返回S_OK,但返回前,没有给ptResult赋值,以致出现野指针的情况。呵呵,有兴趣的可以通过上面的链接了解到更多的信息,反正我安装的Visual Studio 2008 中的ATL代码已经修正了这个问题(多了一行 *ptResult = 0; 搞定!)

在计算好实际要申请的字节后,使用位于atlmem.h文件中的CCRTHeap(这个类实现了IAtlMemMgr接口)Allocate()方法申请了存放字符串的内存。代码如下:
        virtual void* Allocate( size_t nBytes ) throw()
         {
                   return( malloc( nBytes ) );
         }

看到malloc() 倍感亲切呀,哈哈。我也总算是一杆子杵到底了。看明白了上面的内容,对于CAtlStringMgrFreeReallocate就一通百通了,到了最后,就是依靠调用CRTFreeReallocate实现的。这里不再赘述。

   
根据CAtlStringMgr的内存管理代码,我们了解到以下内容:
   
1)       申请字符串缓冲区空间时,实际是以8个字符进行了对齐的。例如申请50个字符的空间时,实际申请的内存会是56个字符的空间;
    
2)       对缓冲区内存的管理最底层是通过CRT的内存管理函数实现的。这样一来申请内存就要遵照CRT定义的阀值。记得《Visual C++技术内
        幕》上写的
CRT的阀值是480个字节,不知道当前版本的CRT阀值有没有改变;
   
3)       申请的内存大小应该是CStringData结构体的大小 + 字符个数以8对气候 * 字符所占空间的字节数;

ChTraitsBase
ChTraitsCRT

ChTraitsBase

   

这个类中没有任何的成员变量和成员函数,而是定义了一些数据类型,简单的typedef就不用多说了吧。

    ChTraitsCRT 继承了ChTraitsBase ,并实现了n多个静态的工具函数,包括判断字符是否是数字的、字符是否是空格的、以及字符串查找的、字符串比较的、格式化字符串的…… 这些静态方法,构成了CString类的字符串操作函数。这个chTraitsCRT类在cstringt.h文件中提供了两个版本,一个版本是char类型的,另一个是wchar_t类型的,也就是说CString类可以分别处理ansi字符和unicode字符。虽然有点儿长,但还是值得把代码贴一下,因为使用者比较看重的字符串操作方法的实现全都在这里了:

这个类中没有任何的成员变量和成员函数,而是定义了一些数据类型,简单的typedef就不用多说了吧。

 ChTraitsCRT 继承了ChTraitsBase ,并实现了n多个静态的工具函数,包括判断字符是否是数字的、字符是否是空格的、以及字符串查找的、字符串比较的、格式化字符串的…… 这些静态方法,构成了CString类的字符串操作函数。这个chTraitsCRT类在cstringt.h文件中提供了两个版本,一个版本是char类型的,另一个是wchar_t类型的,也就是说CString类可以分别处理ansi字符和unicode字符。虽然有点儿长,但还是值得把代码贴一下,因为使用者比较看重的字符串操作方法的实现全都在这里了:

class ChTraitsCRT

 

CString类的构造函数

   看过了上面的n多代码和介绍,我们终于可以看看真正的CString类了。声明如下:
template< typename BaseType, class StringTraits >
class CStringT :
         public CSimpleStringT< BaseType, _CSTRING_IMPL_::_MFCDLLTraitsCheck<BaseType, StringTraits>::c_bIsMFCDLLTraits >
您可能不禁要问,不是要说CString么,怎么是CStringT呢? 其实CString实际上是一个宏而已,在afxstr.h文件中有如下定义:
typedef ATL::CStringT< wchar_t, StrTraitMFC_DLL< wchar_t > > CStringW;
typedef ATL::CStringT< char, StrTraitMFC_DLL< char > > CStringA;
typedef ATL::CStringT< TCHAR, StrTraitMFC_DLL< TCHAR > > CString;

由此,我们可以确切的知道,我们使用的CString就是CStringT模板类的一个特例。如果您是从头看到尾的话,就会知道,我们已经讲了CString的重要成员CStringData、包含了CString字符串操作的方法ChTraitsCRT。那CString里面还剩下什么没有说呢?基本上也就是构造函数、操作符重载了。一个类也就包括了这么多内容,再无其它。

 先说构造函数,CString这个帅类提供了一共17种构造重载,够狠!全说没必要,要说的就是静态构造函数:
        static void __cdecl Construct( CStringT* pString )
        {
              new( pString ) CStringT;
         }

这是一个拷贝构造函数,传入一个有效的指向CString的指针,这个构造就会为你创建一个新的对象,并把这个对象的首地址赋值给传入的指针变量。

MSDN给出了构造函数使用的完整Demo:

CAtlString s1;                    // Empty string

CAtlString s2(_T("cat"));           // From a C string literal

CAtlString s3 = s2;               // Copy constructor

CAtlString s4(s2 + _T(" ") + s3);   // From a string expression

 

CAtlString s5(_T('x'));             // s5 = "x"

CAtlString s6(_T('x'), 6);          // s6 = "xxxxxx"

 

CAtlString s7((LPCSTR)ID_FILE_NEW); // s7 = "Create a new document"

 

VARIANT var;

V_VT(&var) = VT_BSTR;

V_BSTR(&var) = ::SysAllocString(L"Football is a fun sport.");

CAtlString s8(var); // s8 = "Football is a fun sport."

 

// The following statement does not call the assignment operator.

// The compiler considers the following statement equivalent to

// CAtlString city("Paris")

CAtlString city = _T("Paris");

 

关于Format函数

 这个函数的奇特地方就是支持变长参数列表。在上次说道.NET下的String类时,使用的是C#的关键字Params 关键字来实现变长参数的,在C++中采用变长参数列表来实现。Format函数的声明如下:

inline void __cdecl CStringT<BaseType, StringTraits>::Format( _In_ _Printf_format_string_ PCXSTR pszFormat, ... )

这最后的在语法上就代表了变长参数列表。请注意,这个变长参数列表必须放在函数列表的最后声明,如写成(…,int iLength) 是不对的。

 

这个变长参数列表的声明在stdarg.h文件中。实际上va_listva_startva_end 是三个宏。代码如下:

         ATLASSERT( AtlIsValidString( pszFormat ) );

         va_list argList;

         va_start( argList, pszFormat );

         FormatV( pszFormat, argList );

         va_end( argList );

 

LoadString 函数

 

     这个函数也是值得一说的,正因为有了这个LoadString函数和String Table,我们才可以很容易的让我们的程序支持国际化。LoadString的代码是这样的:

         _Check_return_ BOOL LoadString( _In_ HINSTANCE hInstance, _In_ UINT nID, _In_ WORD wLanguageID )

         {

                   const ATLSTRINGRESOURCEIMAGE* pImage = AtlGetStringResourceImage( hInstance, nID, wLanguageID );

                   if( pImage == NULL )

                   {

                            return( FALSE );

                   }

 

                   int nLength = StringTraits::GetBaseTypeLength( pImage->achString, pImage->nLength );

                   PXSTR pszBuffer = GetBuffer( nLength );

                   StringTraits::ConvertToBaseType( pszBuffer, nLength, pImage->achString, pImage->nLength );

                   ReleaseBufferSetLength( nLength );

 

                   return( TRUE );

         }

其最主要的函数是AtlGetStringResourceImage 函数,这个函数通过资源ID能够获取保存在应用程序文件或动态库中的字符串资源。AtlGetStringResourceImage 函数的功能实现最终还是通过Windows API FindResource 函数实现的。有兴趣的话,可以查看一下AtlGetStringResourceImage 函数的源代码。

写在后面

   说起Windows 下的CString,那真是有说不尽的话题。比如,不同类型的字符串之间的转换,这个话题就够再写一篇博客的。本篇只能尽力起到一个抛砖引玉的作用。

 我在Google上搜索到了一篇叫做《谈新手对CString的使用》一文,http://www.poptool.net/server/p52/J526413.shtml 这篇文章被n多网站转载,出处不明。文章中提到了一个问题,说“为了优化效率,就采用在系统软件内部广
泛使用的
"写时复制"概念.即当从一个cstring产生另一个cstring并不复制它的字符缓
冲区内容
,而只是将字符缓冲区的"引用计数"1.当需要改写字符缓冲区内的内容时,
分配内存
,并复制内容”这部分倒是值得大家留意,这也正和前面说道的CStringData引入的引用计数机制不谋而合。

    

posted @ 2008-03-27 21:59  Michael Lee  阅读(6702)  评论(3编辑  收藏  举报