C++堆内存管理
- 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等智能指针来回收有堆分配的对象。
- 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。
- 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成员可以帮上忙。
- 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则可以用在需要引用计数的地方。
- 垃圾回收机制
智能指针可以有效的帮助程序员管理堆内存,但是需要显式的声明智能指针,但是向其他的一些语言如JAVA和python则完全不需要考虑回收指针类型,因为他们支持垃圾回收机制,而C++目前只支持最小垃圾回收机制。
垃圾回收的方法:
基于引用计数
引用计数的方法比较简单,在系统分配堆内存给一个对象后引用计数加一,当某一个对象释放堆内存后引用计数减一,直到引用计数为0,被分配给对象的内存则被回收。
优点:不会造成程序暂停,不会对系统缓存和交换空间造成冲击。
缺点:不能解决"环形引用"的问题,计数开销不小。
基于跟踪处理
基于跟踪处理的垃圾回收机制的基本思想是产生跟踪对象的关系图。
- 标记-清除
从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记,当标记结束后,所有的被标记的对象为可达对象或活对象,没有被标记的则被认为是垃圾,然后这些垃圾被回收。
缺点:活对象由于不会被移动则会产生大量的内存碎片。
- 标记-整理
此方法和和标记清除的方法一样,但是在标记完之后会将可达对象也就是活对象向左靠齐,由此解决了内存水平地问题。
- 标记-拷贝
此算法其实是标记整理的另外一种实现方式,它也有一些问题就是对的利用率只有一半,也需要移动活对象。
- 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分配的对象或其子对象的指针。
- 在解引用基础上的引用,比如:&*p。
- 定义明确的指针操作,比如:p+1;
- 定义明确的指针转换,比如:static_cast<void>(p).
- 指针和整型之间的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大小的内存存在有效的指针。
- C++最小垃圾回收机制的支持
C++11标准中针对垃圾回收的支持仅限于new操作符分配的内存,而用malloc分配内存则不予回收,程序员还是需要自己控制堆内存的回收。