3. 资源管理

13. 以对象管理资源

1
2
3
4
5
6
void f()
{
Investment *pInv = createInvestment();
···
delete pInv;
}
  1. 可能无法执行delete
    1. 在···中过早return
    2. 若delete位于某循环中,由于continue或者goto语句过早退出
    3. ···内抛出异常
    4. 泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源
  2. 为确保createInvestment返回的资源总是被释放,我们需要将这些资源放在对象内,当控制流离开f,该对象的析构函数会自动释放那些资源
  3. 实际上这正是隐身于本条款背后的半边想法,把资源放进对象内,我们便可以依赖c++的析构函数自动调用机制确保资源被释放
  4. 许多资源被动态分配于heap内而后被用于单一区块或函数内,他们应该在控制流离开那个区块或者函数时被释放,标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品
  5. auto_ptr是个”类指针对象“,也就是智能指针,其析构函数自动对齐所指对象调用delete
1
2
3
4
5
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());

}
  1. 两个想法
    1. 获得资源后立刻放进管理对象内
    2. 管理对象运用析构函数确保资源被释放
  2. 由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让给多个auto_ptr同时指向一个对象。
  3. auto_ptrs有一个不寻常的性质,若通过copy构造函数或copy assignment操作符复制他们,他们会变成null,而赋值所得的指针将取得资源的唯一拥有权
  4. 收auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它,意味着auto_ptrs并非管理动态分配资源的神兵利器。
    1. 例如:STL容器要求其元素发挥正常的复制行为,因此这些容器容不得auto_ptr
  5. auto_ptr的替代方案是”引用计数型智慧指针(reference-counting smart pointer RCSP)
    1. 持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源
    2. 类似于垃圾回收,但无法打破环状引用,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用"状态
  6. tr1::shared_ptr就是个RCSP
1
2
3
4
void f()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());
}
  1. 复制行为正常

14. 在资源管理类中小心copying行为

  1. 资源取得时机便是初始化时机
  2. 并非所有资源都是heap_based,auto_ptr和tr1::shared_ptr这样的智能指针往往不适合作为资源掌管者,需要建立自己的资源管理类
  3. 例如,使用C api函数处理Mutex的互斥锁现象,共有lock和unlock两函数
1
2
void lock(Mutex* pm);		//锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解除锁定
1. 为确保绝不会忘记将一个被锁住的Mutex解锁,希望建立一个class用来管理机锁
2. 这样的class的基本结构由RAII守则支配,也就是”资源在构造期间获得,在析构期间释放“
1
2
3
4
5
6
7
8
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm)
{lock(mutexPtr);} //获得资源
~Lock() {unlock(mutexPtr);}
private:
mutex *mutexPtr;
};

3. 当一个RAII对象被复制,会发生什么事?
1
2
Lock ml1(&m);	//锁定m
Lock ml2(ml1); //将ml1复制到ml2身上,会发生什么是
    1. 禁止复制。许多时候允许RAII对象被复制并不合理,因为很少能够合理拥有”同步化基础器物“的副本。如果复制动作对RAII class并不合理,便应该禁止。条款6:如果coipying操作声明为private
1
2
3
class Lock: private Uncopyable{
public:
};
    2. 对底层资源基础”引用计数法“。有时需要保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将该资源的”被引用数”递增。tr1::shared_ptr是如此。
        1. 通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可出现reference-counting copying行为。
        2. 如果Lock打算使用reference counting,可以改变mutexPtr类型,将Mutex* 改为tr1::shared_ptr<Mutex>。然而不幸的是tr1::shared_ptr的缺省行为是“当引用次数为0时删除其所指物“,这不是我们所要的行为。当我们用上一个Mutex,我们想要做的释放动作是解除锁定而非删除
        3. 幸运的是tr1::shared_ptr允许指定所谓的”删除器”,那是一个函数或者函数对象,当引用次数为0时便被调用(此技能并不存在于auto_ptr——它总是将其指针删除)。删除器对tr1::shared_ptr构造函数而言时可有可无的第二参数,![](https://cdn.nlark.com/yuque/0/2024/png/35229143/1706957885163-8a0df2e6-aa4f-4198-b2a1-3535512eecc7.png)
        4. 本例Lock class不声明析构函数,class析构函数会自动调用其non-static成员变量的析构函数。而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用tr1::shared_ptr的删除器
    3. 复制底部资源
        1. 复制资源管理对象时,进行的时深度拷贝
        2. 某些标准字符串是由指向heap内存之指针构成。那种字符串对象内含一个指针指向一块heap内存。当这样一个字符串对象被复制,不论指针或其所指内存都会被制作出一个复件。这样的字符串展现深度复制行为
    4. 转移底部资源的拥有权
        1. 某些罕见场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源,即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。这是auto_ptr奉行的复制意义
        2. copying函数有可能被编译器自动创建出来,因此除非编译器所生版本做了你想做的事,否则你要自己i安歇他们
  1. tips:
    1. 复制RAII对象把必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
    2. 普遍而常见的RAII class copying行为是,抑制copying、施行引用计数法。不过其他行为也都可能被实现

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

  1. 资源管理类是对抗资源泄露的堡垒,在一个完美世界中依赖这样的classes处理和资源之间的所有互动,而不是直接处理原始资源
1
2
std::tr1::shared_ptr<Investment>pInv(createInvestment());
int dayHeld(const Investment* pi);
  1. 需要一个函数可将RAII class对象转换为其所内含之原始资源。有两个做法可以达成目标:显示转换和隐式转换
  2. 显示转换tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是它返回智能指针内部的原始指针(的复件)
1
int days = daysHeld(pInv.get());    		//将pInv内的原始指针传给daysHeld
  1. 就像几乎所有智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针取值和操作符,它们允许隐式转换至底部原始指针
1
2
3
4
5
6
7
8
9
10
11
12
class Investment{
public:
bool isTaxFree() const;
}
Investment* createInvestment();

std::tr1::shared_ptr<Investment> pil(createInvestment()); //tr1::shared_ptr管理一笔资源

bool taxable1 = !(pil->isTaxFree()); //经由operator->访问资源
std::auto_ptr<Investment> pi2(createInvestment()); //令auto_ptr管理一笔资源

bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源
  1. 取得RAII对象内的原始资源,提供一个隐式转换函数
1
2
3
4
class Front{
public:
FontHandle get() const {return f;}
};

这使得客户每次使用API必须调用get

1
2
3
4
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f.get(),newFontSize); //明白地将Font转换为FontHandle

提供隐式函数

1
2
3
4
5
6
7
8
9
class Font{
public:
operator FontHandle() const//隐式转换函数
{return f;}
};

Font f(getFont());
int newFontSize;
changeFontSize(f,newFontSize);
  1. 但是这个隐式转换会增加错误发生机会,例如客户可能会在需要Font时意外创建一个FontHandle
1
2
Font f1(getFont());
FontHandle f2 = f1; // 原意要拷贝一个Font对象,却反而将f1隐式转换为其底部的FontHandle才复制它
  1. 以上程序有个FontHandle由Font对象f1管理,但那个FontHandle也可通过直接使用f2取得。那几乎不会有好下场,例如当f1被销毁,字体被释放,而f2因此成为"虚吊的"dangle。
  2. 是否该提供一个显示转换函数(例如get函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII class被设计执行的特定工件,以及它被使用的情况。最佳设计可能是坚持条款18的忠告”让接口容易被正确使用,不易被误用“。
  3. 通常显示转换函数如get是比较受欢迎的路子,因为它将非故意之类型转换的可能性最小化了,然而有时,隐式类型转换所带来的自然用法也会引发天秤倾斜
  4. RAII class内的那个返回原始资源的函数,与”封装“发生矛盾,但一般而言不是什么设计灾难。RAII class并不是为了封装某物存在,它们的存在是为了确保一个特殊行为——资源释放——会发生。如果一定要,当然也可以在这基本功能之上再加一层资源封装,但并非必要。
  5. 也可以和身份松散的底层资源封装,获得真正的封装实现
    1. 例如tr1::shared_ptr将它所有计数机构封装了起来,但还是让外界很容易访问其所内含的原始指针。就像多数设计良好的classes一样,它隐藏了客户不需要看的部分,但备妥客户需要的所有东西
  6. APIs往往要求访问原始资源,所以每一个RAII class应该提供一个取得其所管理之资源的办法
  7. 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便

16. 成对使用new和delete时采用相同形式

  1. 当使用new时,有两件事发生
    1. 内存被分配出来,通过名为operator new的函数,
    2. 针对此内存会有一个或更多析构函数被调用
  2. 当使用delete时,也有两件事发生
    1. 针对此内存会有一个或更多析构函数被调用,然后内存才被释放
  3. 即将被删除的指针,所指的是单一对象还是对象数组
    1. 单一对象的内存布局一般而言不同于数组的内存布局
    2. 数组所用的内存通常还包括数组大小的记录,以便delete知道需要调用多少次析构函数,单一对象的内存则没有这笔记录。
1
2
delete stringPtr1; //删除一个对象
delete [] stringPtr2; //删除一个由对象组成的数组
  1. 调用new时使用[],必须在调用delete时也用[],调用new时没有使用[],也不应该在对应delete时使用[]
1
2
3
typedef std::string AddressLines[4];
std::string* pal = new AddressLines;
delete [] pal;
  1. 尽量不要对数组做typedefs动作,可将其改为vector

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

1
2
3
int priority();		//揭示处理程序的优先权
void processWidget(std::str1::shared_ptr<Widget> pw, int priority);
//在某动态分配所得的Widget上进行某些带有优先权的处理
  1. 由于”以对象管理资源“,processWidget决定对其动态分配得来的Widget运用智能指针(tr1::shared_ptr)
1
processWidget(new Widget, priority());//调用函数
  1. 不能通过编译,tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,要转换为智能指针
1
processWidget(std::tr1::shared_ptr<Widget>(new Widget, priority());
  1. 虽然使用了对象管理式资源,却可能泄露资源
  2. 编译器在产出一个processWidget调用码之前,必须核算即将被传递的各个实参,
    1. 第二个实参只是一个单纯的对priority函数的调用,
    2. 第一个实参std::tr1::shared_ptr(new Widget)由两部分组成
      1. 执行”new Widget"表达式
      2. 调用tr1::shared_ptr构造函数
    3. 在调用processWidget之前,编译器必须创建代码
      1. 调用priority
      2. 执行“new Widget"
      3. 调用tr1::shared_ptr构造函数
    4. new Widget执行于tr1::shared_ptr构造函数被调用之前,但priority的调用可以排在第一第二或者第三执行
    5. 万一priority调用异常,new Widget返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内,后者使我们用来防止资源泄露的
    6. 资源被创建和资源被转换为资源管理对象之间发生了异常干扰
    7. 避免方法:使用分离语句,分别写出
      1. 创建Widget
      2. 将它置入一个智能指针内,然后再把那个智能指针传给processWidget
1
2
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());//不会造成资源泄露
  1. 请记住,以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露

3. 资源管理
http://binbo-zappy.github.io/2024/11/27/effective-cpp/3-资源管理/
作者
Binbo
发布于
2024年11月27日
许可协议