CString 的部分实现剖析

CString 的部分实现剖析

96 牧秦丶 关注

2017.11.03 10:23* 字数 2415 阅读 36评论 0喜欢 1

1、CString初探:

在CString的实现中,其最基础的类结构如下:

基础类结构

CString 其实只有一个数据成员 m_pszData,这个成员指向了字符串的首地址。但在 MFC 的具体实现中, m_pszData 指向的其实是 CStringData 后面的一块数据的首地址。比如执行:

CString strHello = _T("hello");

这样一条语句之后,m_pszData的指向其实是下面这个样子:

                 m_pszData
                    ↓
    +---------------+--+--+--+--+--+---+
    |  CStringData  | h  |  e |   l |  l |   o |  \0 |
    +---------------+--+--+--+--+--+---+

我们知道,CStringData 里面的信息如下:

IAtlStringMgr* pStringMgr;       --> 执行 Allocate、Reallocate、Free 等操作;重要的一点,提供 GetNilString 方法的实现(下文会讲到);  
int            nDataLength;      --> 字符串的实际长度(通过 SetLength 等函数可操作这个大小);  
int            nAllocLength;     --> 实际分配的空间大小(除非重新分配,否则这个大小不可变);  
int            nRefs;            --> 明显为了支持 CopyOnWrite 机制,为引用计数  

我们可以看出,CStringData 里面有字符串的长度信息,但在 CAfxStringMgr::Allocate 的时候确实又为 '\0' 分配了空间。

CAfxStringMgr::Allocate

也就是说,每当字符串发生更改或者触发了 CopyOnWrite 的机制时,就会调用 CAfxStringMgr 的 Allocate/Reallocate 函数进行分配空间,分配的大小为:

      (nChars + 1) * nCharSize + sizeof(CStringData)

2、CStringData 和 m_pszData 的关联

当执行 CString 的默认构造函数时,会调用前面我们提到的 CAfxStringMgr::GetNilString返回一个 CStringData 的指针,这个指针指向全局的一个 CNilStringData。CNilStringData 如下:

CNilStringData

CNilStringData 派生自 CStringData,额外拥有一个 achNil 的数组成员,这个数组初始化为空字符串。通过这个 achNil,保证了一个经过调用默认构造函数初始化的 CString,其指向的真正的字符串是一个空串。CSimpleStringT 的构造函数如下:

CSimpleStringT

注意,这里为什么是一个长度为 2 的数组?原来,有时候我们需要两个 '\0' 结尾的字符串——比如用 GetOpenFileName 打开一个文件的时候,需要在 OPENFILENAME 的 lpstrFilter 填入一个两个 '\0' 结尾的字符串,这样,万一我们用一个默认的 CString 空串来传值的时候,不会造成 Crash。

重要的是接下来的 Attach 操作,通过 Attach 操作,将这个 CStringData* 与 CSimpleStringT::m_pszData 执行了关联:

Attach

pData->data() 具体做了哪些操作呢?

pData->data()

可以看出,data() 是 CStringData 类里的一个成员函数,它返回 this 指针加 1 之后的一个指针。我们知道,对于一个类型为 T* 的指针,对它取偏移,得到的实际地址是:ptr + sizeof(T) * offset。所以,针对一个 CStringData* 的指针作偏移,得到的地址是紧挨在 CStringData 之后的那块数据块的地址。

这样,就顺理成章的将字符串的真正的指针 m_pszData 和描述字符串信息的 CStringData 关联了起来。那么,我们也可以很容易的通过 m_pszData 反推出 CStringData 的指针,CSimpleStringT::GetData 这个成员方法就提供了这么一个操作:

GetData

先把 m_pszData 强转为 CStringData* 的类型,再在这个基础上做 -1 的偏移,得到的就是真正的 CStringData 的地址。

3、CopyOnWrite机制的触发

CopyOnWrite——写时复制机制,这个机制也算非常常见了。我第一次接触这个机制,是 DLL 的写时复制,当要手动 Hook 一个 DLL 中的 API 时,会在 API 开头手动写入跳转汇编,这时候,系统会复制一份 DLL 镜像给我们,不会影响到加载该 DLL 的其他进程。

CopyOnWrite,说白了:就是大家先共享一份数据,可以进行共享只读操作,事情顺利进行;突然有个家伙想修改这份数据里的某一个地方,如果发现这块数据是由多个人共享的,那好,你自己把这份数据复制一份,然后把共享的引用计数减一,然后你自己去玩吧。

CString 也是提供了这样一个 CopyOnWrite 机制的,其中,CSimpleStringT::Fork 函数就提供了这样一个操作,具体分为下面几步:

  1. 它根据传入的一个长度分配一段新的空间;—— Allocate(nLength, ...)
  2. 把旧数据拷贝到新的空间里面;—— CopyChars(...)
  3. 旧数据块的引用技术减1; —— pOldData->Release()
  4. 把 m_pszData 和新的数据块关联起来。—— Attach(pNewData)

CSimpleStringT::Fork

那么,什么时候会触发 CopyOnWrite 机制呢?一般来说,对 CString 进行写操作的所有方法,都会触发该机制,Write 操作都会进行,但只有该字符串的数据块被共享的时候,或者旧的 CStringData::nAllocLength 不足以存放新的字符串的时候,才会执行 Copy 操作。这些对 CString 进行写操作的方法,大家通过使用经验和肉眼,很容易就可以分辨出来。

4、operator LPCTSTR 及 GetBuffer 的故事

4.1、operator LPCTSTR:

OK,有些 API 接受的入参可能不是 CString,而是一个 char* 或者 wchar_t* 的字符串指针,这时候,我们往往会用到 LPCTSTR 的一个隐式转换函数 —— operator LPCTSTR,如你所想,它干了你想让它干的,就是返回 m_pszData:

operator PCXSTR

呃,PCXSTR,说好的 LPCTSTR 呢?原来,对 wchar_t 类型的字符串,PCXSTR 的定义是这样的,还是 LPCWSTR,这里夹杂的大写 “C”,保留了 const 属性:

ChTraitsBase

这里我们要注意了:当我们执行 (LPCTSTR)str 这样一个强转操作,就会调用到 operator PCXSTR 这个转换函数,返回的是带 const 属性的字符串指针,所以,我们不应该对这个指针做任何的写操作。比如:

CString str1 = _T("hello");  
CString str2 = str1;                                 // 这时候 str1 和 str2 共享字符串 "hello" 的数据块  
  
LPCTSTR pcszAddr = (LPCTSTR)str1;  
LPTSTR  pszEvil  = const_cast<LPTSTR>(pcszAddr);     // 我们邪恶一下  
pszEvil[0] = _T('H');                                // 强制改一下,这时候 str1 和 str2 都变成了 "Hello" 了!  

所以,当我们要对字符串只读的时候,应该使用这个隐式转换符,或者调用 CSimpleStringT::GetString 方法,这两个操作完全等价:

CSimpleStringT::GetString

4.2、GetBuffer:

比起GetString或者operator PCXSTR,GetBuffer 函数就有趣多了。

CSimpleStringT::GetBuffer

这里我们注意到,返回的是 PXSTR 而不是PXCSTR,也就是说,GetBuffer 返回的字符串,是不带 const 属性的,我们可以进行写操作——那么,为了不影响其他共享的字符串,这里触发了 CopyOnWrite 机制!——当然,如果 pData->IsShared 返回 FALSE 的话,说明没有共享,是不会 Copy 的。我们再尝试邪恶一把:

CString str1 = _T("hello");  
CString str2 = str1;                        // 这时候 str1 和 str2 共享字符串 "hello" 的数据块  
  
LPTSTR pszEvil = str1.GetBuffer();  
pszEvil[0] = _T('H');                       // 强制改一下,这时候 str1 变成了 "Hello",str2 依然为 "hello"!  

可以看出,我们通过 GetBuffer 得到的字符串指针,是可以写的,不会影响到其他字符串。很遗憾,这里,我们没有邪恶成功。

4.3、GetBuffer的重载版本:

What!还有重载版本?对的,CString 还有一个重载了的 GetBuffer 函数,这个重载版本接收一个 int 的长度作为入参:

CSimpleStringT::GetBuffer(int)

继续调用了 PrePareWrite2,继续往下跟:

CSimpleStringT::PrePareWrite2

发现新需求的长度比已经分配的小,或者字符串数据块被共享,就调用 PrepareWrite2,否则,直接返回 m_pszData,我们继续往下跟:

CSimpleStringT::PrePareWrite2

这里,第二个 if 分支,发现数据被共享,直接执行 Fork 进行 Copy 操作,接下来的 elseif 分支,如果没被共享,但已分配的最大长度小于用户请求的长度,则进行扩容,然后调用 Reallocate 进行重新分配。

Reallocate 的执行,大家可以参见源代码,这里就不贴了,其实现,大概可以想到个八九分吧。Fork 和 Reallocate 最后都执行了 Attach 操作,将新数据块和 m_pszData 关联起来。

5、“到底要不要 ReleaseBuffer,This is a Question!”

那么,大家的疑问一直纠结在这里,GetBuffer 之后,到底要不要 ReleaseBuffer

5.1、ReleaseBuffer干了什么?

我们要判断一个函数该不该调用的时候,如果一直找不到想要的结果,参考源代码,不失为一个好选择:

ReleaseBuffer

ReleaseBuffer 如果你不传任何参数进去,它会取字符串的真实长度(这里通过调用 wcslen 获取),然后进行 SetLength 操作。但如果你传了一个长度,它会直接用这个长度进行 SetLength 操作。

SetLength 干了什么?只是把新的长度赋到 CStringData 里面,并且把字符串按新长度,在对应的位置塞入 '\0':

SetLength

“哦,哦,怎么感觉满世界都是坑呐!”——你这样埋怨道!我们发现,ReleaseBuffer 干了一件与它的名字完全不符的一件事,你这是闹哪样?结合 ReleaseBuffer 做的操作,我们完全有理由相信:UpdateBuffer 这个函数名,更适合这么一个操作!

5.3、什么情况下需要调用 ReleaseBuffer:

那么什么情况下需要调用 ReleaseBuffer 呢?我们看到,GetBuffer 返回的是可写的指针,也就是说,我们得到这个字符串指针的时候,如果发生了一些写操作,那么,CString 是不知道我们干了什么的,因为我们没通过 CString 提供的接口去操作。所以,我们需要 ReleaseBuffer(UpdateBuffer什么时候能被扶正?)来把字符串的新长度更新到 CString 里面——具体点,更新到 CStringData 里面,因为我们调用 CString::GetLength 的时候,需要用到这个长度:

GetLength

举个具体的例子:

CString str = _T("Hello World!");  
LPSTR pszAddr = str.GetBuffer();                // pszAddr 为 "Hello World!"  
int nStrLength = str.GetLength();               // nStrLength 为12  
  
pszAddr[6] = 0;                                 // pszAddr 变成了 "Hello",但str这个对象并不知道,它的m_pszData已经不是从前的那个它了  
int nStrAfterChangeLength = str.GetLength();    // str依然相信,nStrAfterChangeLength 依然是 12  
  
str.ReleaseBuffer();                            // 我们让第三方悄悄告诉str,你的m_pszData已经变了,你最好重新审视一下它  
int nStrAfterUpdateLength = str.GetLength();    // nStrAfterUpdateLength 变成了 5,虽然变短了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值