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

3 资源管理

所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就是动态分配内存(如果分配内存却从不归还它, 就会导致内存泄漏),但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接以及网络sockets。无论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。

当你考虑到异常、函数内多重回传路径、程序维护员改动软件却没能充分理解随之而来的冲击,显然,资源管理的特殊手段还不很充分利用。

条款13:以对象管理资源

void f()     
{  
    Investment *pInv = createInvestment();
    ...                   //这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。   
    delete pInv;     
} 

如上代码,createInvestment的调用端使用了函数返回的对象后,有责任删除之。但一些情况下delete可能无法执行:

  • “...”区域内有一个过早的return语句;
  • createInvestment和delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出;
  • “...”区域内的语句抛出异常,控制流不会临幸delete。

把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。

auto_ptr

许多资源被动态分配于heap(堆)内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。

void f()     
{      
    std::auto_ptr<Investment> pInv(createInvestment());  
    ...      
}          //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。

上面的例子师范了“以对象管理资源”的两个关键想法:

  • 获得资源后立刻放进管理对象(managing object)内。
    “资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象运用析构函数确保资源被释放。
    一旦对象被销毁,其析构函数被自动调用来释放资源。如果资源释放动作可能导致抛出异常,参见条款8的解决方法。

由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。为了预防这个问题,auto_ptr有一个性质:auto_ptr若通过copy构造函数或copy assignment操作符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!但这一性质限制了元素不能发挥“正常的”复制行为,如在STL容器上就不适合,因此并非管理动态分配资源的神兵利器。

RCSP

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

TR1的tr1::shared_ptr就是一个"引用计数型智能指针",且可执行正常的复制行为,因此可以被用于STL容器以及其他“auto_ptr之非正统复制行为并不适用”的语境上。

void f()     
{
    ...
    std::tr1::shared_ptr<Investment>  pInv1(createInvestment()); // pInv1指向createInvestment()返回物;
    std::tr1::shared_ptr<Investment>  pInv2(pInv1);              //pInv1,pInv2指向同一个对象; 
    pInv1 = pInv2;                                               //同上,无变化      
    ...      }         // pInv1,pInv2被销毁,它们所指的对象也被自动销毁

auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr是个潜在危险,资源得不到释放。还有,vector和string几乎总是可以取代动态分配而得的数组。

note:

  1. 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  2. 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。

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

我们在条款13中讨论的资源表现在heap堆上申请的资源,而有些资源并不是heap-based,因此不适合被auto_ptr和tr1::shared_ptr所管理。此时,我们需要建立自己的资源管理类。

假设我们处理Mutex类型的互斥器对象,有两种操作:

void lock(Mutex *pm);     //锁定pm所指的互斥量
void unlock(Mutex *pm);        //将互斥器解除锁定

建立遵循RAII守则(资源在构造期间获得,在析构期间释放)的资源管理类:

class Lock { 
public: 
    explicit Lock(Mutex *pm) 
       : mutexPtr(pm)  
       { lock(mutexPtr); }     // 获得资源
    ~Lock() { unlock(mutexPtr); }    // 释放资源         
private: 
    Mutex *mutexPtr;     
}; 
// 客户对Lock的正确用法符合RAII方式
Mutex m;         // 定义你需要的互斥器
....
{                         // 建立一个区块用来定义critical section
    Lock ml(&m);      // 锁定互斥器
    ......
}                        // 在区块最末尾,自动接触互斥器锁定

上面的使用没有问题,但问题是,如果Lock对象被复制,会发生什么事呢?就像这样:

Lock ml1(&m);
Lock ml2(ml1);

当一个RAII对象被复制,会发生什么事?

  1. 禁止复制。
    如果复制动作对RAII class并不合理,就应该禁止它。详见条款06:将copying操作声明为private。
  2. 对底层资源祭出“引用计数法”。
    有时候希望保有资源,直到它的最后一个使用者被销毁,这种情况下复制RAII对象时,应该讲资源的“被引用数”递增。例如trl::shared_ptr。

通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现”引用计数“行为。

但是,tr1::shared_ptr缺省行为是”当引用计数为0时删除其所指物“,因此,我们还需指定所谓“删除器”(一个函数或对象),当引用计数为0时便被调用(此机能并不存在于auto_ptr,它总是将指针删除)。

class Lock     {        
public:  
        explicit Lock(Mutex *pm)        // 以某个Mutex初始化shared_ptr
          : mutexPtr(pm, unlock)        // 并以unlock函数为删除器
        {
            lock(mutexPtr.get());       // get见条款15
         }         
private: 
        std::tr1::shared_ptr<Mutex> mutexPtr;      使用shared_ptr
}; 

本例中,并没说明Lock class的析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。

note:

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

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

前几个条款提到的资源管理类是你对抗资源泄漏的堡垒。但许多APIs直接指涉资源,这时候我们需要直接访问原始资源。

std::tr1::shared_ptr<Investment> pInv(createInvestment());    // 见条款13

int dayHeld(const Investment* pi);    // 返回投资天数

int days = dayHeld(pInv);    // 错误,dayHeld需要的是Investment*指针(原始指针,raw pointer),
                             // 传给的却是tr1::shared_ptr<Investment>对象

这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源(Investment*)。有两种做法可以达成目标:显式转换和隐式转换。

显式转换

tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。

int days = dayHeld(pInv.get()); 
隐式转换

像所有智能指针一样, tr1::shared_ptr和auto_ptr重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。(即在对智能指针对象实施->和*操作时,实际被转换为被封装的资源的指针。)

class Investment {
public:
        bool isTaxFree() const;
        ...
}
Investment * createInvestment();

std::tr1::shared_ptr<Investment> pi1(createInvestment());    // shared_ptr管理资源
bool taxable1 = !(pi1->isTaxFree());    // operator ->访问资源
...

std::auto_ptr<Investment> pi2(createInvestment());    // auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree());    //operator *访问资源
显式转换函数和隐式转换函数
FontHandle getFont();                             // 这是个C API,参数已经简化
void releaseFont(FontHandle fh);                  // 来自同一组C API
void changeFontSize(FontHandle f, int newSize);   // 来自同一组C API

class Font {
public:
  explicit Font(FontHandle fh)     // 获得资源
    :f(fh)
  {}
  ~Font() {releaseFont(f);}        // 释放资源

  FontHandle get() const {return f;}         // 显式转换函数
  operator FontHandle() const{return f;}     // 隐式转换函数

private:
  FontHandle f;    // 原始(raw)字体资源
}

使用:

int newFontSize;
Font f1(getFont());
...
changeFontSize(f1.get(),newFontSize);   // 显式转换

Font f2(getFont());
changeFontSize(f2,newFontSize);         // 隐式转换

这个隐式转换会增加错误发生机会。例如客户可能会在需要Font时意外得到一个FontHandle:

Font f1(getFont());
...
FontHandle f2 = f1;        // 原意是要拷贝一个Font对象
                           // 却反而将f1隐式转换为其底部的FontHandle,
                           // 然后才复制它。

以上程序有个FontHandle由Font对象f1管理,但那个FontHandle也可以通过直接使用f2取得。如果f1被销毁,字体被释放,f2因此成为"虚吊的"(dangle)。

note:

  1. APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的方法。
  2. 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

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

当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。

如果new数组时使用[],那么释放资源时就要用delete[],这会调用多个析构函数去释放资源;如果使用new对象不使用[],释放时一定不要使用[]。保持两者一致。

std::string str = new std::string;
std::string strArr = new std::string[20];
//释放资源
delete str;
delete[] strArr;

最好尽量不要对数组形式作typedefs动作。C++标准库的string,vector等template,可将数组的需求降至几乎为零。

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


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

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

在调用processWidget前,编译器必须创建代码,做以下三件事:

  • 调用priority()
  • 执行new Widget
  • 调用tr1::shared_ptr构造函数

但C++的编译器对这三个执行的次序并不固定。但C++中可以确定的是,new Widget一定比tr1::shared_ptr先执行,但对priority()函数的调用却没有限定。如果以下面的顺序:

  • 执行new Widget
  • 调用priority()函数
  • 调用tr1::shared_ptr构造函数

这就会引发一个问题,如果第二步priority()函数发生异常,那么new Widget就无法放入shared_ptr中,这样就会造成资源泄漏(shared_ptr用来进行资源管理)。

正确的做法是将语句分离,先创建资源并放到资源管理器后,再进行下步操作。

std::tr1::shared_ptr<Widget> pw(new Widget);    // 在单独语句中以智能指针存储newed所得对象
processWidget(pw, priority);    // 这个调用动作绝不至于造成泄漏

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容