Effective C++读书笔记三(资源管理) £神魔★判官ぃ 2022-05-17 12:07 173阅读 0赞 所谓资源就是,一旦你用了它将来必须还给系统。 C++程序中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。例如:文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接及网络socket。**不论是哪一种资源,当你不再使用它的时候,必须将它还给系统。** ### 条款13:以对象管理资源 ### 先来看个例子: class Investment { ...}; //继承体系中的root class Investment* createInvestment(); //返回指针,指向Investment继承体系内动态分配的的对象,调用者有责任删除它。 //函数fun履行了这个责任 void fun() { Investment* pInv=createInverstment(); ... delete pInv; } 这个例子乍一看没有什么问题,动态申请的对象在函数结尾也delete了,但有些情况fun可能无法删除pInv * 假设在“…”区域内有一个过早的return语句,如果这个return语句被执行了,那么程序就不可能会触及delete语句。 * 还可能是在pInv的使用和delete动作在某个循环内,而该某个循环由于某个continue或者goto语句过早退出,也可能不会delete * 最后一种可能是“…”区域内的语句抛出异常,那么同样不会执行到delete 为了确保createInvestment返回的资源总是被释放,**我们需要将资源放进对象内,当控制流立刻fun,该对象的析构函数会自动释放那些资源。** 标准程序库提供的auto\_ptr正是针对这种形式设计的特质产品,也就是所谓的“智能指针”。来看看这个对象的使用: void fun() { std::auto_ptr<Investment> pInve(createInverstment()); ... } 调用createInvestment函数,一如既往的使用pInv,经由auto\_ptr的析构函数自动删除pInv。 以对象管理资源主要有两个关键想法: * 获得资源后立刻放进管理对象内 * 管理对象运用析构函数确保资源被释放 由于auto\_ptr被销毁的时会自动删除它所指之物,所以不要让多个auto\_ptr指向同一个对象,如果这样做了,那么就会出现同样的对象被删除多次的情况。为了防止这个问题,auto\_ptr有一个性质:若通过拷贝构造函数或赋值运算符重载复制它们,它们就会变成NULL,而复制所得的指针将取得资源唯一的拥有权。但这意味着auto\_ptr并非管理动态内存的神兵利器,因为它无法有正常的复制行为,而我们有的时候需要这种行为,例如STL。 取代auto\_ptr就是shared\_ptr——–引用计数型智能指针,在下一个条款会详细介绍。 请记住: * 为了防止内存泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源 ### 条款14:在资源管理类中小心copying行为 ### 刚刚的条款13引入的RAII机制和智能指针,并非所有的资源都适用,对于一些不是heap\_based的资源,智能指针往往不适合作为资源掌管者。那么这个时候,你就需要建立自己的资源管理类 例如下面这个类: class Lock { public: explict Lock(Mutex* pm) :mutexPty(pm) { lock(mutexPtr); } ~Lock() { unlock(mutexPtr); } private: Mutex* mutexPtr; }; 客户对Lock的用法符合RAII方式,但是如果Lock对象被复制会发生什么现象? Mutex m; Lock mu1(&m); //锁定m Lock mu2(mu1); //将mu1复制到mu2身上??? 当一个RAII对象被复制,会发生什么事?大多数时候你会选择以下两种情况: * 禁止复制—-许多时候允许RAII对象被复制并不合理。那就可以将拷贝构造函数和赋值运算符重载声明为私有,而且不实现。—-unique\_ptr * 底层资源用“引用计数法”—-也就是shared\_ptr的实现方法。但是,有的时候我们并不希望析构的时候删除所指物,例如刚刚的mutex,我们想要做的释放动作是解除锁定。这就要用到“定制删除器”了。所谓的删除器也就是一个函数或函数对象。当引用次数为0时便被调用。 复制RAII对象必须一起复制它所管理的资源,所以资源的拷贝行为决定RAII的拷贝行为。 ### 条款15:在资源管理类中提供对原始资源的访问 ### 条款13说过,使用智能指针保存createInvestment函数的调用结果,假设你希望以某个函数处理Investment对象,像这样 int daysHeld(const Investment* pi); //返回投资天数 你想这样调用它: int days = daysHeld(pInv); //错误 这样是不能通过编译的,因为daysHeld需要的是Investment\*类型的指针,而pInv却是一个shared\_ptr的对象,这时候你需要一个函数可以将RAII类对象转换为其内部的原始资源。有两个做法可以达成目标:隐式转换、显式转换 shared\_ptr和auto\_ptr内部都提供一个get成员函数,用来执行显式转换,也就是get函数会返回智能指针内部的原始指针。 int days = daysHeld(pInv.get()); 还有一种方式是提供一个隐式转换函数,看下面的例子: FontHandle getFont(); //这是一个C API,省略参数 void releaseFont(FontHandle fh); //来自同一组C API class Font { public: explicit Font(FontHandle fh) :f(fh) {} ~Font() { releaseFont(f); } private: FontHandle f; }; 假设现在有大量C API都要处理FontHandle,那么将Font对象转换为Fonthandle对象会是一种和频繁的操作,那么Font类就可以提供一个显示转换函数,像get那样: class Font { public: FontHandle get()const { return f; } ... }; 但是可以想到,只有我们想使用API的时候就需要调用get函数,那么就可以令Font类提供隐式转换函数,将类型转为FontHandle: class Font { public: operator FontHandle()const { return f; } ... }; 但是这种隐式类型转换有时候会出现错误,假设我们可能会在需要Font类型时,却意外创建了一个FontHandle类型。 是否应该提供一个显示转换函数还是隐式转换函数,取决于RAII类被设计执行的特定工作,以及它被使用的情况。一般而言显式转换比较安全,但隐式转换对客户比较方便。 ### 条款16:成对使用new和delete时要采取形同形式 ### 当你使用new的时候有两件事发生: * 内存被分配出来 * 针对此内存会有一个或多个构造函数被调用 当你使用delete的时候也有两件事发生: * 针对此内存有一个或多个析构函数被调用 * 内存被释放 delete的最大问题在于:即将被删除的内存里究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须被调用。实际上这个问题可以更简单些:即被删除的那个指针,所指的是单一对象或对象数组? 当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组的方法就是由你来告诉它。如果你使用delete时加上方括号\[ \],delete便认定指针指向一个数组,否则它便认为指针指向单一对象。 也就是说,如果你调用new时使用\[ \],那么对应的在调用delete时也使用\[ \]。如果你调用new时没有使用\[ \],那么也不应该在对应调用delete时使用\[ \]。 ### 条款17:以独立语句将new的对象置入智能指针 ### 先来看个例子: int priority(); void processWidget(shared_ptr<Widget> pw, int priority); 现在调用processWidget: processWidget(new Widget, priority()); 这个调用形式是不能通过编译的,因为shared\_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得到“newWidget”的原始指针转换为processWidget所要求的shared\_ptr。下面就可以通过编译: processWidget(shared_ptr<Widget>(new Widget), priority()); 来剖析一个这个式子: 编译器产生出一个processWidget调用码之前,必须先核对参数。上述第二个实参只是单纯对priority函数的调用,但第一个实参shared\_ptr(new Widget)由两部分组成 * 执行“new Widget”表达式 * 调用shared\_ptr的构造函数 编译器完成的这些事情是以什么顺序呢?不是很确定,但是可以肯定的是“new Widget”一定执行在shared\_ptr构造函数之前,如果是下面的顺序: * 执行“new Widget”表达式 * 调用priority * 调用shared\_ptr的构造函数 如果在调用priority函数期间导致异常,那么“new Widget”指针将会遗失,因为它尚未置入shared\_ptr内,所以就可能引发内存泄漏。来看看避免的方法: shared_ptr<Widget> pw(new Widget); processWidget(pw, priority()); 只需要使用分离语句,分别写出创建Widget,将它置入智能指针内,然后再把智能指针传给processWidget。 所以:**以独立语句将new的对象存储于只能指针内,如果不这样做,一旦异常被抛出就会产生难以察觉的内存泄漏。**
还没有评论,来说两句吧...