一、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' 分配了空间。


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

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

二、CStringData和m_pszData的关联

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


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


注意,这里为什么是一个长度为2的数组?原来,有时候我们需要两个'\0'结尾的字符串——比如用
GetOpenFileName
打开一个文件的时候,需要在
OPENFILENAME

lpstrFilter填入一个两个'\0'结尾的字符串
,这样,万一我们用一个默认的CString空串来传值的时候,不会造成Crash。

 

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


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

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

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


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

三、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)


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

四、 operator LPCTSTR及GetBuffer的故事

1> operator LPCTSTR

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

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


这里我们要注意了:当我们执行 (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方法,这两个操作完全等价:

2> GetBuffer

比起GetString或者operator PCXSTR,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得到的字符串指针,是可以写的,不会影响到其他字符串。很遗憾,这里,我们没有邪恶成功。

3> GetBuffer的重载版本:

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

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

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

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

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

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

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

1> ReleaseBuffer干了什么?

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


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

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


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

2> 什么情况下需要调用ReleaseBuffer:

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

CString的部分实现剖析的更多相关文章

  1. GetBuffer与ReleaseBuffer的用法,CString剖析

    转载: http://blog.pfan.cn/xman/43212.html GetBuffer()主要作用是将字符串的缓冲区长度锁定,releaseBuffer则是解除锁定,使得CString对象 ...

  2. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  3. 【转载】STL"源码"剖析-重点知识总结

    原文:STL"源码"剖析-重点知识总结 STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点 ...

  4. 对 cloudwu 简单的 cstring 进行简单解析

    题外话 以前也用C写过字符串,主要应用的领域是,大字符串,文件读取方面.写的很粗暴,用的凑合着.那时候看见云风前辈的一个开源的 cstring 串. 当时简单观摩了一下,觉得挺好的.也没细看.过了较长 ...

  5. STL&quot;源码&quot;剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  6. [转] 深入剖析 linux GCC 4.4 的 STL string

    本文通过研究STL源码来剖析C++中标准模板块库std::string运行机理,重点研究了其中的引用计数和Copy-On-Write技术. 平台:x86_64-redhat-linux gcc ver ...

  7. STL"源码"剖析

    STL"源码"剖析-重点知识总结   STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略 ...

  8. 深入剖析 linux GCC 4.4 的 STL string

    转自: 深入剖析 linux GCC 4.4 的 STL string 本文通过研究STL源码来剖析C++中标准模板块库std::string运行机理,重点研究了其中的引用计数和Copy-On-Wri ...

  9. cJSON序列化工具解读一(结构剖析)

    cJSON简介 JSON基本信息 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式.易于人阅读和编写.同时易于机器解析和生成.是一种很好地数据交换语言. 官方 ...

随机推荐

  1. TTimerThread和TThreadedTimer(都是通过WaitForSingleObject和CreateEvent来实现的)

    //////////////////////////////////////////////////// // // // ThreadedTimer 1.24 // // // // Copyrig ...

  2. SwifThumb.com 第一家Swift开发人员论坛 QQ群 343549891

     官方QQ群2: 兴许会有app出来让大家随时地学习Swift并在线交流~ watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQW5ld2N6cw==/font ...

  3. Python笔记之面向对象

    1.类和对象 #create a class class fruit: def say(self): print "hello, python" if __name__ == &q ...

  4. C++辛格尔顿

    设计模式是编程的焦点.经常在面试时进行审查,Singleton模式是最简单的.最常见的.大部分的主模式.所以大部分的采访是测试考试的Singleton设计模式. 以下我们就来看看单例模式怎样实现(C+ ...

  5. vc怎么去掉烦人的“驱动器未准备好”错误

    在我们写程序的时候,如果访问一个软驱中没有软盘或者光驱中没有cd的时候,windows总是弹出一个恼人的错误框说“驱动器未准备好” 其实我们可以通过如下的步骤禁止这个错误框的弹出 一.用SetErro ...

  6. delphiXE调用Objective-c库

    http://stackoverflow.com/questions/16515218/xe4-firemonkey-ios-static-library-pascal-conversion-from ...

  7. 如何关闭IE浏览器在生成原型时候的安全警告

    在上一节中,我们学习了如何生成网页原型的三种方法,当时我们采用的默认浏览器,搜狗浏览器,没有弹出安全警告,一般情况下,如果你的浏览器是IE的话,在每次生成网页原型的时候都会弹出如下安全警告,如图: 暂 ...

  8. 理解javascript中的for语句

    程序实现中经常要用到循环语句,其中for循环是多数语言都有的.在javascript中,for循环有几种不同的使用情况,下面就分别来讲述我的理解. 第一种:(通常情况,循环执行相关操作) var ob ...

  9. Kendo UI开发教程(23): 单页面应用(一)概述

    Kendo单页面应用(Single-Page Application,缩写为SPA)定义了一组类用于简化Web应用(Rich Client)开发,最常见的单页面应用为Gmail应用,使用单页面可以给用 ...

  10. 使用SetLocaleInfo设置时间后必须调用广播WM_SETTINGCHANGE,通知其他程序格式已经更改

    uses messages; Procedure SetDateFormat; //设置系统日期格式var buf:pchar; i:integer; p:DWORD;begin getmem(buf ...