C++: 资源管理的注意事项

资源管理

C++编程中最常见的资源就属于内存了。资源在我们使用完毕之后,是需要归还给系统的,让系统知道这部分资源当前没有被占用,是可以重新利用的。一旦资源没有归还给系统,系统会认为这部分资源一直被占用而实际却未被使用,这就是我们常说的内存泄露。严重的内存泄露会导致系统的可用内存越来越少,左后导致系统崩溃等严重问题。

因此,当资源使用完毕时,需要及时归还给系统!但考虑到代码从编写到上线再到最后的运维之中会发生很多种情况,完全确保这个条件是非常困难的。但一些基本的方法,还是可以尽量减小这类情况的发生。

用对象管理资源

考虑一种实际中常见的用法:我们使用new 申请一块动态内存,然后用delete 去释放这块内存。 理想地情况下,这样是完全没问题的。但是,在我们申请内存到释放内存之间,我们一般会在这块内存上进行数据操作。这些操作,可能会导致delete 操作未能正常执行, 从而造成内存泄露的情况发生。导致这种情况的发生可能有以下几种情况: 过早的return; 抛出异常等。

class A { ... };

A* a = new A{};
// some operations
...
delete a;

一种解决方案是利用C++语言自身的特性来解决这个问题。C++ 函数内变量的局部变量申请的内存一般在栈上,当局部变量离开作用域时, 会自动释放。如果我们用一个局部变量来管理我们动态申请的内存,让其自动释放我们申请的动态内存,那么这个问题就可以初步的解决了。那么问题就是定义一个变量在销毁时自动执行某些操作。 而这一需求恰恰可以通过类对象的自动析构来实现。我们只需要在类中保留我们申请的内存的指针,在类的析构函数中对这个指针执行delete操作即可。

幸运地是,C++提供了一些类似功能的类----智能指针。它们包括:auto_ptr, shared_ptr, uniqu_ptr。 其使用情况类似如下:

std::shared_ptr<A> a_ptr(new A());

以对象管理资源,就是在获取资源的时候立即将其用来初始化对象,即RAII(Resource Acquisiton Is Initialization)。此时不论控制流怎么离开,一旦该对象离开其所在的区块则会自动调用析构函数,从而释放所管理的资源。(当然,不要在析构函数中抛出异常)。

使用智能指针,有时候会发生二次释放的未定义行为,因此要注意选择合适的智能指针! 智能指针在其内部调用delete来释放对象,这也就意味着,当指针指向的是数组类似的对象时,并不能理想地调用delete [ ] 来进行释放操作,因此智能指针也不是万能的。

建立自己的资源管理类

之前的描述中,针对于分配在堆上的资源,我们可以通过智能指针来进行自动释放。但是,也存在一些情况,并不是通过内建的智能指针就能合理地自动释放资源,此时就需要建立我们自己的资源管理类。这样的类在设计时,存在几个注意点,可以避免一些意外情况的发生。

copy 行为

下面是一个简单的例子,并不严谨,仅做示例。

class A;
class A_manager
{
public: 
	A_manager(A* a) { a_ptr = a; }
	~A_manager() { delete a_ptr; }
private:
	A* a_ptr;
}

A* a = new A{};
A_manager A_1(a);
A_manager A_2(A_1);

在上述情况下,按照执行顺序,这里应该会发生二次释放的行为。面对这种情况,一般可以做出两种选择:禁止复制和根据条件判断是否实行真正的释放操作。

禁止复制操作,只需要在类中加工copy构造函数和赋值构造函数声明为private 就可以了。这样就能避免拷贝操作。如果选择第二种方案,常见的方法就是拷贝操作进行引用计数,如果引用计数为1, 则释放, 反之则不。

其实, 之前的shared_ptr执行的就是引用计数的方案。但可惜的是,它析构的时候调用的是delete操作,这在一些情况下是不适用的。但是,它与auto_ptr不同的是,它可以指定删除器(deleter)。 这里的deleter 是在引用数为0时执行的操作,默认的操作是delete操作符,它可以是一个函数或者一个函数对象。如果资源管理类中使用了智能指针,没必要早实现构造函数。

class A;

void deleter_A(A* a)
{
	delete a;
}

class A_manager
{
public: 
	A_manager(A* a):a_ptr(a, deleter_A) {}
private:
	std::shared_ptr<A> a_ptr;
}

当然,如果确实需要进行拷贝,则可以有以下选项:
也存在一些情况,我们可以进行深度拷贝,这样析构时释放的只是某个副本。深度拷贝后的对象仍旧存在。或者为了保证只有唯一的副本,我们可以选在适当的时候转移资源的拥有权(类似 unique_ptr)。

访问原始资源

智能指针 shared_ptr, auto_ptr 都提供了对原始资源的调用: 都可以使用get 函数获取原始资源的指针,从而进行相关的操作。同时, 它们也重载了常见指针的操作符: -> 和 * 。

我们自己设计的资源管理类,应该也提供合理的方式用以获取原始资源的指针。隐式转换和显式转换都是合适的选择。 显式转换能极可能减小出错的几率,更俺去眼一些; 隐式转换对用户来说非常方便。

new 和 delete 要呼应

如果不采用智能指针来进行资源的释放,而是采用new 和 delete 操作符来手动进行资源释放。那么需要进行注意的就是: 如果使用new 一个类似数组一样的一组对象,则需要使用 delete [] 来进行释放; 如果只是用new 生成单个对象,则只需要调用delete 进行释放。如果两者发生匹配错误,均会发生未定义的行为。

int* a  = new int(); 
delete a;

int* b = new int[10]();
delete [] b;

new出来的对象以独立语句置入智能指针

智能指针的初始化必须是显式的, 不能执行隐式的转换操作。

int fun(){ return 0; }
void foo(std::shared_ptr<A> a, int a){  }

 // 错误,不能执行隐式转换
foo(new A(), fun());

// 能通过, 不安全, 函数的执行中,参数中的解析顺序是未定义的,因此可能造成资源泄露
foo(std::shared_ptr<A>(new A()), fun()); 

// 独立语句置入
std::shared_ptr a(new A());
foo(a, fun());
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值