item05 了解C++默默编写并调用哪些函数
对于一个空类,编译器可能默默生成下面四个函数。
class A {};
class A {
public:
A() {...}
A(const A& rhs) {...}
~A() {...}
A& operator=(const A& rhs) {...}
};
:::info
- 构造函数
- 拷贝函数
- 析构函数
- copy assignment
这些函数都是public且inline ::: :::tips 编译器生成的copy函数只是简单的将源对象的每一个non-static成员拷贝到目标对象
- 若是成员自己没有拷贝运算符,那么就会报错
因此对于引用和const成员,应该定义自己的copy :::
item06 若不想使用编译器自动生成的函数,就应该明确拒绝
编译器生成的函数时public,若是想阻止生成,最简单的方式就是将函数显示地声明为private
- 但是若是在成员函数中或者是友元函数中调用拷贝,链接器会报错而非编译器报错;
:::tips
把报错转移到编译时期的方式:
利用一个专门为了阻止copy的base class ::: ```cpp class Uncopyable { protected: Uncopyable(); ~Uncopyable(); private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };
class A : private Uncopyable { … };
:::info
任何人企图拷贝该对象时,编译器会尝试自动生成相关拷贝函数,而默认生成的拷贝构造函数会调用基类的版本,因此编译器会拒绝
:::
<a name="HeD51"></a>
# item07 为多态基类声明virtual析构函数
若是base class的析构函数不是虚函数
:::danger
若是一个derived class对象经由一个base class的指针删除,那么只能删除base class的部分。
:::
- 消除方法是给base class一个virtual的析构函数。或者说如果class如果带有一个virtual函数,那么他也需要一个virtual的析构函数
- class如果不是作为base class使用或者不是为了实现多态性,就不应该声明virtual的虚构函数。
<a name="P5Jaq"></a>
# item08 别让异常逃离析构函数
如果在析构过程中抛出异常,可能会导致资源释放不完全。
<a name="WxCcF"></a>
## 方法一:明确终止或吞掉异常
```cpp
~DBManager() {
try { db.close(); }
catch (...) {
log();
std::abort(); // 明确终止
}
}
~DBManager() {
try { db.close(); }
catch (...) {
log(); // 直接吞掉异常
}
}
方法二:将析构函数中可能吐出异常的操作提取到普通函数,给用户自行处理异常的机会
class DBManager {
public:
...
~DBManager() {
if (!closed) {
try {
db.close();
}
catch (...) {
log();
}
}
}
void close() // 由客户显式调用,自行处理异常
{
db.close();
closed = true;
}
private:
DBConnection db;
bool closed;
}
item09 绝不在构造和析构过程中调用virtual函数
在构造和析构过程中调用virutal函数会带来意想不到的效果。
class Transaction{
public:
Transaction();
virtual void log() const=0;
};
Transaction::Transaction(){
//...
log();
}
//------------------------------
class BuyTransaction:public Transaction{
public:
virtual void log() const;
};
//------------------------------
class SellTransaction:public Transaction{
public:
virtual void log() const;
};
若是执行:BuyTransaction b
- 首先执行基类的构造函数
- 构造函数最后会调用log;
- 但是这时派生类还没构造,因此调用的会是基类的log :::danger 在构造或是析构中调用虚函数,此时虚函数并不是虚函数! ::: 析构函数也类似,一旦开始执行析构函数derived class会先被释放,因此虚函数会无效
item10 令operator=
返回一个reference to *this
为了实现“连锁赋值”,需要令赋值操作符返回一个操作符左侧实参的引用
item11 在operator=
中实现“自我赋值”
class Widget { ... };
Widget w;
...
w = w; // 赋值给自己
// 潜在的自我赋值
a[i] = a[j];
// 潜在的自我赋值
*px = *py;
// 潜在的自我赋值
class Base { ... };
class Derived : public Base { ... };
void doSomething (const Base* rb, Derived* pd); // rb 和 pd 可能指向同一对象
问题一 自我赋值安全性
源和目的可能是同一对象,此时 delete pb 将会销毁当前对象的 bitmap,导致返回后该指针未定义,一种修改方式如下
Widget& Widget::operator=(const Widget& rhs)
{
if (rhs == *this) return *this; // 证同测试
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
//添加“证同测试”使得其具备“自我赋值”安全性,但是仍然存在问题二。
问题二 异常安全性
即使按照问题一的修改方式,仍可能存在问题,如果 new Bitmap 时发生异常,将会导致 pb 失效,一种简单的修改方式如下
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrg;
return *this;
//复制pb所指的东西前不要删除pb
}
即使没有“证同测试”,这种修改方式也已经同事解决了问题一。如果比较关心效率,可以在加上“证同测试”,此时需要从效率上衡量必要性
另一种修改方式是 copy and swap 技术
class Widget
{
...
void swap(Widget* rhs);
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // 为 rhs 制作一份副本
swap(temp); // 将 *this 与上述副本交换
return *this;
}
item12 复制对象时勿忘每一个成分
如果不使用编译器生成的copy函数,那么会出现的问题是: :::danger 如果遗漏了某个成员的复制,编译器不会报错! :::
- 复制所有局部成员变量
- 调用所有base class的copy函数 :::tips 如果发现copy函数和copy assignment有相似的代码,消除重复代码的方式是建立一个新的成员函数给二者使用。 :::