智能指针原理
由于C++中没有垃圾清理机制,分配的内存需手动释放,否则就会产生内存泄漏,C++11中引入了智能指针的概念,使用其可以有效解决该问题。智能指针是存储指向动态分配对象指针的类。使用引用计数的技术,使用一次内部引用计数 +1,每析构一次,内部引用计数 -1,减为0时,删除所指向的堆内存。
使用智能指针可以确保在离开指针作用域时,自动销毁动态分配的对象,防止内存泄漏。C++11中提供了三种智能指针,包含在<memory>中:
1. 共享的智能指针:std::shared_ptr
2. 独占的智能指针:std::unique_ptr
3. 弱引用智能指针:std::weak_ptr
std::unique_ptr
unique_ptr是作用域指针,指超出作用域就会销毁并调用delete。unique_ptr是唯一的,不可拷贝,不可共享。
使用C++14中引入的std::make_unique<>()函数来创建,可以保证内存安全。
std::unique_ptr<student> uptr = std::make_unique<student>();
#include <iostream>
#include <memory>
class student{
public:
student(){
std::cout << "constructor" << std::endl;
}
~student(){
std::cout << "destructor" << std::endl;
}
void print(){std::cout<<"print"<<std::endl;}
};
int main(){
{
std::unique_ptr<student> stu = new student(); // error! unique_ptr不能隐式转换
std::unique_ptr<student> stu(new student());//ok,可以但不建议
std::unique_ptr<student> uptr = std::make_unique<student>(); //推荐使用
uptr->print(); //像一般原始指针使用
}
return 0;
}
std::shared_ptr
shared_ptr的工作方式是采用引用计数。
shared_ptr需要分配另一块内存,叫控制块,来存储引用计数。比如说new一个student类的实例,传递给shared_ptr的构造函数,那么shared_ptr要做两次分配:一次new student的分配,一次控制块的分配。可以使用make_shared提高效率,保证安全。
std::shared_ptr<student> sptr = std::make_shared<student>();
代码举例:
#include<iostream>
#include<memory>
using namespace std;
class student{
public:
student(){ cout<<"constructor"<<endl; }
~student(){ cout<<"destructor"<<endl; }
};
int main(){
{
shared_ptr<student> sp;
{
// shared_ptr<student> sptr = sptr(new student()); 不推荐这种
shared_ptr<student> sptr = make_shared<student>();
sp = sptr; //可以拷贝
} //此时离开了作用域,sptr已经销毁了,但是没有调用析构,因为sp还活着,持有着该student的引用
//计数由2 -> 1
} //这时所有引用消失,计数变为0,调用析构,释放内存。
return 0;
}
shared_ptr提供了几个函数:
初始化:
void reset() noexcept;
template< typename T >void reset( T* ptr );
template< typename T, typename Deleter >
void reset( T* ptr , Deleter d );
ptr:指向要获得所有权的对象的指针
d:指定删除器的指针
获取原始指针:
T* get() const noexcept;
统计指向同一内存对象的智能指针的个数:
long use_count() const noexcept;
交换两个智能指针所指向的内存地址:
void swap(shared_ptr &r) noexcept;
shared_ptr的循环引用
当两个对象(主体是对象)使用shared_ptr
相互引用时,那么当超出范围时,都不会删除内存。发生这种情况的原因是shared_ptr
在其析构函数中递减关联内存的引用计数后,检查count
是否为0,如果不为0,析构函数就不会释放相应的内存。当出现了循环引用后,就会发现count
的值总是不为0。
为了解决循环引用问题,采用weak_ptr弱引用可以避免产生此问题。weak_ptr不会增加引用计数,如果对象活着,那么它可提升为有效的shared_ptr,如果对象已经死了,提升失败返回空的shared_ptr。提升/lock()行为是线程安全的。
std::weak_ptr
可以和shared_ptr一起使用。weak_ptr可以被拷贝,但是不会增加额外的控制块来控制计数,仅表示该指针存活。
将一个shared_ptr赋值给另一个shared_ptr,引用计数加一;将一个shared_ptr赋值给一个weak_ptr时,不增加引用计数。 weak_ptr不控制对象的生命期,但是知道对象是否活着。
{
std::weak_ptr<student> stu;
{
std::shared_ptr<student> sptr = std::make_shared<student>();
stu = sptr;
} //此时,此析构被调用,内存被释放
}
weak_ptr的应用场景:
弱智能指针 weak_ptr 与 shared_ptr 的区别在于:
1. weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在。
2. weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数。
3. weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock()提升为shared_ptr强智能指针,才能访问资源。
根据以上特性,weak_ptr可以用来在以下场景得到应用:
1. observer模式
2. 解决循环引用
3. 弱回调
shared_ptr的线程安全
shared_ptr的引用计数本身是安全且无锁的,但是对象的读写并不是,因为shared_ptr有两个数据成员,读写操作不能原子化。
shared_ptr的线程安全级别:
1. 一个shard_ptr对象实体可被多个线程同时读取;
2. 两个shard_ptr对象实体可以被两个线程同时写入,析构算写操作;
3. 如果要从多个线程中读取同一个shard_ptr对象,需要加锁。
在多个线程中同时访问同一个shard_ptr,使用mutex保护:
class Foo{
//......
};
mutex mt;
shared_ptr<Foo> ptr;
void doit(const shared_ptr<Foo>& pFoo) {
//......
}
void read() {
shared_ptr<Foo> localptr;
{
lock_guard<mutex> lock(mt);
localptr = ptr;
}
doit(localptr);
}
void write() {
shared_ptr<Foo> newptr = make_shared<Foo>();
{
lock_guard<mutex> lock(mt);
ptr = newptr;
}
doit(newptr);
}
在上面的read()和write()函数中,临界区外都没访问ptr,而是采用一个指向同一Foo对象的栈上的拷贝。使用这种local copy,shared_ptr 在作为函数参数传递时不必复制,用reference to const 作为参数类型即可。
如果想要销毁对象,可以在临界区内执行 ptr.reset(),但是这样会增长临界区长度。一种改进方法就是像上面那样定义一个localptr,让它在临界区内与ptr交换,这样能保证把对象的销毁推迟到临界区外。
void write() {
shared_ptr<Foo> localptr = make_shared<Foo>();
{
lock_guard<mutex> lock(mt);
localptr.swap(ptr);
}
localptr.reset();
}
引用当年孟岩老师说过的话:“C++ 利用智能指针达成的效果是:一旦某对象不再被引用,系统刻不容缓,立刻回收内存。这通常发生在关键任务完成后的清理(clean up)时期,不会影响关键任务的实时性,同时,内存里所有的对象都是有用的,绝对没有垃圾空占内存。”