写时拷贝(Copy On Write)方案详解
本文旨在通过对 写时拷贝 的四个方案(Copy On Write)分析,让大家明白写时拷贝的实现及原理。
关于浅拷贝与深拷贝,我在之前的博客中已经阐述过了
浅拷贝容易出现指针悬挂的问题,深拷贝效率低,但是我们可以应用引用计数来解决浅拷贝中多次析构的问题,写时拷贝也就应运而生了。
首先要清楚写时拷贝是利用浅拷贝来解决问题!!
方案一
class String
{
private:
char* _str;
int _refCount;
};
方案一最不靠谱,它将用作计数的整形变量_refCount定义为类的私有成员变量,任何一个对象都有它自己的成员变量_refCount,它们互不影响,难以维护。只要拷贝出了对象,_refCount大于了0,每个对象在调用自己的析构函数时--_refCount不等于0,那么它们指向的那块内存都将得不到释放,无法达到我们要的效果。


//以下是对方案一的简单实现,大家可以结合上图感受到方案一的缺陷 class String
{
public:
String(char* str = "") //不能strlen(NULL)
:_refCount(0)
{
_str = new char[strlen( str) + 1];
strcpy(_str, str);
_refCount++;
}
String(String &s)
:_refCount( s._refCount)
{
_str = s._str;
_refCount++;
s._refCount = _refCount; //这里虽然可以让两个对象的_refCount相等,
//但如果超过两个对象的_str指针都指向同一块内存时,
//就无法让所有对象的_refCount都保持一致
//这是方案一的缺陷之一
}
~String()
{
if (--_refCount == 0)
{
delete[] _str;
_str = NULL;
cout << "~String " << endl;
}
}
friend ostream& operator<<( ostream& output, const String &s);
private:
char* _str;
int _refCount;
};
ostream& operator<<( ostream& output, const String & s)
{
output << s._str;
return output;
}
void Test()
{
String s1("aaa");
String s2(s1);
String s3(s2);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
方案二
class String
{
private:
char* _str;
static int count;
};
设置了一个静态整形变量来计算指向一块内存的指针的数量,每析构一次减1,直到它等于0(也就是没有指针在指向它的时候)再去释放那块内存,看似可行,其实不然!
这个方案只适用于只调用一次构造函数、只有一块内存的情形,如果多次调用构造函数构造对象,新构造的对象照样会改变count的值,那么以前的内存无法释放会造成内存泄漏。

结合上图和下面的代码,我们可以清楚地看到该方案相比方案一的改善,以及缺陷
class String
{
public:
String(char* str = "") //不能strlen(NULL)
{
_str = new char[strlen( str) + 1];
strcpy(_str, str); count++;
}
String(const String &s)
{
_str = s._str;
count++; }
String& operator=( String& s)
{
_str = s._str;
count++;
return *this;
}
~String()
{
if (--count == 0)
{
delete[] _str;
_str = NULL;
cout << "~String " << endl;
}
}
friend ostream& operator<<( ostream& output, const String &s);
friend istream& operator>>( istream& input, const String &s);
private:
char* _str;
static int count;
}; int String::count = 0; //初始化count
void Test() //用例测试
{
String s1("abcdefg");
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl; String s4("opqrst");
String s5(s4);
String s6 (s5);
s6 = s4;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
}
方案三
问题的关键是,我们不是要为每一个对象建立一个引用计数,而是要为每一块内存设置一个引用计数,只有这样才方便我们去维护。当指向这块内存的指针数为0时,再去释放它!
class String
{
private:
char* _str;
int* _refCount;
};
方案三设置了一个int型的指针变量用来引用计数,每份内存空间对应一个引用计数,而不是每个对象对应一个引用计数,而且内存之间的引用计数互不影响,不会出现方案一和方案二出现的问题。

1.在实现赋值运算符重载时要谨慎,不要遇到下图的情形
s1指向内存1,s2指向内存2,利用s2拷贝出的对象s3也指向内存块2,这时候内存块1的引用计数等于1 ,内存块2的引用计数等于2。一切似乎都很正常,但是调用赋值运算符重载执行语句:s2=s1后,错误慢慢显现出来了。将s2指向内存1 并把内存1 的引用计数加1,这理所当然,但是不能把s2原本指向的空间直接delete,s3还指向内存2着呢!这里千万在释放一块空间前,对指向这块内存的引用计数进行检查,当引用计数为0的时候再去释放,否则只做减引用计数就行。
//错误代码
String& operator=(String& s)
{
if (_str!= s._str)
{
delete[] _str;
delete _refCount;
_str = s._str;
_refCount = s._refCount;
(*_refCount)++;
}
return *this;
}

2.改变字符串的某个字符时要谨慎,不要遇到类似下图所遇到的问题。
如果多个对象都指向同一块内存,那么只要一个对象改变了这块内存的内容,那所有的对象都被改变了!!
如下图:当s1和s2都指向内存块1,s3经过赋值运算符重载后也指向内存块1,现在s2如果对字符串进行修改后,所有指向内存块1 的指针指向的内容都会被改变!

可以用下图的形式改善这种问题:新设置一块内存来存要改变的对象,这样就不会影响其他的对象了

案例3我画的图较多,方便大家结合代码去理解
//案例三
class String
{
public:
String(char* str = "") //不能strlen(NULL)
{
_refCount = new int(1); //给_refCount开辟空间,并赋初值1
_size = strlen(str);
_capacity = _size + 1;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String &s)
{
_refCount = s._refCount;
_str = s._str;
_size = strlen(s._str);
_capacity = _size + 1;
(*_refCount)++; //拷贝一次_refCount都要加1 } //要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存
//如果要释放原内存时,要考虑它的_refCount减1后是否为0,为零再释放,否则其它对象指针无法再访问这片空间
String& operator=(String& s)
{
if (_str!= s._str)
{
_size = strlen(s._str);
_capacity = _size + 1;
if (--(*_refCount) == 0)
{
delete[] _str;
delete _refCount;
} _str = s._str;
_refCount = s._refCount;
(*_refCount)++;
}
return *this;
}
//如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变
//如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间,
//把原字符串拷贝过来
//再去改变它的内容,就不会产生链式反应
// 1.减引用计数 2.拷贝 3.创建新的引用计数
char& String::operator[](const size_t index) //参考深拷贝
{
if (*_refCount==1)
{
return *(_str + index);
}
else
{
--(*_refCount);
char* tmp = new char[strlen(_str)+1];
strcpy(tmp, _str);
_str = tmp;
_refCount = new int(1);
return *(_str+index);
}
}
~String()
{
if (--(*_refCount)== 0) //当_refCount=0的时候就释放内存
{
delete[] _str;
delete _refCount;
_str = NULL;
cout << "~String " << endl;
}
_size = 0;
_capacity = 0;
}
friend ostream& operator<<(ostream& output, const String &s);
friend istream& operator>>(istream& input, const String &s);
private:
char* _str; //指向字符串的指针
size_t _size; //字符串大小
size_t _capacity; //容量
int* _refCount; //计数指针
}; ostream& operator<<(ostream& output, const String &s)
{
output << s._str;
return output;
}
istream& operator>>(istream& input, const String &s)
{
input >> s._str;
return input;
} void Test() //用例测试
{
String s1("abcdefg");
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
s2[3] = '0';
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
方案四
class String
{
private:
char* _str;
};

方案四与方案三类似。方案四把用来计数的整型指针变量放在所开辟的内存空间的首部。
用*((int*)_str)就能取得计数值
class String
{
public:
String(char * str = "" ) //不能strlen(NULL)
{
_str = new char[strlen( str) + 5];
_str += 4;
strcpy(_str, str);
GetRefCount(_str) = 1;
}
String(const String &s)
{
_str = s._str;
++GetRefCount(_str);
} //要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存
//如果要释放原内存时,要考虑它的_refCount减1后是否为0,
//为零再释放,否则其它对象指针无法再访问这片空间
String& operator=(String& s)
{
if (this != &s )
{
if (GetRefCount(_str ) == 1)
{
delete (_str-4);
_str = s._str;
++GetRefCount(_str );
}
else
{
--GetRefCount(_str );
_str = s._str;
++GetRefCount(_str );
}
}
return *this ;
}
//如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变
//如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间,
//把原字符串拷贝过来.
//再去改变它的内容,就不会产生链式反应 char& String ::operator[](const size_t index ) //深拷贝
{ if (GetRefCount(_str) == 1)
{
return _str[index ];
}
else
{
// 1.减引用计数
--GetRefCount(_str );
// 2.拷贝 3.创建新的引用计数
char* tmp = new char [strlen(_str) + 5];
*((int *)tmp) = 1;
tmp += 4;
strcpy(tmp, _str);
_str = tmp;
return _str[index ];
}
} int& GetRefCount(char* ptr) //获取引用计数(隐式内联函数)
{
return *((int *)(ptr -4));
}
~String()
{
if (--GetRefCount(_str) == 0)
{
cout << "~String" << endl;
delete[] (_str-4);
} }
friend ostream& operator<<( ostream& output, const String &s);
friend istream& operator>>( istream& input, const String &s);
private:
char* _str; }; ostream& operator<<(ostream& output, const String &s)
{
output << s._str;
return output;
}
istream& operator>>(istream& input, const String &s)
{
input >> s._str;
return input;
} void Test() //用例测试
{
String s1("abcdefg" );
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
s2[3] = '0';
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
写时拷贝(Copy On Write)方案详解的更多相关文章
- [转] Linux写时拷贝技术(copy-on-write)
PS:http://blog.csdn.net/zxh821112/article/details/8969541 进程间是相互独立的,其实完全可以看成A.B两个进程各自有一份单独的liba.so和l ...
- 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ...
- Java编程的逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Linux写时拷贝技术(copy-on-write)
COW技术初窥: 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内 ...
- 【转】Linux写时拷贝技术(copy-on-write)
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html 源于网上资料 COW技术初窥: 在Linux程序中,fork()会 ...
- copy-on-write(写时拷贝技术)
今天看<Unix环境高级编程>的fork函数与vfork函数时,看见一个copy-on-write的名词,貌似以前也经常听见别人说过这个,但也一直不明白这究竟是什么东西.所以就好好在网上了 ...
- 关于Linux平台malloc的写时拷贝(延迟分配)【转】
Linux内核定义了“零页面”(内容全为0的一个物理页,且物理地址固定),应用层的内存分配请求,如栈扩展.堆分配.静态分配等,分配线性地址后,就将页表项条目指向“零页面”(指定初始值的情况除外),这样 ...
- String类的实现(4)写时拷贝浅析
由于释放内存空间,开辟内存空间时花费时间,因此,在我们在不需要写,只是读的时候就可以不用新开辟内存空间,就用浅拷贝的方式创建对象,当我们需要写的时候才去新开辟内存空间.这种方法就是写时拷贝.这也是一种 ...
- 并发容器之写时拷贝的 List 和 Set
对于一个对象来说,我们为了保证它的并发性,通常会选择使用声明式加锁方式交由我们的 Java 虚拟机来完成自动的加锁和释放锁的操作,例如我们的 synchronized.也会选择使用显式锁机制来主动的控 ...
随机推荐
- Eclipse 工作空间(Workspace)
Eclipse 工作空间(Workspace) eclipse 工作空间包含以下资源: 项目 文件 文件夹 项目启动时一般可以设置工作空间,你可以将其设置为默认工作空间,下次启动后无需再配置: 工作空 ...
- MongoDB API和python操作
安装 下载mongodb的版本,两点注意 根据业界规则,偶数为稳定版,如1.6.X,奇数为开发版,如1.7.X 32bit的mongodb最大只能存放2G的数据,64bit就没有限制 到官网,选择合适 ...
- JAVA中BufferedReader设置编码的必要性
实验环境 Myeclipse 默认编码 UTF-8 先看两种读文件的方式: 方式一: InputStreamReader fReader = new InputStreamReader(new Fil ...
- KnockOut 绑定之foreach绑定(mvc+knockout)
什么时候使用foreach绑定 foreach绑定对于数组中的每一个元素复制一节标记语言,也就是html,并且将这节标记语言和数组里面的每一个元素绑定.当我们呈现一组list数据,或者一个表格的时候, ...
- python学习【第四篇】python函数 (一)
一.函数的介绍 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.你已经知道Python提供了许多内建函数,比如print().但你也可以 ...
- 《从零开始学Swift》学习笔记(Day 33)——属性观察者
原创文章,欢迎转载.转载请注明:关东升的博客 为了监听属性的变化,Swift提供了属性观察者.属性观察者能够监听存储属性的变化,即便变化前后的值相同,它们也能监听到. 属性观察者主要有以下两个: l ...
- 《从零开始学Swift》学习笔记(Day 22)——闭包那些事儿!
原创文章,欢迎转载.转载请注明:关东升的博客 我给Swift 中的闭包一个定义:闭包是自包含的匿名函数代码块,可以作为表达式.函数参数和函数返回值,闭包表达式的运算结果是一种函数类型. Swif ...
- 巨蟒python全栈开发django1:自定义框架
今日大纲: 1.val和text方法的补充 2.信息收集卡用bootstrap实现 3.自定义web框架 4.http协议 5.自定义web框架2 今日内容详解: 1.val和text方法的补充 ht ...
- iOS 多线程之 GCD 的基本使用
什么是GCD 全称Grand Central Dispatch 中暑调度器 纯C语言 提供了很多强大的函数 GCD 的优势 GCD是苹果公司为多核的并行运算提出的解决方案 GCD会自动利用更多的CPU ...
- 【python】-- Ajax
Ajax AJAX,Asynchronous JavaScript and XML (异步的JavaScript和XML),一种创建交互式网页应用的网页开发技术方案. 异步的JavaScript:使用 ...