C++语言标准库STL提供了一套智能指针接口,且一直处于完善状态,如下图所示:
最早的智能指针是std::auto_ptr,到c++11才开始广泛使用,平时用得最多的是这三个:
其中unique_ptr只能指向一个对象,shared_ptr是通用的智能指针,weak_ptr可以理解为只读用途的指针,这种指针不改变引用计数。
在旧式的C++程序开发过程中,我们会比较习惯用传统的裸指针来管理资源,通过成对使用new和delete来保证内存安全,这种做法不会造成太大问题,只是在某些情况下会出现内存难于管理的局面。最典型的是存在分支的情况,如下图所示:
void test()
{
auto p = new std::string();
if(...){
delete p;
return;
}
...
delete p;
}
每一处返回,都需要删除指针p,再来看看智能指针的写法:
void test()
{
auto p = std::make_unique<std::string>();
if(...){
return;
}
...
}
这样采用对象的方式来管理内存,通过对象的生命周期来自动释放内存。
目前智能指针主要使用unique_ptr和share_ptr,两者的区别如下:
std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 指向同一个对象,当引用计数变为零的时候对象会自动执行delete。
std::make_shared 能够用来消除显式的使用 new, 并返回这个对象类型的std::shared_ptr指针。例如:
auto pointer = std::make_shared<int>(10);
std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全,如下所示:
std::unique_ptr<int> pointer = std::make_unique<int>(10);
std::unique_ptr<int> pointer2 = pointer; // 非法
独占也就是不可复制,但是,我们可以利用 std::move 函数将其转移给其他的 unique_ptr。
还有一个智能指针是std::weak_ptr,这个智能指针主要是针对std::shared_ptr的不足而设计的。如下所示:
struct A;
struct B;
struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}
a和b都引用了对方,导致计数都为2,当a和b离开作用域时,引用计数会是1,也就不能释放内存资源。
解决这个问题的办法就是使用弱引用指针 std::weak_ptr,std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如下图所示:
std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 true,否则返回 false。
智能指针的线程安全需要考虑2个问题,一个是多线程状态下智能指针内部的引用计数是否是线程安全的,另外一个问题是多线程中智能指针指向的值的读写是否是线程安全的。
对于前一个问题,C++标准库声明shared_ptr是线程安全的,不用担心引用次数出问题,也不用担心析构函数会调用多次。
后一个问题其实是对shared_ptr对象的写入,这个不是线程安全的,可以通过atomic_store和atomic_load来处理安全读写。
一般而言,如果一个智能指针在初次赋值后,后续不需要写入操作,那么所有的读取操作都是线程安全的。而如果后续需要改变这个指针的指向,那么就需要加锁。
智能指针这种技术并不新奇,在很多语言中都是一种常见的技术,现代 C++ 将这项技术引进,在一定程度上消除了 new/delete 的滥用,是一种更加成熟的编程范式。