二、构造,析构,赋值运算--条款09-12
条款09:绝不在构造和析构过程中调用virtual函数
为什么?
作者用了一段简单的买卖订单代码来辅助解释:
//交易的base class
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0; //用来写日志的日志记录函数
}
Transaction::Transaction()
{
... // 诸如初始化等操作
logTransaction(); // 写日志
}
// 买入的类,继承自基类
class BuyTransaction
{
public:
...
virtual void logTransaction() const;
}
// 卖出的类,继承自基类
class SellTransaction
{
public:
...
virtual void logTransaction() const;
}
有了以上代码,接着考虑执行以下代码段:
BuyTransaction b;
声明一个变量b,按照继承体系的规则,我们要先执行基类Transaction的构造函数,基类的构造函数中调用了虚函数logTransaction,所以这个时候调用的事基类中的logTransaction,并不是BuyTransaction的logTransaction函数!就算b这个变量是一个BuyTransaction类型的,它也不会执行自己的logTransaction函数。
我们通过以下3个方面来解释
(1) 基类的构造期间virtual函数是绝不会下沉到derived class层的。所以在构造函数中调用虚函数在此时并不能达到我们需要的结果。
(2) (解释为何不能下沉)当基类的构造函数在执行的时候,派生类的成员变量尚未初始化,如果此时下沉到了派生类之中,去执行了派生类的virtual函数,virtual函数中非常有可能用到这些未初始化的成员变量,那这将是通往不明确行为和彻夜调试大会的门票。
(3) 根本原因:在派生类对象的base class构造期间,此对象的类型是一个base class而不是derived class.不只是virtual函数会被编译器解析成基类的virtual函数,若使用运行期类型信息(如dynamic_cast何typeid),也会把对象视为base class类型。所以一开始初始化的是derived class中的base class成分。
同样的,析构函数也是如此。 一旦派生类对象进入了析构函数开始执行,对象内的派生类的成员变量就呈现了未定义的值,如果这时候调用了virtual函数,就会使用这个未定义的值,这也会导致不明确的行为和通往彻夜调试大会的门票。
作者总结
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数那层)。
条款10: 令operator=返回一个reference to *this
这只是一个协议,并不强制性要求,但是习惯上都这么做。
因为返回一个reference to * this 可以实现连锁赋值。
int x,y,z;
x=y=z=10;
就像上述的简单代码一样。
所以我们写operator=的时候,最最最最好都要返回reference to *this.
Widget& operator=(const Widget& rhs)
{
...
return *this;
}
作者总结
令赋值操作符返回一个reference to *this.
条款11:在operator=中处理“自我赋值”
为什么要处理?
1.1 先看一下一个不安全的operator=函数:
存在一个位图类和Widget类:
class BitMap
{
...
}
class Widget
{
...
private:
BitMap *pb;
}
Widget& Widget::operator=(Widget& rhs)
{
delete pb;
pb = new BitMap(*rhs.pb);
return *this;
}
乍一看好像没有错误,现在考虑“自我赋值”的问题:
假设rhs和 * this是同一个对象的时候。我们在operator=中第一步就删除了pb,那么rhs对象的pb就也被我们删除了,那么就根本无法new出来一个pb给this。
1.2 现在看一个经过“证同测试”的operator函数:
Widget& Widget::operator=(Widget& rhs)
{
if(&rhs == this)
return *this;
delete pb;
pb = new BitMap(*rhs.pb);
return *this;
}
这个是可以用的。但还是存在一些风险:当new抛出了异常的时候,那么pb已经被删除了,返回的将是一个指向被删除位置的指针。
1.3 在复制pb所指的东西之前不要删除pb即可。
Widget& Widget::operator=(Widget& rhs)
{
BitMap *pOrig = pb; //记录原来的pb
pb = new BitMap(*rhs.pb);
delete pOrig;
return *this;
}
相比于1.2的代码来看:
(1) 记录了原来的pb指向的数据。这样待会删除pOrig指针就可以达到删除pb的效果。
(2) 使用rhs的数据new一块新内存出来。
- new失败:我们也没有把原来的数据删除。此次操作不会影响任何东西。
- new成功:就分配了一个新内存来保存数据,在“自我赋值”的情况下,就是在新的地址里面又保存了一分副本。待会删除原来的地址即可。
(3) 删除原来this->pb的内存。这样在“自我赋值”的情况下也不会出现删除掉之后返回已被删除的指针了。因为这是两块不同的内存,不会相互影响。
tips: 这里虽然可以达到“自我赋值”的作用,但是其实也可以在代码最前面加上:
if(&rhs == this)
return *this;
这样做的效率反而会更高,但其实没有频繁用到的话也是没什么差别的。
作者总结
确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、记忆copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象的时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
假设一开始你有个Customer类:
void logCall(const string &funcName)
class Customer
{
public:
Customer(const Customer& rhs);
Customer &operator=(const Customer& rhs);
...
private:
string name;
}
// 构造函数的实现
Customer::Customer(const Customer& rhs)
:name(rhs.name)
{
}
// copy assignment函数实现
Customer& Customer::operator=(const Customer& rhs)
{
this->name = rhs.name;
return *this;
}
现在看起来是正确的,但是一旦加入了一个新的成员,我们切记一定要去operator=函数中将新的成员变量也拷贝进去。
现在我们用一个PriorityCustomer类继承Customer类:
class PriorityCustomer : public Customer
{
public:
PriorityCustomer(const PriorityCustomer &rhs);
PriorityCustomer& operator=(const PriorityCustomer &rhs);
...
private:
int Priority;
}
这时候我们实现operator=的时候,不仅仅需要拷贝当前类的成分,还需要拷贝在基类所继承下来的成分,才是完整的。
// copy 构造函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs)
: Customer(rhs),Priority(rhs.Priority)
{
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
Customer::operator=(rhs);
Priority = rhs.Priority;
return *this;
}
从上面的代码可以看到,我们必须拷贝对象的每一个成分,包括它的基类。每一份都不要忘记。
所以,编写一个copying函数,确保:
(1) 复制所有的local成员变量。
(2) 调用所有base class内的适当的copying函数。
作者总结
Copying函数应该确保复制“对象内的所有成员变量”及“所有的base class成分。”
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。