1.资源与内存分配
资源的概念:资源是数量有限且对系统正常运转具有一定作用的元素。比如,内存,文件句柄,网络套接字(network sockets),互斥锁(mutex locks)等等
对于进程,这些资源都作为某种数据结构存储在内存中。
程序运行需要分配内存来管理以上资源,内存分配可以分为三类:
- 静态分配:如创建一个进程执行某段代码,需要加载该代码的代码段,数据段等数据到内存中,其中数据段包含已初始化的全局数据,可以称为是静态的内存分配
- 自动分配:进程内函数的调用和返回,以及其内部的局部变量创建和销毁,对应该进程高地址的入栈出栈,这个是操作系统自动处理的,无需应用程序控制
- 动态分配:静态数据和堆栈之前的空间(称为堆),可由应用程序动态分配,同时,也必须由应用程序释放。所谓的内存的动态分配与释放,通常讨论的是这种情况
以32位Linux环境的应用程序为例,每个进程可见的(虚拟)内存分布如下,C/C++常用的malloc/free, new/delete对应的内存分配释放都在.heap段内
2.动态内存管理的缺陷
我们在使用资源时必须严格遵循的步骤是:
- 获取资源
- 使用资源
- 释放资源
代码形式:
void UseResources()
{
// 获取资源1
// ...
// 获取资源n
// 使用这些资源
// 释放资源n
// ...
// 释放资源1
}
当代码量和复杂度达到一定程度,这种手动资源管理容易出错,且难以避免
例如C++使用new和delete时可能发生的一些错误是:
- 内存泄漏:例如,使用new分配对象,而忘记删除该对象,打开文件,忘记关闭文件等等
- 过早删除(或悬挂引用):持有指向对象的另一个指针,删除该对象,但是还有其他指针在引用它。
- 双重删除:尝试两次删除一个对象
3.RAII:将资源管理交给系统
- 自动内存管理,局部变量能在调用函数时分配,退出函数时释放
- 类是 C++ 中的主要抽象工具,将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务
RAII 就是基于以上思想,折中了全手动和全自动的内存管理,手动的选择管理哪些资源,自动的分配和释放资源。有效地实现了 C++ 资源管理的自动化
RAII(Resource Acquisition Is Initialization, 资源获取即初始化):
是80年代,Bjarne Stroustrup为C++发明了的范例。
具体实现方法:将资源的声明周期,绑定到对象的生命周期,即将资源分配和释放操作,包含到指定对象的构造函数和析构函数中,这些构造函数和析构函数在适当的时候由编译器自动调用,资源数据包含到对象的成员中。
一个简单示例:
(1)常规内存管理
#include <iostream>
using namespace std;
int main()
{
int *testArray = new int [10];
// Here, you can use the array
delete [] testArray;
testArray = NULL ;
return 0;
}
(2)RAII方式
#include <iostream>
using namespace std;
class ArrayOperation
{
public :
ArrayOperation()
{
m_Array = new int [10]; //构造函数包含资源的分配
}
void InitArray() //使用资源
{
for (int i = 0; i < 10; ++i)
{
*(m_Array + i) = i;
}
}
void ShowArray() //使用资源
{
for (int i = 0; i <10; ++i)
{
cout<<m_Array[i]<<endl;
}
}
~ArrayOperation() //析构函数包含资源的释放
{
cout<< "~ArrayOperation is called" <<endl;
if (m_Array != NULL )
{
delete[] m_Array;
m_Array = NULL ;
}
}
private :
int *m_Array; //成员变量包含资源
};
int main()
{
ArrayOperation arrayOp; //资源自动分配
arrayOp.InitArray();
arrayOp.ShowArray();
return 0; //资源自动释放
}
根据RAII对资源的所有权控制,分为常性类型和外部初始化类型
上述示例即为常性类型,也是最纯粹的RAII形式,最容易理解,最容易编码。获取资源的地点是构造函数,释放点是析构函数,并且在这两点之间的一段时间里,任何对该RAII类型实例的操纵都不应该从它手里夺走资源的所有权
外部初始化类型是指资源在外部被创建,并被传给RAII实例的构造函数,后者进而接管了其所有权。boost::shared_ptr<>和std::auto_ptr<>都是此类型
4.RAII的应用场景
常见的应用有:
- 文件操作
- 智能指针
- 互斥量
4.1文件操作
(1)常规形式
void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 在此处使用文件句柄f...代码 // 使用资源
fclose(f); // 释放资源
}
(2)RAII
文件类:
class FileHandle {
public:
FileHandle(char const* n, char const* a) { p = fopen(n, a); }
~FileHandle() { fclose(p); }
private:
FileHandle(FileHandle const&);
FileHandle& operator= (FileHandle const&); // 禁止拷贝操作
FILE *p;
};
FileHandle 类的构造函数调用 fopen() 获取资源;FileHandle类的析构函数调用 fclose()释放资源。请注意,考虑到FileHandle对象代表一种资源,它并不具有拷贝语义,因此将拷贝构造函数和赋值运算符声明为私有成员
使用:
void UseFile(char const* fn)
{
FileHandle file(fn, "r");
// 在此处使用文件句柄
// 超出此作用域时,系统会自动调用file的析构函数,从而释放资源
}
4.2互斥量
C++标准库提供lock_guard类实现mutex分配与释放,其实现就是RAII方式。
template<class... _Mutexes>
class lock_guard
{ // class with destructor that unlocks mutexes
public:
explicit lock_guard(_Mutexes&... _Mtxes)
: _MyMutexes(_Mtxes...)
{ // construct and lock
_STD lock(_Mtxes...);
}
lock_guard(_Mutexes&... _Mtxes, adopt_lock_t)
: _MyMutexes(_Mtxes...)
{ // construct but don't lock
}
~lock_guard() _NOEXCEPT
{ // unlock all
_For_each_tuple_element(
_MyMutexes,
[](auto& _Mutex) _NOEXCEPT { _Mutex.unlock(); });
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
tuple<_Mutexes&...> _MyMutexes;
};
使用多线程时,经常会涉及到共享数据的问题,C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过这意味着必须记住在每个函数出口都要去调用unlock(),也包括异常的情况,这非常麻烦,而且不易管理。C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在构造函数的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。上面的代码属于mutex头文件
4.3智能指针
先看一个例子,用RAII管理指针
#include <iostream>
#include <mutex>
#include <fstream>
using namespace std;
enum class shape_type {
circle,
rectangle,
};
class shape {
public:
shape() { cout << "shape" << endl; }
virtual void print() {
cout << "I am shape" << endl;
}
virtual ~shape() {}
};
class circle : public shape {
public:
circle() { cout << "circle" << endl; }
void print() {
cout << "I am circle" << endl;
}
};
class rectangle : public shape {
public:
rectangle() { cout << "rectangle" << endl; }
void print() {
cout << "I am rectangle" << endl;
}
};
// 利用多态上转,如果返回值为shape,会存在对象切片问题。
shape *create_shape(shape_type type) {
switch (type) {
case shape_type::circle:
return new circle();
case shape_type::rectangle:
return new rectangle();
}
}
class shape_wrapper {
public:
explicit shape_wrapper(shape *ptr = nullptr) : ptr_(ptr) {}
~shape_wrapper() {
delete ptr_;
}
shape *get() const {
return ptr_;
}
private:
shape *ptr_;
};
int main() {
// 第一种方式, 手动管理指针
shape *sp = create_shape(shape_type::circle);
sp->print();
delete sp; //显式delete
// 第二种方式, RAII管理指针,一般封装到函数,更快释放
shape_wrapper ptr(create_shape(shape_type::circle));
ptr.get()->print();
return 0;
}
C++标准库的智能指针:auto_ptr(C++11弃用), unique_ptr,shared_ptr, weak_ptr
可以参考WindSun:详解C++11智能指针
4.4实现自己的RAII类
一般情况下,RAII临时对象不允许复制和赋值,当然更不允许在heap上创建,所以先写下一个RAII的base类,使子类私有继承Base类来禁用这些操作:
class RAIIBase
{
public:
RAIIBase(){}
~RAIIBase(){}//由于不能使用该类的指针,定义虚函数是完全没有必要的
RAIIBase (const RAIIBase &);
RAIIBase & operator = (const RAIIBase &);
void * operator new(size_t size);
// 不定义任何成员
};
要写自己的RAII类时就可以直接继承该类的实现
template<typename T>
class ResourceHandle: private RAIIBase //私有继承 禁用Base的所有继承操作
{
public:
explicit ResourceHandle(T * aResource):r_(aResource){}//获取资源
~ResourceHandle() {delete r_;} //释放资源
T *get() {return r_ ;} //访问资源
private:
T * r_;
};
将Handle类做成模板类,这样就可以将class类型放入其中。另外,ResourceHandle可以根据不同资源类型的释放形式来定义不同的析构函数。由于不能使用该类的指针,所以不使用虚函数。
5.GC和RAII
在没有RAII的时代,GC和非GC语言是水火不容,GC追求开发效率和稳健设计,非GC如C++最求极致性能和绝对控制。RAII的设计机制,兼顾了两者的优点。
如果用三个等级代表程序员对系统资源的使用权限,如下:
- 动态分配:C++的new/delete之类,程序员100%负责内存使用和释放,编译器、操作系统不额外干预
- 垃圾回收(GC):java/go语言之类,程序员只负责要内存,而不用管,也管不了内存释放,其由该语言运行环境管理,规则可以描述成:如果一个资源没被任何对象使用(例如没有指针指向它),运行环境定时或者其他方式检测到后,自动释放该资源,该过程对程序员不可控。可以说程序员有50%的权限,即想要就能要,但想甩却不能甩
- RAII:程序员负责资源编排,运行时的分配与释放由系统自动完成,可以说程序员有90%的权限,放权10%给系统
小结
RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。
具体实现:
- 资源在构造函数中获取
- 资源在析构函数中释放
- 资源是类的成员变量
- 类的实例是堆栈分配的
相关文章
C++那些事:RAII