C++中指针的使用在为语言提供极大的灵活性的同时,也使得内存管理变得极其复杂。如何利用好指针,并在工程实践中组织好高效的内存管理策略,是一个C++项目按时交付、并取得成功的关键。
本文将从理论和实践上分析,如何通过良好的类的设计策略,来实践一个可在工程中使用的有效的内存管理方案。
本文的组织结构如下:
1:首先,以面向对象的观点,从总体架构上分析,一个有效的内存管理策略应该达到一个怎样的效果。
2:接着,从类层次出发,讨论针对类的设计的准则和策略。
3:然后,再深入细节,阐述实践中可以使用的设计方法和需要注意的一些设计细节。
4:最后,再一次回到宏观的架构上,谈谈在一个实际的项目中,如何根据项目的实际情况,权衡得失,并考虑到运用自定义的内存管理自身的利弊,从而来选择真正合适具体项目的实现方案。
1:怎样才算是一个有效的内存管理方案?
C++中一个核心的概念就是面向对象。面向对象的思想告诉我们:一个项目的基本组成元素是类的对象。而项目功能的实现是通过对象之间的发送和接收消息来实现的。所以,在高层的设计中,一个理想的main函数以及其他大部分的函数中,我们应该只会看到类对象和消息(或称 接口、函数)的存在,而并没有指针的概念的存在。即:将指针的使用隐藏在类的实现的背后!
指针的使用是为了提高程序的灵活性,而这一灵活性的实现具体就体现在动态内存的分配上,而一说到动态内存分配问题,则内存管理的问题也就接踵而来。由此可以,指针和内存管理这两个属于低层设计的概念是与生俱来的被联系在一起的。要实现合理的内存管理的过程,也就是实现合理的指针的封装和使用的过程。
2:用类来封装指针,隐藏内存管理的细节
类的设计的重要职能就是:提供高层抽象,隐藏实现细节。
客户在使用一个类的时候,关注的是它所提供的功能,而对于类的内存布局、实现细节等,一概不需要知道。客户甚至不应该知道类的对象是怎样创建的(是堆上还是栈上)。
注意:我们这里所说的客户主要指的是类的调用方程序代码。为了实现这一职能目标,类的设计应该满足以下准则:
(1)RAII: Resource Acquisition IsInitialization
(2)类关注的是其成员变量的内存分配和释放,而无需关心类自身的内存分配和释放。
这是一个递归的定义:类的成员变量可以是一个类对象,而类本身所产生的对象也可以作为其他类的成员变量。那么,这一递归在啥时应该终止呢?
答案是:我们所要实现的功能在哪个抽象层上,那么相应的类就定义到该抽象层上为止。即:在该抽象层上的类已经包含了我们要实现的功能的完整的描述,所以可以直接使用,而不需要作为其他类的一个成员变量。
(3)用栈对象来封装堆对象
智能指针是这一准则的一个实作。
这里给出一个标准,可以用来判断你是否很好地使用了上述三个准则:new/delete,malloc/free只会出现在类内部的构造/析构函数以及栈对象的初始化中,而在程序实现的业务逻辑中不会出现显式的动态资源分配。
当然,这一标准也不应该绝对化,但我们应该尽量向之看齐。
3:可用的实现方法和技术
(1) 重载operator new/delete
(2) 在constructor/destructor中完成资源的分配和释放,并根据需要重载/禁止 复制/赋值 构造/析构 函数。
(3) 智能指针(引用计数、所有权的转移)
(4) 从严格意义上来说,每个类都有属于为自己而设计的resource allocator,以针对类自身的特点,给出相应的资源分配方案。
如:针对大量的小对象,可考虑使用资源池。
4:具体问题具体分析
(1) 项目有大有小
如果项目很小,或者类比较少,又或者动态分配资源的情况不多,则引入复杂的类的内存管理可能不是必需的。而如果项目较大,则应该给出完整的内存管理方案。
(2) 使用第三方库的利与弊
Boost中的shared_ptr,scoped_ptr提供了可用的智能指针,可以为己所用,但也要考虑项目的扩展性以及后期维护的成本。引入第三方库,也同时意味着引入了外来的不确定性,这会影响项目的进度预算的可信度。
(3) 实现内存管理的代码自身所引入的错误
为了实现完整的内存管理,这势必增加代码量。这里要记住的是:增加的工作量永远都是值得的,但是前提是 一定要确保内存管理代码本身的正确性!