zoukankan      html  css  js  c++  java
  • 读书笔记_Effective_C++_条款十一:在operator=中处理自我赋值

     

    直观的operator=是这样定义的:

     1 class SampleClass
     2 {
     3 private:
     4          int a;
     5          double b;
     6          float* p;
     7 public:
     8          SampleClass& operator= (const SampleClass& s)
     9          {
    10                    a = s.a;
    11                    b = s.b;
    12                    p = s.p;
    13                    return *this;
    14          }
    15 };

    就是将自身的私有成员的值全部赋值成另一个对象的私有成员的值。若没有显式定义operator=,编译器会生成的默认的operator=,生成的结果也是这个样子。但注意此时私有成员中含有指针float *p,为了达到深拷贝的目的(不拷贝指针的地址,而拷贝指针所指向的空间内容),应该这样写:

     1 class SampleClass
     2 {
     3 private:
     4          int a;
     5          double b;
     6          float* p;
     7 public:
     8          SampleClass& operator= (const SampleClass& s)
     9          {
    10                    a = s.a;
    11                    b = s.b;
    12                    delete p;
    13                    p = new float(*s.p);
    14                    return *this;
    15          }
    16 };

    大致思路就是删除指针所指向的旧内容,而后再用这个指针指向一块新的空间,空间的内容填充s.p所指向的内容。但有两件事会导致这段代码崩溃,其一就是本条款所说的“自我赋值”。读者不妨想想看,如果这样:

    1 SampleClass obj;
    2 obj = obj;

    所发生的事情。在赋值语句执行时,检测到obj.p已经有指向了,此时会释放掉obj.p所指向的空间内容,但紧接着下一句话就是:

    p = new float(*s.p);

    注意*s.p会导致程序崩溃,因为此时s.p也就是obj.p,对其取值*obj.p(根据优先级,这相当于*(obj.p)),obj.p已经在前一句话被释放掉了,所以这样的操作会有bug。

    也许读者不以为意,认为用户不可能傻到会写obj = obj这样的代码出来。事实上也确实如此,明显的错误不大可能会犯,但万一写一个:

    1 SampleClass obj;
    2 3 SampleClass& s = obj;
    4 5 s = obj;

    或者

    1 SmapleClass* p = &obj;
    2 3 *p = obj;

    这种错误就不那么直观了,甚至*pa = *pb也有可能出问题,因为pa与pb大有可能指向的是同一个地址空间。自我赋值一个不小心就会发生,决不要假设用户不用写出自我赋值的语句来。

    解决自我赋值只要一句话:

     1 class SampleClass
     2 {
     3 private:
     4          int a;
     5          double b;
     6          float* p;
     7 public:
     8          SampleClass& operator= (const SampleClass& s)
     9          {
    10                    if(this == &s) return *this; // 解决自我赋值的一句话
    11                    a = s.a;
    12                    b = s.b;
    13                    delete p;
    14                    p = new float(*s.p);
    15                    return *this;
    16          }
    17 };

    我以前曾经这样写过:

     1 class SampleClass
     2 {
     3 private:
     4          int a;
     5          double b;
     6          float* p;
     7 public:
     8          SampleClass& operator= (const SampleClass& s)
     9          {
    10                    if(*this == s) return *this; // 注意条件判断的不同,这样写有问题!
    11                    a = s.a;
    12                    b = s.b;
    13                    delete p;
    14                    p = new float(*s.p);
    15                    return *this;
    16          }
    17 };

    但这样是不对的,因为==经常是用于对象内每一个成员变量是否相同的判断,而不是地址是否重叠的判断。所以用this == &s才能从地址上来捕捉到是否真的是自我赋值。

    这样做确实能解决上面所说的第一问题:自我赋值。事实上还可能出现另一个问题导致代码崩溃,试想,如果p = new float(*s.p)不能正常分配空间怎么办,突然抛出了异常怎么办,这将导致原有空间的内容被释放,但新的内容又不能正常填充。有没有一个好的方法,在出现异常时,还能保持原有的内容不变呢?(可以提升程序的健壮性)

    这有两种思路,书上先给出了这样的:

     1 SampleClass& operator= (const SampleClass& s)
     2 {
     3          if(this == &s) return *this; //可以删掉
     4          a = s.a;
     5          b = s.b;
     6          float* tmp = p; // 先保存了旧的指针
     7          p = new float(*s.p); // 再申请新的空间,如果申请失败,p仍然指向原有的地址空间
     8          delete tmp; // 能走到这里,说明申请空间是成功的,这时可以删掉旧的内容了
     9          return *this;
    10 }

    大致的思路是保存好旧的,再试着申请新的,若申请有问题,旧的还能保存。这里可以删掉第一句话,因为“让operator具备异常安全往往自动获得自我赋值安全的回报”。

    还有一种思路,就是先用临时的指针申请新的空间并填充内容,没有问题后,再释放到本地指针所指向的空间,最后用本地指针指向这个临时指针,像这样:

     1 SampleClass& operator= (const SampleClass& s)
     2 {
     3          if(this == &s) return *this; //可以删掉
     4          a = s.a;
     5          b = s.b;
     6          float* tmp = new float(*s.p); // 先使用临时指针申请空间并填充内容
     7          delete p; // 若能走到这一步,说明申请空间成功,就可以释放掉本地指针所指向的空间
     8          p = tmp; // 将本地指针指向临时指针
     9          return *this;
    10 }

    上述两种方法都是可行,但还要注意拷贝构造函数里面的代码与这段代码的重复性,试想一下,如果此时对类增加一个私有的指针变量,这里面的代码,还有拷贝构造函数里面类似的代码,都需要更新,有没有可以一劳永逸的办法?

    本书给出了最终的解决方案:

    1 SampleClass& operator= (const SampleClass& s)
    2 {
    3          SampleClass tmp(s);
    4          swap(*this, tmp);
    5          return *this;
    6 }

    这样把负担都交给了拷贝构造函数,使得代码的一致性能到保障。如果拷贝构造函数中出了问题,比如不能申请空间了,下面的swap函数就不会执行到,达到了保持本地变量不变的目的。

    一种进一步优化的方案如下:

    1 SampleClass& operator= (const SampleClass s)
    2 {
    3          swap(*this, s);
    4          return *this;
    5 }

    注意这里去掉了形参的引用,将申请临时变量的任务放在形参上了,可以达到优化代码的作用。

    最后总结一下:

    (1)     确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap;

    (2)     确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

  • 相关阅读:
    JAVA调用WebService总结
    关于购物车的想法
    ASP.NET中初试Ajax
    转帖:从FxCop归纳出来的一些规范建议
    数据结构(二叉树)C#描述
    FormView控件和DetailsGridView控件实现MasterSlave
    在.NET中使用MySql数据库
    Oracle学习总结1
    Oracle学习总结2
    关于字符匹配所引起的的问题
  • 原文地址:https://www.cnblogs.com/jerry19880126/p/2972648.html
Copyright © 2011-2022 走看看