深浅拷贝以及引用计数
来源:互联网 发布:matlab 最优化算法 书 编辑:程序博客网 时间:2024/06/06 20:44
标准库类型string表示可变长的字符序列,使用string类型必须首先包含它的头文件。
作为标准库的一部分,string定义在命名空间std中。
#include<string>//注意这里没有.husing namespace std;
下面让我们一起来模拟string类中的几个比较重要的成员函数,一步一步的剖析它们的实现机制:
1.构造函数:
示例①
class String{public : String(char *str = "")//construction function :_str(str) {} ~String() { if (NULL != _str) { delete[] _str; } }private : char *_str;};void Test2(){ String s1("hello"); String s2(new char[3]);}
从监视窗口可以看出,s1貌似构建成功了,但是在Test2()快要结束时,程序崩溃了,我们知道,析构函数是在对象销毁前执行的,
所以程序崩溃的原因就是执行析构函数的时候,析构函数中释放对象中指针_str指向的空间,
但是S1中指针_str指向的是一个常量字符串—->存放在代码区(c语言)或者说常量区(操作系统),而delete[]释放的空间必须要是动态分配的,至此我们就可知程序崩溃的原因。
经过上例的讨论,我将构造函数做了些许的修改:
示例②:
class String{public : String(char *str = "")//构造函数 { if (NULL == str) { _str = new char[1];//为了和delete[]配合使用 *_str = '\0'; } else { _str = new char[strlen(str) + 1]; strcpy(_str, str); } } ~String() { if (NULL != _str) { delete[] _str; } }private : char *_str;};
构造函数的参数列表带有默认值也是为了和类库中的一致,当以string s1;形式构造对象时,希望构造出的字符串为空串(含有’\0’)。
2.拷贝构造函数:
①浅拷贝:
String::String(const String& s)//浅拷贝:_str(s._str){}
测试:
void Test2(){ String s1("hello"); String s2(s1);}
上面的程序会崩溃,下面我们来分析原因:
可以看出这里的s2采用了浅拷贝,而析构的时候,先析构的是s2,析构s2时会释放掉“hello”这块空间,接下来再释放s1时,程序就会崩溃,因为这块空间被释放了两次。
浅拷贝:又称为位拷贝,编译器只是将指针的内容拷贝过来,导致多个对象共用一块内存空间,当其中任意对象将这块空间释放之后,另外一些对象并不知道这块空间已经还给了操作系统,以为还有效,所以再对这块空间进行操作时,造成了违规访问。
为了解决这个问题,我们引入了深拷贝(给要拷贝构造的对象重新分配空间):
代码如下:
String::String(const String& s)//深拷贝:_str(new char[strlen(s._str) + 1]){ strcpy(_str, s._str);}
测试:
void Test2(){ String s1("hello"); String s2(s1);}
由监视窗口可知,拷贝的对象s2中_str的值(字符串的地址)和s1对象中的_str的值不同,说明是重新开辟了空间(即就是深拷贝)。
3.赋值运算符重载:
//方法①String& String::operator=(const String& s){ if (this != &s) { delete[] _str; _str = new char[strlen(s._str) + 1]; strcpy(_str, s._str); } return *this;//为了支持链式访问}//方法②(优)String& String::operator=(const String& s){ if (this != &s) { char* tmp = new char[strlen(s._str) + 1]; strcpy(tmp, s._str); delete[] _str; _str = tmp; } return *this;//为了支持链式访问}
测试:
一般情况下,上面的两种写法都可以,但是相对而言,第二种更优一点。
对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。
对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。
综上:第二种方法更优一点。
最后的返回值是为了支持链式访问。
例如:s3 = s2 = s1;
上面所写的拷贝构造函数和赋值运算符重载函数属于传统写法,下面我们一起来看看它们的现代写法:
拷贝构造的现代写法:
赋值运算符重载函数的两种现代写法:
在面试时,一般写出上面四个string类的成员函数即可,除非面试官特别要求。
从上面的深拷贝我们可以看出,相比浅拷贝,深拷贝的效率明显较低,因为每拷贝一个对象就需要开辟空间和释放空间,再有就是赋值运算符重载也是一样的需要重新开辟空间并释放空间。
假如有这样的一种情况,拷贝和赋值得到的对象只用于”读”,而不用于”写”,那么是不是就不需要重新开辟空间了呢?
下面让我们一起来了解一下引用计数的浅拷贝:
我们需要使用一个变量可以标记同一块空间同时有几个对象在使用—–>为了析构(当标记的这个变量为1时,说明只有一个对象在使用这块空间,那么在使用完成后就可以去释放空间,否则让这个计数的变量减1)。
引用计数的浅拷贝:
首先,我们需要讨论下这个所谓的计数变量:
①使用数据成员 int _count;
在构造函数中使得_count = 1;
在拷贝构造函数中_count(++s._count).
但是析构的时候无法改变每个对象中的_count的值。
String.h
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public: String(const char* str = ""); String(String& s); String& operator=(const String& s); ~String();private: int& GetRef(char *str); void release(); char *_str; int _count;};
String.cpp
String::String(const char* str ){ if (NULL == str) { _str = new char[1]; *_str = '\0'; } else { _str = new char[strlen(str) + 1]; strcpy(_str, str); } _count = 1;}String::String(String& s):_str(s._str), _count(++(s._count)){ strcpy(_str, s._str);}String::~String(){ if (--_count == 0 && NULL != _str)//_count是每个对象的,减去一个共享一块空间的其中一个对象的成员,并不会改变其他对象的成员变量_count的值 { cout << this << endl; delete[] _str; }}
所以,使用数据成员变量—–>pass
②使用静态数据成员:static int _count;
构造函数内:_count =1;
拷贝函数内部:_count++;
当出现情况:String s1;
String s2(s1);
String s3(s2);
这时的引用计数_count = 3;
但是当重新构造一个对象时:
String s4;——–>使得该类所有的引用计数_count = 1;
所以:静态数据成员也不可以。
下面详细讲解:
String.h
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public: String(const char* str = ""); String(String& s); String& operator=(const String& s); ~String();private: int& GetRef(char *str); void release(); char *_str; static int _count;};
String.cpp
String::String(const char* str ){ if (NULL == str) { _str = new char[1]; *_str = '\0'; } else { _str = new char[strlen(str) + 1]; strcpy(_str, str); } _count = 1;}String::String(String& s):_str(s._str){ strcpy(_str, s._str); _count++;}String::~String(){ if (--_count == 0 && NULL != _str) { cout << this << endl; delete[] _str; }}int String:: _count = 0;void Test1(){ String s1("hello"); String s2(s1); String s3(s2);}
从上图可以看出静态成员变量是这个类所有的对象所共享的,所以当只是用一个对象去拷贝另一个对象时,这样不会出错,而且貌似也达到我们的要求了,可是当重新构造一个对象时,就会发现所有对象中的_count都修改为了1。
所以,使用静态成员(static int count)————>pass
③使用指针:
使得拷贝的对象共用指向一个引用计数。
赋值运算符重载函数中的两种情况:
第一种情况:
//String.h#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public: String(const char* str = ""); String(String& s); String& operator=(const String& s); ~String();private: int& GetRef(char *str); void release(); char *_str; int *_RefCount;};
//String.cpp#include"String-Ref.h"String::String(const char* str ){ if (NULL == str) { _str = new char[1]; *_str = '\0'; } else { _str = new char[strlen(str) + 1]; strcpy(_str, str); } _RefCount = new int(1);}String::String(String& s):_str(s._str),_RefCount(s._RefCount){ strcpy(_str, s._str); (*_RefCount)++;}String::~String(){ if (--(*_RefCount) == 0 && NULL != _str) { cout << this << endl; delete[] _str; _str = NULL; delete _RefCount; _RefCount = NULL; }}String& String::operator=(String& s){ if (this != &s) { if (--*_RefCount == 0) { delete[] _str; _str = NULL; delete _RefCount; _RefCount = NULL; } _str = s._str; _RefCount = s._RefCount; (*s._RefCount)++; } return *this;}void Test1(){ String s1("hello"); String s2(s1); String s3(s2); String s4("world"); s4 = s2;}
由监视窗口的数据可得,使用指针可以完成引用计数的浅拷贝,但是因为每构造一个对象,都需要开辟两块空间,这样容易造成内存碎片。
由new[]可以联想到类似模型—->只开辟一块空间(多开四个字节),把引用计数放在字符串首地址的前四个字节上。
这样不但解决了内存碎片问题,而且也可以程序的运行效率。
引用计数的浅拷贝的最优解决方案:
(这里我就不一一分析了,思路和上面使用指针是一样的)
//String.h#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public : String(const char* str = ""); String(const String& s); String& operator=(const String& s); ~String();private : int& GetRef(char *str); void release(); char *_str;};
//String.cpp#include"string_simi.h"String::String(const char* str)//构造函数{ if (NULL == str) { _str = new char[5]; _str += 4; *_str = '\0'; } else { _str = new char[strlen(str) + 5]; _str += 4; strcpy(_str, str); } GetRef(_str) = 1;}String::String(const String& s)//拷贝构造函数:_str(s._str){ GetRef(_str)++;}//s1 = s2;String& String::operator=(const String& s)//赋值运算符重载{ if (_str != s._str)//判断自赋值 { //1.当s1的引用计数为1时 //①释放空间 //②改变s1指针的指向 //③s2的引用计数加1 release(); //当s1的引用计数大于1时 //①s1的引用计数减1 //同上②③ _str = s._str; GetRef(s._str)++; } return *this;}void String::release(){ if (--GetRef(_str) == 0) { delete[](_str - 4); }}int& String::GetRef(char *str)//获取引用计数{ return *(int*)(str - 4);}String::~String()//析构函数{ if (--GetRef(_str) == 0) { delete[] (_str - 4); }}void TestString1(){ String s1("hello"); String s2(s1); String s3("world"); s1 = s3;}int main(){ TestString1(); return 0;
引用计数的浅拷贝同样存在缺陷—>当几个共用同一块空间的对象中的任一对象修改字符串中的值,则会导致所有共用这块空间的对象中的内容被破坏掉。
由此—->引入了写时拷贝(Copy On Write)
顾名思义,写时拷贝—>写的时候进行拷贝(深拷贝)
//String.h#pragma once#include<iostream>using namespace std;#include<assert.h>class String{public: String(const char* str = "")//构造函数 { if (NULL == str) { char* pTemp = new char[1 + 4]; _pStr = pTemp + 4; _GetRefer() = 1; *_pStr = '\0'; } else { char* pTemp = new char[strlen(str) + 1 + 4]; _pStr = pTemp + 4; strcpy(_pStr, str); _GetRefer() = 1; } } String(const String& s) :_pStr(s._pStr) { _GetRefer()++; } ~String() { Release(); } String& operator=(const String& s); char& operator[](size_t index); const char& operator[](size_t index)const; int& String::_GetRefer(); void Release(); friend ostream& operator<<(ostream& _cout, const String& s);private: char* _pStr;};
//String.cpp#define _CRT_SECURE_NO_WARNINGS 1#include"String3.h"int& String::_GetRefer()//获取引用计数{ return *((int*)_pStr - 1);}void String::Release()//释放空间{ if (--_GetRefer() == 0) { _pStr -= 4; delete[] _pStr; _pStr = NULL; }}String& String::operator=(const String& s){ if (_pStr != s._pStr) { if (--_GetRefer() == 0)//需要考虑两种情况 { Release(); _pStr = s._pStr; _GetRefer()++; } else { _pStr = s._pStr; _GetRefer()++; } } return *this;}char& String::operator[](size_t index){ if (_GetRefer() > 1) { _GetRefer()--; char* pTemp = new char[strlen(_pStr) + 1 + 4]; pTemp += 4; strcpy(pTemp, _pStr); _pStr = pTemp; _GetRefer() = 1;//新开的空间 } return _pStr[index];}const char& String::operator[](size_t index)const{ return _pStr[index];}void Test1(){ const String s1("hello"); String s2(s1); //String s3; //s3 = s2; cout << s1[4] << endl;}int main(){ Test1(); return 0;}
[]重载运算符需要成对重载:char& String::operator[](size_t index){ if (_GetRefer() > 1) { char* pTemp = new char[strlen(_pStr) + 1 + 4]; pTemp += 4; strcpy(pTemp, _pStr); _GetRefer()--; _pStr = pTemp; _GetRefer() = 1;//新开的空间 } return _pStr[index];}const char& String::operator[](size_t index)const{ return _pStr[index];}
- 深浅拷贝以及引用计数
- 深浅拷贝与引用计数
- 深浅拷贝&引用计数写时拷贝
- 【c++】深浅拷贝,引用计数写时拷贝
- 漫步IOS--引用计数函数和深浅拷贝
- iOS 内存管理~深浅拷贝~引用计数器
- 引用计数+写时拷贝
- String浅拷贝---引用计数
- 拷贝控制和引用计数
- 引用计数写时拷贝
- 引用计数写时拷贝
- 引用计数写时拷贝
- 引用计数+写时拷贝
- 引用计数--写时拷贝
- java深浅复制以及引用传递
- 深浅拷贝
- 深浅拷贝
- 深浅拷贝
- 201409-2-画图
- ios 百度推送
- EasyUI两种方式
- linux centos 下 ftp服务器配置 :vsftp简易配置 附带功能说明
- 大数据的存储—HBase
- 深浅拷贝以及引用计数
- Mac下github初始化及代码提交
- mysql 远程权限分配
- 多渠道打包
- Vulkan Programming Guide 第一章(1)
- ListView怎么添加表头
- Groovy开发工具包
- 《unix/linux编程实践教程》学习笔记:第五章 连接控制:学习stty
- 活出自己的传奇