Effective C++读书笔记(8)(一)

2014-11-24 12:20:03 · 作者: · 浏览: 2

条款12:复制对象勿忘其每一个成分

Copy all parts of an object

设计良好的面向对象系统中,封装了对象内部,仅留两个函数用于对象的拷贝:拷贝构造函数和拷贝赋值运算符,统称为拷贝函数。编译器生成版的copy函数会拷贝被拷贝对象的所以成员变量。

考虑一个表现顾客的类,这里的拷贝函数是手工写成的,以便将对它们的调用志记下来:

void logCall(const std::string&funcName); // 制造一个log entry

class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...

private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) // 复制rhs的数据
{logCall("Customer copy constructor");}

Customer& Customer::operator=(constCustomer& rhs)
{
logCall("Customer copy assignment operator");

name= rhs.name; //复制rhs的数据

return*this;
}

这里的每一件事看起来都不错,实际上也确实不错——直到Customer 中加入了另外的数据成员:

class Date { ... }; // 日期

class Customer {
public:
... // 同前 www.2cto.com

private:
std::string name;
Date lastTransaction;
};

在这里,已有的拷贝函数只进行了部分拷贝:它们拷贝了Customer 的name,但没有拷贝它的lastTransaction。然而,大部分编译器即使是在最高的警告级别也不出任何警告。结论显而易见:如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数,你还需要更新类中的全部的构造函数以及任何非标准形式的operator=。

一旦发生继承,可能会造成此主题最暗中肆虐的一个暗藏危机。考虑:

PriorityCustomer::PriorityCustomer(constPriorityCustomer& rhs)
: Customer(rhs), // 调用基类的copy构造函数
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;
}

无论何时,你打算自己为一个派生类写拷贝函数时,必须注意同时拷贝基类部分。那些成分往往是private,所以你不能直接访问它们,应该让派生类的拷贝函数调用相应的基类函数。当你写一个拷贝函数,需要保证(1)拷贝所有本地数据成员以及(2)调用所有基类中的适当的拷贝函数。

· 拷贝函数应该保证拷贝一个对象的所有数据成员以及所有的基类部分。



在实际中,两个拷贝函数经常有相似的函数体,而这一点可能吸引你试图通过用一个函数调用另一个来避免代码重复。你希望避免代码重复的想法值得肯定,但是用一个拷贝函数调用另一个来做到这一点是错误的。

“用拷贝赋值运算符调用拷贝构造函数”和“用拷贝构造函数调用拷贝赋值运算符”都是没有意义的。如果发现你的拷贝构造函数和拷贝赋值运算符有相似的代码,通过创建第三个供两者调用的成员函数来消除重复。这样的函数当然是private 的,而且经常叫做init。这一策略可以消除拷贝构造函数和拷贝赋值运算符中的代码重复,安全且被证实过。

· 不要试图依据一个拷贝函数实现另一个。作为代替,将通用功能放入第三个供双方调用的函数。







条款13:以对象管理资源

Use objects to manage resources

假设我们使用一个用来塑模投资行为(例如股票、债券等)的程序库,各种各样的投资类型继承自root class Investment。进一步假设这个库使用了通过一个factory 函数为我们提供特定Investment 对象的方法:

class Investment { ... }; // “投资类型”继承体系中的root class

Investment* createInvestment(); /*返回指向Investment继承体系内的动态分配对象的指针。调用者有责任删除它。这里为了简化,刻意不写参数*/

当createInvestment 函数返回的对象不再使用时,由调用者负责删除它。下面的函数f 来履行以下职责:

void f()
{
Investment *pInv = createInvestment(); // 调用factory对象
...
delete pInv; // 释放pInv所指对象
}

以下几种情形会造成f 可能无法删除它得自createInvestment 的投资对象:

1. "..." 部分的某处有一个提前出现的return 语句,控制流就无法到达delete 语句;

2. 对createInvestment 的使用和删除在一个循环里,而这个循环以一个continue 或goto 语句提前退出;

3. "..." 中的一些语句可能抛出一个异常,控制流不会再到达那个delete。

单纯依赖“f总是会执行其delete语句”是行不通的。

为了确保createInvestment 返回的资源总能被释放,我们需要将资源放入对象中,当控制流离开f,这个对象的析构函数会自动释放那些资源。将资源放到对象内部,我们可以依赖C++ 的“析构函数自动调用机制”确保资源被释放。

许多资源都是动态分配到堆上的,并在单一区块或函数内使用,且应该在控制流离开那个块或函数的时候释放。标准库的auto_ptr 正是为这种情形而设计的。auto_ptr 是一个类似指针的对象(智能指针),它的析构函数自动对其所指对象调用delete。下面就是如何使用auto_ptr 来预防f 的潜在的资源泄漏:

void f()
{
std::auto_ptr pInv(createInvestment()); // 调用工厂函数
... // 一如以往地使用pInv
} // 经由auto_ptr的析构函数自动删除pInv

这个简单的例子示范了“以对象管理资源”的两个关键想法:

· 获得资源后应该立即放进管理对象内。如上,createInvestment 返回的资源被用来初始化即将用来管理它的auto_ptr。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机” (Resource Acquisition Is Initialization ;RAII),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时被获