# C++学习笔记------String写时拷贝
c/c++中耗时最大的几个操作:
(1)文件操作
(2)内存的申请和释放
写时拷贝(copy_on_write)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个对象同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个对象试图修改资源的内容时,系统才会真正复制一份专用副本给对象,而其他调用者所见到的最初的资源仍然保持不变。(参考)
引用计数
String类中的写时拷贝技术是指用浅拷贝的方法拷贝其他对象,多个指针指向同一块空间,只有当对其中一个对象修改时,才会开辟一个新的空间给这个对象,和它原来指向同一空间的对象不会受到影响,显然内存的申请和释放操作少了很多,所以写时拷贝的效率远高于深拷贝。
可以通过增加一个成员变量,作为计数器来实现写时拷贝,这个变量叫做引用计数,统计这块空间被多少个对象的_str同时指向。当用指向这块空间的对象拷贝一个新的对象出来时引用计数加1,当指向这块空间的一个对象指向别的空间或析构时引用计数减1。只有当引用计数等于0时才可以真正释放这块空间,否则说明还有其他对象指向这块空间,不能释放。
以上部分(参考)
引用计数该怎么设置?
- 设置成普通成员变量(int _num)----->error,每个对象都有独立的__num
- 设置成静态成员变量 static int _num----->error,所有对象使用一个__num,但实际上只有指向相同资源的对象才可以共有一个引用计数(不知道怎么表达,但是确实明显与写时拷贝的策略不同)
- static map<char*, int> _map; 这种方式是可以的,但是现在还没学过这个
- 如下所示,在开辟_str时在前面4个字节即在这块空间的头部保存引用计数
基于上一节的Mstring类的实现,我们要进行一定修改,下面只列出修改的部分
写时拷贝新增的操作(私有成员方法,不对外提供接口)
- 获取字符串的实际起始地址
char* get_str_begin()//常对象无法调用普通方法
{
return _str+STR_MORE_LEN;//找到有效字符串的实际位置
}
const char* get_str_begin()const
{
return _str + STR_MORE_LEN;
}
-
获取引用计数
返回值是引用,这样对返回值进行修改,就可以直接修改引用计数了
int& get_str_num()//获取引用计数, 注意返回的是引用, 常对象无法调用该方法
{//强转+解引用--->目的:取前四字节
return *((int*)_str);
}
const int& get_str_num()const
{//强转+解引用--->目的:取前四字节
return *((int*)_str);
}
-
引用计数的增加和减少
注意,有对象要共享资源时,引用计数才加1(用已构造的对象*拷贝构造新的对象,显然可以处理成共享一块内存资源)
我的问题:等号赋值运算符(==)要不要增加引用计数? 答:要加
当有对象要对共享的资源进行修改时,拷贝一份副本给这个对象,原来的引用计数减一,新申请的空间中,引用计数初始为1
int add_str_num()//引用计数增加,利用已构造的对象拷贝构造新的对象时,需要增加引用计数
{
if (_str != NULL)//引用计数放在申请的_str的头部
{
get_mutex()->lock();
int num = ++get_str_num();
get_mutex()->unlock();
return num;
}
return 0;
}
int down_str_num()//引用计数减少,对象析构或者是要对共享资源进行写操作,得到了一份共享资源的副本
{
if(_str != NULL)
{
get_mutex()->lock();
int num = --get_str_num();
get_mutex()->unlock();
return num;
}
return 0;
}
-
销毁_str原来指向的区域
注意引用计数为0时才会实际进行摧毁
void destory_str()//摧毁_str
{
if (_str != NULL && down_str_num()==0)//引用计数为0时才会实际进行摧毁
{
delete get_mutex();
delete[]_str;
_str = NULL;
_len = 0;//老师这里写的是_len = 0
_val_len = 0;
}
}
- 写时拷贝的实现
- 要调用写时拷贝,至少有两个对象完成了构造,并指向一块相同的区域,所以引用计数(get_str_num()) >1
- 注意this->str指针的指向将会发生变化, this指针没变, 我在代码旁做了注释
void copy_while_write(/*this*/)
{
if (get_str_num() > 1)//要调用写时拷贝,至少有两个对象完成了构造,并指向一块相同的区域,所以
{ //深拷贝
char* tmp = new char[_len];
strcpy_s(tmp + STR_MORE_LEN, _val_len + 1, get_str_begin());
down_str_num();
_str = tmp;//this->_str现在指向了一块复制好的空间
get_str_num() = 1;//新申请的内存的引用计数部分初始值为1, this->get_str_num()
get_mutex() = new mutex();
}
}
加入写时拷贝策略后修改的部分
- 拷贝构造的实现
Mstring(const Mstring& src)//利用以构造的对象拷贝构造另一个对象,那么我们就让它们访问相同的资源
{
_len = src._len;
_val_len = src._val_len;
_str = src._str;//浅拷贝(共用一块内存)
//没问题,指向一块内存区域就增加引用计数,如果没有指向,就不操作
if (_str != NULL)
{
add_str_num();//增加引用计数
}
}
- 等号赋值运算符重载
- 防止自赋值
- 拷贝一份副本给调用该方法的对象,引用计数减一,若为0,要销毁 _str原来指向的区域,防止内存泄漏
- 浅拷贝
Mstring& operator=(/*this,*/const Mstring& src)
{
//防止自赋值
if (&src == this)
{
return *this;
}
destory_str();//引用计数先减,减后若为0则销毁
//这里有问题吧
_len = src._len;
_val_len = src._val_len;
_str = src._str;//this->_str与src._str指向相同资源
add_str_num();
return *this;
}
- push_back()的实现
void push_back(/*this, */char c)//对象要对共享资源的区域进行修改时,复制一份副本给调用的对象
{
if (_str == NULL)//默认构造后push_back
{
_val_len = 1;
_len = _val_len + 1;
}
if (_str != NULL)
{//写时拷贝
copy_while_write();
}//this->_str指针已经指向了复制好的副本
if (is_full())//this->is_full()
{
revert();//this->revert()
}
get_str_begin()[_val_len] = c;//把原来'\0'的位置覆盖掉
get_str_begin()[++_val_len] = 0;//后一位放'\0',_val_len加一
}
- pop_back()的实现
char pop_back(/*this*/)//对共享资源有修改,同样要进行写时拷贝
{
if (_str == NULL || is_empty())
{
return 0;//我觉得实际应该要进行异常处理
}
if (_str != NULL)
{
copy_while_write();
}//this->_str指针已经指向了复制好的副本
char c = get_str_begin()[_val_len - 1];
get_str_begin()[_val_len-1] = 0;
_val_len--;
return c;//不能返回局部变量的引用和指针
}
- +运算符重载
Mstring operator+(/*this, */const Mstring& src)//字符串中是拼接操作
{
Mstring tmp = *this;
//注意i=0, src._str[0]是第一个元素的位置,src._str[_val_len-2]是'\0'的位置
for (int i = 0; i < src._val_len - 1; i++) //(_val_len-1)---->'\0'的位置
{
tmp.push_back(src.get_str_begin()[i]);
}
return tmp;
}
- []运算符重载
//1.为啥返回引用?---->能够直接修改字符串内容
//2.为啥返回引用可能造成写时拷贝--->对返回值修改,会直接对共享资源进行修改
char& operator[](int pos)//普通对象可以进行修改,为了修改才返回引用
{
if (_str != NULL)
{
copy_while_write();
}
return get_str_begin()[pos];
}
//常对象调用该方法
//不返回引用,因为常对象不能被修改
char operator[](/*常对象指针*/int pos)const //对常对象无法进行修改
{
return get_str_begin()[pos];
}
测试部分
int main()
{
Mstring str1;
//cout << str1 << endl;
Mstring str2 = "qqqq123456";
cout << str2 << endl;
str1 = str2;
cout << str1 << str2 << endl;
Mstring str3(str1);
cout << str3 << endl;
str1[1] = '0';
cout << str1 << endl;
cout << str3 << endl;
str2.push_back('a');
cout << str2 << endl;
cout << str3 << endl;
str3.pop_back();
cout << str3 << endl;
}
结果(注意地址和引用计数_num的变化):


浙公网安备 33010602011771号