剑指offer:赋值运算符函数和复制构造函数
赋值运算符函数
对于定义一个赋值运算符函数时,需要注意一下几点:
(1)函数的返回类型必须是一个引用,因为只有返回引用,才可以连续赋值
(2)传入的参数声明为常量引用,可以提高代码效率,同时赋值运算函数内不会改变传入的实例状态
(3)一定要记得释放实例自身已有的内存,否则程序容易出现内存泄露
(4)注意传入的参数和当前的实例是不是同一个实例,如果是同一个,则不用进行赋值操作,直接返回即可。
复制构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是复制构造函数。
什么时候调用复制构造函数?
(1)当用类的一个对象初始化该类的另一个对象时;
(2)将一个对象作为实参传递给一个非引用类型的形参时;
(3)从一个返回类型为非引用类型的函数返回一个对象时;
深拷贝和浅拷贝的区别:
1. 默认拷贝构造函数
很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
Rect::Rect(const Rect& r) { width = r.width; height = r.height; }
2. 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
3. 深拷贝
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
深拷贝主要解决的问题是指针成员变量浅拷贝的问题。
1. 防止默认拷贝(也能够禁止复制)
有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。如下程序:
#include <iostream> using namespace std; class CExample { private: int value; public: //构造函数 CExample(int val) { value = val; cout << "creat: " << value << endl; } private: //拷贝构造,只是声明 CExample(const CExample& C); public: ~CExample() { cout << "delete: " << value << endl; } void Show() { cout << value << endl; } }; //全局函数 void g_Fun(CExample C) { cout << "test" << endl; } int main() { CExample test(1); // g_Fun(test); // 按值传递将出错 return 0; }
而根据《C++ Primer》第四版13.1.3节,要禁止类的复制, 类必须显示声明其复制构造函数为private。
小问题:一个类中可以有多个拷贝构造函数吗?
解答:类中可以存在超过一个拷贝构造函数。
1 class X {
2 public:
3 X(const X&); // const 的拷贝构造
4 X(X&); // 非const的拷贝构造
5 };
关于拷贝构造函数与拷贝赋值操作符的区别:
两都都是用已存在的对象A来初始化另一个对象B。不同之处在于:
复制构造函数是针对一个未存在的对象进行初始化;赋值是针对已存在的对象进行初始化。
#include<iostream> #include<cstring> using namespace std; class CMyString { private: //int value; char *m_pdata; public: CMyString(char *pdata=NULL); CMyString(const CMyString &str);//复制构造函数 CMyString & operator = (const CMyString &str);//赋值运算符函数 ~CMyString(void); void print(); }; CMyString::CMyString(char *pdata) { if(pdata==NULL) { m_pdata=new char[1]; m_pdata[0]='\0'; } else { int len=strlen(pdata); m_pdata=new char[len+1]; strcpy(m_pdata,pdata); } } CMyString::CMyString(const CMyString &str)//深拷贝 默认的是浅拷贝 { // cout<<"hello xiaoming"<<endl; int len=strlen(str.m_pdata); m_pdata=new char[len+1]; strcpy(m_pdata,str.m_pdata); } CMyString::~CMyString() { delete []m_pdata; } CMyString& CMyString::operator =(const CMyString&str) { if(this==&str) return *this; delete []m_pdata; m_pdata=NULL; m_pdata=new char[strlen(str.m_pdata)+1]; strcpy(m_pdata,str.m_pdata); return *this; } void CMyString::print() { cout<<m_pdata<<endl; } void test1() { cout<<"test() begin"<<endl; char text[]="hello world"; CMyString str1(text); CMyString str2,str3; str3=str2=str1;//调用赋值运算操作符 cout<<"The expected result is: "<<text<<endl; cout<<"the str2 actual result is: "<<endl; str2.print(); cout<<endl; cout<<"The expected result is: "<<text<<endl; cout<<"the str3 actual result is: "<<endl; str3.print(); cout<<endl; } void test2() { cout<<"test4() begin"<<endl; char text[]="hello world"; CMyString str1(text); //初始化操作 调用构造函数 CMyString str2=str1; //标准写法CMyString str2(str1); 调用复制构造函数而不是赋值 str2.print(); cout<<endl; } int main() { test1(); test2(); return 0; }
代码
class CMyString { public: CMyString(char *ptr = nullptr); CMyString(const CMyString &str); ~CMyString(); CMyString& operator=(const CMyString& str); private: char *pData; }; CMyString& CMyString::operator=(const CMyString& str) { pData = str.pData; return *this; }
存在问题:
这个赋值运算符重载函数存在的问题如下:
1)浅拷贝;
2)没有(检查)释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,程序将出现内存泄漏;
3)没有判断传入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行复制操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
修改之后的赋值运算符重载函数如下:
CMyString& CMyString::operator=(const CMyString& str) { if (this == &str) return *this; delete []pData; pData = nullptr; pData = new char[strlen(str.pData) + 1]; strcpy(pData, str.pData); return *this; }
上述代码现在的问题在于4)异常安全性,即new可能会抛出异常,而我们却没有处理!所以我们可以将程序继续修改:
CMyString& CMyString::operator=(const CMyString& str) { if (this == &str) return *this; char *tmp = new(nothrow) char[strlen(str.pData) + 1]; if (tmp == nullptr) return *this; strcpy(tmp, str.pData); delete []pData; pData = tmp; tmp = nullptr; return *this; }
除了前边提到的4个点,赋值运算符重载还有两点需要注意:
5)是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this)。只有返回一个引用,才可以允许连续赋值。否则如果函数的返回值是void,应用该赋值将不能做连续赋值。假设有3个CMyString对象:str1、str2和str3,在程序中语句str1=str2=str3将不能通过编译。
6)是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次拷贝构造函数。把参数声明为引用可以避免这样的无谓消耗,从而提高代码效率。同时,我们在赋值运算符函数内不会修改传入的实例的状态,因此应该为传入的引用参数加上const关键字。