本文旨在通过对 写时拷贝 的四个方案(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;
} 

转载于:https://www.cnblogs.com/Lynn-Zhang/p/5400714.html

写时拷贝(Copy On Write)方案详解相关推荐

  1. python3多进程写时拷贝_Python实现多进程的详解(附示例)

    本篇文章给大家带来的内容是关于Python实现多进程的详解(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. fork函数创建子进程 基本使用 Linux 操作系统提供了一个 ...

  2. 写时复制,写时拷贝,写时分裂,Copy on write

    2019独角兽企业重金招聘Python工程师标准>>> 写时复制,写时拷贝,写时分裂 (Copy-on-write,简称COW)是计算机资源管理方面的一种优化技术,有着广泛的应用,比 ...

  3. C++ 写时拷贝 3

    http://blog.csdn.net/ljianhui/article/details/22895505 字符串一种在程序中经常要使用到的数据结构,然而在C中却没有字符串这种类型.在C++中,为了 ...

  4. 写时复制(Copy On Write)

    前言 在读<Redis设计与实现>关于哈希表扩容的时候,发现这么一段话: 执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操 ...

  5. php zval_copy_static_var(),zval _ 引用计数 _ 变量分离 _ 写时拷贝

    zval.引用计数.变量分离.写时拷贝 我们一步步来理解 1.php语言特性 PHP是脚本语言,所谓脚本语言,就是说PHP并不是独立运行的,要运行PHP代码需要PHP解析器,用户编写的PHP代码最终都 ...

  6. C++ String类写时拷贝 4

    http://blog.51cto.com/zgw285763054/1839752 维基百科: 写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略.其核心思 ...

  7. C++ 写时拷贝 2

    什么情况下会用到c++中的拷贝构造函数]: 1)用已经存在的同类的对象去构造出另一个新的对象 2)当函数的形参是类的对象时,这时调用此函数,使用的是值的拷贝,也会调用拷贝构造函数   3)当函数的返回 ...

  8. 【Linux进程、线程、任务调度】二 fork/vfork与写时拷贝 线程的本质 托孤 进程睡眠和等待队列

    学习交流加(可免费帮忙下载CSDN资源): 个人微信: liu1126137994 学习交流资源分享qq群1(已满): 962535112 学习交流资源分享qq群2(已满): 780902027 学习 ...

  9. linux进程--写时拷贝技术copy-on-write(七)

    COW技术初窥: 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了"写时复制"技术,也就是只 ...

最新文章

  1. 【CyberSecurityLearning 34】Linux脚本编写(Shell脚本)
  2. 电子设计竞赛电源题(4)-Buck与Boost电路
  3. js 延迟几秒执行_深入研究 Node.js 的回调队列
  4. ACL2020 | 基于Knowledge Embedding的多跳知识图谱问答
  5. 年终总结 Trustdata:2017年中国移动互联网行业发展分析报告
  6. ORACLE 11GR2 RAC new features 新特性。
  7. 20181102_WCF简单双工
  8. COCOS2D-X 动作 CCSequence动作序列
  9. Java入门到精通——工具篇之Maven概述
  10. ETERM操作和错误集合
  11. Flink中的Window计算-增量计算全量计算
  12. 【中医学】8 中药-1
  13. 配置可访问外网静态IP
  14. 老徐和阿珍的故事:缓存穿透、缓存击穿、缓存雪崩、缓存热点,傻傻分不清楚
  15. 目标检测---教你利用yolov5训练自己的目标检测模型
  16. ESXI VM快照删除卡在99%
  17. 如何删除tmp计算机桌面,win10系统下tmp临时文件删除不了如何解决
  18. 阿里云专网服务器连接互联网的设置
  19. 小麦苗数据库巡检脚本,支持Oracle、MySQL、SQL Server和PG等数据库
  20. Beeline 的进阶使用

热门文章

  1. 通讯录管理系统(C语言)
  2. IE6 png背景图片显示不正常处理
  3. 美团性能分析框架和性能监控平台
  4. SQL case when then 的用法
  5. Script:列出失效索引或索引分区
  6. 2008年IT业界10大预言 [转]
  7. 如果我要...(开发版)
  8. t test formula
  9. 剑桥大学的组织架构和行政机构:学院高度自制
  10. 关于 Visual stdio 编译报错:error MSB6006: “CL.exe”已退出