《Effective C++》第3章 资源管理(1)-读书笔记

章节回顾:

《Effective C++》第1章 让自己习惯C++-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

《Effective C++》第3章 资源管理(1)-读书笔记

《Effective C++》第8章 定制new和delete-读书笔记

内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、以及网络sockets。无论哪一种资源,重要的是当你不再使用它时,将它还给系统。

条款13:以对象管理资源

下面有一个Investment继承体系:

class Investment { ... };

Investment是root class,获得Investment体系的对象是通过工厂函数:

Investment* createInvestment(); //返回指针指向Investment继承体系动态分配对象

显然,createInvestment的调用者使用后,有责任删除返回的指针,以释放对象。假设函数f()负责这个行为:

void f()
{
    Investment *pInv = createInvestment();
...
    delete pInv;
}

但至少存在几种情况使得f()无法删除createInvestment对象:

(1)…区域有一个过早的return语句,导致delete没执行。

(2)…区域抛出异常,控制流不会转向delete。

当delete时,我们泄露的不只是内含Investment对象的那块内存,还包括其所保存的任何资源。

为确保createInvestment()返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f()时,该对象的析构函数会自动释放那些资源。标准库提供的auto_ptr是个“类指针对象”,即智能指针,其析构函数自动对其所指对象调用delete。

下面是f()的修正版:

void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());            //由auto_ptr的析构函数自动删除pInv
    ...
}

f()说明了以对象管理资源的两个重要方面:

(1)获得资源后立刻放进管理对象内。

以资源管理对象的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization, RAII)。每一笔资源在获得的同时立刻被放进管理对象中。

(2)管理对象运用析构函数确保资源被释放。

无论控制流如何离开f(),一旦对象离开作用域(对象被销毁),其析构函数被调用,资源被释放。

注意:由于auto_ptr被销毁时会自动删除它所指之物,所以别让多个auto_ptr同时指向某一对象。为了预防这个问题auto_ptr有一个很好的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一权。

std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);            //pInv2指向对象,pInv1被设为null
pInv1 = pInv2;                                    //pInv1指向对象,pInv2被设为null

正是因为auto_ptr对象具备“非正常”的拷贝性质,所以不能用于STL容器的元素等。

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

TR1的tr1::shared_ptr就是一个RCSP,f()的新写法:

void f()
{
    std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
}

f()的新版本与使用auto_ptr几乎相同,但拷贝行为正常了:

void f()
{
    ...
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());
    std::tr1::shared_ptr<Investment> pInv2(pInv1);            //pInv1和pInv2指向同一对象
    pInv1 = pInv2;                                            //pInv1和pInv2指向同一对象
    ...
}

因为shared_ptr的复制行为是正常的,所以可用于STL容器以及其他auto_ptr非正常复制行为并不适用的情况。

特别注意:auto_ptr和shared_ptr两者都在析构函数内做delete而不是delete[]。所以下面的操作是非常错误的:

std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);

之所以没有特别针对C++动态分配数组而设计的类似auto_ptr或tr1::shared_ptr是因为vector和string总是可以取代动态数组,这是你的第一选择。

请记住:

(1)为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

(2)两个常被使用的RAII class分别是:tr1::shared_ptr和auto_ptr。前者是最好的选择,因为其copy行为比较正常。auto_ptr的copy会使被复制物指向null。



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

并非所有资源都是heap-based的,对这种资源auto_ptr和tr1::shared_ptr这样的智能指针往往不适用。举例如下:

有两个函数用来处理Mutex互斥器对象:

void lock(Mutex *pm);            //锁定pm指向的互斥器
void unlock(Mutex *pm);            //解锁

为确保不会忘记解锁,可以建立一个class管理锁,这个class基本结构由RAII支配,即“资源在构造期间获得,在析构期间释放”。

class Lock
{
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm)
    {
        lock(mutexPtr);
    }
    ~Lock()
    {
        unlock(mutexPtr);
    }
private:
    Mutex *mutexPtr;
};

客户是这样使用的:

Mutex m;                    //定义所需的互斥器
{
...
    Lock ml(&m);           //锁定互斥器
...
}                            //自动解锁

这样使用是可以的。但如果发生copy的情况呢?即当一个RAII对象被复制,会发生什么事情?

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

有四种可能:

(1)禁止复制。

许多时候允许RAII对象被复制并不合理。

(2)对底层资源使用“引用计数法”。

有时候我们希望保存资源,直到它的最后一个使用者被销毁。tr1::shared_ptr便是如此处理。

注意:tr1::shared_ptr的缺省行为是当引用次数为0时删除其所指物,我们想要的是释放锁,而不是删除。我们可以指定“删除器”,这是一个函数,当引用次数为0时会被调用。auto_ptr无此性质,它总是删除它的指针。

更改后的版本是:

class Lock
{
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm, unlock)        //unlock为删除器
    {
        lock(mutexPtr.get());
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr;            //使用shared_ptr替换raw_pointer
};

注意:这个lock class不需要析构函数了,因为编译器会自动调用mutexPtr的析构函数,当引用计数为0时,自动调用删除器。

(3)复制底部资源

可以针对一份资源拥有任意数量的副本。这是的复制是深拷贝。

(4)转移底部资源的拥有权

你可能希望永远只有一个RAII对象指向一个raw recource,即使RAII对象被复制。这是资源的拥有权会从被复制物转移到目标物。auto_ptr便是如此。

注意:copying函数(包括copy构造函数和copy assignment操作符)有可能被编译器创造出来,因此除非编译器版本做了你想做的事,否则你要自己编写它们。

请记住:

(1)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

(2)普遍常见的RAII class copying行为是:禁止copying、使用引用计数。其他行为也可能被实现。

时间: 2024-12-16 22:58:25

《Effective C++》第3章 资源管理(1)-读书笔记的相关文章

Java 线程第三版 第四章 Thread Notification 读书笔记

一.等待与通知 public final void wait() throws InterruptedException 等待条件的发生. public final void wait(long timeout) throws InterruptedException 等待条件的发生.如果通知没有在timeout指定的时间内发生,它还是会返回. public final void wait(long timeout, int nanos) throws InterruptedException

《DirectX 9.0 3D游戏开发编程基础》 第一章 初始化Direct3D 读书笔记

REF设备 参考光栅设备,他能以软件计算方式完全支持Direct3D Api.借助Ref设备,可以在代码中使用那些不为当前硬件所支持的特性,并对这此特性进行测试. D3DDEVTYPE 在程序代码中,HAL设备用值D3DDEVTYPE_HAL来表示.该值是一个枚举变量.REF设备用D3DDEVTYPE_REF来表示.这种类型非常重要,你需要铭记,因为在创建设备的时候,我们必须指定使用哪种设备类型. COM(组件对象模型) 创建COM接口时不可以使用c++关键字new.此外使用完接口,应调用Rel

Java 线程第三版 第三章数据同步 读书笔记

多线程间共享数据问题 一.Synchronized关键字 atomic一词与"原子"无关,它曾经被认为是物质的最小的单元,不能再被拆解成更小的部分. 当一个方法被声明成synchronized,要执行此方法的thread必须先取得一个token,我们将它称为锁.一旦该方法取得(或者说是获得)锁,它将运行此方法然后释放掉(或者返回)此锁.不管方法时怎样返回的(包括通过异常)该锁会被释放. 二.Volatile关键字 如果变量被标示为volatile,每次使用该变量时都必须从主寄存器中读出

《C++primer》v5 第2章 C++基础 读书笔记 习题答案

2.1 int,long long ,short 可表示范围和占用内存空间不同.具体与计算机有关. 无符号类型只能表示0和正数,带符号类型可以表示负数,0,正数. float是单精度,一般占用4个字节,double是双精度,一般占用8个字节,它们可表示的数据范围也不相同. 2.2 利率用double,本金和付款用int 2.3 unsigned u=10,u2=42; cout<<u2-u<<endl; cout<<u-u2<<endl; int i=10,

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock

读《Effective C++》的感受以及对于读书笔记的看法和规划(个人感受)

楼主最近在整理两本书的读书笔记<Effective C++>和<TCP/IP详解卷1:协议>,这两本都是经典书,笔记也都有了一些了.因为主要是谈谈<Effective C++>,所以把这本书已经写完的目录列出来,这个目录不会再更新了. <Effective C++>目录: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti

《TCP/IP详解卷1:协议》第2章 链路层-读书笔记

章节回顾: <TCP/IP详解卷1:协议>第1章 概述-读书笔记 1.引言 从图1-4可以看出,在TCP/IP协议族中,链路层主要有三个目的: (1)为IP模块发送和接收IP数据报: (2)为ARP模块发送ARP请求和接收ARP应答. (3)为RARP发送RARP请求和接收RARP应答. TCP/IP支持多种不同的链路层协议,这取决于网络所使用的硬件,如以太网.令牌环网.FDDI(光纤分布式数据接口)及RS-232串行线路等. 2.以太网和IEEE 802封装 (1)以太网 以太网一般是指数字

《C++primer》v5 第12章 动态内存 读书笔记 习题答案

这一章暂时没写完,先留着以后再写. 在C++程序中,程序员可以给手动开辟内存,但是这块内存需要手动释放,不便管理,因此新标准提供智能指针类型来管理动态对象.它负责自动释放所指向的对象. shared_prt允许多个指针指向同一个对象 unique_ptr独占所指向的对象 weak_ptr是一个弱引用,指向shared_ptr所管理的对象 一些操作: p=q;//递减p的引用计数,递增q的引用计数 shared_ptr<T> p(q);//p是q的拷贝,递增q的引用计数 通过make_share

《C++primer》v5 第8章 IO库 读书笔记 习题答案

8.1.8.2 这一章不咋会啊.. istream &read(istream &is) { int a; auto old_state=is.rdstate(); is.clear(); is>>a; is.setstate(old_state); return is; } int main() { read(cin); return 0; } 8.3 读到eof或错误类型的时候 8.4 #include<fstream> using namespace std;

《python基础教程》第3章使用字符串 读书笔记

第三章:使用字符串 1.字符串格式化操作符是一个百分号 % 2.只有元组和字典可以格式化一个以上的值.列表或者其他序列只会被解释为一个值. 3.in操作符只能查找字符串中的单个字符. 4.字符串方法: ①find():find方法可以在一个较长的字符串中查找子串,它返回子串所在位置的最左端索引,如果没有找到则返回-1.这个方法还能提供起始点和结束点的范围(提供第二,第三个参数),范围包含第一个索引,但不包含第二个索引,这在python中是个惯例. ②join():这个方法用来连接序列中的元素(序