首页 > 代码库 > 《Effective C++》学习笔记(五)
《Effective C++》学习笔记(五)
原创文章,转载请注明出处: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指向同一个对象,引用计数加1 pInv1 = 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); // 调用daysHeld daysHeld(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); // 调用processWidget processWidget(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对象存储于(置于)智能指针内。如果不这样做,一旦异常被跑出,有可能导致难以察觉的资源泄漏。
《Effective C++》学习笔记(五)