首先string的浅拷贝是让两个不同的指针指向同一块空间,而这在析构的时候会出现将一块空间释放两次,程序会崩溃,因此我们才需要进行深拷贝,即第二个指针开辟和第一个指针一样大小空间,然后将内容复制过去,不过深拷贝又分传统写法和现代写法,两者的区别主要在于拷贝构造和赋值运算符的重载上
第一种就是传统写法,拷贝构造和赋值的时候正常开辟空间,正常拷贝内容
class String
{
public:
String()//这是先给pstr开辟一个空间来存放'\0',注意'\0'的写法
:p(new char[1])
{
*p = '\0';\
}
//显示的给出 开辟空间时候加1是为了放'\0'
//也可以合起来写 改成String(char*p="")
String(char *pstr)
:p(new char[strlen(pstr)+1])
{
if (pstr == NULL)
{
p = new char[1];
*p = '\0';
}
else
{
p = new char[strlen(pstr) + 1];
strcpy(p, pstr);
}
}
//拷贝构造函数
String(const String &s)
{
p = new char[strlen(s.p) + 1];
strcpy(p, s.p);
}
//赋值运算符重载 注意四个问题
//1)返回值
//2)参数
//3)检测其是否给自身赋值
//4)返回为*this
String & operator = (const String &s)
{
if (this != &s)
{
char *tmp = new char[strlen(s.p) + 1];
strcpy(tmp, s.p);
delete []p;
p = tmp;
}
return *this;
}
//析构函数
~String()
{
if (p)
{
delete[] p;
}
}
private:
char *p;
};
2 深拷贝的现代写法
现代写法构造函数和析构函数并没有改变,改变的是复制运算符重载,拷贝构造
先看这段实现代码,第一次我是这样写的,但是崩溃了。。。
String(const String &s)
{
String tmp(s.p);
std::swap(p, tmp.p);
}
//赋值运算符重载
String &operator=( String s)
{
std::swap(p, s.p);
return *this;
}
//另一种写法
String & operator=(const String &s)
{
if ( this != &s)
{
String tmp(s.p);
std::swap(p, tmp.p);
}
return *this;
}
看起来没毛病啊,所以这个时候可以单步进去跟踪一下看看程序到底是在哪里崩溃的。单步执行就会看到在拷贝构造的时候会出现下面的图情况,我们没有将_str的地址初始化,即访问了非法地址,但是这个时候还不会崩溃,而在交换之后,将它交换给tmp对象,在出了作用域之后tmp对象在析构的时候就会崩溃,因为是在释放非法内存空间。
因此,我们必须在使用之前在初始化的列表里面将_str置空,切记千万不能忘记了。
写到这突然想起来在写日期类的时候我写了一个这样的代码,
Date& operator=(const Date& d)
{
if (this != &d)
{
Date tmp(d);
std::swap(*this, tmp);
}
return *this;
}
运行的时候会出现死循环,因为swap在交换的时候也是要创建临时变量,而自定义类型的创建又会调用拷贝构造,但是拷贝构造的时候有在创建临时变量,这样的话就会陷入无限的递归调用,最终栈溢出。
因此,在使用库函数swap进行交换的时候最好只去交换内置类型,对于自定义类型的类型,一定要自己去实现。
3.但是在有的时候我们并不需要每次都去开辟空间,拷贝数据,那么存不存在一种方法可以让我们一般情况下只实行浅拷贝,然后当我们需要改变新的空间的内容的时候,才会重新开辟空间呢?可以想到下面这些方法:
1)首先我们会想到增加一个类成员 int count,但是这样造成的后果是每一个成员都有一个不同的count 在析构的时候就很混乱还是会出错
2)然后呢我们会想到使用静态成员的办法,因为此时 static int count 是放在静态区,它是所有对象共享的,不会为每个对象单独生成一个count,可是当我们有三个不同的成员共同管理一块空间,而此时我们又用构造函数创建一个对象时候,count又会会变为1,所以这种方法还是不行 。
3)于是我们想到了引用计数,就是再创建对象的时候增加一个指针来存储当前有几个对象在管理_str 这块空间,代码实现如下:
class String
{
public:
String(char *pstr = "")
{
_pstr = new char[strlen(pstr) + 1];
strcpy(_pstr, pstr);
_pcount = new int(1);
}
String(const String &s)
:_pstr(s._pstr)
, _pcount(s._pcount)
{
GetCount()++;
}
String &operator=(const String& s)
{
//1.this指向空间引用计数为1,这时候减减它以后还要释放这块空间
//2.所指向的引用计数不为1,所以只用减减引用计数
if (this != &s)
{
this->Release();
_pstr = s._pstr;
_pcount = s._pcount;
GetCount()++;
}
return *this;
}
//为当前对象重新开辟空间拷贝数据并设置它的引用计数,然后返回这个地址
char& operator[](size_t index)
{
if (GetCount() > 1)
{
--GetCount();
char* tmp = new char[strlen(_pstr) + 1];
strcpy(tmp,_pstr);
_pstr = tmp;
_pcount = new int(1);
}
return _pstr[index];
}
~String()
{
assert(_pstr);
assert(_pcount);
this->Release();
}
int& GetCount()
{
return *(this->_pcount);
}
void Printf()
{
cout << this->_pstr;
}
void Release()
{
--GetCount();
if (GetCount() == 0)
{
delete[] _pstr;
_pstr = NULL;
delete _pcount;
_pcount = NULL;
}
}
private:
char* _pstr;
int* _pcount;
};
还有一种方式是在对象的头部加上四个字节存一个int型的整数来标记当前空间有几个指针在管理,类似于new[ ]的实现,代码和上面的很类似:
class String
{
public:
String(char* str = "")
{
if (str == NULL)
{
_str = new char[1 + 4];
*(int*)(_str) = 1;
_str += 4;
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 5];
*(int*)_str = 1;
_str += 4;
strcpy(_str, str);
}
}
String(const String& s)
:_str(s._str)
{
++GetRef();
}
String &operator=(const String & s)
{
if (_str != s._str)
{
--GetRef();
ReduceRef();
}
else
{
_str = s._str;
}
return *this;
}
~String()
{
--GetRef();
ReduceRef();
}
//[]的重载 为了实现写时拷贝的数组[]下标形式的访问
char &operator[](size_t index)
{
if (GetRef() > 1)
{
--GetRef();
char* tmp = new char(strlen(_str) + 1 + 4);
*(int*)tmp = 1;
tmp += 4;
strcpy(tmp, _str);
_str = tmp;
}
return _str[index];
}
public:
int& GetRef()
{
return *(int*)(_str-4);
}
void ReduceRef()
{
if (0 == --GetRef())
{
_str += 4;
delete[]_str;
_str = NULL;
}
}
void Printf()
{
cout << this->_str << endl ;
}
private:
char* _str;
};