2 构造/析构/赋值运算
条款 05 :了解 C++ 默默编写并调用哪些函数
// empty class : 编译器自动生成 default 构造函数、 copy 构造函数、copy assignment 操作符
// 以及析构函数
class Empty { };
// 这就好像你写下了这样的代码:
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... }
~Empty() { ... }
Empty& operator=(const Empty& rhs) { ... }
};
// 当 class 内含 reference 成员和 const 成员,编译器会拒绝为 class 生成 operator=
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
...
private:
std::string& nameValue; // 这是个 reference
const T objectValue; // 这是个 const
};
请记住:
- 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。
条款 06 :若不想使用编译器自动生成的函数,就该明确拒绝
// 禁用 copy 构造函数和 copy assignment 操作符的两种方式
// 1.将其声明为 private 并不予实现;
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // 只有声明
HomeForSale& operator=(const HomeForSale);
};
// 2.将连接期错误移至编译期,设计一个专门为了阻止 copying 动作而设计的 base class 内。
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
// 为阻止 HomeForSale 对象被拷贝,可以继承 Uncopyable
class HomeForSale: private Uncopyable {
...
};
请记住:
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyalbe 这样的 base class 也是一种做法。
条款 07 :为多态基类声明 virtual 析构函数
请记住:
- polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该有一个 virtual 析构函数。
- Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically), 就不该声明 virtual 析构函数。
条款 08 :别让异常逃离析构函数
// 假设使用一个 class 负责数据库连接:
class DBConnection {
public:
...
static DBConnection create();
};
// DBConn 管理 DBConnection 资源,析构中调用 close
class DBConn {
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
// 一旦close 调用失败,DBConn 析构函数会传播异常
// 1. 调用 abort 结束程序
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对 close 的调用失败;
std::abort();
}
}
// 2. 吞下异常
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对 close 的调用失败;
}
}
// 较佳策略,重新设计 DBConn 接口,提供一个 close 函数,因而赋予客户一个机会得以处理
// "因该操作而发生的异常"
class DBConn {
public:
...
void close() // 供客户使用的函数
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try {
db.close();
}
catch (...) {
制作运转记录,记下对 close 的调用失败;
...
}
}
}
private:
DBConnection db;
bool closed;
};
请记住:
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class 应该提供一个普通函数(而非在析构函数中)执行该操作。
条款 09 :绝不在构造和析构过程中调用 virtual 函数
// 在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class。
// 错误范例1
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
...
};
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
...
};
Transaction::Transaction()
{
...
logTransaction();
};
class BuyTransaction: public Transaction
{
public:
virtual void logTransaction() const;
...
};
class SellTransaction: public Transaction
{
public:
virtual void logTransaction() const;
...
};
// 现在,当下面这行被执行
BuyTransaction b;
// 错误范例2
class Transaction {
public:
Transaction()
{ init(); } // 调用 non-virtual
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // 这里调用virtual!
}
};
// 正确示范
class Transaction
{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);
}
class BuyTransaction: public Transaction
{
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters )) //将log信息传递给base class构造函数
{ ... }
...
private:
// 使用static,避免指向“初期未成熟之BuyTransaction对象内尚未初始化的成员变量”
static std::string createLogString( parameters );
}
请记住:
- 在构造和析构期间不要调用 virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
条款 10 :令 operator= 返回一个 reference to *this
int x, y, z;
x = y = z = 15;
// 赋值采用右结合律,所以上述连锁赋值被解析为:
x = (y = (z = 15));
// 为了实现连锁赋值,赋值操作符必须返回一个 reference 指向操作符左侧实参
class Widget {
public:
Widget& operator=(const Widget& rhs)
{
...
return *this; // 返回左侧对象
}
Widget& operator+=(const Widget& rhs) // 这个协议同样适用 -= *= /= 等等
{
...
return *this;
}
Widget& operator=(int rhs) // 即使参数类型不符协定,也适用
{
...
return *this;
}
};
// Ps: 这只是一个协议,并无强制性。
请记住:
- 令赋值(assignment) 操作符返回一个 reference to *this.
条款 11 :在 operator= 中处理“自我赋值”
// 潜在自我赋值举例
a[i] = a[j];
*px = *py;
class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd); // rb 和 *pd 有可能是同一个对象
// 尝试自行管理资源,可能会掉进“在停止使用资源之前释放了它”的陷阱
class Bitmap { ... };
class Widget {
...
private:
Bitmap *pb; // 指针,指向一个从heap分配而得的对象
};
// 以下示例不具备异常安全性
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
// 如果*this 和 rhs 是同一对象,那么delete操作就不只是销毁当前对象的bitmap,它也销毁了rhs的bitmap
// 传统做法: 在函数最前面加一个“证同测试”达到自我赋值的检验目的
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
// 保证异常安全性往往自动获得自我赋值安全的回报。只需要调整语句的顺序即可达到目的
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // 记住原先的pb
pb = new Bitmap(*rhs.pb); // 令pb指向 *pb的一个复件
delete pOrig; // 删除原先的pb
return *this;
}
// 在 operator= 函数内手工排列语句的替代方案,使用 copy and swap 技术
class Widget
{
...
void swap(Widget& rhs); // 交换*this 和 rhs 的数据; 详见条款29
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
// 变体: class 的 copy assignment 操作符被声明为“以by value方式接受实参”
Widget& Widget::operator=(Widget rhs)
{
swap(rhs);
return *this;
}
// 缺点: 为了巧妙伶俐的修补而牺牲了清晰性
// 优点:将 copying 动作从函数本体移至函数参数构造阶段可能令编译器有时生成更高效的代码
请记住:
- 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款 12 :复制对象时勿忘其每一个成分
void logCall(const std::string& funcName);
class Customer
{
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name)
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
// 目前一切ok,但是当另一个成员变量加入战局:
class Date { ... };
class Customer
{
public:
... // 同前
private:
std::string name;
Date lastTransaction;
};
// 此时必须更新 copying 函数,否则在最高警告级别的情况下,大多数编译器不会有任何怨言
// 然而,一旦发生继承,情况会更糟糕
class PriorityCustomer: public Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
// 因为 PriorityCustomer 的 copy构造函数并没有指定实参传给其 base class 构造函数,因此
// PriorityCustomer对象的 Customer 成分会被不带实参之 Customer 构造函数(必定有一个) 初始化。
// 如果为派生类撰写 copying 函数,必须小心地复制其基类成分
// 因为成分为私有,所以调用基类相应的 copying 函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 显示调用基类成分进行赋值动作
priority = rhs.priority;
return *this;
}
请记住:
- Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由连个copying函数共同调用。