条款 16: 在 operator=中对所有数据成员赋值

来源:互联网 发布:owncloud源码分析 编辑:程序博客网 时间:2024/05/22 09:42

             实际编程中,这意味着写赋值运算符时,必须对对象的每一数据成员赋值:

template<class T> //  名字和指针相关联的类的模板class NamedPtr { //  (源自条款 12)public:NamedPtr(const string& initName, T *initPtr);NamedPtr& operator=(const NamedPtr& rhs);private:string name;T *ptr;};template<class T>NamedPtr<T>& NamedPtr<T>::operator=(const NamedPtr<T>& rhs){if (this == &rhs)return *this; //  见条款 17// assign to all data membersname = rhs.name; //  给 name 赋值<strong><span style="color:#ff0000;">*ptr = *rhs.ptr; //  对于 ptr,赋的值是指针所指的值,</span><span style="color:#ff0000;">//   不是指针本身</span></strong>return *this; //  见条款 15}<span style="color:#ff0000;font-weight: bold;"> </span>
注意:赋值运算符对于对象的每一数据成员赋值的注意事项:如果对象中含有一个指针,那么赋值赋的是指针所指的值,而不是指针本身。

初写这个类时当然很容易记住上面的原则,但同样重要的是,当类里增加新的数据成员时,也要记住更新赋值运算符函数。例如,打算升级 NamedPtr模板使得名字改变时附带一个时间标记,那就要增加一个新的数据成员,同时需要更新构造函数和赋值运算符。但现实中,因为忙于升级类的具体功能和增加新的成员函数等,这一点往往很容易被忘记。

总结:如果类要增加新的数据成员,也要记住更新赋值运算符函数,和构造函数。


当涉及到继承时,情况就会更有趣,因为派生类的赋值运算符也必须处理它的基类成员的赋值!

class Base {public:Base(int initialValue = 0): x(initialValue) {}private:int x;};class Derived: public Base {public:Derived(int initialValue): Base(initialValue), y(initialValue) {}Derived& operator=(const Derived& rhs);private:int y;};

逻辑上说,Derived 的赋值运算符应该象这样:

// erroneous assignment operatorDerived& Derived::operator=(const Derived& rhs){if (this == &rhs) return *this; //  见条款 17y = rhs.y; //  给 Derived 仅有的//   数据成员赋值return *this; //  见条款 15}

不幸的是,它是错误的,因为 Derived 对象的 Base 部分的数据成员 x 在赋值运算符中未受影响

例如,考虑下面的代码段:

void assignmentTester(){Derived d1(0); // d1.x = 0, d1.y = 0Derived d2(1); // d2.x = 1, d2.y = 1d1 = d2; // d1.x = 0, d1.y = 1!}
请注意 d1 的 Base 部分没有被赋值操作改变。

解决这个问题最显然的办法是在 Derived::operator=中对 x 赋值。但这不合法,因为 x 是 Base 的私有成员。所以必须在Derived 的赋值运算符里显式地对 Derived 的 Base 部分赋值

总结:及定义派生类的赋值运算符,必须显示调用基类的赋值运算符对基类进行赋值。

//  正确的赋值运算符Derived& Derived::operator=(const Derived& rhs){if (this == &rhs) return *this;<span style="color:#cc0000;">Base::operator=(rhs); //  调用 this->Base::operator=</span>y = rhs.y;return *this;}

这里只是显式地调用了 Base::operator=,这个调用和一般情况下的在成员函数中调用另外的成员函数一样,以*this 作为它的隐式左值。Base::operator=将针对*this 的 Base 部分执行它所有该做的工作——正如你所想得到的那种效果。
总结:Derived继承自Base,则Derived也含有Base的赋值运算符函数和数据成员。

Base::operator=(rhs);函数原型是Base&Base::operator=(const Base& rhs)  传入的实参是派生类Derived,rhs,所以,调用该函数会产生一个临时对象Base,用派生类型来初始化,

          但如果基类赋值运算符是编译器生成的,有些编译器会拒绝这种对于基类赋值运算符的调用(见条款 45 ) 。为了适应这种编译器,必须这样实现

Derived::operator=:Derived& Derived::operator=(const Derived& rhs){if (this == &rhs) return *this;<span style="color:#cc0000;">static_cast<Base&>(*this) = rhs; //  对*this 的 Base 部分</span>//  调用 operator=y = rhs.y;return *this;}
        这段怪异的代码将*this 强制转换为 Base 的引用然后对其转换结果赋值。这里只是对 Derived 对象的 Base 部分赋值。还要注意的重要一点是,转换的是 Base 对象的引用,而不是 Base 对象本身如果将*this 强制转换为 Base 对象,就要导致调用 Base 的拷贝构造函数,创建出来的新对象(见条款 M19)就成为了赋值的目标,而*this 保持不变。这不是所想要的结果

         不管采用哪一种方法,在给 Derived 对象的 Base 部分赋值后,紧接着是Derived 本身的赋值,即对 Derived 的所有数据成员赋值。

赋值运算符面临的问题,拷贝构造函数比然会面临,因为,两个函数确实很像。

     另一个经常发生的和继承有关的类似问题是在实现派生类的拷贝构造函数时。看看下面这个构造函数,其代码和上面刚讨论的相似:

class Base {public:Base(int initialValue = 0): x(initialValue) {}Base(const Base& rhs): x(rhs.x) {}private:int x;};class Derived: public Base {public:Derived(int initialValue): Base(initialValue), y(initialValue) {}<span style="color:#ff0000;">Derived(const Derived& rhs) //  错误的拷贝: y(rhs.y) {} //  构造函数</span>private:int y;};
          类 Derived 展现了一个在所有 C++环境下都会产生的 bug:当 Derived 的拷贝创建时,没有拷贝其基类部分。当然,这个Derived 对象的 Base 部分还是创建了,但它是用 Base 的缺省构造函数创建的成员 x 被初始化为 0(缺省构造函数的缺省参数值) ,而没有顾及被拷贝的对象的 x 值是多少!

总结:定义一个变量,不论是通过调用拷贝构造函数,还是调用构造函数产生,都必须从基类构造到派生类,或调用构造函数,或调用拷贝构造函数。本例中:

Derived(const Derived& rhs) : y(rhs.y) {} //  错误的拷贝构造函数

本例中,先构造基类,因为初始化化列表中没有基类的初始化,所以,指向默认构造函数构造基类,然后继续执行初始化列表,最后再执行函数体。

        为避免这个问题,Derived 的拷贝构造函数必须保证调用的是 Base 的拷贝构造函数而不是 Base 的缺省构造函数。这很容易做,只要在 Derived 的拷贝构造函数的成员初始化列表里对 Base 指定一个初始化值:

      

class Derived: public Base {public:Derived(const Derived& rhs): Base(rhs), y(rhs.y) {}...};

总结:对于条款16:在 operator=中对所有数据成员赋值

谨记以下几点:

1、operator=中含有指针成员必定要注意,我们拷贝的是指针指向的值,而不是指针的值,不然就会导致多个指针指向同一个内存,智能指针解决这个问题。

2、operator= 一定要记得对基类进行赋值

3、拷贝构造函数也一定要对基类进行拷贝,不然,就会调用基类的默认构造函数来初始化基类。






0 0
原创粉丝点击