您的位置:首页 > 编程语言 > C语言/C++

c++ primer plus 学习笔记(1)——复制构造函数与赋值运算符

2016-09-12 22:20 399 查看
来源: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,然后结束。

注:赋值运算符是只能由类成员函数重载的运算符之一。


如果不同,进行深复制
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: