Effective C++——条款10条,条款11和条款12(第2章)

条款10:    令operator=返回一个reference to *this

Have assignment operators return a reference to *this

关于赋值,可以把它们写成连锁形式:

int x, y, z;
x = y = z = 15;        // 赋值连锁形式 

赋值采用右结合律,所以上述连锁赋值被解析为:

x = (y = (z = 15)); 

这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x.

为了实现"连锁赋值",赋值操作符必须返回一个reference指向操作符的左侧实参,这是为classes实现赋值操作符时应该遵循的协议:

class Widget {
public:
    Widget& operator=(const Widget& rhs) {
        ...
        return *this;
    }
}; 

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:

class Widget {
public:
    ...
    Widget& operator+=(const Widget& rhs) {        // 这个协议适用于+=,-=,*=等等
        ...
        return *this;
    }
    Widget& operator=(int rhs) {                // 此函数也适用,即使参数类型不符规定
        ...
        return *this;
    }
};
  

这只是个协议,并无强制性.如果不遵循它,代码一样可通过编译.然而这份协议被所有内置类型和标准程序库提供的类型共同遵守,所以最好还是遵守它.

 注意:

令赋值(assignment)操作符返回一个reference to *this.

条款11:    在 operator=中处理"自我赋值"

Handle assignment to self in operator=

"自我赋值"发生在对象被赋值给自己时:

class Widget { ... };
Widget w;
...
w = w;            // 赋值给自己

这看起来有点愚蠢,但它合法,所以不要认定客户绝不会那么做.此外赋值动作并不总是那么可被一眼辨识出来,例如:

a[i] = a[j];    // 潜在的自我赋值 

而过i和j具有相同的值,这便是个自我赋值.再看:

*px = *py;        // 潜在的自我赋值 

如果px和py恰巧指向同一个东西,这也是自我赋值.这些并不明显的自我赋值,是"别名"(aliasing)带来的结果:所谓"别名"就是"有一个以上的方法指向某对象".一般而言如果某段代码操作pointers或references而它们被用来"指向多个相同类型的对象",就需要考虑这些对象是否为同一个.实际上两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成"别名",因为一个base class 的reference或pointer可以指向一个derived
class 对象:

class Base { ... };
class Derived : public Base { ... };
void doSomething(const Base& rb, Derived* pd);    // rb和*pd可能是同一对象

如果尝试自行管理资源,可能会掉进"在停止使用资源之前以外释放了它"的陷阱.假设建立一个 clas 来保存一个指针指向一块动态分配的位图:

class Bitmap { ... };
class Widget {
    ...
private:
    Bitmap* pb;        // 指针,指向一个从heap分配而得的对象
};

下面是 operator= 实现代码,表面上看起来合理,但自我赋值出现时就不完全:

Widget &Widget::operator=(const Widget& rhs) {    // 一份不安全的operator=实现版本
    delete pb;
    pb = new Bitmap(&rhs.pb);
    return &this;
}
 

这里的自我赋值问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象.如果这样的话,delete 就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap.在函数末尾,Widget发现自己持有一个指针指向一个已被删除的对象.

欲阻止这种错误,传统做法是借由 operator=最前面的一个"证同测试"达到"自我赋值"的检验目的:

Widget& Widget::operator=(const Widget& rhs) {
    if (this == &rhs)
        return *this;
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

这样做行得通.但这个新版本仍然存在异常方面的麻烦.如果"new Bitmap"导致异常(不论是因为分配时内存不够或因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap.这样的指针有害,无法安全地删除它们,甚至无法安全地读取它们.

令人高兴的是,让 operator=具备"异常安全性"往往自动获得"自我赋值安全"的的回报.因此只需要把焦点放在实现"异常安全性"上.例如以下代码,只需要注意在复制pb所指东西之前别删除pb:

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;                    // 记住原先的pb
    pb = new Bitmap(*rhs.pb);        // 令pb指向*pb的一个副本
    delete pOrig;                                // 删除原先的pb
    return *this;
}

现在,如果"new Bitmap"抛出异常,pb就维持原状.即使没有证同测试,这段代码还是能够处理自我赋值,因为对原bitmap做了一份复件,删除原bimap,然后指向新制造的那个复件.它或许不是处理"自我赋值"的最高效办法,但它行得通.

在 operator=函数内手工排列语句(确保代码不但"异常安全"而且"自我赋值安全")的一个替代方案是,使用所谓的copy and swap技术.这个技术和"异常安全性"有密切关系,所以由条款29详细说明.然而由于它是一个常见而够好的 operator=撰写方法,所以值得看看其实现手法像什么样子:

class Widget {
    ...
    void swap(Widget& rhs);        // 交换*this和rhs的数据,详见条款29
};
Widget& Widget::operator=(const Widget& rhs) {
    Widget temp(rhs);            // 为rhs数据制作一份副本
    swap(temp);                    // 将*this数据和上述副本的数据交换
    return *this;
}

这个主题的另一个变奏曲利用了以下事实:(1)某 class 的copy assignment操作符可能被声明为"以by value方式接受实参";(2)以by value方式传递东西会造成一份副本(详见条款20);

Widget& Widget::operator=(Widget rhs) {    // rhs是被传对象的一份副本
    swap(rhs);                            // pass by value
    return *this;
}

它为了伶俐巧妙的修补而牺牲了清晰性,然而将"copying动作"从函数本体内移至"函数参数构造阶段"却可令编译器有时生成更高效的代码.

(高效是否类似于返回值优化????)

注意:

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

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

条款12:    复制对象时勿忘其每一个成分

Copy all part of an object

设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,那便是带着适切名称的copy构造函数和copy assignment操作符.称它们为copying函数.条款5观察到编译器会在必要时候为class创建copying函数,并说明这些"编译器生成版"的行为:将被拷贝对象的所有成员变量都做一份拷贝.

如果拒绝编译器写出copying函数,则需要在copying函数对所有变量都进行拷贝.为 class 添加一个成员变量,必须同时修改copying函数.

因此在"为derived class撰写copying函数"时一定要非常小心地复制其base class 成分.那些成分往往是 private,所以无法直接访问它们,应该让derived class 的copying函数调用相应的base class 函数:

DerivedCustomer& DerivedCustom::operator=(const DerivedCustom& rhs) {
    Custom::operator=(rhs);
    ...
    return *this;
}

    本条款所说的"复制每一个成分"现在应该很清楚了.当编写一个copying函数,确保(1)复制所有local成员变量,(2)调用所有base classes内的适当的copying函数.

如果发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者使用.这样的函数往往是 private 而且常被命名为init.这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复.

注意:

copying函数应该确保复制"对象内的所有成员变量"以及"所有base class成分".

不要尝试以某个copying函数实现另一个copying函数.应该将共同机能放进第三个函数中,并由两个copying函数共同调用.

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-10 22:27:02

Effective C++——条款10条,条款11和条款12(第2章)的相关文章

More Effective C++ 条款10 在构造函数内阻止内存泄露

1. “C++ 只会析构已完成的对象”,“面对未完成的对象,C++ 拒绝调用其析构函数”,因为对于一个尚未构造完成的对象,构造函数不知道对象已经被构造到何种程度,也就无法析构.当然,并非不能采取某种机制使对象的数据成员附带某种指示,“指示constructor进行到何种程度,那么destructor就可以检查这些数据并(或许能够)理解应该如何应对.但这种机制无疑会降低constructor的效率,,处于效率与程序行为的取舍,C++ 并没有使用这种机制.所以说,”C++ 不自动清理那些’构造期间跑

effective c++ 条款10 handle assignment to self operator =

非强制性,但是个好习惯 当使用连锁赋值时很有用 x=y=z=10; class Window { public: Window& operator=(int size) { ... return *this; } } 这个规则适用于 -,+, +=,-= etc effective c++ 条款10 handle assignment to self operator =

effective C++ 读书笔记 条款10

条款10:  令operator= 返回一个reference to *this; 关于赋值,我们可以这样写: int  x,y,x; x =y = z; 这就是所谓的连续赋值 为了实现"连锁赋值"赋值操作符必须返回一个reference指向操作符的左侧实参.这是我们为class实现赋值操作符时应该遵循的协议: #include <iostream> using namespace std; class Widget { public: Widget() { cout<

Effective C++ (笔记) : 条款05 -- 条款10

条款05:了解C++默默编写并调用哪些函数 编译器可以暗自为class创建default构造函数.copy构造函数.copy assignment操作符,以及析构函数. 只有这些函数需要(被调用)时,它们才会被编译器创建出来.在编译器产生的复制构造函数和赋值运算符执行的都是浅拷贝.当数据成员是引用或者常量的时候,编译器不知道该怎么处理,两手一摊,无能为力. 当某个基类将copy assignment操作符声明为私有的,编译器将拒绝为派生类生成copy assignment操作符,因为它无权(也不

《Effective C++》构造/析构/赋值 函数:条款10-条款12

条款10:令operator=返回一个reference to *this 赋值操作符运算是由右向左运算的.例如一个连锁赋值 <span style="font-size:14px;">int x, y, z; x=y=z=15;</span> 编译器解释时时这样的: x=(y=(z=15)); 先给z赋值,用赋值后的z再给y赋值,用赋值后的y再给x赋值. 为了实现连锁赋值,操作符必须返回一个reference指向操作符左侧的实参. 其实,如果operator=

转:10条建议让你创建更好的jQuery插件

在开发过很多 jQuery 插件以后,我慢慢的摸索出了一套开发jQuery插件比较标准的结构和模式.这样我就可以 copy & paste 大部分的代码结构,只要专注最主要的逻辑代码就行了.使用相同的设计模式和架构也让修复bug或者二次开发更容易.一套经过验证的架构可以保证我的插件不出大的问题,不论插件简单还是复杂.我在这里分享10条我总结的经验. 1. 把你的代码全部放在闭包里面 这是我用的最多的一条.但是有时候在闭包外面的方法会不能调用. 不过你的插件的代码只为你自己的插件服务,所以不存在这

“取出数据表中第10条到第20条记录”的sql语句+select top 用法

1.首先,select top用法: 参考问题  select top n * from和select * from的区别 select * from table --  取所有数据,返回无序集合 select top n * from table  -- 根据表内数据存储顺序取前n条,返回无序集合 select * from table order by id desc -- 取所有数据,按id逆序返回有序列表 select top n * from table order by id des

“取出数据表中第10条到第20条记录”的sql语句+select top 使用方法

1.首先.select top使用方法: 參考问题  select top n * from和select * from的差别 select * from table --  取全部数据.返回无序集合 select top n * from table  -- 依据表内数据存储顺序取前n条,返回无序集合 select * from table order by id desc -- 取全部数据.按id逆序返回有序列表 select top n * from table order by id d

提高Axure设计效率的10条建议 (转)

Axure 是创建软件原型的快速有力的工具.上手很容易,但是,其中存在一个危险.这款软件是如此的直观以至于很多用户可以在没有接受过任何正式培训的情况下进行使用.他们可能不知道的是他们可能没有以恰当的方式来使用 Axure. 作为一位有经验的用户体验设计师,我很少在画一页的时候第一次就能把它设计正确.大部分时候,我要经历5到10次的反复迭代(iterations).当你的用户体验设计是用来作为敏捷项目(agile project)的蓝图,那你可能需要在项目周期内跟上整个项目.有时候,这些变化将会影