《Effective C++》第三章笔记

条款13:以对象管理资源

  • 获得资源后立即放入对象内:当申请一块动态内存之后,往往要记得释放内存。但是,有些情况(1.执行到释放资源语句之前return;2.continue或者goto提前退出;3.发生异常抛出异常)会导致无法执行到释放资源的语句,这就导致内存资源没有归还给系统。因此需要把资源放到对象中,因为对象一旦离开生命周期,就会被销毁,其资源也就释放。(RAII)

    资源获取即初始化:20230412:意思就是资源获得之后要进行初始化,初始化指的是一个对象创建时赋予一个值,因此意思就是资源获取之后就放到一个对象中。

  • 管理对象运用析构函数确保资源被释放:不管控制流如何离开区块,只要对象被销毁,其析构函数自动执行,资源也就被释放了。当然过程有可能发生异常,这就是前面所提到的“析构函数如何处理异常”的话题了。

智能指针auto_ptr

auto_ptr会在它自动销毁时释放其指向的资源。所以不要让两个指针同时指向同一对象,否则会释放一个资源两次,这种行为是未定义的。

特性:若被copy构造函数或者copy assignment运算符复制,它们就会变成nullptr,而复制所得的指针具有所有权。

因此其缺点就是不能同时指向同一个对象,但是对于STL容器来说,需要进行复制操作。

注:现在一般不再使用auto_ptr指针,而是使用unique_ptr指针。详细可参考《C++.Primer.Plus》(中文第六版)16.2.3

智能指针shared_ptr

引用计数型智能指针(Reference-Counting Smart Pointer),即会追踪一共有多少指针指向资源,并在无指针指向时释放资源。

RCSP的行为类似于垃圾回收,但是它无法打破环状引用,比如两个指针互相指向。

**需要注意的是:以上两种指针在其析构函数中调用的时delete而不是delete[],**因此不能指向类似于数字类型的连续空间。

条款14:在资源管理类中注意copy行为

当一个RAII对象被复制时,应该采用哪种策略:

  • 禁止复制:如果复制并不合理,可以拒绝复制,即在资源管理类中显式拒绝copying操作(C++11之后可以使用delete关键字);

  • 引用计数法:即采用类似shared_ptr智能指针的方法,记录一共有多少个指针指向资源,当指针数为0时释放资源。如果指针数为0时不想释放资源,shared_ptr提供了删除器(deleter),即一个函数对象,当引用次数为零时被调用。例如解除互斥锁而不是删除它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void lock(Mutex *pm);
    void unlock(Mutex *pm);

    class Lock {
    private:
    std::shared_ptr<Mutex> mutexPtr;
    public:
    // 使用pm作为智能指针指向的空间
    // 并使用unlock函数作为当引用计数为0时的删除器
    Lock(Mutex *pm) : mutexPtr(pm, unlock) {
    lock(pm);
    }
    };
  • 复制底部资源:(深复制)就是说复制资源管理对象的时候把其资源也复制一份。

  • 转移底部资源的所有权:即像auto_ptr指针那样把资源所有权从被复制物转移到复制物,被复制的指针指向空。

条款15:在资源管理类中提供对于资源的访问功能

虽然使用资源管理类去管理资源的方式降低了资源未被归还的可能,但是在使用时往往是需要使用资源的,比如申请了一块char类型的内存,在使用时是希望使用char类型而不是使用资源管理对象,因此资源管理类需要对外提供直接访问资源的方式。

  • 显式转换:提供一个接口(成员函数),比如get()方法,返回值是资源。

  • 隐式转换:定义隐式转换函数,将资源管理类的类型转换成资源的类型。比如auto_ptr和shared_ptr都提供了隐式转换为所指向资源类型指针的隐式转换函数,可以直接隐式转换为底部资源的指针类型,并且重载了指针取值操作符->和*。

    1
    2
    3
    4
    5
    6
    7
    8
    class Test {
    public:
    // 隐式类型转换函数
    // 将管理的资源转换为int类型返回
    operator int() const {
    return ...;
    }
    };

但是需要注意的是:隐式转换可能会导致某些问题,可能被误用(比如上面的例子中如果将资源转赋值给一个int类型,就是导致Test类对象隐式转换为int对象,这不一定是用户想要做的)。

另外其实RAII对象不算是严格意义上的封装,设计它仅仅是为了管理资源,因此向外提供了访问底部资源的接口和转换。

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

当使用new时:1.内存被分配出来(通过名为operator new的函数,条款49和51);2.针对此内存有一个或者更多构造函数被调用(内置类型也看作具有构造函数)。

当使用delete时:1.针对此内存有一个或更多析构函数被调用;2.内存释放(通过名为operator delete的函数,条款51)。

具体有多少个构造函数和析构函数被调用,需要关注内存中究竟有多少个对象(一种理解内存布局的方式):

  • 单一内存布局:即使用new Type申请出来的空间,可以看作 [object],只有一块内存;
  • 对象数组布局:即使用new Type []申请出来的空间,需要包含这块内存的数组大小的记录 [n][object 1]…[object n]。

虽然对于对象数组的布局并非一定是上面描述,但是可以看到两种形式的内存布局实现应该是不一样的,因此需要使用不同的delete形式:

  • 针对单一内存布局:即new,需要使用delete;
  • 针对对象数组布局:即new type [],需要使用delete [] type;

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

非独立语句形式:

1
2
3
4
int other(); // 一个其他函数的声明
void processedFunc(shared_ptr<Type> pt, int); // 参数为一个智能指针和一个int类型
...
processedFunc(new Type, other()); // !!!有可能会引发内存泄漏

为什么有可能引发内存泄露呢?主要原因是对于一个语句中多项操作C++未定义顺序:

1.调用other函数;2.new Type;3.调用shared_ptr<Type>的构造函数。

在一条语句中这3个操作的顺序是未被定义的,可以确定的是2一定在3之前,因为2的返回值作为3的参数。

如果操作顺序为:2.new Type –> 1.调用other函数 –> 3.调用shared_ptr<Type>的构造函数,那么假设调用other函数时发生异常,那么new出来的内存就没有存储到智能指针中,也就无法实现资源管理,从而导致内存泄露。

因此,为了保证将new出来的资源一定能存储到资源管理类中,应该把操作放到一条语句中:

1
2
shared_ptr<Type> pt(new Type);
processedFunc(pt, other());

对于不同语句的操作,C++是不允许调整顺序的。