Item 5:了解 C++ 默默编写并调用哪些函数
-
“编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。”
-
“C++ 不允许 “让 reference” 改指向不同对象。”
-
“如果你打算在一个 “内含 reference 成员” 或者 “内含 const 成员” 的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。如果没有自定义的 copy assignment 操作符,C++ 会拒绝编译赋值动作。”
Item 6:若不想使用编译器自动生成的函数,就该明确拒绝
- 为驳回编译器自动(暗自)提供的机能,有两种做法:
- ① 将相应的成员函数声明为 private 并且不予实现
class HomeForSale{ public: ... private: ... HomeForSale(const HomeForSale&); // 只有声明 HomeForSale& operator=(const HomeForSale&); };
- ② 增加为了阻止 copying 动作而设计的 base class
class Uncopyable{ protected: // 允许 derived 对象构造和析构 Uncopyable() {} ~Uncopyable() {} private: // 阻止 copying Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); }; class HomeForSale : private Uncopyable{ ... };
Item 7:为多态基类声明 virtual 析构函数
-
“polymorphic(带多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。”
- 当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的 derived 成分没被销毁。
-
“Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。”
Item 8:别让异常逃离析构函数
- “析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。”
- 如果 close 抛出异常就结束程序。通常通过调用 abort 完成:
DBConn::~DBConn() { try {db.close();} catch(...) { // 制作运转记录,记下对 close 的调用失败 std::abort(); } }
- 吞下因调用 close 发生的异常
DBConn::~DBConn() { try {db.close();} catch(...) { // 什么都不做 // 不会导致程序崩溃,但缺点是你会失去异常的上下文和细节。 } }
- “如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。”
class DBConn
{
public:
...
void close() // 供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed)
{
try {
db.close();
}
catch(...)
{
// 制作运转记录,记下对 close 的调用失败
// 记录下来并结束程序或吞下异常
...
}
}
}
private:
DBConnection db;
bool closed;
};
Item 9:绝不在构造和析构函数中调用 virtual 函数
- “在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class(比起当前执行构造函数和析构函数的那层)。”
下面是一个例子:
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
// 其他操作
};
Transaction::Transaction()
{
// 其他操作
logTransaction(); // 最后动作:记录这笔交易
}
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const;
// 其他操作
};
当执行 BuyTransaction b;
时,调用的 logTransaction
是 Transaction
内的版本。
“如何确保每次一有 Transaction
继承体系上的对象被创建,就会有适当版本的 logTransaction
被调用呢?”
下面是一种解决方法:
class Transaction {
public:
explicit Transaction(const std::string&logInfo);
void logTransaction(const std::string& logInfo);
// 其他操作
};
Transaction::Transaction(const std::string& logInfo)
{
// 其他操作
logTransaction(logInfo); // 最后动作:记录这笔交易
}
class BuyTransaction : public Transaction {
public:
// 将 log 信息传递给 base class 构造函数
BuyTransaction(parameters) : Transaction(createLogString(parameters))
{
// ...
}
// 其他操作
private:
static std::string createLogString(parameters);
};
即由于无法使用 virtual 函数从 base class 向下调用,在构造期间,你可以藉由 “令 derived class 将必要的构造信息向上传递至 base class 构造函数” 替换之而加以弥补。
Item 10:令 operator= (赋值操作符) 返回一个 reference to *this
Item 11:在 operator= 中处理 “自我赋值”
-
“确保当对象自我赋值时 operator= 有良好的行为。其中技术包括比较 “来源对象” 和 “目标对象” 的地址、精心周到的语句顺序以及 copy-and-swap。”
-
“确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。”
观察下面的例子:
class Bitmap {...};
class Widget{
...
private:
Bitmap* pb;
};
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。这样的指针有害。
第二次改进:
// 复制 pb 所指东西前别删除 pb
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
替代方案:copy and swap 技术
class Widget
{
...
void swap(Widget& rhs); // 交换 *this 和 rhs 的数据
...
};
// ①
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
// ②
Widget& Widget::operator=(Widget rhs)
{
swap(temp);
return *this;
}
Item 12:复制对象时勿忘其每一个成分
-
“Copying 函数应该确保复制 ‘对象内的所有成员变量’ 及 ‘所有 base class 成分’。
-
“不要尝试以某个 copying 函数实现另一个 copying 函数。 应该将共同机能放进第三个函数中,并由两个 copying 函数共同调用。”