浅谈深浅拷贝问题(这里只针对拷贝构造函数和赋值运算符重载)和简易srting类模拟实现
一、什么是浅拷贝:
浅拷贝也叫值拷贝、位拷贝,是编译器将被拷贝的对象原封不动的拷贝过来(注意是原封不动的拷贝),也就是编译器只把被拷贝的对象的值拷贝过来而已;注意编译器提供默认浅拷贝的拷贝构造函数不一定会造成操作违规,但如果对象中涉及资源管理问题,就会导致其中一个对象使用完这份资源后,把这份资源给释放了,然而其他共享这些资源的对象以为该资源还存在,当对该资源进行访问操作时,就会导致访问操作违规。
二、再谈浅拷贝:
假如模拟实现string类时,我们没有显式提供拷贝构造函数和赋值运算符重载,编译器就会默认生成一个拷贝构造函数和一个赋值运算符重载,但编译器默认生成的都是浅拷贝函数;如:
在上图中:
void Test()
{
string s1("hello world");//调用构造函数
string s2(s1);//此时调用拷贝构造函数
}
代码中我们没有显式定义拷贝构造函数,编译器会默认生成一个浅拷贝的拷贝构造函数,此时拷贝构造函数会把s1原封不动的拷贝过去,但s1._pstr此时是指向"hello world"字符串,然后s2._pstr把s1._pstr拷贝过去,使得s2._pstr得到字符串的首地址,后s2._pstr也就指向"hello world"字符串,使得s1、s2共享一份空间的资源;当完成s2的拷贝构造后,出Test函数时,首先调用s2的析构函数销毁s2对象,s2为了防止内存泄漏,就会把其使用后的资源回收,当释放s1时,s1._pstr虽然仍指向那个空间,此时那片空间已被s2的析构函数回收,s1._pstr已没有那片空间的使用权,造成s1._pstr是一个野指针,但s1不知到该资源已经被s2释放,然后s1继续释放该空间资源,造成一份资源多次释放,且s1想调用析构函数回收s1._pstr指向的那份空间时,因为s1._pstr已没有那片空间的使用权,两者都会引起程序崩溃。
三、浅拷贝的解决方法(只针对拷贝构造函数和赋值运算符重载):
1.深拷贝:
从上面可以知道产生浅拷贝的原因主要有一份资源被多次释放和想销毁一个野指针指向的空间,那么深拷贝就从根源上解决问题:
此时对于拷贝构造函数:
string(const string&s)
:_str(new char[strlen(s._str]+1)
{
strcpy(_str,s._str);
}
对于赋值运算重载:
string& operator=(const string& s)
{
if(this!=&s)
{
char*_pstr=new char[strlen(s._str)+1];//防止第二个字符串太长,所以另外申请空间
strcpy(_pstr,s._str);
delete[] _str;
_str=_pstr;
}
return *this;
}
通过上面的代码,就解决了引起浅拷贝的两个问题,主要解决思想是另外申请一个空间,把内容拷贝过去,这样就不会存在资源共享问题,也就不存在浅拷贝,但上面代码利用率低,复用性不高。
当然深拷贝也可以这样解决,代码如下:
对于拷贝构造函数:
string(const string& s)
:_str(nullptr)
{
string strTemp(s._str);
swap(_str,strTemp._str);
}
上面的代码中对_str(nullptr)的初始化是必须的,因为构造函数肯定是不会出现浅拷贝问题的,所以首先调用构造函数构造一个临时对象strTemp,把s的内容进行一份拷贝,然后用swap函数把_str和strTemp._str这两个指针进行交换,这样_str就得到s的那份拷贝,而strTemp._str就得到nullptr,当出拷贝构造函数是,释放临时对象strTemp,因为此时strTemp._str已经为空,析构函数就不会对其空间进行释放,所以_str(nullptr)的初始化是必须的,这样就解决了拷贝构造函数的浅拷贝问题。
对于赋值运算符重载:
string& operator=(string s)
{
swap(_str,s._str);
return *this;
}
/*
string& operator=(string& s)
{
if(this!=&s)
{
string strTemp(s._str);
swap(_str,strTemp._str);
}
return*this;
}
*/
对于上面的代码,第一种方法传递的参数为string s,相当于是s的一份临时拷贝,此时s的改变不会引起实参的改变,然后把_str和s._str进行交换,然后返回this,出作用域后,临时对象就被销毁,不用显示销毁。对于第二种方法:首先进行判断,避免了自己给自己赋值,和拷贝构造函数思想基本一样,赋值运算符重载首先通过构造函数构造一个临时变量strTemp,这样strTemp就得到s._str的一份拷贝,然后把_str和strTemp._str进行交换,这样_str就得到s._str的一份拷贝,strTemp._str得到_str的内容,最后释放strTemp临时对象时,strTemp._str得到_str的内容也就被析构函数回收释放了,不会造成资源泄漏。
2.浅拷贝+引用计数:
(1).通过这种方法解决主要思想是:
a.怎么解决多次释放一份资源问题:此时是和教室里最后一个人走关灯一样,如果我们知道当前共有多少个对象共享一份资源,当一个对象准备释放时,如果发现还有对象使用这份资源,就不释放,此时就解决一份资源多次释放问题
b.怎么知道当前共有多少个对象共享这份资源:引用计数
(2)拷贝构造函数的解决方法:
(注意之所以把错解写出来,是因为后面每一步都是在错解的基础上进行改正的,方便理解)
错解1:在成员中定义一个int型变量,用来记录共享这份资源的对象的个数
#if 0
namespace wolf
{
class string
{
public:
string(const char* str = "")//构造函数
:_str(new char[strlen(str) + 1])
{
if (str == nullptr)
{
str = "";
}
_count = 1;//因为一旦通过构造函数来构造一个对象,说明这个对象的资源只有它自己用,还没来得及共享,就把_count置为1
strcpy(_str, str);
}
string(string& s)//拷贝构造函数
:_str(s._str)//在拷贝构造函数中采用浅拷贝的方式
,_count(++s._count)//string s1("wolf");string s2(s1);此时有对象s2通过拷贝构造函数共享s1这个对象的资源时,
//就把s1._count加1,表明现在除了s1自己外还有别人共享这份资源,然后把s1._count赋值给需要共享的对象s2,
//此时s2._count也记录了当前共享一份资源的对象的个数;
{
}
string& operator=(const string& s)//赋值运算符重载
{
if (this != &s)//防止自己给自己赋值
{
//.....
}
return *this;
}
~string()//析构函数
{
if (_str&&(--_count==0))//_count-1==0的意思是除去自己这个对象使用这份资源外,如果为0,表明除了自己
//没有其他对象共享这份资源,此时释放就不会引起一份资源被多次释放的问题,反之_count-1!=0就表示
//还有对象使用这份资源,此时就不能释放。
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
int _count;
};
}
#endif
string s1(“wolf”);string s2(s1);通过调试发现这种放法是错误的,因为当s2释放后,s2._count确实减少了,但是当我们去释放s1时,s1的s2._count仍为2,这样s2就没有释放,最终导致资源泄露
错解2:因为第一种方法放法主要是_count不同步造成的,所以采用static修饰_count的方法
#if 0
namespace wolf
{
class string
{
public:
string(const char* str = "")//构造函数
:_str(new char[strlen(str) + 1])
{
if (str == nullptr)
{
str = "";
}
_count = 1;//因为一旦通过构造函数来构造一个对象,说明这个对象的资源只有它自己用,还没来得及共享,所以就把_count置为1
strcpy(_str, str);
}
string(string& s)//拷贝构造函数
:_str(s._str)//在拷贝构造函数中采用浅拷贝的方式
{
_count++;//
}
string& operator=(const string& s)//赋值运算符重载
{
if (this != &s)//防止自己给自己赋值
{
//.......
}
return *this;
}
~string()//析构函数
{
if (_str && (--_count == 0))//_count-1==0的意思是除去自己这个对象使用这份资源外,如果为0,表明除了自己
//没有其他对象共享这份资源,此时释放就不会引起一份资源被多次释放的问题,反之_count-1!=0就表示
//还有对象使用这份资源,此时就不能释放。
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
static int _count;
};
int string::_count = 0;
}
#endif
这种方法有一个严重的问题:就是因为_count时静态的,是所有对象共享的,当执行string s1(“wolf guidao”);string s2(s1);是完全没有问题的,但一旦执行string s3;因为s3是另外通过构造函数创建的对象,使用的是另外一份资源,没有和s1s2共享,此时_count=1;这样就会影响到s1s2的释放问题,计数因该和资源个数保持一致。
正解:如果每一份资源都有自己的一个计数,这样计数就和资源个数保持一致,所以采用共享计数的方法
namespace wolf
{
class string
{
public:
string(const char* str = "")//构造函数
:_str(new char[strlen(str) + 1])
,_pcount(new int (1))//在堆上申请一个空间并初始化为1;
{
if (str == nullptr)
{
str = "";
}
strcpy(_str, str);
}
string(string& s)//拷贝构造函数
:_str(s._str)//在拷贝构造函数中采用浅拷贝的方式
,_pcount(s._pcount)//共享同一份资源的计数
{
++(*_pcount);
}
string& operator=(const string& s)//赋值运算符重载
{
if (this != &s)//防止自己给自己赋值
{
//.........
}
return *this;
}
~string()//析构函数
{
if (_str && (--(*_pcount) == 0))//_pcount-1==0的意思是除去自己这个对象使用这份资源外,如果为0,表明除了自己
//没有其他对象共享这份资源,此时释放就不会引起一份资源被多次释放的问题,反之_pcount-1!=0就表示
//还有对象使用这份资源,此时就不能释放。
{
delete[] _str;
_str = nullptr;
delete _pcount;
_pcount = nullptr;
}
}
private:
char* _str;
int* _pcount;
};
}
#endif
执行string s1(“wolf guidao”);string s2(s1);通过这种方法每份资源都有自己的计数空间,如果有其他对象s2共享这份
资源s1,同时也就共享了这份资源的计数;string s3;s3是另外一份资源,s3的资源内容和计数空间都是另外一份,就不会
共享s1的内容和s1的资源计数,这种方法就解决了上面一种法方法的问题。
(3).赋值运算符重载的解决方法:
经过上面的铺垫就很容易的到赋值运算符重载的解决方法:
namespace wolf
{
class string
{
public:
string(const char* str = "")//构造函数
:_str(new char[strlen(str) + 1])
, _pcount(new int(1))//在堆上申请一个空间并初始化为1;
{
if (str == nullptr)
{
str = "";
}
strcpy(_str, str);
}
string(string& s)//拷贝构造函数
:_str(s._str)//在拷贝构造函数中采用浅拷贝的方式
, _pcount(s._pcount)//共享同一份资源的计数
{
++(*_pcount);
}
//1.s3 = s2;如果赋值成功,s2,s3就会共享同一份资源,s2那份资源就多一个共享对象,所以共享s2那份资源的计数加1
//2.s3原来的那份资源就不会再使用,所以要释放s3,那么共享s3这份资源的对象就少一份,所以就因该把共享s3的计数减1
//如果把s3的计数减1后刚好为0,表示只有s3使用者一份资源,那么为了防止资源泄露,需要手动释放
string& operator=(const string& s)//赋值运算符重载
{
//此时_pcount为s3,s._pcount为s2
if (this != &s)//防止自己给自己赋值
{
if (--(*_pcount) == 0)
{
delete[] _str;
delete _pcount;
}
_str = s._str;
_pcount = s._pcount;
++(*_pcount);
}
return *this;
}
~string()//析构函数
{
if (_str && (--(*_pcount) == 0))//_pcount-1==0的意思是除去自己这个对象使用这份资源外,如果为0,表明除了自己
//没有其他对象共享这份资源,此时释放就不会引起一份资源被多次释放的问题,反之_pcount-1!=0就表示
//还有对象使用这份资源,此时就不能释放。
{
delete[] _str;
_str = nullptr;
delete _pcount;
_pcount = nullptr;
}
}
private:
char* _str;
int* _pcount;
};
}
#endif
以上解决浅拷贝的方法也有其问题:如果修改对象s3中的资源时s3[0] = ‘W’;,就发现其他和其共享资源的对象s1,s2也发生了改变,相当于一改全改接下来就是想怎么解决这一问题。
(4).解决浅拷贝+引用计数的缺陷:写时拷贝copy on write(COW)(这里只针对部分)
string s1(“wolf guidao”);string s2(s1);string s3;s3 = s2;前面的代码都是没有问题的,但是s3[0] = ‘W’;就会出现问题,如果想解决这个问题;就因该进行判断,如果有其他对象s1,s2共享这份资源,就应该另外申请空间把s3对象分离出来,且把共享的这份资源拷贝一份,避免一改全改的问题,这就是写时拷贝。如果没有其他对象和s3共享同一份资源,就不用分离。
分离对象的时机:所有的写操作||或可能会引起写操作都应分离
namespace wolf
{
class string
{
public:
string(const char* str = "")//构造函数
:_str(new char[strlen(str) + 1])
, _pcount(new int(1))//在堆上申请一个空间并初始化为1;
{
if (str == nullptr)
{
str = "";
}
strcpy(_str, str);
}
string(string& s)//拷贝构造函数
:_str(s._str)//在拷贝构造函数中采用浅拷贝的方式
, _pcount(s._pcount)//共享同一份资源的计数
{
++(*_pcount);
}
//1.s3 = s2;如果赋值成功,s2,s3就会共享同一份资源,s2那份资源就多一个共享对象,所以共享s2那份资源的计数加1
//2.s3原来的那份资源就不会再使用,所以要释放s3,那么共享s3这份资源的对象就少一份,所以就因该把共享s3的计数减1
//如果把s3的计数减1后刚好为0,表示只有s3使用者一份资源,那么为了防止资源泄露,需要手动释放
string& operator=(const string& s)//赋值运算符重载
{
//此时_pcount为s3,s._pcount为s2
if (this != &s)//防止自己给自己赋值
{
if (--(*_pcount) == 0)
{
delete[] _str;
delete _pcount;
}
_str = s._str;
_pcount = s._pcount;
++(*_pcount);
}
return *this;
}
~string()//析构函数
{
if (_str && (--(*_pcount) == 0))//_pcount-1==0的意思是除去自己这个对象使用这份资源外,如果为0,表明除了自己
//没有其他对象共享这份资源,此时释放就不会引起一份资源被多次释放的问题,反之_pcount-1!=0就表示
//还有对象使用这份资源,此时就不能释放。
{
delete[] _str;
_str = nullptr;
delete _pcount;
_pcount = nullptr;
}
}
char& operator[](size_t index)//下标运算符重载
{
//该操作可能会改变当前对象的内容,从而引起其他共享对象内容的改变,所以因该采用分离当前对象操作
if (Getpcount() > 1)//满足条件就表示有其他对象共享资源,就因该分离
{
string strtemp(_str);
this->Swap(strtemp);
}
return _str[index];
}
//执行s3[0] = 'W'时,因为s1,s2和其共享一份资源,临时对象strtemp._str指向原本s3共享的那份_str,
//strtemp._pcount指向_pcount,当出该函数时,根据_pcount的值看是否要释放资源。
//最后当释放对象s3时,因为strtemp是通过构造函数string strtemp(_str);来创建的,所以strtrmp._pcount=1,经过
//交换后s3._pcount=1;所以s3可以正常释放。
void Swap(string& s)//交换两个对象
{
swap(_str, s._str);//交换字符串内容
swap(_pcount, s._pcount);//交换资源计数
}
private:
int Getpcount()
{
return *_pcount;
}
char* _str;
int* _pcount;
};
}
main函数:
void TestMystring()
{
wolf::string s1("wolf guidao");
wolf::string s2(s1);
wolf::string s3;
s3 = s2;
s3[0] = 'W';
}
int main()
{
TestMystring();
system("pause");
return 0;
}
以上代码中每一步都有详细解释,如果还不明白,下面链接中有介绍浅拷贝+引用计数的使用方法和这种方法的缺点;
浅拷贝+引用计数解决浅拷贝
浅拷贝+引用计数解决浅拷贝问题的缺陷
四、简易模拟实现string类:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<Windows.h>
#include<stdlib.h>
#include<string.h>
using namespace std;
namespace wolf
{
class string
{
public:
typedef char* iterator;
public:
string(const char* str = "")//构造函数
{
if (str == nullptr)//如果传进来为空指针,就让他改为"",这样就可以放止程序崩溃
{
str = "";
}
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity+1];//申请空间
strcpy(_str, str);//把str中的内容拷贝过去
}
string(const string& s)//拷贝构造函数;采用深拷贝
:_size(s._size)
,_capacity(s._size+1)
{
_str = new char[_capacity];
strcpy(_str, s._str);
}
string& operator=(string& s)//赋值运算符重载
{
if (this != &s)
{
string temp(s._str);//调用构造函数把s的字符串拷贝一份,防止浅拷贝问题
swap(_str, temp._str);
}
return *this;
}
~string()//析构函数
{
if (_str)//不为空才进行释放,否则会多次释放
{
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
}
size_t Size()const//返回字符串个数
{
return _size;
}
size_t Capacity()const//返回空间容量daxiao
{
return _capacity;
}
bool Empty()const//判空
{
return _size == 0;
}
iterator begin()//迭代器(注意迭代器的范围都是[begin,end))
{
return _str;//返回字符串首地址
}
iterator end()
{
return _str + _size;//返回字符串最后一个位置,即\0出
}
void Reserve(size_t newcapacity)//调整空间大小
{
if (newcapacity > _capacity)//如果新空间大小大于旧空间大小才进行
{
char* strtemp = new char[newcapacity + 1];//多一个是为了放'\0'
strcpy(strtemp, _str);
delete[]_str;
_str = strtemp;
_capacity = newcapacity;//新空间大小不是newcapacity+1是为了和类库中string保持一致
}
}
void Resize(size_t newsize,char ch)//把空间中有效字符个数调整为newsize的,多余的空间用ch填充
{
size_t oldsize = Size();
if (newsize < oldsize)//如果调整后有效字符个数小于原有字符个数
{
_size = newsize;
_str[_size] = '\0';
}
else
{
if (newsize > _capacity)//如果调整后字符个数大于空间容量,就进行扩容
{
Reserve(newsize);
}
memset(_str+_size, ch, newsize - oldsize);//从原有字符后面进行填充ch
}
_size = newsize;
_str[_size] = '\0';//记得加上\0
}
void Push_back(char ch)//在原来字符串末尾追加一个字符ch
{
if (_size == _capacity)//判满
{
Reserve(_capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
string& operator+=(char ch)//运算符重载
{
if (_size == _capacity)
{
Reserve(_capacity * 2);
}
Push_back(ch);
_size = _size + 1;
return *this;
}
void Append(const char* str);//追加字符串
string& operator+=(const char* str);//运算符重载
void Clear()//把空间中有效字符个数清空
{
_size = 0;
_str[_size] = '\0';
}
void Swap(string& s)//把两个string对象中的内容交换,注意要全部交换
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
}
const char* C_str()const//返回当前对象的C格式字符串
{
return _str;
}
char& operator[](size_t index)//运算符重载
{
if (index < _size)
{
return _str[index];
}
}
bool operator>(const string& s)const;
bool operator>=(const string& s)const;
bool operator<(const string& s)const;
bool operator<=(const string& s)const;
bool operator==(const string& s)const;
bool operator!=(const string& s)const;
int Find(char ch, size_t pos = 0)const//从pos开始,从前往后找字符,返回ch的下标
{
for (size_t i = pos;i < _size;i++)
{
if (_str[i] == ch)
{
return i;
}
}
return -1;
}
int rFind(char ch, size_t pos = -1)const//从pos开始,从后往前找字符,返回ch的下标
{
if (pos == npos)
{
pos = _size - 1;
}
for (size_t i = pos;i >= 0;i--)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
int Find(char* str, size_t pos = 0)const//从pos开始,从前往后找字符串,返回str的下标
{}
int rFind(char* str, size_t pos = 0)const//从pos开始,从后往前找字符串,返回str的下标
{}
string& Erase(size_t pos,size_t num)//从pos开始删除num个字符
{}
string& Insert(size_t pos,char ch)//在pos处插字符ch
{}
string& Insert(size_t pos, char* str)//在pos处插字符串str
{}
friend ostream& operator<<(ostream& _cout, const string& s)
{
_cout << s.C_str();
return _cout;
}
friend istream& operator>>(istream& _cin, const string& s)
{
_cin >> s._str;
return _cin;
}
private:
char* _str;//存放字符串
size_t _size;//字符串个数
size_t _capacity;//字符串容量
static size_t npos;
};
}
size_t wolf::string::npos = -1;
void Teststeing()
{
wolf::string a("wolf");
//cout << a.Size() << endl;
//cout << a.Capacity() << endl;
wolf::string b(a);
//cout << b.Size() << endl;
//cout << a.Capacity() << endl;
b.Resize(10, '!');
//cout << b << endl;
//cout << b.Size() << endl;
//cout << b.Capacity() << endl;
b.Reserve(30);
//cout << b.Capacity() << endl;
b.Push_back(' ');
b.Push_back('i');
//cout << b << endl;
//b.Clear();
//cout << b << endl;
//cout << b.Find('o') << endl;
//cout << b.rFind('o') << endl;
//cout << b.C_str() << endl;
wolf::string::iterator it = b.begin();
/*while (it < b.end())
{
cout << *it;
it++;
}*/
//cout << endl;
/*for (auto e: b)
{
cout << e;
}*/
//cout << endl;
wolf::string c;
cin >> c;
cout << c;
}
int main()
{
Teststeing();
system("pause");
return 0;
}