测试环境:vs2013
什么是浅拷贝
- 也称位拷贝,编译器只是将对象中的值拷贝过来,如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进行操作时,就会发生发生了访问违规。
先看下面的代码有问题吗
class String
{
public:
String(const char* ptr = "")//构造函数,缺省放'\0'
:_ptr(new char[strlen(ptr) + 1])//strlen计算出的长度不加\0
{
strcpy(_ptr, ptr);
}
~String()
{
if (_ptr)
{
cout << "~String()" << this << endl;
delete[] _ptr;
}
}
private:
char* _ptr;
};
int main()
{
String s1("hello");
String s2(s1);
return 0;
}
执行结果如下:
- 可以发现这里程序会崩,为什么会造成这样的原因呢,那就必须搞清楚上面的代码都做了什么
这就是浅拷贝,多个对象共享同一份资源,造成的问题也显而易见,一份资源被释放了多次。那么,怎么解决呢?
引用计数
- 当多个对象共享一块资源时,要保证该资源只释放一次,只需记录有多少个对象在使用该资源即可,每减少(增加)一个对象使用,给该计数减一(加一),当最后一个对象不使用时,该对象负责将资源释放掉即可;观察下面代码:
class String
{
public:
String()
{}
String(const char* str )
{
if (str == NULL)
{
_str = new char[4+1];//多申请一个int用来存储计数器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount(_str) = 1;//将计数器初始值设置为1
}
String(const String& s)//拷贝构造
:_str(s._str)
{
GetCount(_str)++;
}
//s1=s2
String& operator=(const String& s)//赋值操作符重载
{
if (_str != s._str)
{
//1.当s1的引用计数为1时
//①释放空间
//②改变s1指针的指向
//③s2的引用计数加1
Release();
//当s1的引用计数大于1时
//①s1的引用计数减1
//同上②③
_str = s._str;
++GetCount(_str);//引用计数+1
}
return *this;
}
~String()//析构函数
{
Release();
}
private:
int& GetCount(char* str)
{
return *(int*)(str - 4);//因为这块内存类型为char
}
void Release()
{
if (_str != NULL && (--GetCount(_str)) == 0)
{
delete [](_str - 4);//一定要释放存储计数器的空间
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3("world");
String s4(s3);
String s5(s3);
s1 = s3;
}
上面的代码是在构造的时候多申请4字节的空间用来存储计数器,从而实现引用计数。那么具体是怎么做的呢?
但是这样还是有问题,看下面的代码
class String
{
public:
String()
{}
String(const char* str)
{
if (str == NULL)
{
_str = new char[4 + 1];//多申请一个int用来存储计数器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount(_str) = 1;//将计数器初始值设置为1
}
String(const String& s)//拷贝构造
:_str(s._str)
{
GetCount(_str)++;
}
//s1=s2
String& operator=(const String& s)//赋值操作符重载
{
if (_str != s._str)
{
Release();
_str = s._str;
++GetCount(_str);//引用计数+1
}
return *this;
}
char& operator[](size_t index)//可以采用下标的方式访问String类
{
return _str[index];
}
~String()//析构函数
{
Release();
}
private:
int& GetCount(char* str)
{
return *(int*)(str - 4);//因为这块内存类型为char
}
void Release()
{
if (_str != NULL && (--GetCount(_str)) == 0)
{
delete[](_str - 4);//一定要释放存储计数器的空间
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s1);
s3[1] = 'a';
return 0;
}
- 当几个共用同一块空间的对象中的任一对象修改字符串中的值,则会导致所有共用这块空间的对象中的内容被破坏掉。像上面的代码中,我们只想改变s3中的值,但是和他公用空间的另外两个对象s2,s1中的值也改变了,这就引出了写时拷贝
写时拷贝
- 有多个对象共享同一个空间时,当对其中一个对象只读时,不会有什么影响,但是如果想要改变(写入)某一个对象中的值时,这时就要为这个对象重新分配空间。代码实现如下:
class String
{
public:
String()
{}
String(const char* str)
{
if (str == NULL)
{
_str = new char[4 + 1];//多申请一个int用来存储计数器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount() = 1;//将计数器初始值设置为1
}
String(const String& s)//拷贝构造
:_str(s._str)
{
GetCount()++;
}
//s1=s2
String& operator=(const String& s)//赋值操作符重载
{
if (_str != s._str)
{
Release();
_str = s._str;
++GetCount();//引用计数+1
}
return *this;
}
char& operator[](size_t index)//可以采用下标的方式访问String类
{
if (GetCount() > 1)
{
--GetCount();
char* pTmp = new char[strlen(_str) + 1 + 4];
pTmp += 4;
strcpy(pTmp, _str);
_str = pTmp;
GetCount() = 1;//将新空间值赋1
}
return _str[index];
}
const char& operator[](size_t index)const//[]操作符必须成对重载
{
return _str[index];
}
~String()//析构函数
{
Release();
}
private:
int& GetCount()
{
return *(int*)(_str - 4);//因为这块内存类型为char
}
void Release()
{
if (_str != NULL && (--GetCount()) == 0)
{
delete[](_str - 4);//一定要释放存储计数器的空间
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s1);
s3[1] = 'a';
return 0;
}
深拷贝
- 给要拷贝构造的对象重新分配空间
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
String(const String& s)//深拷贝
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
//赋值操作符重载
//方法一
//String& operator=(const String& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);
// }
// return *this;//为了支持链式访问
//}
//方法二(优)
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;//为了支持链式访问
}
~String()
{
if (_str != NULL)
{
delete[]_str;
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3("world");
s1 = s3;
}
- 一般情况下,上面对赋值操作符重载的两种写法都可以,但是相对而言,第二种更优一点。
对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。