赋值运算符函数作为类的一个成员函数,主要用于对象之间的赋值。类一般都有默认的赋值运算符函数,然而默认赋值运算符函数只会浅拷贝,无法满足需求,有时还会产生致命错误。
如下:
class CString
{
public:
CString()
: _buf(nullptr)
{
}
CString(const CString& str)
: _buf(nullptr)
{
const int size = strlen(str._buf) + 1;
_buf = new char[size];
memset(_buf, 0, size);
strcpy(_buf, str._buf);
}
CString(const char* str)
: _buf(nullptr)
{
const int size = strlen(str) + 1;
_buf = new char[size];
memset(_buf, 0, size);
strcpy(_buf, str);
}
~CString()
{
if (_buf != nullptr)
{
delete _buf;
_buf = nullptr;
}
}
private:
char* _buf;
};
int main()
{
{
CString str1("hello world!");
CString str2;
str2 = str1;
}
}
如上代码所示,如果使用默认赋值运算符函数,
str2 = str1;
str2和str1将会指向同一块内存地址,当离开代码块作用域之后,str1和str2都将析构,这里将产生double free 内存错误。
对于这种需要深拷贝的赋值运算符函数需要自己定义。
下面是针对CString类的几种赋值运算符函数的实现:
CString& CString::operator=(const CString& str)
{
if(this == &str)
{
return *this;
}
delete [] _buf;
_buf = nullptr;
const int size = strlen(str._buf) + 1;
_buf = new char[strlen(str._buf) + 1];
memset(_buf, 0, size);
strcpy(_buf, str._buf);
return *this;
}
如上赋值运算符返回值定义成引用类型,方便连续赋值,如
str1 = str2 = str3;
赋值运算符参数定义成对象的常量引用,主要保证不会修改被赋值的对象;
实现中,先判断是否是对象自己,如果是则返回自己,
接着释放_buf内存,并置空
最后申请新的内存,将参数str中_buf数据拷贝到当前对象的buf中。
以上实现就是一个深拷贝。但是严格来讲也是存在问题的,当new char 抛出异常时,由于在这之前已经delete [] _buf
,则当前对象就不能在保持有效状态,这就违背了异常安全性原则。好的做法应该是先申请内存,再释放内存,如下:
CString& CString::operator=(const CString& str)
{
if(this == &str)
{
return *this;
}
const int size = strlen(str._buf) + 1;
char* buf = new char[strlen(str._buf) + 1];
memset(_buf, 0, size);
delete [] _buf;
_buf = buf;
strcpy(_buf, str._buf);
return *this;
}
还有另一种更优雅的实现:
CString& CString::operator=(const CString& str)
{
if(this == &str)
{
return *this;
}
CString strTemp(str);
char* buf = strTemp._buf;
strTemp._buf = _buf;
_buf = buf;
return *this;
}
这种实现将new出现异常的情况转移到拷贝构造函数中,即便出现异常也不会影响当前对象的状态。
一般我们自己定义的类都需要实现赋值运算符函数,特别是含有动态内存的对象。赋值运算符函数要保证对象的赋值是深拷贝,而且在实现的过程中也要注意异常安全性,这样才能能写出健壮而优雅的代码。