2 构造/析构/赋值运算(续)
条款09:绝不在构造和析构过程中调用 virtual 函数
class Transaction { //所有交易的base class
public:
Transaction();
virtual void logTransaction() const = 0; //做出一份因类型不同而不同的日志记录(log entry)
...
};
Transaction::Transaction() //base class构造函数之实现
{
...
logTransaction(); //最后动作是志记这笔交易
}
class BuyTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const; //志记(log)此型交易
...
};
class SellTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const; //志记(log)此型交易
...
};
如果执行BuyTransaction b;
,其执行过程为:
会有一个BuyTransaction构造函数被调用,但首先会调用Transaction构造函数(因为派生类对象内的基类成分会在派生类自身成分被构造之前先构造妥当)。Transaction构造函数的最后一行调用virtual函数logTransaction被调用的logTransaction是Transaction(基类)内的版本,不是BuyTransaction内的版本。
换句话就是:在base class构造函数执行期间,virtual函数不是virtual函数。
在初始化派生类对象时,基类的构造函数的执行要早于派生类的构造函数。即,派生类对象内的基类成分会在派生类自身成分被构造前先构造妥当。
基类构造期间,virtual函数绝不会下降到派生类阶层。
当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。后一种情况通常不会引发任何编译器和连接器的问题,但它也是错误的。
解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
// 在class Transaction内将logTransaction函数改为non-virtual,
// 然后要求派生类构造函数传递必要信息给Transaction构造函数,
// 而后那个构造函数便可安全地调用non-virtual logTransaction。
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 );
};
** note: **
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。
条款10:令 operator= 返回一个 reference to *this
对于赋值操作符,可以进行连锁赋值:
int x, y, z;
x = y = z = 15; // 等价于 x=(y=(z=15)); 赋值采用右结合律
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
class Widget{
public:
Widget& operator+=(const Widget& src){
...
return *this;
}
Widget& operator=(const Widget& src){
...
return *this;
}
}
所有内置类型和标准程序库提供的类型如string,vector,complex,tr1::shared_ptr或即将提供的类型(条款54)共同遵守。
** note: **
令赋值(assignment)操作符返回一个reference to *this。
条款11:在 operator= 中处理“自我赋值”
// 自我赋值的例子
Widget w;
w = w;
a[i] = a[j]; // i == j时自我赋值
*px = *py; // px,py指向同个地址时自我赋值
以上情况都是对“值”的赋值,但我们涉及对“指针”和“引用”进行赋值操作的时候,才是我们真正要考虑的问题了。
class Bitmap{...};
class Widget {
...
private:
Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; // 对pb指向内存对象进行delete
pb = new Bitmap(*rhs.pb); // 如果*this == rhs,那么这里new出错
return *this;
}
如果operator=函数内的rhs和*this(赋值的目的端)是同一个对象,将会导致*this对象里的pb指针指向一个已被删除的对象.
- 传统做法是藉由operator=最前面的一个“证同测试”达到“自我赋值”的检验目的:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; // 证同测试,如果是自我赋值,就不做任何事
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
虽然增加证同测试后,可以达到自我赋值的安全性,但不具备异常安全性。如果new过程发生异常(不论是因为分配时内存不足或因为Bitmap的copy构造函数抛出异常),Widget最终持有一个指针指向一块被删除了的Bitmap。这样的指针有害,我们无法安全地删除它们,甚至无法安全地读取它们.
- 通过合理安排语句,可以实现异常安全的代码。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // 记住原先的pb
pb = new Bitmap(*rhs.pb); // 令pb指向*pb的一个副本
delete pOrig; // 删除原先的pb
return *this; //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
}
如果"new Bitmap"抛出异常,pb会保持原状。即使没有证同测试,上述代码还是能够处理自我赋值.
- 还有一种方法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;
}
上述代码中,创建临时变量temp,作为rhs数据的一份复件。由于swap函数会交换*this和参数对象的数据,如果直接将rhs作为参数,则会改变rhs对象的值,与operator=的操作不符。
** note: **
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
copying函数:copy构造函数和copy assignment操作符。
- 编译器在必要时会为我们的classes创建copying函数,以将被拷对象的所有成员变量都做一份拷贝。但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们也应确保对“每一个”成员进行拷贝,否则拷贝不完全时编译器也不会提示你。
void logCall(const std::string& funcName); // make a log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
// copy构造函数
Customer::Customer(const Customer& rhs)
: name(rhs.name) // copy rhs’s data
{
logCall("Customer copy constructor");
}
// copy assignment操作符
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name; // copy rhs’s data
return *this;
}
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
在派生类的构造函数,copy构造函数和copy assignment操作符中应当显示调用基类相对应的函数,否则编译器可能会调用基类的default构造函数(必定有一个否则无法通过编译)对基类成分进行缺省的初始化。
// 派生类
class PriorityCustomer: public Customer { // a derived class
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
// copy构造函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // 调用base class的copy构造函数
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
// copy assignment操作符
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 对base class成分进行赋值操作
priority = rhs.priority;
return *this;
}
- 当你编写一个copying函数,请确保:
- 复制所有local成员变量;
- 调用所有base classes内的适当的copying函数。
- 我们不该令拷贝赋值操作符调用拷贝构造函数,也不该令拷贝构造函数调用拷贝赋值操作符。如果copy构造函数和copy assignment操作符有相近的代码,消除代码重复的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。
** note: **
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。