《Effective C++》学习笔记(五)

来源:互联网 发布:淘宝网儿童玩具迫击炮 编辑:程序博客网 时间:2024/04/25 22:28

原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38865869


前言

昨天已作出预告,今天学习的是整个第三章,资源管理,通读了一遍之后,感觉似懂非懂,于是又再读了一遍。

所谓资源,一旦用了它,将来必须要还给系统。C++中最常用得动态分配内存既是如此,但内存只是你管理的众多资源之一,还有数据库连接、网络socket、图形界面中的字体和笔刷等。

尝试在任何情况下确保都还给系统是很难的,但如果考虑上异常、函数内多重回转路径、其他程序员维护等就更难了。


条款13:以对象管理资源

前言里说到,我们很难确保每次都资源的调用后都还给系统。当然,这并不一定是你没有写释放,也有可能是发生了意外。为了确保资源最后总是会被释放,我们需要将资源放进对象内,当控制流离开范围,该对象的析构函数会自动释放那些资源。

investment* createInvestment(); // 返回指针,是一个工厂函数。void f(){std::auto_ptr<investment> pInv(createinvestment());...}
当离开pInv使用范围后,经由auto_ptr的析构函数自动删除pInv。

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

1)获得资源后立刻放进管理对象内。

2)管理对象运用析构函数确保资源被释放
由于auto_ptr呗销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。为了预防这个问题,auto_ptr有个不寻常的特质,若通过copy构造函数和copy assignment操作符赋值它们,它们会变为null,复制所得的指针将取得资源的唯一使用权。

std::auto_ptr<Investment> pInv1(createInvestment());std::auto_ptr<Investment> pInv2(pInv1); // pInv1变为null,pInv2获取资源管理权限pInv1 = pInv2; // pInv2变为null,pInv1获取资源管理权限
STL容器要求起元素发挥“正常的复制行为”,因此这些容器容不得auto_ptr。

auto_ptr的替代方案十“引用计数型智能指针”("reference-counting smart pointer", RCSP),它会持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。但是RCSP无法打破环状引用,例如两个其实已经没有被使用的对象彼此互指,因此好像还处于被使用状态。

shared_ptr就是个RCSP。

void f(){..std::shared_ptr<Investment> pInv1(createinvestment());std::shared_ptr<Investment> pInv2(pInv1); // pInv1与pInv2指向同一个对象,引用计数加1pInv1 = pInv2; // 依旧指向同一个对象,引用计数不变...}
因为shared_ptr的复制行为“一如预期”,它可被用在STL容器以及其他“auto_ptr因复制行为而导致的不可使用”的情况下。
因为auto_ptr和shared_ptr两者在析构函数中做的是delete而非delete[]。那意味着在动态分配的数组上使用auto_ptr和shared_ptr十个馊主意。但这样做仍能通过编译,所以一定要注意这个情况。

可以使用boost::shared_array或者以vector代替数组来实现目的。


总结

1)为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

2)两个常被使用的RAII class分别是shared_ptr和auto_ptr。前者通常是最佳选择,因为copy行为更直观。如果选择auto_ptr,复制动作会使它(被复制物)指向null。


条款14:在资源管理类中小心copying行为

shared_ptr和auto_ptr很好的将RAII的概念表现在了heap-based资源上,但是并不是所有的资源都是heap-based,对于那种资源,我们需要建立自己的资源管理类。

书上提供了一个互斥器对象的例子:

class Lock{public:explicit Lock(Mutex *pm):mutexPtr(pm){lock(mutexPtr);}~Lock(){unlock(mutexPtr);}private:Mutex *mutexPtr;}Mutex m; // 建立互斥器...{Lock ml(&m); // 锁定互斥器...} // 区块末尾自动解除互斥器锁定// 用户对Lock的用法符合RAII方式// 但是如果Lock对象被复制会发生什么事?Lock ml1(&m);Lock ml2(ml1);

这是一个一般化问题的特定例子,为了避免这个问题,我们可能会采用下面两种可能:

1)禁止复制:很多时候RAII对象被复制并不合理,对于一个像Lock这样的class这是有可能的,因为很少能够合理拥有”同步化基础器物“的附件。

如果复制动作对RAII class不合理,那么应该明确禁止复制。

2)对底层资源使用”引用计数法“,有时候我们希望保有资源,直到最后一个使用者使用完再被销毁。这种情况下复制RAII对象时,应该将引用计数递增。

通常利用一个shared_ptr即可实现,不幸的是shared_ptr的默认行为是当引用计数为0调用delete,所以我们需要自己指定一个动作(删除器):

class Lock{public:explicit Lock(Mutext *pm):mutexPtr(pm, unlock){lock(mutexPtr.get());}private:std::shared_ptr<mutex> mutexPtr;}</mutex>
需要注意的十,这次的Class中不再声明析构函数,因为没有必要,直接调用编译器默认生成的即可。

当然,除了上面两种方法还有其他方法:

3)复制底部资源,有时候只要你喜欢,可以针对一份资源拥有其任意数量的副本。而你使用资源管理类的理由是,当你不再需要某个附件时确保它被释放,这种情况下复制资源管理对象,应该同时也复制其所包括的资源,也就是说进行深度拷贝。

4)转移底部资源的拥有权,如:像auto_ptr的复制一样。


总结:

1)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
2)普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其它行为也都可能被实现。


条款15:在资源管理类中提供对原始资源的访问

资源管理类是对抗资源泄漏的堡垒,在一个完美的世界里你将依靠这些类来处理和资源之间所有的互动,而不是直接处理原始资源。

但是这个世界并不完美,许多API直指资源,这可怎么办呢?举个例子:

std::shared_ptr<Investment> pInv(createInvestment());int daysHeld(const Investment *pi);// 调用daysHelddaysHeld(pInv);
这样调用是错的,因为daysHeld要的是Investment指针,但是传的却是std::shared_ptr<Investment>对象。

解决方案:

1)显示转换,在auto_ptr和shared_ptr中都提供了一个get成员函数,用来执行显示转换,他们会返回智能指针内部的原始指针。

2)隐式转换:

class Font{public:...operator FontHandle() const{return f;}...}void changeFontSize(FontHandle f, int newSize);Font f(getFont());int newFontSize;...changeFontSize(f, newFontSize);
但是隐式转换会增加错误发生机会。例如客户可能需要Font时意外创建了一个FontHand
Font f1(getFont());...FontHandle f2 = f1; // 原意是要拷贝一个font,但是实际上是先将f1隐式转换为FontHandle,然后执行拷贝
是否提供一个显示转换函数,或者提供隐式转换,答案主要取决于RAII class被设计执行的特定工作,以及它被使用的情况。


总结

1)API往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的方法。

2)对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。


条款16:成对使用new和delete时要采用相同形式

其实这一点在之前看《C++ Primer》的时候便已经有所了解,当你使用new创建了一个数组,那么在删除的时候也要以删除数组的形式进行删除。

为什么呢?因为数组所用得内存布局还包括数组大小,唯一能够让delete知道内存中有“数组大小记录”的方法就是你告诉它。

std:string *str1 = new std::string;std:string *str2 = new std::string[100];delete str1; // 删除一个对象delete [] str2; // 删除一个由对象组成的数组

所以这条规则简单来说就是,如果你调用new的时候使用了[],你必须在对应调用delete时也使用[]。如果你调用new的时候没有使用[],那么也不应该在delete的时候用。

在此,特别要注意typedef了的数组。

typedef std::string AddressLines[4];std::string *pal = new AddressLines;delete pal; // 行为未定义delete [] pal; // 正确删除
为了避免此类错误,应当尽量不要对数组形式作typedef操作。


总结:
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中没有使用[],一定不要在相应的delete表达式中使用[]。


条款17:以独立语句将newed对象置于智能指针

看到这个条款会让人有些不解,不过我们通过实例来看就好了。假设有这么一个函数:

int priority();void processWidget(std::shared_ptr<Widget>, int priority);// 调用processWidgetprocessWidget(new Widget, priority);
这样调用是错的,它不能通过编译,因为shared_ptr的构造函数需要一个原始指针,但该构造函数是个explicit构造函数,所以无法进行隐式转换。所以我们自然而然想到了这种形式:
processWidget(std::shared_ptr<new Widget>, priority);
虽然说出来你可能不信,但是上门这种用法可能会造成资源泄漏,即使你使用了shared_ptr来管理资源,在调用processWidget之前,编译器要做三件事

1)调用priority

2)执行“new Widget”

3)调用shared_ptr构造函数

我们并不知道C++会以怎样的顺序去完成这些事情,如果调用的顺序是2、1、3,且对priority的调用出了问题,new widget创建的指针将会遗失。

避免造成这类问题的方法很简单,使语句分离:

std::shared_ptr<Widget> pw(new Widget);processWidget(pw, priority);


总结:

以独立语句将newed对象存储于(置于)智能指针内。如果不这样做,一旦异常被跑出,有可能导致难以察觉的资源泄漏。

0 0