标签:
operator=
中处理“自我赋值”
自我赋值有时候不是那么明显,在处理循环(a[i] = a[j]
)、指针(*px
= *py
)和参数传递(func(const Base &rb, const Derived *pd)
)的时候可能会发生。
Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这是一份不安全的实现版本,在自赋值的情况下会形成野指针。
第一种方式:
Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这种方式具备“自我赋值安全”,但不具备异常安全:当new Bitmap
抛出异常("不管是内存不足还是Bitmap
的拷贝构造函数异常"),Widget
最终会有一个指针指向被删除的Bitmap
。
第二种方式:
Widget::operator=(const Widget& rhs)
{
Bitmap * pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
第三种方式:
void swap(Widget& rhs); //详见条款29
Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
确保当对象自我赋值时
operator=
有良好的行为,其中技术包括:比较来源对象和目标对象的地址、精心周到的语句顺序和copy-and-swap
。
在处理派生类的构造函数和拷贝构造函数时除了要考虑派生类自身的局部变量以外,还要保证基类的成员变量(通过构造函数或者拷贝构造函数)能够正确拷贝。
如果只是考虑了局部变量,派生类的构造函数或者拷贝构造函数没有传递参数给基类,因此基类部分会被基类默认构造函数缺省默认初始化(一定存在默认构造函数,否则不能通过编译)。通常这不是我们期望发生的,也就是说,错误发生了。
解决方案也很直接:让派生类的构造函数、拷贝构造函数或者赋值运算符除完成局部变量复制以外一定要调用基类对应的函数完成基类部分的赋值。传递给基类对应函数的参数会发生切割。
如果这些函数中间有相同的部分,不要尝试互相调用,而是添加一个私有的
init
工具函数。
假设我们获取了一种资源,使用指针指向了它,那么最后释放资源的时候会使用delete
释放掉这个资源。问题是,我们的函数不一定执行到释放语句,无论delete
如何被略过去,我们泄露的不只是内含投资对象的那块内存,还包括对象所包含的资源。这也就发生了资源泄露,申请资源之后没有将资源归还回系统。正因为如此,单纯的依靠函数f在最后总是会执行其delete语句
是不可行的。
为了确保指针指向对象的资源总是能被释放,我们需要将资源放进对象内,当控制流离开函数的时候,该对象的析构函数会自动释放这些资源。
因此在函数内,使用智能指针(是个类指针对象)指向这个对象,当控制流离开时,调用智能指针的析构函数,析构函数会自动对其所指对象调用delete
。这里有两个关键想法:
RAII
(Resource
Acquisition Is Initialization
) - 资源取得的时机便是初始化时机。毙掉你那想自己手动管理资源(
delete ptr
)的想法吧,借助对象,借助析构函数的隐式调用delete
。因为这比你愚蠢的犯错误要明智的多。
除了上述条款涉及到的智能指针类,我们有时候也需要建立自己的资源管理类。
比如,建立自己的互斥器管理类,有两个函数供使用:
void lock(Mutex* pm); //锁定pm所指的互斥器
void unlock(Mutex* pm); //解除锁定
我们构建下面的类来管理资源:
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm)
{ lock(mutexPtr); }
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
Mutex m;
Lock m1(&m);
这样使用起来是没有问题的,但是如果Lock
对象被复制,那会是怎样呢?
Lock m2(m1);
"当一个RAII
对象被复制,会发生什么?", 这是一个不得不回答的问题,通常你会根据需要选择下面的一种:
copying
(包含拷贝构造和赋值构造)声明为私有的。还有要使用私有基类,记得吗?shared_ptr
就是这样。通常只要内含一个shared_ptr
成员变量便可以实现这种行为。当然,针对这个智能指针,我们不应该使用它默认的释放动作(delete
它所指对象),而应该定制释放动作。
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm, unlock)
{ lock(mutexPtr.get()); //这里需要原始资源的访问}
private:
shared_ptr<Mutex> mutexPtr;
};
上面不再需要析构函数,编译器会生成。析构函数会调用non-static
成员变量的析构函数--也就是智能指针的释放动作(也是在智能指针类的析构函数中调用,本来应该调用delete
,但是我们重新定制了)。
资源管理类真的很棒。但是很多的API
接口直接指涉资源,所以你在资源管理类中也要能处理这种情况。
这个时候你需要一个函数可将RAII class
对象(比如智能指针)转换为其所内含之原始指针。有两个方法可以实现:显示转换和隐式转换。
直白点说,显示转换就是提供一个get()
函数,让用户显示的调用,就行shared_ptr
那样。隐式转换就是operator
FontHandle() const { return handle; }
,这样就能这样调用func(f)
就可以。其中f
是对象。
delete
的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定有多少个析构函数必须被调用起来。也就是说,我们要删除的指针指向的是单一对象还是对象数组。数组所用的内存通常包括“数组大小”的记录,以便delete
时知道要调用多少次析构函数。然而必须由你显示来告诉delete
是否存在这条记录。
声明的是数组,释放的时候就是用delete []
,否则就是delete
。不能乱掉。
条款17:以独立语句将 newed
对象置于智能指针
有两个函数:
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);
这样调用:
processWidget(new Widget, priority());
不能通过编译,智能指针构造函数需要一个原始指针,但它是个explicit
,不能隐式调用。这样就可以:
processWidget(shared_ptr<Widget>(new Widget), priority());
但是这样子还是会有问题,造成资源泄露,因为这两个参数需要3个过程才能生成:
priority()
new Widget
shared_ptr
构造函数
这三个步骤的顺序是不确定的(2会早于3,因为要使用生成的对象作为构造函数的参数),当prioruty()
执行在另外两者中间并且导致异常,那么new
Widget
返回的指针将会遗失。造成资源泄露。
解决的方式也很简单:使用分离语句,分别写出(1)创建Widget
(2)将它置于智能指针内,然后才传递给函数。
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
Effective C++ (笔记) : 条款11 -- 条款17
标签:
原文地址:http://blog.csdn.net/yapian8/article/details/42804059