WTL :: CString 包含在 WTL 头文件 atlmisc.h 中,并且在未定义宏 _WTL_NO_CSTRING 时可用。
WTL :: CString 是一个非常独立的类,内存结构与 BSTR 类似,即有一个额外的字符串信息头标识字符串相关信息,信息头后紧跟有效字符串数据。此外它还借鉴 COM ,使用引用计数管理字符串对象指针的复制和对象的析构,使得对字符串对象的复制只是复制指针而引用相同内存区的有效字符串数据。这样提高了内存利用率和操作的高效性。引用计数也管理对象的生命期,使其超出生命期没有被使用时自动释放内存。更为关键的是,利用三个全局变量,WTL :: CString 让所有初始化为空的字符串对象都指向同一个内存块,这提供了相当多的便利。
WTL :: CString 兼容 UNICODE/ANSI,提供了 7 个重载版本的构造函数接收不同类型的参数。重载的赋值操作符“=,+=”、拼接操作符“+”以及查找替换,子串操作,使得 WTL :: CString 无与伦比。WTL::CString 所提供的全部操作可以查看 WTL Documentation。
下面具体分析 WTL :: CString 的内存结构和初始化。不过在这之前,先讲讲指针与内存块的关系:
指针的真正含义,在于它用类型标识符指示编译器如何解析它所指向的内存块。一旦它指明了内存块的具体类型,编译器就安装其指明的类型来访问此内存块。这里“访问”的含义需要引起注意,并且“访问”的过程发生时,编译器必须要知道相应类型的具体定义。因此,这就间接地决定了“访问”是运行时行为。比如:
struct Data
{
char a;
char b;
int c;
int d;
}
int nTemp[4] = {10, 10, 0, 0};
(Data*)pdata = (Data*)&nTemp;
一个结构体和一个初始化了的整型数组,最后一句含义如下:
取得整型数组内存块地址,转化为 Data* 也就是说把整型数组的内存块按照 Data 结构体类型的内存块来解析。这意味着对此内存块的访问方式可以是 nTemp[0],nTemp[1]···也可以是 pdata.a,pdata.b等等。访问可以是取值也可以是赋值。具体操作是根据内存块按位取值或赋值,相当于是一个内存块的拷贝过程。此过程遵循存储规则,比如是高位字节在前还是低位字节在前。
对于诸如:
int * p = nTemp;
p += 1;
int * q = &nTemp;
q += 1;
涉及到内存块的解析方式,并未涉及到内存块的访问。
进入正题:
#ifndef _WTL_NO_CSTRING
struct CStringData
{
long nRefs; // reference count
int nDataLength;
int nAllocLength;
// TCHAR data[nAllocLength]
TCHAR* data()
{ return (TCHAR*)(this + 1); }
};
// Globals
// For an empty string, m_pchData will point here
// (note: avoids special case of checking for NULL m_pchData)
// empty string data (and locked)
_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof(CStringData));
class CString
{
public:
···
protected:
LPTSTR m_pchData; // pointer to ref counted string data UNICODE/ANSI 均适用。
CStringData* GetData() const
{
ATLASSERT(m_pchData != NULL);
return ((CStringData*)m_pchData) - 1;
}
void Init()
{
m_pchData = _GetEmptyString().m_pchData;
}
static const CString& __stdcall _GetEmptyString()
{
return *(CString*)&_atltmpPchNil;
}
···
}
#endif // !_WTL_NO_CSTRING
我们来详细分析一下上述代码。
定义了一个结构体 CStringData ,三个数据成员表示 WTL::Cstring 的引用计数、数据区长度、缓冲区长度。一个成员函数
TCHAR* data()
{ return (TCHAR*)(this + 1); }
this代表的是当前字符串信息头指针,(this + 1)使指针往后移动sizeof(CStringData)长度。即指向字符串信息头的下一个位置,指针类型为 TCHAR* 。
接下来是三个全局变量,
第一行定义一个一维 int 型数组内存块,含有4个元素并赋初始值。此内存块保存在全局数据区。我们将会看到,这一变量就是一个初始化为空字符串的 WTL::CString 对象内存指针,所有初始化后的空字符串 WTL::Cstring 类对象均指向此内存块。具体实现后面分析。
第二行定义一个 CStringData 指针,指向转换为 CStringData 结构的该数组内存块。则此指针所指的 CstringData 对象的数据成员由此内存块中的全局数组相应元素初始化,相当于:
nRefs = -1,nDataLength = 0, nAllocLength = 0;
因此这一全局变量就是一个初始化了的字符串信息头 CStringData 对象内存指针。
第三行 定义一个 字符串指针指向从数组开始地址跨越了sizeof(CStringData)长度的地址,此即指向数组最后一个元素的地址,并且此内存块转换为 LPCTSTR 类型,内容为0(NULL)。因此这一变量就是初始化为空字符串的字符串指针。后面我们将看到,所有初始化为空字符串的 WTL::CString 对象的有效字符串指针都等于此指针。
注:_declspec(selectany)使得可以在头文件中定义变量,多次包含而不会引起错误,即编译器自动优化删除多余的重复定义始终保持只有一个定义,满足语法要求。
好了,现在来看 Cstring 的定义。只有一个成员变量,
protected:
LPTSTR m_pchData;
指向有效字符串内存区的指针。
多个重载的构造函数可以接受不同的参数进行初始化,更为重要的是每个构造函数中均调用了初始化函数 Init(),复制构造函数中若需要初始化时也调用此函数。那么函数 Init()似乎很关键。它的作用是:
将第三个全局变量所在的内存块转换为 WTL::CString 对象内存块;即此时该内存块中的内容被解释为一个 WTL::CString 对象。
获取该内存块中 WTL::CString 对象的数据成员(也就是上述第三个全局变量),并赋值给待初始化的 CString 对象。
这样做的结果就是,所有经过调用 Init()初始化后的 WTL::CString 对象都指向相同的内存块并且有效字符串内容为空。
如下所示为 WTL::CString 字符串对象内存结构图:
我们再来看WTL::CString中对内存的分配和释放,受保护成员函数
BOOL AllocBuffer(int nLen)
void Release()
负责 WTL::CString对象的内存分配与释放。
我们看到如果请求分配的内存长度为0则调用 Init()函数,使其初始化后指向全局变量。否则,分配 nLen+1 长度内存,额外的一个字节用以嵌入结束字符‘/0’(pData->data()[nLen] = _T('/0'))但不算在有效字符串长度中(pData->nDataLength = nLen),并且设置引用计数为1(pData->nRefs = 1)。最后把分配的内存块挂接到 CString对象上(m_pchData = pData->data())。
对于内存释放,首先判断不是空字符串,然后递减引用计数,若引用计数减为0表示此对象不在被引用,因此释放内存。调用 Init()函数的作用是令该对象的 m_pchData = NULL。避免漂浮指针。