1、 概念
Scott Meyers在《More Effective C++》中举了个例子,不知你是否还记得?在你还在上学的时候,你的父母要你不要看电视,而去复习功课,于是你把自己关在房间里,做出一副正在复习功课的样子,其实你在干着别的诸如给班上的某位女生写情书之类的事,而一旦你的父母出来在你房间要检查你是否在复习时,你才真正捡起课本看书。这就是“拖延战术”,直到你非要做的时候才去做。 当然,这种事情在现实生活中时往往会出事,但其在编程世界中摇身一变,就成为了最有用的技术,正如C++中的可以随处声明变量的特点一样,Scott Meyers推荐我们,在真正需要一个存储空间时才去声明变量(分配内存),这样会得到程序在运行时最小的内存花销。执行到那才会去做分配内存这种比较耗时的工作,这会给我们的程序在运行时有比较好的性能。必竟,20%的程序运行了80%的时间。 当然,拖延战术还并不只是这样一种类型,这种技术被我们广泛地应用着,特别是在操作系统当中,当一个程序运行结束时,操作系统并不会急着把其清除出内存,原因是有可能程序还会马上再运行一次(从磁盘把程序装入到内存是个很慢的过程),而只有当内存不够用了,才会把这些还驻留内存的程序清出。 写时才拷贝(Copy-On-Write)技术,就是编程界“懒惰行为”——拖延战术的产物。举个例子,比如我们有个程序要写文件,不断地根据网络传来的数据写,如果每一次fwrite或是fprintf都要进行一个磁盘的I/O操作的话,都简直就是性能上巨大的损失,因此通常的做法是,每次写文件操作都写在特定大小的一块内存中(磁盘缓存),只有当我们关闭文件时,才写到磁盘上(这就是为什么如果文件不关闭,所写的东西会丢失的原因)。更有甚者是文件关闭时都不写磁盘,而一直等到关机或是内存不够时才写磁盘,Unix就是这样一个系统,如果非正常退出,那么数据就会丢失,文件就会损坏。 呵呵,为了性能我们需要冒这样大的风险,还好我们的程序是不会忙得忘了还有一块数据需要写到磁盘上的,所以这种做法,还是很有必要的。 2、 标准C++类std::string的Copy-On-Write 在我们经常使用的STL标准模板库中的string类,也是一个具有写时才拷贝技术的类。C++曾在性能问题上被广泛地质疑和指责过,为了提高性能,STL中的许多类都采用了Copy-On-Write技术。这种偷懒的行为的确使使用STL的程序有着比较高要性能。 这里,我想从C++类或是设计模式的角度为各位揭开Copy-On-Write技术在string中实现的面纱,以供各位在用C++进行类库设计时做一点参考。 在讲述这项技术之前,我想简单地说明一下string类内存分配的概念。通过常,string类中必有一个私有成员,其是一个char*,用户记录从堆上分配内存的地址,其在构造时分配内存,在析构时释放内存。因为是从堆上分配内存,所以string类在维护这块内存上是格外小心的,string类在返回这块内存地址时,只返回const char*,也就是只读的,如果你要写,你只能通过string提供的方法进行数据的改写。 2.1、 特性 由表及里,由感性到理性,我们先来看一看string类的Copy-On-Write的表面特征。让我们写下下面的一段程序: #include #include using namespace std; main() { string str1 = "hello world"; string str2 = str1; printf ("Sharing the memory:"n"); printf (""tstr1's address: %x"n", str1.c_str() ); printf (""tstr2's address: %x"n", str2.c_str() ); str1[1]='q'; str2[1]='w'; printf ("After Copy-On-Write:"n"); printf (""tstr1's address: %x"n", str1.c_str() ); printf (""tstr2's address: %x"n", str2.c_str() ); return 0; } 这个程序的意图就是让第二个string通过第一个string构造,然后打印出其存放数据的内存地址,然后分别修改str1和str2的内容,再查一下其存放内存的地址。程序的输出是这样的(我在VC6.0和g++ 2.95都得到了同样的结果): > g++ -o stringTest stringTest.cpp > ./stringTest Sharing the memory: str1's address: 343be9 str2's address: 343be9 After Copy-On-Write: str1's address: 3407a9 str2's address: 343be9 从结果中我们可以看到,在开始的两个语句后,str1和str2存放数据的地址是一样的,而在修改内容后,str1的地址发生了变化,而str2的地址还是原来的。从这个例子,我们可以看到string类的Copy-On-Write技术。 2.2、 深入 在深入这前,通过上述的演示,我们应该知道在string类中,要实现写时才拷贝,需要解决两个问题,一个是内存共享,一个是Copy-On-Wirte,这两个主题会让我们产生许多疑问,还是让我们带着这样几个问题来学习吧: 1、 Copy-On-Write的原理是什么? 2、 string类在什么情况下才共享内存的? 3、 string类在什么情况下触发写时才拷贝(Copy-On-Write)? 4、 Copy-On-Write时,发生了什么? 5、 Copy-On-Write的具体实现是怎么样的? 喔,你说只要看一看STL中stirng的源码你就可以找到答案了。当然,当然,我也是参考了string的父模板类basic_string的源码。但是,如果你感到看STL的源码就好像看机器码,并严重打击你对C++自信心,乃至产生了自己是否懂C++的疑问,如果你有这样的感觉,那么还是继续往下看我的这篇文章吧。 OK,让我们一个问题一个问题地探讨吧,慢慢地所有的技术细节都会浮出水面的。 2.3、 Copy-On-Write的原理是什么? 有一定经验的程序员一定知道,Copy-On-Write一定使用了“引用计数”,是的,必然有一个变量类似于RefCnt。当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的RefCnt为1或是0,此时,程序才会真正的Free这块从堆上分配的内存。 是的,引用计数就是string类中写时才拷贝的原理! 不过,问题又来了,这个RefCnt该存在在哪里呢?如果存放在string类中,那么每个string的实例都有各自的一套,根本不能共有一个RefCnt,如果是声明成全局变量,或是静态成员,那就是所有的string类共享一个了,这也不行,我们需要的是一个“民主和集中”的一个解决方法。这是如何做到的呢?呵呵,人生就是一个糊涂后去探知,知道后和又糊涂的循环过程。别急别急,在后面我会给你一一道来的。 2.3.1、 string类在什么情况下才共享内存的? 这个问题的答案应该是明显的,根据常理和逻辑,如果一个类要用另一个类的数据,那就可以共享被使用类的内存了。这是很合理的,如果你不用我的,那就不用共享,只有你使用我的,才发生共享。 使用别的类的数据时,无非有两种情况,1)以别的类构造自己,2)以别的类赋值。第一种情况时会触发拷贝构造函数,第二种情况会触发赋值操作符。这两种情况我们都可以在类中实现其对应的方法。对于第一种情况,只需要在string类的拷贝构造函数中做点处理,让其引用计数累加;同样,对于第二种情况,只需要重载string类的赋值操作符,同样在其中加上一点处理。 唠叨几句: 1)构造和赋值的差别 对于前面那个例程中的这两句: string str1 = "hello world"; string str2 = str1; 不要以为有“=”就是赋值操作,其实,这两条语句等价于: string str1 ("hello world"); //调用的是构造函数 string str2 (str1); //调用的是拷贝构造函数 如果str2是下面的这样情况: string str2; //调用参数默认为空串的构造函数:string str2(“”); str2 = str1; //调用str2的赋值操作:str2.operator=(str1); 2) 另一种情况 char tmp[]=”hello world”; string str1 = tmp; string str2 = tmp; 这种情况下会触发内存的共享吗?想当然的,应该要共享。可是根据我们前面所说的共享内存的情况,两个string类的声明和初始语句并不符合我前述的两种情况,所以其并不发生内存共享。而且,C++现有特性也无法让我们做到对这种情况进行类的内存共享。 2.3.2、 string类在什么情况下触发写时才拷贝(Copy-On-Write)? 哦,什么时候会发现写时才拷贝?很显然,当然是在共享同一块内存的类发生内容改变时,才会发生Copy-On-Write。比如string类的[]、=、+=、+、操作符赋值,还有一些string类中诸如insert、replace、append等成员函数。 修改数据才会触发Copy-On-Write,不修改当然就不会改啦。这就是托延战术的真谛,非到要做的时候才去做。 2.3.3、 Copy-On-Write时,发生了什么? 我们可能根据那个访问计数来决定是否需要拷贝,参看下面的代码: If ( RefCnt>0 ) { char* tmp = (char*) malloc(strlen(_Ptr)+1); strcpy(tmp, _Ptr); _Ptr = tmp; } 上面的代码是一个假想的拷贝方法,如果有别的类在引用(检查引用计数来获知)这块内存,那么就需要把更改类进行“拷贝”这个动作。 我们可以把这个拷的运行封装成一个函数,供那些改变内容的成员函数使用。 2.3.4、 Copy-On-Write的具体实现是怎么样的? 最后的这个问题,我们主要解决的是那个“民主集中”的难题。请先看下面的代码: string h1 = “hello”; string h2= h1; string h3; h3 = h2; string w1 = “world”; string w2(“”); w2=w1; 很明显,我们要让h1、h2、h3共享同一块内存,让w1、w2共享同一块内存。因为,在h1、h2、h3中,我们要维护一个引用计数,在w1、w2中我们又要维护一个引用计数。 如何使用一个巧妙的方法产生这两个引用计数呢?我们想到了string类的内存是在堆上动态分配的,既然共享内存的各个类指向的是同一个内存区,我们为什么不在这块区上多分配一点空间来存放这个引用计数呢?这样一来,所有共享一块内存区的类都有同样的一个引用计数,而这个变量的地址既然是在共享区上的,那么所有共享这块内存的类都可以访问到,也就知道这块内存的引用者有多少了。 请看下图:![](http://www.91linux.com/upimg/allimg/080317/1532360.jpg)
http://blog.csdn.net/haoel/
补充1:std::string的另一个bug--非线程安全的
线程A里的string strA赋值线程B里的string strB通过某个方法(void setstr(string& strA){ strB=strA;})。这样,当strA,strB到析构时都还共享内存,当它们同时析构时,就可能出现异常.
比如是这样的流程:strA 对引用计数-1,strB对引用计数-1,这样原本是2的计数变成了0, 然后strA释放内存,strB释放内存(crash).这个bug在现在双核的cpu上发生的概率比较高.(单核的cpu概率低多了).
解决的办法和上面的bug一样,通过.c_str()方法(void setstr(LPCSTR lpszA){ strB=lpszA;})消除共享内存.
补充2:
昨天做一个dll,代码很快写完了,然而使用得时候总是遇到string内部指针删除错误,郁闷了一天,今天没去公司,好好研究了一下。
首先看下下面这段代码,声明两个string对象:![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
调试状态下可以看到内部指针:
s1=0x00364ff9 s2=0x00365061 然后执行按下f11,进入xstring源文件:
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
继续进入assign(_X)函数:
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
继续进入assign函数,好戏都在这里面:
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
这样结果就是调用=号以后,s2地址和s1地址一样,都是0x00364ff9。
假如我们动态库有这样一个类class DLL接口:![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
在客户调用时候:
![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
![](http://www.cppblog.com/Images/OutliningIndicators/None.gif)
在调用结束的时候,dll内部删除成员变量的时候,会判断m_str内部指针合法性,由于实际分配是在调用端,在dll内部自然检查指针非法。
解决方法就是避免std::string引用计数,接口处修改为SetString(const char*),这样在dll内部分配内存,内部释放,就不会有问题。
bug分析:
此是老问题了,即跨module(exe、dll)间申请/释放内存违例的问题,对发生在传递c++对象并使用时,不仅仅发生在std::string上
原因是由于程序中使用的内存管理多来源于crt提供的例程,而非直接使用操作系统的接口,这些例程都需要维护一些module全局数据(例如维护 池、维护空闲块、或者标记已申请的块等等,不同的实现中有不同的作用),当他们被静态连编时,实际上这些“全局数据”就不“全局”了,不同的module 各自为政,每份module都有自己的“全局数据”,自身的内存信息不为他人所知,module A的合法内存快自然不可能通得过module B的合法性验证 解决问题的方法有: 1、不要跨module传递c++对象,或者避免释放跨module申请的内存 2、将参与合作的module统统以multithreaded dll方式链入crt库,让他们的“全局”数据真正全局,注意,所有有交互的module都需要动态链入crt,
STL std::string class causes crashes and memory corruption on multi-processor machines:
http://support.microsoft.com/kb/813810/en-us