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

《Effective C++》读书笔记(三) 构造/析构/赋值运算 (第二部分)

2013-09-16 22:39 381 查看
构造/析构/赋值运算
Constructors,Destructors,and Assignment Operators

条款09:绝不在构造和析构过程中调用virtual函数

Never call virtual functions during construction or destruction.

不要在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来预想的结果。当有个class继承体系,有base class和derived class存在时,由于base class构造函数的执行更早于derived class构造函数,所以当base class构造函数执行时,derived class的成员变量尚未初始化。如果此时在derived class构造函数中调用virtual函数,就很容易引起“要求使用对象内部尚未初始化的成分”。同理,对应的析构函数也是如此。
更根本的原因是:在derived class对象的base class构造期间,对象的类型是base class而不是derived class.不只virtual函数会被编译器解析至base class,若使用运行阶段类型识别,也会把对象视为base class类型。
侦测“构造函数和析构函数运行期间是否调用virtual函数”并不总是想象般轻松,唯一能避免此问题的做法是:确定构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束。有个方案可以解决这个问题,参照下面代码例子:

class Transaction
{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo)const;   //non-virtual函数
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);        //non-virtual调用
}
class BuyTransaction: public Transaction
{
public:
BuyTransaction(parameters)
:Transaction(createLogString( parameters ))
{...}                       //将log信息传给base class构造函数
...
private:
static std::string createLogString( parameters );
};


这种做法是在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction函数。

☆在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

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

Have assignment operators return a reference to *this
虽然这只是个协议,并无强制性,如果不遵循它,代码一样可以通过编译。然而这份协议被所有内置类型和标准程序库提供的类型共同遵守。如果没有什么非常好的理由去标新立异,还是大众化合适些。

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

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

Handle assignment to self in operator=
“自我赋值”发生在对象被赋值给自己的时候。比如:

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


也有些不太明显或者潜在的自我赋值,一个以上的方法指涉某个相同对象,就很容易带来,比如:
a[i]=a[j]; 和 *px=*py; 都是潜在的自我赋值
假设建立一个class用来保存一个指针指向一块动态分配的Bitmap:

class Bitmap {...};
class Widget
{
...
private:
Bitmap* pb;
};


要阻止自我赋值,一个传统的方法是借用operator= 最前面的一个“证同测试”(identity test),达到目的:

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


传统做法有些缺陷,看起来还可以。因为它不具备异常安全性以及自我赋值安全性。第二种方法是不去管“自我赋值”,而是把注意力集中在实现“异常安全性”上:

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


第三种方法是利用copy-and-swap技术,但对于时间和空间上消耗比较高。因为copy-and-swap要对每一个即将被改动的对象做出一个temp副本

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


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

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

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

Copy all parts of an object.

设计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象复制,那便是带着适当名称的copy构造函数和copy assignment操作符。可以把它们统称为copying函数。

需要注意的是,如果为class添加一个成员变量,也必须同时修改所有的copy构造函数和所有copy assignment操作符。

如果发生了继承,就更加要注意“为derived class撰写copying函数”的重要大任。必须很小心地复制其base class成分,要保证该有的部分都有。而且对于private部分,应该让derived class的copying函数调用相应的base class函数。

总而言之,当编写一个copying函数时,要确保复制所有local成员变量和调用所有base classes内的适当copying函数。

令copy assignment操作符调用copy构造函数是不合理的,就像试图构造一个已经存在的对象一样,非常荒谬。同理,令copy构造函数调用copy assignment操作符同样没意义。构造函数用来初始化新对象,assignment操作符只施行于已初始化对象身上。

如果发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复。

☆Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。

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

参考文献:

《Effective C++》3rd Scott Meyers著,侯捷译
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: