条款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("Customrer copy constructor");
}
Customrer::Customrer::operator=(const Customrer& rhs){
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
当另一个新成员变量加入后,如果程序员没有把该新成员变量在拷贝赋值和拷贝构造中处理,大多数编译器不会警告。发生继承时,可能会造成此一主题最暗中肆虐的一个潜在危机:
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& operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
以上两个实现复制了PriorityCustomer声明的成员变量,但每个PriorityCustomer还内含它所继承的Customer成员变量复件,而那些成员变量却未被复制。所以任何时候只要你承担起为派生类撰写拷贝函数的重大责任,必须很小心的也复制其基类成分。那些基类成分往往是private,所以无法直接访问,应该让派生类的拷贝函数调用积累的拷贝函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority){
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
请记住:
1.拷贝函数应该确保复制对象内的所有成员变量及所有基类成分。
2.不要尝试某个拷贝函数实现另一个拷贝函数,应该将共同技能放进第三个函数中,并油两个拷贝函数共同调用。
资源管理
所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,槽糕的事情就会发生。C++程序中最常用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述符、互斥锁、图形界面中的字形和笔刷、数据库连接、网络socket。
条款13: 以对象管理资源
class Investment{
};
Investment* CreateInvestment();
void f(){
Investment* pInv = CreateInvestment(); //factory function
//...
delete pInv;
}
某些情况下f可能无法删除它得自CreateInvestment的投资对象,因为可能有return语句,可能在delete之前有异常。所以为了确保CreateInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构寒暑会自动释放那些资源:把资源放进对象内,我们便可依赖C++的析构函数自动调用机制确保资源被释放。
标准库的auto_ptr是个类指针对象,即只能指针,其析构函数自动对其所指对象调用delete:
void f(){
std::auto_ptr<Investment> pInv(CreateInvestment());
}
这个例子示范“以对象管理资源”的两个关键想法:
1.获得资源后立刻放进管理对象内。
2.管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象,这样对象会被销毁多次。为了预防这个问题,auto_ptr规定:若通过拷贝构造函数或者拷贝赋值操作符复制它们,它们会变成null。但是STL容器要求其元素能够复制,因此这些容器容不得auto_ptr。
auto_ptr的替代方案是引用计数型智慧指针,可以持续追踪共有多少个对象指向某笔资源,当无人指向它们时自动删除该资源:
void f(){
std::tr1::shared_ptr<Investment> pInv(CreateInvestment());
}
由于shared_ptr的复制行为一如预期,它们可以被用于STL容器。
auto_ptr和tr1::shared_ptr在其析构函数内做的是delete而不是delete[],意味着在动态分配而得的array身上使用auto_ptr和shared_ptr是个馊主意,但是编译不会有问题:
std::auto_ptr<std::string> aps(new std::string[10]);//错误,会用上错误的delete
std::tr1::shared_ptr<int> spi(new int[1024]);
并没有特别针对C++ 动态分配数组而设计的类似auto_ptr或者shared_ptr那样的东西。那是因为vector和string几乎总是可以取代动态分配而得的数组。但是boost库中的boost::scoped_array和boost::shared_array class可以提供这些行为。
请记住:
1.为防止资源泄漏,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源。
2.两个常被使用的RAII分别是tr1::shared_ptr和auto_ptr,前者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使它指向null
条款14:在资源管理类中小心copying行为
RAII(Resource Acquisition Is Initialization)是资源管理类的脊柱。但是并非所有的资源都是heapbased,对于这种资源,auto_ptr和shared_ptr这样的智能指针往往不适合作为资源掌管者,此时需要建立自己的资源管理类。例如,假设我们使用Mutex的互斥器,共有lock和unlock两个函数可用:
void lock(Mutex* pm);
void unlock(Mutex* pm);
建立一个类来管理锁:
class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm){
lock(mutexPtr);
}
~Lock(){
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
如果Lock对象被复制,会发生什么事???????(作者没解释)
可以有以下两种选择:
1.禁止复制:许多时候允许RAII对象被复制不合理。应该禁止复制,Lock类就是如此。
2.对底层资源祭出“引用计数法”。有时候希望保有原始资源,直到它的最后一个使用者被销毁。可以使用shared_ptr,所以mutexPtr需要从Mutex* 改为shared_ptr<Mutex>。然而很不幸,shared_ptr的缺省行为是当引用次数为0时删除其所指物,不是我们要的行为,我们只是想解除锁定。幸运的是shared_ptr允许指定所谓的deleter,那是一个函数或者函数对象,当引用次数为0时被调用:
class Lock{
public:
explicit Lock(Mutex* pm):
mutexPtr(pm, unlock){ //unlock函数作为删除器
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
3.复制底部资源:需要资源管理类的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包含的资源,即要进行深度拷贝。
4.转移底部资源的拥有权:例如auto_ptr的复制,会把原始对象中的资源置为null。
请记住:
1.复制RAII对象必须一并复制它所管理的资源,所以资源的拷贝行为决定RAII对象的拷贝行为。
2.普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用计数法。