0%

Item 14: Think carefully about copying behavior in resource-managing classes.

设计一个 RAII 对象:

1
2
3
4
5
6
7
8
9
class Lock {
public:
explicit Lock(Mutex *pm):mutexPtr(pm){
lock(mutexPtr);
}
~Lock(){ unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};

客户对Lock的使用:

1
2
3
4
5
6
Mutex m;
...
{
Lock ml(&m);
...
}

当一个 RAII 对象被复制,会发生什么事? 不确定?

1
2
Lock ml1(&m);
Lock ml2(&ml1)

记住资源管理对象的拷贝行为取决于资源本身的拷贝行为,同时资源管理对象也可以根据业务需要来决定自己的拷贝行为。一般有如下四种方式:

  • 禁止复制。参考若不想使用编译器自动生成的函数,就该明确拒绝。对Lock而言看起来是这样:

    1
    2
    3
    4
    class Lock : private Uncopyable {
    public:
    ...
    }
  • 引用计数,采用 shared_ptr 的逻辑。shared_ptr 构造函数提供了第二个参数 deleter,当引用计数到 0 时被调用。 所以 Lock 可以通过聚合一个 shared_ptr 成员来实现引用计数:

    1
    2
    3
    4
    5
    6
    7
    8
    class Lock{
    public:
    explicit Lock(Mutex *pm): mutexPtr(pm, unlock){
    lock(mutexPtr.get());
    }
    private:
    std::shared_ptr<Mutex> mutexPtr; //shared_ptr替换 raw pointer
    };

    Lock 的析构会引起 mutexPtr 的析构,而 mutexPtr 计数到0时unlock(mutexPtr.get()) 会被调用。

  • 拷贝底部资源。复制资源管理对象时,进行的是深拷贝。比如 string 的行为:内存存有指向对空间的指针,当它被复制时会复制那片空间。

  • 转移底部资源的拥有权auto_ptr 就是这样做的,把资源移交给另一个资源管理对象,自己的资源置空。

Item 12: Copy all parts of an object

正确拷贝函数实现:

1
2
3
4
5
6
7
8
9
class Customer{
string name;
public:
Customer(const Customer& rhs): name(rhs.name){}
Customer& operator=(const Customer& rhs){
name = rhs.name; // copy rhs's data
return *this; // see Item 10
}
};

情形一: 新添加了一个数据成员,忘记了更新拷贝函数

1
2
3
4
5
6
7
8
9
10
class Customer{
string name;
Date lastTransaction;
public:
Customer(const Customer& rhs): name(rhs.name){}
Customer& operator=(const Customer& rhs){
name = rhs.name; // copy rhs's data
return *this; // see Item 10
}
};

这时 lastTransaction 便被你忽略了,编译器也不会给出任何警告(即使在最高警告级别)

情形二: 继承父类忘记了拷贝父类的部分

1
2
3
4
5
6
7
8
9
10
11
class PriorityCustomer: public Customer {
int priority;
public:
PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority){}

PriorityCustomer&
operator=(const PriorityCustomer& rhs){
priority = rhs.priority;
}
};

正确写法:

1
2
3
4
5
6
7
8
9
10
11
12
class PriorityCustomer: public Customer {
int priority;
public:
PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(rhs.priority){}

PriorityCustomer&
operator=(const PriorityCustomer& rhs){
Customer::operator=(rhs);
priority = rhs.priority;
}
};

Item 11: Handle assignment to self in operator=

我们在重载一个类的赋值运算符时要考虑自我赋值的问题。有了指针和引用自我赋值不总是第一时间能够识别出来。

1
2
3
4
5
6
7
8
9

a[i] = a[j];

*px = *py;

class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd);// rb和女pd 有可能其实是同一对象
rb = pd;

自我赋值主要考虑到 自我赋值安全性异常安全性

1
2
3
4
5
class Bitmap { ... };
class Widget {
private:
Bitmap* pb; //指针,指向一个从heap 分配而得的对象
};

既不自我赋值安全性也不异常安全性, 当 rhs == *this时,delete pb使得rhs.pb成为空值,接下来 new 的数据便是空的。

1
2
3
4
5
Widget& Widget::operator=(const Widget& rhs) {
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

判断两个地址是否相同,如果是自我赋值,就不做任何事。但开始就delete pb, 但 new 出现异常, pb就会置空出现风险。

1
2
3
4
5
6
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return this; // 证同测试
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

在C++中仔细地排列语句顺序通常可以达到异常安全, 比如我们可以先申请空间,最后再delete:

1
2
3
4
5
6
Widget& Widget::operator=(const Widget& rhs) {
Bitmap *pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}

一个更加通用的技术便是复制和交换(copy and swap):

1
2
3
4
5
6
7
8
9
10
class Widget {
void swap(Widget& rhs); // 交换*this rhs 的数据
};

Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //rhs 数据制作一份复件(副本)
swap (temp); //*this 数据和上述复件的数据交换
return *this;
}

Item 10:Have assignment operators return a reference to *this.

赋值运算符要返回自己的引用只是个协议,并无强制性。这份协议被所有内置类型和标准程序库提供的类型如string, vector, complex std::shared_ptr等共同遵守。可以用来支持链式的赋值语句。

1
2
int x, y, z;
x = y = z = 15; //赋值连锁形式

相当于:

1
x = ( y = ( z = 15 ) );

我们自定义的对象最好也能支持链式的赋值,这需要重载=运算符时返回当前对象的引用:

1
2
3
4
5
6
7
8
9
10
11
class Widget {
public:
Widget& operator=(const Widget& rhs){
return *this;
}

//这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算 +=, -=, *=, etc.
Widget& operator+=(const Widget& rhs){
return *this;
}
};

Item 9: Never call virtual functions during construction or destruction.

在构造和析构期间不要调用 virtual 函数,因为这类调用不会下降至 derived class
(比起当前执行构造函数和析构函数的那层)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Transaction {                               // base class for all
public: // transactions
Transaction(){ // base class ctor
logTransaction(); // as final action, log this
}
virtual void logTransaction() const = 0; // make type-dependent
};

class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
};
...
BuyTransaction b;

b 在构造时,调用到父类Transaction的构造函数,其中对 logTransaction 的调用会被解析到 Transaction 类。 那是一个纯虚函数,因此程序会非正常退出。

derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived classo 不只 virtual 函数会被编译器解析至(resolve to) base class ,若使用运行期类型信息 RTTI(runtime type information, 例如 dynamic_cast typeid) ,也会把对象视为 base class 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Transaction{
public:
Transaction(){
cout<<typeid(this).name()<<endl;
}
};
class BuyTransaction: public Transaction{
public:
BuyTransaction(){
cout<<typeid(this).name()<<endl;
}
};
void main(){
BuyTransaction b;
}

输出

1
2
P11Transaction
P14BuyTransaction

相同道理也适用于析构函数.

Item 8: Prevent exceptions from leaving destructors.

C++ 本身不阻止在析构函数抛出异常,但在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。例如:

1
2
3
4
5
6
7
8
9
class Widget {
public:
...
~Widget() { ... } //假设这里可能抛出异常
};

void doSomething(){
std::vector<Widget> v; // v 这里被自动析构
}

当v被调用析构函数,它包含的所有Widget对象也都会被调用析构函数。又因为v是一个容器,如果在释放第一个元素时触发了异常,它也只能继续释放别的元素,否则会导致其它元素的资源泄露。如果在释放第二个元素的时候又触发了异常,那么程序同样会导致崩溃。

不仅仅是std::vector,所有STL容器的类甚至包括数组也都会像这样因为析构函数抛出异常而崩溃程序,所以在 C++ 中,不要让析构函数抛出异常!

但是如果析构函数所使用的代码可能无法避免抛出异常呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DBConnection{                   //某用来建立数据库连接的类
public:
...
static DBConnection create(); //建立一个连接
void close(); //关闭一个连接,假设可以抛出异常
};

class DBConn{ //创建一个资源管理类来提供更好的用户接口
public:
....
~DBConn{ db.close(); } //终止时自动调用关闭连接的方法
private:
DBConnection db;
};


...{
DBConn dbc(DBConnection::create()); //创建一个DBConn类的对象
... //使用这个对象
} //对象dbc被释放资源

析构函数所调用的 close() 方法可能会抛出异常,那么有什么方法来解决呢?

吞掉异常

1
2
3
4
5
6
7
8
DBConn::~DBConn(){
try{
db.close();
}catch(...){
//记录访问历史
}
}

主动关闭程序

1
2
3
4
5
6
7
8
DBConn::~DBConn(){
try{
db.close();
}catch(...){
//记录访问历史
std::abort();
}
}

把可能抛出异常的代码移出析构函数

客户在需要关闭的时候主动调用 close() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DBConn{
public:
...
~DBConn();
void close(); //当要关闭连接时,手动调用此函数
private:
...
closed = false; //显示连接是否被手动关闭
};

void DBConn::close(){ //当需要关闭连接,手动调用此函数
db.close();
closed = true;
}

DBConn::~DBcon(){
if(!closed) //析构函数还是要留有备用,但不用每次都承担风险了
try{
db.close();
}catch(...){
//记录访问历史
//消化异常或者主动关闭
}
}
  • 析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析
    构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提
    供一个普通函数(而非在析构函数中)执行该操作。

Item 7: Declare destructors virtual in polymorphic base classes.

析构函数声明为虚函数目的在于以基类指针调用析构函数时能够正确地析构子类部分的内存。 否则子类部分的内存将会泄漏,正确的用法如下:

1
2
3
4
5
6
7
8
9
// 基类
class TimeKeeper{
public:
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper(): // 可能返回任何一种子类
...
delete ptk;
  • polymorphic (带多态性质的) base classes 应该声明一个 virtual 析构函数。如果
    class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性
    (polymorphically) ,就不该声明 virtual 析构函数。

Item 6: Explicitly disallow the use of compiler-generated functions you do not want.

在C++中,编译器会自动生成一些你没有显式定义的函数。可以参考:了解c++默默编写并调用哪些函数
然而有时候我们希望禁用掉这些函数,可以通过把自动生成的函数设为 private 来禁用它或者在 c++11 中使用 delete 关键字。

比如我们禁用拷贝的功能:

1
2
3
4
5
6
7
8
9
10
class HomeForSale
{
public:
...

private:
HomeForSale(const HomeForSale &); // 只有声明
HomeForSale& operator=(const HomeForSale&) = delete// c++11

};

我们可以专门设计一个阻止copying 的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace noncopyable_ {
class noncopyable {
protected:
noncopyable() {}
~noncopyable(){}
/** C++11
noncopyable() = default;
~noncopyable() = default;
*/
private:
noncopyable(const noncopyable&);
noncopyable& operator=( const noncopyable& );
/** C++11
noncopyable( const noncopyable& ) = delete;
noncopyable& operator=( const noncopyable& ) = delete;
*/
};
}
1
2
3
4
5
6
7
8
9
10
class HomeForSale : private noncopyable_::noncopyable
{
};

HomeForSale p1, p2;
p1 = p2;

error: object of type 'HomeForSale' cannot be assigned because its copy assignment operator is implicitly deleted
p1 = p2;
^

Item 5: Know what functions C++ silently writes and calls

默认函数

C++ 中,一个类有八个默认函数:

1
2
3
4
5
6
7
8
9
10
class Empty {
Empty () {} //默认构造函数
Empty (const Empty &) {} // 默认拷贝构造函数
Empty (const Empty &&) {} // 默认移动构造函数(`C++11`)
~Empty() {} // 默认析构函数
Empty& operator=(const Empty&) {} // 默认重载赋值运算符函数
Empty& operator=(const Empty&&){} // 默认重载移动赋值操作符函数函数
Empty* operator &() {} // 默认重载取址运算符函数
const Empty* operator &() const {} // 默认重载取址运算符 `const` 函数
};

调用时机

只有你需要用到这些函数并且你又没有显示的声明这些函数的时候,编译器才会贴心的自动声明相应的函数。

引用成员

如果你打算在一个“内含引用成员”或者“内含const成员”的类内支持赋值操作,就必须定义自己的默认拷贝赋值操作符。因为 C++ 本身不允许引用改指不同的对象,也不允许更改 const 成员。

1
2
3
4
5
6
7
8
9
class Person {
public:
string & name;
Person(string &str):name(str) {}
};

string s1 = "hello", s2 = "world";
Person p1(s1), p2(s2);
p1 = p2;
1
error: object of type 'Person' cannot be assigned because its copy assignment operator is implicitly deleted