C++的内存管理

转自http://www.easycpp.org/content/science_doc/c的内存管理

 

摘要:C++是一种流行且功能强大的程序设计语言,利用C++已经产生出世界上各异的程序软件包。C++是在C语言的基本之上发展而来,它能够对C语言向下兼容,所以它也C语言的许多不足,其中内存管理就是社区中大家反应很大的“遗憾”。

概述

 

内存是计算机系统最重要的资源之一,要是管理不当会引起许多的问题。当然对整个系统的内存管理是操作系统的功能,这里我们不去关注这一点,这里我们关注是一般程序的内存管理,相对而言操作系统对内存管理是非一般的。

内存管理顾名思义就是程序对自身内存资源使用的管理,内存资源存在一个申请、使用、释放的过程,对内存的管理就是对这个过程的按排与统筹,从而实现正确、高效的内存使用。

为什么要内存管理

 

在许多的其它语言中内存管理是由运行时系统自动完成的,这个特点也是许多语言相比较C++语言经津津乐道的高级特性。C++对内存管理是半自动的,也就是说有些内存的使用是系统自动管理的,而同时也有一些内存是系统不管理的,用户必需自己手工去管理。

让我们看看C++系统内存的建立的模型。C++中存在五类不同却相互补充的内存形式,即全局静态存储区、堆存储区、栈存储区、自由存储区和常量存储区。这五类存储区域系统提供了不同的支持,分别用于不同情况。对于用户的内存管理主要是对于堆存储区和自由存储区域的,而自由存储区域是C++从C中继承而来,社区里极力反对使用它,事实上C++程序员很少使用或根本不用,对于其它三种存储区都是系统自动管理的,用户很少需要去手工管理,但是用户却需要在几种内存区域里选择一个合理的区域。

先让我们看两个例子,通过这个大家可以感性地感觉到内存管理的必要性和必需性。

While(1000*1000)

{

Int * p=new int;

(对 p或*p一些操作)(可能产生异常,从而产生内存泄漏)

Delete p;

}

这个例子中会看在一个大的循环我们从堆中分配了一个,然后对它做了一些事,最后把它释放了。我们不防对比一下这个例子与下面的例子:

While(1000*1000)

{

Int p;

(对 p或&p一些操作)(就算有异常,也是异常安全的)

}

这个例子与上面的例子最大的区别就是一个是从堆中分配内存,一个是从栈中分配内存,其它的他们没有区别在功能上它们完全做到一样。然而有经验的C++程序员都偏向于使用后者。因为前者较之后者容易引起如下问题:

在堆是分配与释放效率是比栈中慢许多,如果大的循环中,这种慢会被放大,从而严重危害运行效率。

在堆中分配的内存必须手工管理,也就是说必须手工显式地去释放,系统不会自动去释放,从而容易孳生内存泄漏——C++的一个雷区。

不是异常安全的,当发生异常时,即使我们没有忘记释放内存,可是流程没有到释放的地方可能就因为异常而返回。

通过这个例子的对比我们发现不同的选择、不同的处理方法会引起不同的效果,有的会更少出现问题,有的却问题总是难以避免。从而内存区域的选择与具体内存区域的使用上就存在许多注意点,同时产生许多内存的使用的习惯,这就是为什么要管理内存的原因。

如何管理内存

 

内存管理的目的有两个,一个是让程序正确,这是一个基本的问题,因为C++程序可能存在内存泄漏,让程序正确是首要的目的。另一个目的就是让程序高效,这是一个相对次要的问题,因为程序首先要是正确的,然后才是性能问题。

C++社区里对内存的使用已经形成一大套有效且可行的办法。由于程序正确是首要的,这里我们主要讲述让程序正确的设计哲学和惯用法。

内存管理最境界——不去管理

 

内存管理的最高境界就是“不去管理”,把系统能够处理的全部交给系统处理。正如上面的例子一样,我们完全可以不用堆,使用栈完全可以更高效地完成相同的事务,所以这时我们就应该使用堆,而应该毫不忧郁地选择栈,不仅因为栈比堆高效,更因为栈上内存是系统自动管理的。可能有要问,那我们可以把所有内存的都在栈上申请,这个想法是不现实的,一方面系统的栈内存要比堆内存小得多,可能存在一些非常大的数据需要大量的内存,栈内存可能不够,这时只有使用堆了。另一方面,栈内存的生命期也是系统自动管理的,有是可能让内存的生命期穿过多个作用域,栈内存使用起来就不如堆内存了,相应的管理也不见得比堆内存管理方便,因为这时似乎在与系统反着做事一样。

“不去管理”的方法有许多。最重要也是被社区广泛推荐的方法就是使用库,把内存管理的任务交给库。C++标准库就是一个最重要的库,熟练使用这个库,可以有效地减少打工内存管理的情况,从而间接实现“不去管理”的目标。

例如:我们现在需要写一个函数,它要把用户从标准输入中输入的文字传入程序里以供进一步处理。如果我们使用标准库中string类将会有效地降低问题的复杂度,同时也不用我们手工去管理内存,内存的管理已经由string的实现者完成了。如果我不使用string类,反而向C语言的方法处理,我们使用指向字符的数组或指针,这时内存管理就是必须的了,因为我不可能知道用户的输入有多大,所以这就必须从堆中分配内存,从而不免产生一系列问题,就算没有问题产生,所以作出的努力也不能与前相比。

内存管理惯用法——RAII

 

“不去管理”哲学已经深入人心,其中著名的RAII惯用法在社区里已经广泛传播。RAII是英文“resource acquisition is initlization”的首字母缩写,意思是“资源获取就是初始化”。

这里的资源泛指系统的各种各样的资源,如内存、锁、文件等等。解释一下这句话的意思就是把资源的获取与对象的初始化相联系,把资源获取转换为对象的初始化,从而可以把系统于对象的支持转换为对资源管理的支持。让我们先来看一个例子。

foo()

{

Object *p=new Object;

(对 p 做一些事)

Delete p;

}

这个例子是一个经典的例子,这样代码广泛地存在世界的每一个地方,初一看上去,这个并没有错误,可是仔细推敲一下就会发现大量不足,最大的不足就是,这样做不是异常安全的,并且有时可能产生代码冗余,就好比下面的样子:

foo()

{

Object *p=new Object;

(对 p 做一些事)

(--》异常发生,p 指向的内存没有释放)

If(something)

{

Delete p; (每个出口都需要,复杂又易错)

Return 0;

}

(对 p 做一些事)

Delete p;

}

如上面的例子,函数可能存在多个出口,这样在每个出口都要加上delete 语句,这样即烦琐又不安全,程序员可能会不能承受这样的复杂与多变,从而埋下错误的根源。同时如果在处理的过程,如果发生异常,那么p所指向的内存将不会释放,从而导致程序错误,内存泄漏已经发生。这样做不足已经看过,再让我们看看RAII的做法。

Class object_piont

{

Object *p;

Public:

Object_piont():p(new object){}

~object_piont(){ delete p;}

(其它函数的定义,重载与指针相关的重要运算符)

};

foo()

{

Object_piont p;

(对p做自己想做的事)

(这里我不需要在担心内存泄漏,也不需要在每个出口释放内存,因为内存释放已经

委托给析构函数了,而析构函数是系统自动调用的。)

}

让我们来分析一下这样做法的特点。首先它把内存的获取放在对象的初始化里完成,这正是RAII的名字由来,“资源获取就是初始化”。经过这样处理之后就可以做到异常安全与内存管理的自动化,因为建构函数与析构函数都是系统自动完成的。

foo()

{

Object_piont p;

(对 p 做一些事)

(--》异常发生,p 指向的内存会自动释放)

If(something)

{

//Delete p; (出口不需要再释放内存,系统会自动完成)

Return 0;

}

(对 p 做一些事)

Delete p;

}

上面的例子解释了什么是RAII惯用法,如何实现RAII惯用法。其实RAII惯用法是如此重要,以至于标准库都已经提供了直接的支持,在 C++1998-2003的标准里存在一个智能指针auto_ptr ,这经个智能指针就会像上岸的例子一样完成自己的任务,当然这是一个模板类,无能哪种对象都可以轻松产生出相应的指针类型。事实上,auto_ptr设计上存在许多不让人满意之处,所以还存在大量第三方库提供了不同类型的智能指针。其中最著名的就是boost库提供的智能指针库,并且在新C++0x标准中已经把这个库加入了标准。可见智能指针是多重要。当然智能指针只是RAII的思想了一个实现,我们在自己设计过程要时刻不忘这个思想方法,它可以让我们的设计更优秀,实现的中产生错误的机会更小。

使用内存池,让内存使用变得更高效

 

我们知道使用的分配与释放是一个消耗CPU的事情,这个过程可能需要上下文切换,所以它的时间级别是毫秒级的,相对栈的时间相差许多倍。如果一个程序大量使用了堆内存的话,并且平凡地分配、释放,这样的话内存的分配与释放就会影响到系统的性能和程序执行的效率上。所以为了在堆上高效地使用内存,人们开发出内存池技术。

其实堆上内存分配与释放的低效在于可能的上下文切换与中断处理,如果减少需要的上下文切换与中断的话,就可以提高效率,实现快速的分配与释放堆内存。内存池技术在于一个内存池管理模块,这个模块在初始化的时候会向系统堆中申请大量空余的内存,而其它模块需要内存的时候不会向系统申请,而向这个内存池模块申请,这样就省去了操作系统的中断与上下文切换处理,从而提高了运行效率与运行速度。

当然如果其它模块需要的内存比内存池模块拥有的内存少,内存的分配是不需要操作系统参于的,不过可能会一些内存的始终没有用上,从浪费一些内存,可是对于这个空间浪费也是必须的,牺牲空间换来时间在程序设计中是常有的事;可是如果其它模块需要的内存大于内存池所有的容量,那么内存池模块会向操作系统申请内存,这也会产生中断与上下文切换,不过这个机会要大大小于没有内存池的时候,对系统性能的影响不是很大。

内存池实现对于大多数来说都不是需要的,因为已经有许多做过这个事了,在大名鼎鼎的boost库中就有这样一个内存池库,在大名鼎鼎的apach网络服务器的背也有他们实现的内存池,就是在GNU libc中也有这样一个功能的库。当然后两者都C语言的库,不过在C++中可以正常使用,前者却是正统的C++库,一般比较推荐这个。除了这三个以外,其实还大量存在其它的实现,不见如此有名,但也是多种多样。

结束语

 

C++内存管理是程序对内存申请、使用、释放过程的按排与统筹,系统会为我们做好多数的事,只有一些情况下需要手工去管理内存,需要手工管理的内存主要是堆内存。社区里已经堪一套有效的内存管理惯例思想方法。不去管理内存,把所以的事都交给系统是内存管理的最境界,但有时手工管理也是难免的,这时我们要优先选择库,再接着自己去设计一个RAII的类,最后才会考虑其它的情况。

C++的内存管理是一个比较深入的课题,相对那些可以完全自动管理内存的语言,这一点好像要遗憾一下。许多新手都在感言,要是C++可以自动管理内存的话该多好,其实有许多都在尝试。垃圾回收在C++社区里一直有讨论,可是社区里的重量级人物一般都不看好垃圾回收,一方面它C++中的析构机制不兼容,另一方面C++某种意义还是一个中级语言,它直接提供指针以及整数与指针的转换,所以垃圾回收的算法等方法要比其它语言复杂多变与不确定,更重要的是 C++提供给内存管理的机制已经足够强大,如果运用得当,不见得要多少精力去手工管理内存。要是一个程序里面这样的手工管理内存机会太多,可能映射出设计上的缺陷与不足。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值