【Effective C++读书笔记】篇六(条款13~条款15)

本文探讨了C++中的资源获取即初始化(RAII)原则,重点介绍了如何使用智能指针管理和释放资源,以及在资源管理类中实现复制行为的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款13:以对象管理资源                                                                                                   


所谓资源就是,一旦用了它,将来就必须还给系统。

对于那些分配在堆区的资源,单纯依赖“函数总会执行其 delete 语句”是不靠谱的,如提前 return, delete 前发生异常等,控制流就绝不会触及 delete 语句。一个绝妙的应对措施是“以对象管理资源”,确保资源在析构函数被释放。相对应的,C++提供了形如 auto_ptr 、tr1::shared_ptr 这样的智能指针(类指针对象),便于我们使用。

需要注意的是,auto_ptr 使用 copying 函数会使得以前的 auto_ptr 对象变成 NULL,而复制所得到的指针将取得资源的唯一拥有权。

tr1::shared_ptr 使用了 reference-count 机制,支持 copying 行为,当资源 reference-count 为 0 时,自动调用析构函数。不过,这两种方法在其析构函数内做的是 delete 而不是 delete[]。即对分配的数组资源管理支持还不够。此时,看看 boost 吧,里面的 boost::scoped_array 和 boost::shared_array classes ,它们提供我们想要的行为。


简单的使用以上两种智能指针:

#include <iostream>
#include <tr1/memory>
using namespace std;

class base
{
	public:
		base(){cout << "base()" << endl;}
		~base(){cout << "~base()" << endl;}
};

int main()
{
	auto_ptr<base> pb1(new base);
	auto_ptr<base> pb2(pb1);
	cout << pb1.get() << endl;
	cout << pb2.get() << endl;

	tr1::shared_ptr<base> pb3(new base);
	tr1::shared_ptr<base> pb4(pb3);
	cout << pb3.get() << endl;
	cout << pb4.get() << endl;
	return 0;
}

输出如下:

base()
0
0x961a008
base()
0x961a018
0x961a018
~base()
~base()

请记住:

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

2、两个常被使用的 RAII classes 分别是:tr1::shared_ptr 和 auto_ptr 。前者通常是较佳选择,因为其 copying 行为比较直观。若选择 auto_ptr ,复制动作会使它(被复制物)指向NULL。





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



以上的 tr1::shared_ptr 和 auto_ptr 都是用来管理 heap-based 资源,但并非所有资源都是 heap-based,对于那些资源,我们需要使用其他的资源管理者,有时后需要我们自己建立自己的资源管理类。这样就有了条款14条款15


就拿管理互斥锁为例,此时我们关心的资源管理并不是锁资源的分配与释放,而是锁的上锁与解锁。于是我们用一个对象来管理这一组行为。

class Lock
{
    public:
        explicit Lock(mutex *pm): mutexptr(pm){lock(mutexptr);}
        ~Lock(){ulock(mutexptr);}
    private:
        mutex *mutexptr;
}


用户对 Lock 对象的使用符合 RAII 方式:

mutex m;        //定义互斥锁
...
{
    Lock l1(&m);
    ...
}

由于我们使用了 Lock 对象来管理锁,其析构函数会自动释放锁。


那么当一个 RAII 对象被复制,比如出现如下的代码:

Lock l1(&m);
Lock l2(l1);

对于 RAII 对象的复制行为,我们该如何定义呢? (这里我们主要讨论拷贝构造函数)


有四种方法:

1、禁止复制

参见条款06,继承 class Uncopyable 即可。如:

class Lock: private Uncopyable

{

public:

...

}


2、对底层资源祭出"引用计数法"(reference-count)

此种方法与 tr1::shared_ptr 类似,不同的是 tr1::shared_ptr 在引用数为 0 时将会销毁资源。幸运的是, tr1::shared_ptr 允许指定所谓的“删除器”(deleter),那是一个函数或函数对象,当引用次数为 0 是便被调用(此机能并不存在与 auto_ptr——它总是销毁资源)。删除器对于 tr1::shared_ptr 构造函数而言是可有可无的第二参数。

简单应用一下:

#include <iostream>
#include <tr1/memory>
using namespace std;

class base
{
	public:
		base(){cout << "base()" << endl;}
		~base(){cout << "~base()" << endl;}
};

void MyDeleter(base* pb)
{
	cout << "MyDeleter()" << endl;
}

class base_tr1
{
	public:
		base_tr1(base *pb): spb(pb, MyDeleter){cout << "base_tr1()" << endl;}
	private:
		tr1::shared_ptr<base> spb;
};

class base_auto
{
	public:
		base_auto(base *pb): spb(pb){cout << "base_auto()" << endl;}
	private:
		auto_ptr<base> spb;
};

int main()
{
	base *pb1 = new base;
	base *pb2 = new base;
	
	base_tr1 bt1(pb1);
	base_auto ba1(pb2);
	cout << "Enter if(1)" << endl;
	if(1)
	{
		base_tr1 bt2(bt1);
		base_auto ba2(ba1);
	}
	cout << "Leave if(1)"  << endl;
	return 0;
}


运行结果:

base()
base()
base_tr1()
base_auto()
Enter if(1)
~base()
Leave if(1)
MyDeleter()

该 demo 说明 tr1::shared_ptr 确实可以修改其删除器,bt1和 bt2 共同管理的资源在程序结束时由于引用数为 0 而调用了指定函数 MyDeleter() 函数。

同时该段代码进一步说明 auto_ptr 对象的复制行为确实会使得被复制的对象失效,只留下复制的对象。因为 ba2 和 ba1  所管理的资源在离开 if(1) 之后就被销毁了。

这里的例子还举得不够合适。修改删除器的目的在于使得 base_tr1 类的构造函数(注意:是构造函数,并不包括拷贝构造函数)与删除器能够成对管理资源(此处并不是指简单的内存分配与回收这样的管理。比如对于mutex的加锁与解锁例子,在构造函数中调用 lock(),在删除器调用 unlock(),锁本身资源并不由该类管理)。


3、复制底部资源

这种方法是在复制资源管理对象的时候同时复制一份资源,即“深拷贝”。


4、转移底部资源的拥有权

 auto_ptr 使用的就是这种方法。


请记住:

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

2、普遍而常见的 RAII class copying 行为是:抑制 copying、实施引用计数法(reference counting)。不过其他行为也都可能被实现。




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



这里有个小插曲,书中出现了“隐式转换函数”。啥?这词儿还是头回听。详细的介绍可以参见这篇文章http://blog.youkuaiyun.com/onejune2013/article/details/16803321 (《C++转换构造函数和隐式转换函数》码农时代)

估计博主也是在这里第一次听说这个词儿吧。\(^o^)/~


转换构造函数:当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。(很眼熟吧,其实我们平时自己写的一些构造函数就是转换构造函数,只是我们自己不知道罢了)

这里使用转换构造函数举个例子:

有趣的是插曲里又有了插曲,那就是 RVO。RVO(Return value optimization),是C++编译器对值传递返回会产生临时对象这一效率缺陷做出的优化。

详见:https://en.wikipedia.org/wiki/Return_value_optimization(Wikipedia)

其次,这两篇分析也具有参考价值。

https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/RVO_V_S_std_move?lang=en(《RVO V.S. std::move》Zhao Wu)

http://blog.youkuaiyun.com/virtual_func/article/details/48709617(《详解RVO与NRVO(区别于网上常见的RVO)》Virtual_Func的博客)

#include <iostream>
#include <tr1/memory>
using namespace std;

class base
{
	public:
		base(int id): id(id){cout << "base() " << id << endl;}                          
		base(const base& b): id(b.id){cout << "base(const base& b) " << id << endl;}    
		~base(){cout << "~base() " << id << endl;}
		base operator+(const base& b)
		{
			return base(id + b.id);
		}
		void display(){cout << id << endl;}
	private:
		int id;
};

int main()
{
	base b1 = 1;                 //相当于 base b1(1);
	base b2 = b1 + 2;       //相当于 base b2(b1 + base(2));
	return 0;
}



输出结果为:

base() 1
base() 2
base() 3
~base() 2
1
3
~base() 3
~base() 1


不科学啊!一个拷贝构造函数都没有被调用?这就是 RVO 优化过得结果。使用 -fno-elide-constructors 参数在编译时取消掉 RVO,再次打印结果:

base() 1              //这边的 base b1 = 1;在不优化时居然是先将1转换成base(1)后再拷贝赋值到b1,这里先不深究 
base(const base& b) 1
~base() 1
base() 2
base() 3
base(const base& b) 3               
~base() 3
base(const base& b) 3
~base() 3
~base() 2
1
3
~base() 3
~base() 1

这次熟悉了吧。编译器果然优化不少。



对于RAII class,我们需要提供访问其原始资源的方法。有两个做法:

1、显式转换

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


2、隐式转换

就像所有(几乎)智能指针一样,tr1::shared_ptr 和 auto_ptr 也重载了操作符 operator-> 和 operator* ,它们允许隐式转换至底部原始指针。如下:

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

Investment* creatInvestment();

std::tr1::shared_ptr<Investment> pi1(creatInvestment());
bool taxable1 = !(pi1 -> isTaxFree());
...
std::tr1::shared_ptr<Investment>pi2(creatInvestment());
bool taxable2 = !((*pi2).isTaxFree());

有时候还必须取得 RAII 对象内的原始资源,于是有了一种隐式转换函数法。如下:

FontHandle getFont();
void releaseFont(FontHandle fh);
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;                              //原始字体资源
}

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



请记住:

1、APIs往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个“取得其所管理之资源”的办法。

2、对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值