首页 > 代码库 > C++堆内存管理

C++堆内存管理

C++堆内存管理

 

  1. auto_ptr的缺陷

在很早的C++98之前,C++用"auto_ptr"智能指针来管理堆分配的内存,它的使用非常简单:

auto_ptr<int> ap(new int(1024));

即将new操作返回的指针作为auto_ptr的初始值,不用调用delete即可实现堆内存的自动释放(如析构的时候)。

由于auto_ptr本身存在一些问题,它在C++11中被抛弃了。例如

1. auto_ptr不能共享指向对象的所有权,因为auto_ptr不含有赋值语义,而是转移语义,即对象控制权的转移。
2. auto_ptr不能指向数组。因为其实现中调用的是delete而非delete[]。
3. auto_ptr不能作为容器类的元素,因为不满足容器的要求,复制或赋值后,两个对象必须具有相同值。

取而代之的是unique_ptr、share_ptr、weak_ptr等智能指针来回收有堆分配的对象。

  1. unique_ptr指针

unique_ptr顾名思义即无法复制的智能指针,如下:

unique_ptr<int> var_ptr1(new int(11));

它不能与其他的unique_ptr指针对象共享所指向的内存,如下的表达式是不允许的:

unique_ptr<int> var_ptr2= var_ptr1;

但是可以通过:

unique_ptr<int> var_ptr2=move(var_ptr1);

将var_ptr1的所有权转移给var_ptr2。

这里unique_ptr和auto_ptr一样不能共享指向对象的所有权。简单的情况下使用unique_ptrk可以直接代替auto_ptr指针。为了解决auto_ptr不能共享对象内存是所有权的这一问题,C++11引入了share_ptr。

 

  1. share_ptr指针

share_ptr允许多个智能指针共享同一对象由堆所分配的内存,

share_ptr<int> var_ptr3(new int(12));

share_ptr<int> var_ptr4= var_ptr3;

在var_ptr3将内存释放后,

var_ptr3.reset();

即显式的调用var_ptr3.reset()后,var_ptr4所指向的为原来var_ptr3所分配的内存不受任何影响,而只是将指向这块内存的引用计数减一,如果引用计数减到0后,说明这块内存的所有者都不需要这块内存了,share_ptr才真正释放堆内存空间。

 

但是如何知道一个share_ptr智能指针的引用计数减为0了,也就是说如何判断share_ptr的有效性呢?weak_ptr智能智能的lock成员可以帮上忙。

 

  1. share_ptr指针

weak_ptr操作也很简单,如下:

share_ptr<int> var_ptr3(new int(12));

share_ptr<int> var_ptr4= var_ptr3;

weak_ptr<int> w_ptr= var_ptr3;

share_ptr<int> ptr = w_ptr.lock();

通过ptr是否为空即可判断share_ptr的有效性。

总而言之,unique_ptr在一般的情况下可以代替auto_ptr指针,而share_ptr和weak_ptr则可以用在需要引用计数的地方。

  1. 垃圾回收机制

智能指针可以有效的帮助程序员管理堆内存,但是需要显式的声明智能指针,但是向其他的一些语言如JAVA和python则完全不需要考虑回收指针类型,因为他们支持垃圾回收机制,而C++目前只支持最小垃圾回收机制。

垃圾回收的方法:

基于引用计数

引用计数的方法比较简单,在系统分配堆内存给一个对象后引用计数加一,当某一个对象释放堆内存后引用计数减一,直到引用计数为0,被分配给对象的内存则被回收。

优点:不会造成程序暂停,不会对系统缓存和交换空间造成冲击。

缺点:不能解决"环形引用"的问题,计数开销不小。

 

基于跟踪处理

基于跟踪处理的垃圾回收机制的基本思想是产生跟踪对象的关系图。

  1. 标记-清除

    从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记,当标记结束后,所有的被标记的对象为可达对象或活对象,没有被标记的则被认为是垃圾,然后这些垃圾被回收。

    缺点:活对象由于不会被移动则会产生大量的内存碎片。

  2. 标记-整理

    此方法和和标记清除的方法一样,但是在标记完之后会将可达对象也就是活对象向左靠齐,由此解决了内存水平地问题。

  3. 标记-拷贝

    此算法其实是标记整理的另外一种实现方式,它也有一些问题就是对的利用率只有一半,也需要移动活对象。

    1. C++最下垃圾回收机制

C++目前只支持最小垃圾回收机制,这其中最主要的原因是C/C++对指针操作的灵活性,当然这也是C/C++的特点和优势,因为这是的程序员可以直接操作内存,这也是为什么C++程序更加高效的原因之一,但是正是由于这个特点和优势使得C/C++要实现内存垃圾回收会存现一些"不安全的"状况,这导致了C++到目前为止还没有完全支持垃圾回收。

为什么说C++对指针的操作会导致垃圾回收时产生不安全的因素呢?看下式:

Int *p =new int;

P+=10;

p-=10;

*p=10;

在上面的操作中我们可以看出,在指针移动后,如果垃圾回收器被设计为这个时候回收p原来指向的内存,则会导致p再次移动回原来位置的时候指向了一个无效的地址(指针已经被回收了);后面的*p=10对这个无效的指针进行操作可想而知后果是什么,这就导致设计垃圾回收器的时候进入了一个两难的境地,如何设计才能保证内存垃圾被正确的回收,这就给C++的垃圾回收器的设计带来挑战,到底是从编译器端着手解决这些问题,还是其他方式,现在还没有定论。

正是由于这些安全的问题,导致C++目前为止只支持最小垃圾回收机制。最下垃圾回收机制针对提出了安全派生指针,它是指由new分配的对象或其子对象的指针。

  1. 在解引用基础上的引用,比如:&*p。
  2. 定义明确的指针操作,比如:p+1;
  3. 定义明确的指针转换,比如:static_cast<void>(p).
  4. 指针和整型之间的reinterpret_cast,比如:reinterpret_cast<intptr_t>(p)

     

    【查看编译器是否支持这个特性】,可以通过下式:

    Point_safety get_pointer_safty() noexcept

    如果它返回point_safety类型的值,如果值为pointer_safety::strict, 则表明编译器支持最小垃圾回收及安全派生指针,如果返回为pointer_safety::relax或pointer_safety::preferred则表明编译器不支持。

    【通知垃圾回收器不得回收某资源】可通过下面的接口实现。

    Void declare_reachable(void * p);

    即通知垃圾回收器某一资源为可到达,这样垃圾回收器就不会回收该资源。

    Template <class T> T *undeclare_reachable(T *p) noexcept;

    将资源的可达声明取消,垃圾回收器可见该资源,则可以回收该资源。

     

    【对大片连续内存的操作】有以下API实现

    Void declare_no_pointers(char *p,size_t n) noexcept;

    这个函数可以告诉垃圾回收器*p指向的n大小的内存不存在有效的指针。

    Void undeclare_no_pointers(char *p,size_t n) noexcept;

    这个函数可以告诉垃圾回收器*p指向的n大小的内存存在有效的指针。

     

    1. C++最小垃圾回收机制的支持

C++11标准中针对垃圾回收的支持仅限于new操作符分配的内存,而用malloc分配内存则不予回收,程序员还是需要自己控制堆内存的回收。

C++堆内存管理