浅谈C++中的资源管理

时间:2023-01-06 23:58:39

C++的复杂是一个基本事实,这也成了很多人对C++横加指责的原因。事实上,正如陈皓在“C++的数组不支持多态”?这篇文章中提到的,很多人在并不真正了解C++的情况下,就喜欢得出这样的结论。更有甚者,把C语言本身的“坑”也归结为C++的问题。这样的人着实不少,C++11作为最具争议的语言之一,每一次讨论到涉及语言选择的时候,都会引起一场“血战”,但结果往往不了了之,喜欢C++的继续坚守C++阵营,讨厌C++的把精力留到下次黑C++的时候。对于客观公正批评C++的,我内心尊敬佩服;而对于还没搞清楚C++就信口开河的,我表示鄙视。任何一门语言都有自己的历史背景和定位,C++被设计成这样,从历史上来看,是为了兼容C,使得C程序可以不用修改就可以继续使用;从定位上看,就是三大约束:与C的完全兼容、静态类型检查、最高性能。我真心的希望,如果有人不喜欢C++,在搞懂它之后再黑,免得误人子弟。

每次写C++的优点之前,都想好好发泄一下,好了,言归正传。这篇文章想探讨下C++中的资源管理,谈到资源管理,就不得不谈异常安全,正是因为有了异常,才使得资源管理变得更加重要。C++11提供了一套非常好的编程Idom来处理这个问题,C++11的新特性使得这些Idom变得更加易用。

计算中的资源是个非常广泛的概念,内存、锁、文件、Socket等等都是资源,C++中可以通过统一的方式管理这些资源,即RAII(参见The C++ Programming Lauguage, Special Edition, p364, 14.4节)。其基本思路非常简单,用类来封装资源,在类的构造函数中获取资源,在类的析构函数中释放资源。使用的时候,把这个类在栈上面实例化出一个对象,当这个对象超出作用域时,这个对象的析构函数会被调用,从而释放资源。正是这个简单的方式,构成了C++资源管理的基础,并且这样的方式是异常安全的,因为:1、如果在对象构造之前发生异常,则资源还没申请,不会有问题;2、如果在类的构造函数中发生异常,C++编译器保证资源不会发生资源泄漏(参见Exceptional C++, p26, Item8);3、在对象构造好之后发生了异常,stack unwinding(参见The C++ Programming Lauguage, Special Edition, p355, 14.1节)的过程中,C++标准要求编译器保证当前栈上面成功构造的对象的析构函数一定会得到调用,内存一定得到释放。

智能指针就是RAII的实现范例,专门用来管理内存,C++11中有三个智能指针:unique_ptr、shared_ptr和weak_ptr。auto_ptr已经是过时的了,它的功能被unique_ptr取代了,后者可以用于STL容器。

对于其它资源,需要用户自己去封装,同样的资源只要封装一次,以后使用起来就方便了。如果嫌每个资源都要用类包装起来麻烦,可以利用ScopeGuard来处理,这个设施由Andrei Alexandrescu发明,刘未鹏在C++11(及现代C++风格)和快速迭代式开发中做了详细介绍。当然了,ScopeGuard在C++11(得益于std::function和lambda表达式)下,才达到了非常易用的地步。我这边贴一下SocpeGuard的代码,有兴趣的可以参考刘未鹏的文章。

class ScopeGuard
{
public:
explicit ScopeGuard(std::function<void()> onExitScope)
: onExitScope_(onExitScope), dismissed_(
false)
{ }

~ScopeGuard()
{
if(!dismissed_)
{
onExitScope_();
}
}

void Dismiss()
{
dismissed_
= true;
}

private:
std::function
<void()> onExitScope_;
bool dismissed_;

private: // noncopyable
ScopeGuard(ScopeGuard const&);
ScopeGuard
& operator=(ScopeGuard const&);
};

使用起来很简单,在以C的方式申请一个资源的时候:

HANDLE h = CreateFile(...);
ScopeGuard onExit([
&] { CloseHandle(h); });

这样申请资源和释放资源永远写在一起,不会忘记,而且保证资源永远不会泄漏。

说到资源泄漏,当然想到内存泄漏了,Visual C++中可以这样的方式来检测内存泄漏:

在cpp文件开始处,添加这么一段代码:

#ifdef _DEBUG
#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define DEBUG_CLIENTBLOCK
#endif // _DEBUG
#define _CRTDBG_MAP_ALLOC
#include
<stdlib.h>
#include
<crtdbg.h>
#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif // _DEBUG

在某个函数中,添加这么一个函数调用:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);

这样一来,在程序在Debug模式运行结束后,如果有内存泄漏的话,会打印出类似如下的检测信息。

Detected memory leaks!
Dumping objects
->
{
197} normal block at 0x00FBCE98, 44 bytes long.
Data:
< > 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.

谈了C++资源管理的好处,来吐槽下Java。Java是个喜欢喊口号的语言,“一次编译,到处运行(Write once,run anywhere) ”,“程序员再也不用担心内存泄漏了”,“纯粹的面向对象语言”,诸如此类。其它先不说,就谈谈它的资源管理。Java中对资源的管理分为两种,一是内存,通过gc管理;二是其它资源,通过try、catch和finally来管理。第一个问题,不统一,Java里面不统一的地方还有很多,比如基础数据类型不能作为模板参数、明明是“纯粹的面向对象语言”却还有基本数据类型等等。第二个问题,try、catch和finally语法太丑了,而且最不爽的是在finally里面还要在try一下,太蛋疼了!第三个问题,内存被JVM管理了,导致gc时机不确定,严重影响Java的普及范围(在一个大型游戏中,正打boss呢,突然gc了,然后,然后就没有然后了)。我承认,Java是个很好用的语言,能很大程度提高程序员的开发效率,特别是JavaEE体系,的确很成熟。我自己也写过不少Java代码,最方便的地方是出错了以后,异常栈能够快速帮助我定位到bug。但是Java因为屏蔽了太多底层信息,导致它培养了一大批不太合格的程序员。这里不是说Java程序员不优秀,只是如果一直用Java,不去了解C/C++的话,真的很容易退化。