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

来源:c++ primer plus (6th edition) page 433

注:以下内容用到书中一个自己设计的类StringBad,由成员变量 str、len与静态成员变量 num_strings 组成。

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。 也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
Class_name(const Class_name&);
它接受一个指向类对象的常量引用作为参数。

对于复制构造函数,需要知道两点:何时调用和有何功能

  1. 何时调用
    新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有的对象。例如,假设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&);
  1. 赋值运算符的功能及何时使用它
    将已有的对象赋值给另一个对象时,将使用重载的赋值运算符:
    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,然后结束。

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

如果不同,进行深复制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值