资源管理:强鲁棒性应用的基石 (2)

本文探讨了C++中资源管理的重要性,特别是所有权的概念。明确了资源的所有权可以帮助确保资源正确销毁,避免资源泄漏。文章介绍了作用域、生命周期和存储持续期等关键概念,并讨论了资源转移和资源控制的区别。C++标准库通过`std::auto_ptr`和`shared_ptr`提供了资源管理支持,而自定义实现可以通过引用计数技术来实现基于值语义的资源封装类。

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

3. 所有权的控制与转移

前一篇文章的资源封装实现,通过禁用资源封装类的拷贝和赋值,极大地简化了这些类的最终代码,然而同时也极大的限制了它们的使用范围。目前他们只能用在局部的资源管理以及作用常引用在作用域之间传递,这无疑是不方便的。我们要尝试去除这一限制。但是在开始之前,我们先要讨论资源的所有权问题,只有明确了资源的所有权,我们才能有效地在不同的作用域之间传递资源封装类,而不会引起混淆。因为一般的经验显示,代码实现和功能的混淆往往来源于概念的含混不清。我们先把基本的东西搞清楚。

 

对于基于值语义的POD结构体来说,C++完全继承了C的风格,保留它们的值语义,所以其中并没有太多的所有权的问题,因为一个拷贝一个结构对象自然会完全拷贝一份所有的结构成员,修改一个结构实例的成员不会对其他结构实例有任何的影响。这个实现约定极大地简化了一般代码的实现,程序员可以自由地把结构对象传来传去,就好处处理内置的基本类型一样。然而,当结构体包含足够的成员的时候,来恢赋值一个巨大的结构体的开销往往难以令人接受,程序员们开始使用指针来传递巨大的结构实例。然而,使用指针总是导致额外的复杂性,如结构体的构造和销毁的时机的选择。C++通过使用合适的封装技术以及强调所有权的概念,合理的解决了这个问题。

 

我们把所有权定义为负责最终销毁资源的权利。资源的创建和使用一般都不是问题,我们按需而动,但是资源的销毁却是至关重要的,它需要保证:

* 资源可以被销毁,没有资源泄漏

* 资源可以被及时地销毁,自动或者手动驱动

* 资源被销毁以后,不容易被误用以及重复销毁

 

对于第一个问题,我们可以确信,由于编译器可以保证对象在析构的时候一定会调用析构函数,只要把资源销毁的操作置于其中即可,而无论该对象是怎么被析构的,或者是用户直接调用delete操作符,或者该对象离开了其作用域。对于这两种情况,对象的析构函数都是保证被立即调用的,所以上述的第二条也可以得到满足;当对象析构之后,对象就不存在,任何对该对象的引用都归于无效,所以也就不能误用已经被释放的资源(倒是有可能使用引用误用被释放的对象,这是一个 C++中臭名昭著的局部对象引用问题)。这里,你应该可以理解,C代码中强烈要求(尽管不是必须)释放指针后要将指针置零,而C++却并不推荐。It's really make no sence to well designed C++ code.

 

这里,我们不止一次提到作用域,这是一个很简单,但是却十分重要的概念。我们这里复习一下。C++有多种作用域,我们这里强调其中几种。第一种是局部作用域,那就是定义在一个快(最小的使用{}包含起来的部分,如if语句之后的{})内的对象的作用域;另一种是类作用域,那就是类中申明的对象(成员变量)的作用域;再有一种就是函数作用域,那是指函数定义中,函数名字后面括号中形参(parameter,实参叫做argument)的作用域;最后就是名字空间作用域和全局作用域。全局作用域是指不包含在任何的{}之内的名字的作用域,名字空间是松散的构造,这里把它一块归于全局作用域讨论,因为它们有一些共同的属性,但是请明确二者有严格的区别。

 

另一方面要复习的概念是对象的生命周期。这是任何对象的一种运行期属性,一个对象的声明周期开始于:

1. 对象需要的存储(合适的大小并且对齐)完全获得

2. 如果对象有非平凡构造函数(不是编译器自动生成的),则需要该构造函数执行完毕。

它的生命周期结束于:

1. 如果对象有非平凡析构函数,析构函数被执行完毕

2. 对象所占的存储被释放。

我们只考虑这种最简单的情况,对于数组以及派生类对象,细节上还有差异。

 

最后一个概念(保证是最后一个)是对象存储持续期。有三种:静态对象存储持续期,动态对象存储持续期以及自动对象存储持续期。动态对象存储持续期对应于使用 operator new获得的对象,使用自动对象存储持续期对应于局部对象,初次之外的全部对象都是静态对象存储持续期的。

 

有了这三个概念,我们可以说,由于资源所有权的对象需要在其生命周期终止的时候保证释放其资源。属性是静态对象存储持续期的对象都由运行期系统负责构造和释放;而属性是动态对象存储持续期的则要求用户手动调用operator delete。所以和资源管理对象关系最密切的就是那些属性是自动对象存储持续期的对象,我们知道,这是局部对象;我们又知道,局部对象的作用域是局部对象所在的块。当执行流退出这个块的时候,根据作用域原则以及对象的生命周期定义,其中的所有局部对象都会被保证释放。这样,环环相扣,最终保证了我们前面说的资源所有权管理的三大要素!

 

由此,我们也可以顺便了解到:

1. 全部对象不宜使用资源封装类,尽管这类对象的生命周期固定,但是其构造顺序却是不确定的。如果去阿奴资源对象存在依赖关系,其效果则完全不能保证;

2. 动态对象可以使用资源封装类,但是它依赖于手工的对象清理,这恰恰是资源管理要解决的问题,最后发现,所有的动态对象管理都可以使用资源管理的技巧来统一处理,所以在资源管理的世界中,没有动态对象,只有动态对象wrapper。

 

明确了资源的所有权之后,我们就要着手解决资源的转移问题。首先,我们先把我们的需求清理一下。

所谓资源转移,那就意味着原有的对象放弃对资源的拥有(释放的权利),而新的对象拥有该资源。那么原来的对象放弃资源所有权之后,那还能继续使用资源吗?这真是个问题呀。一派意见是当然可以,放弃拥有权和使用资源与否是两码事,我们不是可以租房子住嘛!另一派则以为不妥,那原来的对象怎么知道它使用的这个被其他人拥有的对象不会被悄悄被释放呢?可以继续使用倒是方便,但是它引入了更大麻烦。所以对于资源的转移,必须禁止原来的对象可以继续使用。

这时候有人提问题了,为什么要转移资源的所有权?用户需要的是可以方便地使用资源,而不关心到底最后到底是谁来释放的,只要你保证释放了就好,不是吗?大家仔细一想,确实如此,我们应该强调的是资源“控制”而不是“转移”。通过有效的资源控制,我们可以放心大胆地让用户随意使用资源封装对象,而仅仅在真正需要的时候才提供资源的转换甚至复制操作。由于资源转换和复制的开销的副作用都很大,必须有用户的明确命令才可以进行。这里,所有的资源对象都共享资源所有权,只要有多于一个对象存在,我们就不知道到底谁会最后负责释放对象。

 

总结一下:

资源转移:资源所有权在对象之间的传递。原来对象在释放所有权之后不再刻意继续使用资源。该操作的开销很小

资源复制:复制保护的资源,并使用这个副本来构建另一个资源管理类对象。该操作因为涉及资源的复制,开销可能会很大

资源控制:通过提供有效的控制接口,而是资源管理类对象具有值语义。资源管理类负责协调有那个对象最终释放资源。这种情况下,可以看做是用多个对象共享所有权。对象复制的开销很小。

 

4. 库支持

资源的控制和管理讨论的足够多了,看看C++标准库给我们提供了什么。

在 C++97/2003中,C++通过std::auto_ptr,提供了资源转移的支持。我们已经知道,资源转移尽管完全使用了资源管理的基本技术,但是它带来的问题也不少,所以自从std::auto_ptr出生的第一天起,各方的争论就不断。保守的一方坚定地认为这是一个叛逆者,因为它完全不兼容于 C++容器,不兼容于人们的日常使用习惯,应该严谨在代码中使用;而集、激进的一方则认为这是标准唯一支持的资源管理组件,在保证移植性并且有效使用资源管理技术的唯一标准方案。尽管它的行为违反直觉,但是这也初始人们真正考虑资源的转移问题。而且,合适地限制其使用范围,如仅仅局部使用,可以极大地简化异常安全的代码的实现。几年争论下来,一直的意见没有达成,却促进了C++标准库在这方面的进步。C++0X通过引入了在boost库中广泛使用的 shared_ptr,提供了基于值语义的资源控制支持。这样,C++资源管理技术的支持终于完善!shared_ptr可以用于任何的标准容器,提供了可疑媲美基本对象的操作接口(由于其基于引用计数的实现,在某些特殊的情况下,如使用shared_ptr的双链表,会导致链表中最后一个节点对象不能被释放。这个问题是通过引入一个叫做weak_ptr的构造来打破循环依赖而解决的)。

由于shared_ptr可以接受另一个额外的模板参数作为资源释放的deleter,shared_ptr不仅仅可以用于简单堆对象的管理,还可以用于任何使用基于RAII技术的资源封装类。不行的是 C++0x还得假以时日才能正式发布,我们离标准兼容的资源管理支持还有一段小小的距离。

 

尽管如此,我们可以说,C++从语言核心层到标准库,都(逐渐)提供了基本完善的资源管理构造技术。我们只是需要有效地使用它。使用这些组件或者技术把那个不是很难,困难的时候深入理解其背后的思想并清除其要解决的问题。真正有了资源管理,我们才能深入到另一个使得C++大放异彩的特性:基于异常的错误管理。没有基于 RAII技术的资源管理,就不能有效地进行错误处理,已经成为业界的常识。

 

5. 自定义的实现

在C++0x之前,要使用全套的资源管理技术,我们有几个选择,一个是使用boost的智能指针库,具体可以参考来自Bj?rn Karlsson的“Beyond the C++ Standard Library: An Introduction to Boost“。现在各个主流的C++编译器都提供了对C++0x库部分TR1的支持,所以使用其中的tr1::shared_str也正当时,具体看以参考来自Pete Becker的”The C++ Standard Library Extensions: A Tutorial and Reference“。有了shared_ptr的支持,再加上我们原先的资源封装类,就可以所向披靡了。

 

不过还有另外一种选择,自己实现基于值语义的资源封装类。这个要求我们从头开始做一些工作,但是却解除了对shared_ptr组件的依赖。共享资源实现的基本原理是使用引用计数技术。但是我们不想对于任何一个资源类都实现一套这样的机制。理想的情况,我们实现一个类,然后所有的类都可以从其继承,从而自动获得基于引用计数的资源共享。这是不可能的,因为对象的析构过程中,派生类首先析构;而派生类析构后,基于多态的重载基础也就不存在了,所以基类在析构函数里无法调用派生类的实现。所以实现情况要稍微复杂一些。

 

这里先给出一个比较简单的实现。它基于:

1. 每一个资源管理类包含一个引用计数对象。该对象负责维护对象的计数。

2. 在资源管理类析构的时候,查询引用计数,如果为1,则释放资源,然后执行一般析构函数

3. 是赋值操作符中,也需要查询当前资源的引用计数,如果为1,则释放当前的资源,然后执行一般复制操作符

因为引用计数对象是资源管理对象的一部分,其他操会自然导致引用计数的增减。无需考虑。

 

下面是使用代码;

 

可以注意到,我们要处理的函数只有析构函数和赋值运算符。下面是引用计数类的一个直观的实现

 

细心的读者可能会注意到, 这个实现和“Ruminates on C++”中提供的一个实现相像,不必奇怪,就是从其中衍生出来的:)笔者也建议任何一个严肃的C++工程师仔细读这本小品书。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值