来源:c++ primer plus (6th edition) page 433
注:以下内容用到书中一个自己设计的类StringBad,由成员变量 str、len与静态成员变量 num_strings 组成。
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。 也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
Class_name(const Class_name&);
它接受一个指向类对象的常量引用作为参数。
对于复制构造函数,需要知道两点:何时调用和有何功能
- 何时调用
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有的对象。例如,假设motto是一个StringBad对象,则下面四种声明都将调用复制构造函数:
StringBad ditto(motto);
// calls StringBad(const StringBad&);
StringBad metoo = motto;
// calls StringBad(const StringBad&);
StringBad also = StringBad(motto);
// calls StringBad(const StringBad&);
StringBad* pStringBad = new StringBad(motto); // calls StringBad(const StringBad&);
其中中间两种声明可能会使用复制构造函数直接创造metoo和also,也可能使用复制构造函数先创建临时对象,这取决于具体的实现。最后一种声明使用motto初始化一个匿名StringBad对象,并将其地址赋给pString指针。
每当程序生成了对象副本时,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。编译器生成临时对象时,也将使用复制构造函数:例如将三个Vector(向量)相加时,编译器可能生成临时的Vector对象保存中间结果。何时生成临时对象随编译器而异,但无论哪种编译器,当按值传递和返回对象时,都将调用复制构造函数。(因此应该按引用传递对象,来节省调用构造函数的时间和存储新对象的空间。)
2.默认复制构造函数的功能
默认的复制构造函数逐个复制每个非静态成员(成员复制也叫浅复制),复制的是成员的值。例如:
StringBad sailor = sports;
与下面的代码等效(只是由于私有成员无法访问,因此这些代码不能通过编译)
StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;
如果成员本身就是类对象,则将使用这个类的复制构造函数复制成员对象。静态函数(如num_strings)不受影响,因为它们属于整个类,而不是各个对象。
我们发现,浅复制的结果是,如果成员str是一个指向字符串的指针,复制的结果仅仅是将地址做了备份,原始字符串还是只有一份。更令人担忧的是,复制产生的临时变量如果调用了析构函数,就会将原始数据删除,从而产生错误。
解决办法是采用显式的复制构造函数,并且在其中采用深复制。例如:
StringBad::StringBad(const StringBad& st)
{
num_strings++;
len = st.len;
str = new char [len+1];
std::strcpy(str, st.str);
**//copy string to new location**
cout << num_strings << ":\"" << str
<< "\" object created\n";
}
赋值运算符
同样的问题可能不止出在默认复制构造函数上,还有默认赋值运算符。ANSI C 允许结构赋值,而C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:
Class_name& Class_name::operator=(const Class_name&);
它接受并返回一个指向类对象的引用。例如
StringBad& StringBad::operator=(const StringBad&);
- 赋值运算符的功能及何时使用它
将已有的对象赋值给另一个对象时,将使用重载的赋值运算符:
StringBad headline1("Cerely Stalks at Midnight");
StringBad knot;
knot = headline1;
//assignment operator invoked
初始化对象时,并不一定要用赋值运算符:
StringBad metoo = knot;
//use copy constructor, possibly assignment,too
如果是直接初始化,就只用到复制构造函数;如果是先创建临时对象,再传给metoo,也会用到赋值运算符。
与复制构造函数相似,赋值运算符的隐式(默认)实现也是对成员进行逐个复制。(静态数据成员不受影响)
2.会出现的问题
与复制构造函数相似,由于浅复制,将headline1赋值给knot后,如果knot调用了析构函数,就会将headline1保存的字符串内容抹掉。
3.解决赋值问题
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[] 来释放这些数据。
- 函数应该避免将对象赋值给自身;否则,在对象重新赋值前,释放内存操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用。
进行深复制。
下面的代码说明了如何为StringBad类编写赋值运算符:
StringBad& StringBad::operator=(const StringBad& st)
{
if (this==&st) //object assigned to itself
return *this; //all done
delete [] str; //free old string
len = st.len;
str = new char [len+1];
std::strcpy(str, st.str);
return *this; //return reference to the invoking object
}
首先检查自我复制,这是通过检查赋值运算符右边的地址(&st)是否与接收对象(this)的地址相同来完成的。如果相同,程序将返回*this,然后结束。
注:赋值运算符是只能由类成员函数重载的运算符之一。
如果不同,进行深复制。