c++智能指针与垃圾回收

公众号:智能指针&垃圾回收
c++内存管理交给了程序员,这种显示的内存管理在性能上有一定的优势,但是相对于其他语言,总会碰到一些如内存异常退出的问题,以及程序占用内存越来越多的问题。通常来说,这些问题主要有以下几类:

  • 野指针
  • 重复释放
  • 内存泄露

因此,很多程序员认为编程语言应该提供更好的机制,让程序员摆脱内存管理的细节。在c++中,这样的机制就是智能指针。

c++11的智能指针

C++98中,智能指针通过一个类型模板“auto_ptr”来实现。auto_ptr以对象的方式管理分配的内存,并在适当的时间(比如析构)释放所获得的堆内存。

auto_ptr(new int)

虽然auto_ptr可以在一定程度上避免堆内存忘记释放的问题,但是有几个缺点:

  • 拷贝时返回一个左值
  • 不能调用delete[]

所以在c++11中auto_ptr被废弃b,改用unique_ptr, shared_ptr, weak_ptr等智能指针来自动回收堆分配的对象。

unique_ptr

如其名,unique_ptr是unique的,不和其他unique_ptr共享对象,不能被赋值,只能通过move函数。

unique_ptr<int>p1(new int(11));`在这里插入代码片`
unique _ptr<int>p2 =p1;        错误,编译无法通过
unique _ptr<int>p3=std::move(p1);  //p3成为数据唯一的unique_ptr指针,p1指针失效
p1.reset() ;                //不会导致运行时错误
p3.reset();                //显示的释放内存

从实现上讲,unique_ptr是一个删除了拷贝构造函数,而保留了移动构造函数的指针封装类,仅仅可以使用右值对unique_ptr对对象进行构造,而不能赋值拷贝,一旦构造成功,右值对象的指针就失效了。

shared_ptr

如其名,shared_ptr允许多个智能指针同时拥有同一堆分配对象的内存。在实现上,shared_ptr采用了引用计数的方式,当一个shared_ptr指针销毁的时候如调用reset函数,只是对其拥有的对象内存的引用计数减一,使当前的智能指针悬空而不会释放堆内存,只有当引用计数归0时才会真正释放堆内存所在的空间。
如何安全的使用shared_ptr
我们知道shared_ptr是强引用,只要有一个指向对象的shared_ptr存在,对象就不会被析构。如果不小心遗留一个拷贝,那么对象就永远不会被析构。

void f(shared_ptr<int>,int);
int g();
void bad(){
    f( shared_ptr<int>(new int(2) ), g());
}

bad函数是安全的吗?为什么该函数会有内存泄漏的可能性?由于函数参数以未指定的顺序进行计算,因此可能首先计算新的int(2),然后g()。如果g函数抛出了异常,我们可能永远也无法访问到shared_ptr构造函数。正确的做法应该是:

void ok(){
    shared_ptr<int> p( new int(2) );
    f( p, g() );
}

另外,在std::bind函数中,用来将可调用对象与其参数一起进行绑定。绑定后可以使用std::function进行保存,并延迟到我们需要的时候调用。这样,若果参数是shared_ptr,那么对象的生命周期就不会短于std::function对象,因为std::function对象持有了shared_ptr的一份拷贝,可能会不经意间延长对象的生命周期。

有时候我们需要将shard_ptr作为函数的参数传参,多数情况下我们可以将shard_ptr作为常引用的方式传递参数,这样就避免了额外的拷贝开销。

Void passPara(const shared_ptr&sp)就可以避免反复拷贝shared_ptr导致的性能问题。

避免环形引用

由于shared_ptr采用引用计数的方式,不可避免的会出现环形引用的问题。通常这种问题的解决方式是用weak_ptr,即owner持有child的shared_ptr,而child持有owner的weak_ptr。那么什么是weak_ptr呢?

weak_ptr

weak_ptr更复杂些,weak_ptr只是指向对象的堆内存,但是并不拥有该内存。weak_ptr操作永远不会抛出异常。以下代码线程安全

void check(weak_ptr<int>p){
shared_ptr<int> sp=wp.lock();
if(sp!=nullptr)
cout<<”pointer is valid\n”;
else
cout<<”pointer isinvalid”;
}

weak_ptr的lock()函数可以返回一个指向内存的shared_ptr对象,如果该对象无效时,则返回nullptr,这可以非常有效的判断对象的有效性。但是在多线程环境下,通过指针判空有效吗?shared_ptr是线程安全的吗?

线程安全

shared_ptr本身的引用计数虽然是安全且无锁的,但是并不是百分百线程安全的。参考https://www.boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_thread_safety。

垃圾回收

总的来说,虽然智能指针能帮用户进行有效的堆内存管理,但是还是需要显示的声明智能指针,当然,程序员喜欢的是完全不考虑回收智能指针类型的内存管理方案。就是垃圾回收机制。而java、c#、python早已支持垃圾回收。

总的来说,垃圾回收分为两大类:

  • 基于引用计数的垃圾回收器

引用计数主要记录对象的被引用次数,当计数为0时,该对象被认为垃圾而回收。使用引用计数实现简单,也不会造成程序暂停,另外也不会对系统的缓存或交换空间造成冲击,因此副作用很小。但是这种方法难以处理环形引用的问题。

  • 基于跟踪处理的垃圾回收器

相比引用计数,跟踪处理的垃圾回收更被广泛的应用。

1. 标记-清除

从跟对象开始查找引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有没有标记的对象被认为垃圾,在第二阶段清扫阶段被回收。

这种方法的缺点是对象无法移动造成大量内存碎片的问题。

2. 标记-整理

这种方法标记和标记-清理一样,但是标记完后,不在遍历所有对象清扫,而是将活的对象向左靠齐来解决内存碎片的问题,就是移动活的对象,相应的,程序中所有对堆内存的引用必须更新。

3. 标记-拷贝

这种方法将堆空间分为两部分:from与to,刚开始系统从from堆空间分配内存,当from满了就开始垃圾回收,从from堆空间中所有的活的对象拷贝到to的堆空间,在to里边是紧凑排列的,接下来需要将from与to交换角色,接着从新的from里开始分配。

这种方法的特点事对空间的利用率只有一半,也需要移动活的对象。

C++与垃圾回收

C++11 智能指针支持引用计数,并不能解决环形引用的问题,因此垃圾回收收到一些限制,同时收到安全性与可移植性的特点,c++11提出了最小垃圾回收支持。

什么是最小垃圾回收?最小垃圾回收是基于安全派生指针这个概念的。安全派生指针指的是指向new分配的对象及其其子对象的指针。安全派生指针的操作包括:

  • 在解引用的基础上的引用:如&*p
  • 定义明确的指针操作:如p+1,p++
  • 定义明确的指针转换,如static_cast<void*> §
  • 指针和整形之间的转换,如reinterpret_cast<intptr_t>§

当然,目前并没有编译器实现最小垃圾回收支持,甚至连get_pointer_safety这个函数接口都没有实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值