条款11:在operator=中处理“自我赋值”
如果一段代码操作pointers或者references而它们被用来指向多个相同类型的对象,就需要考虑这些对象是否为同一个。实际上,只要来自同一继承体系,它们甚至不需要声明为相同类型就可以造成别名(如C++的多态性,base class的指针或者引用可以指向derived class)。
class BitMap{};
class Widget{
privete:
BitMap *pb;
};
Widget& Widget::operator=(const Widget &rhs)
{
delete pb; //停止使用当前bitmap
pb=new BitMap(*rhs.pb); //使用rhs.bitmap的副本
return *this;
}
如果rhs与*this是同一个对象,那么delete销毁的就不只是当前对象的bitmap,它也销毁了rhs的bitmap。在函数尾,Widget发现自己持有一个指针指向已经被删除的对象。
解决这个问题,可以增加一个证同测试,增加一行语句:
if(rhs==*this)
return *this;
但operator=不仅应该仅具有自我赋值安全性还应该具有异常安全性。上版本还存在异常安全性,若new bitmap导致异常,widget同样会含有一个指向被删除的bitmap的指针。
通常,让operator=具有异常安全性往往会自动获得自我赋值安全性。因此,我们一般对自我赋值不去管理,把焦点放在异常安全性上:
Widget& Widget::operator=(const Widget &rhs)
{
BitMap *pOrig=pb; //记录原先的pb
pb=new BitMap(*rhs.pb); //令pb指向*pb的副本
delete pOrig; //删除原先的pb
return *this;
}
现在,如果new bitmap抛出异常,pb保持原状
(抛出异常后不再继续执行函数,而是寻找异常处理的catch,因此pb保持原状)
在operator=函数内手工排列语句以保证“两个安全”的替换方案是使用swap,将原先需要释放的原始对象和rhs的副本进行交换,交换后,需要删除的原始对象现在存放于之前rhs的副本这一个temp对象之中,在调用结束之后,自动释放。
请记住:
确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy_and_swap
确定任何函数如果操作一个以上的对象,而且其中多个对象是同一对象时,其行为任然正确
条款12:复制对象时勿忘每一个成分
当derived class的copying函数仅仅复制了derived class的专属成员,而没有显示的复制其通过继承得到的base class成员时,其base class部分会被其default构造函数初始化,当构造了多个derived class对象时,它们的base class部分完全相同。
因此,编写coping函数时确保:1.复制所有local成员变量 2.调用所有base class内合适的copying函数
一般而言,copy assignment操作符与copy构造函数往往有着相似的实现代码,这使我们会考虑令某个函数调用另一个,但这是错误的。对copy assignment来说,是将对两个已经存在的对象进行操作,而copy构造函数是在一个存在的对象的基础上构造出一个新的对象。若copy assignment操作符调用copy构造函数,那就意味着构造一个已经存在的对象,这是错误的。同样,若copy构造函数调用copy assignment所需要的已经存在的对象还未被构造。
解决办法是建立一个新成员给两者调用,这样的函数往往是声明为private。
请记住:
copying函数应该确保复制对象内的所有成员变量以及所有base class成分
不要尝试以某一个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用
条款13:以对象管理资源
class Ivestment{};
Investment* creatIvestment();
void f()
{
Ivestment *pInv=creatIvecstment();
... //可能提前return
delete pInv;
}
若函数体前return,就不会触及delete。因此我们应该讲资源放入对象,便可以依赖C++的析构函数自动调用机制确保资源被释放。auto_ptr正是针对这种情形所设计的:
void f()
{
std::auto_ptr<Investment> pInv(creatIvecstment());
...
}
获得资源后立即放入管理对象:
以上代码中creatInvestment返回的资源被当做其管理者auto_ptr的初值。“以对象管理资源”的观念常被称为“资源取得时机便是初化时机”(resource acquisition is initialization;RAII)。
管理对象运用析构函数确保资源被释放:
不管控制流如何离开区块,一旦对象被销毁其析构函数会被自动调用,于是资源被释放。
auto_ptr有一个不同寻常的性质:其对资源的拥有权是唯一的。从而我们引入了“引入计数型智慧指针”(reference-counting smart pointer;RCSP)。TR1的tr1::shared_ptr就是一个RCSP。
void f()
{
std::tr1::shared_ptr<Investment> pInv(creatIvecstment());
...
}
auto_ptr和tr1::shared_prt两者都在其析构函数内做delete而不是delete[ ]动作。那意味着动态分配而得的arry上使用auto_ptr和tr1::shared_prt不是个好主意,会造成内存泄漏。并且编译器内,没有针对C++动态分配的数组而设计的auto_ptr和tr1::shared_prt。
creatInvestment返回的“未加工指针”(raw pointer)会导致很多问题,详见条款14
请记住:
为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
两个常被使用的RAII分别是auto_ptr和tr1::shared_prt。后者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,赋值动作会使它指向NULL
条款14:在资源管理类中小心copying行为
我们有时候会需要建立自己的资源管理类:
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 m11(&m);
Lock m12(m11);
当运用复制构造函数时,两个类对象包含同一对象,调用析构函数的时候,会产生重复析构。解决办法有两种:
禁止复制:
许多时候对RAII对象被复制并不合理,我们可以参见条款06,将copying操作声明为private或者构造一个uncopyable
对底层资源祭出“引用计数法”(reference-count):
可以使用tr1::shared_ptr成员变量,将Mutex*这样的raw pointer改为tr1::shared_ptr<Mutex>,但是tr1::shared_ptr得缺省行为是“当引用次数为0时删除其所指物”,而我们想要的释放动作是解除锁定,而不是删除。tr1::shared_ptr允许指定所谓的“删除器”(deleter)(类似于使用谓词函数),那是一个函数或者函数对象:
class Lock{
public:
explicit Lock(Mutex *pm):mutexPtr(pm,unlock) //以unlock为删除器
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
请记住:
复制RAII对象必须复制它所管理的资源,所以资源的copying行为决定了RAII对象的行为
普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)
条款15:在资源管理类中提供对原始资源的访问
在条款13中使用智能指针保存资源:
std::tr1::shared_ptr<Investment> pInv(creatInvestment());
int daysHeld(const Investment *pi);
int days=daysHeld(pInv); //错误!
这是需要一个函数可以将RAII class对象转换为原始资源,可以选择显示或者隐式转换:
auto_ptr和tr1::shared_prt都提供一个get成员函数,用来执行显示转换:
int days=daysHeld(pInv.get());
auto_ptr和tr1::shared_prt也都重载了->和*操作符,它们允许隐式转换至底部原始指针:
class Investment{
public:
bool isTaxFree()const;
};
Investment* creatInvestment();
std::tr1::shared_ptr<Investment> pi1(creatInvestment());
bool taxable1=!(pi1->isTaxFree());
bool taxable2=!((*pi1).isTaxFree());
当存在大量需要转换时,显示转换的代码显得太过于繁琐,此时可以提供隐式转换函数
class Font{
public:
operator FontHandle()const
{ return f; }
private:
FontHandle f;
};
此时在需要FontHandle对象的地方使用Font对象,其会自动转换称FontHandle对象
请记住:
API往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个取得其管理资源的办法
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
条款16:成对使用new和delete时要采取相同的形式
new对应delete,new[ ]对应delete[ ]
对于typedef要格外小心:
typedef std::string AddressLines[4]; //每个人的地址有4行
//每行都是一个string
std::string *pal=new AddressLines; //new AddressLines返回一个string*,就像new string[4]
delete pal; //行为未定义
delete [] pal; //ok
为避免这类错误,尽量不要对数组形式做typedef
请记住:
如果在new表达式中使用[ ],必须在相应的delete表达式中也是用[ ]。反之亦然
条款17:以独立的语句来讲newed对象置入智能指针
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);
//调用
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
现在编译器必须创建代码,做一下三件事:1.调用priority 2.执行new Widget 3.调用tr1::shared_ptr构造函数
我们可以确定2一定在3之前执行,而对于1的执行我们并不能确定,如果执行顺序如果是2->1->3。如果在执行1的过程中发生异常,那么new中所新获得的资源就没能按照条款13中那样进入资源管理类,从而像以往一样造成了资源泄露。原因在于资源被创建和资源被转换为资源管理对象两个时间点之间有可能发生异常干扰。
避免这个问题的办法很简单:使用分离语句,分别写出创建Widget,将它放入一个智能指针内
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
请记住:
以独立语句将newed对象存储于智能指针内。如果不这样做,一旦抛出异常,就有可能导致难以察觉的资源泄露