简介

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象(类)。当栈对象的生存周期结束后(离开对象的作用域,如函数结束),会在析构函数中释放掉申请的内存,不需要手动释放内存空间,从而规避内存泄漏的风险。

auto_ptr

auto_ptr是C++ 98标准的方案,在C++ 11标准中已经弃用,采用独占所有权模式,看下面这个例子:

std::auto_ptr<string> p1 (new string ("Hello World")); 
std::auto_ptr<string> p2;
p2 = p1; // 编译不会报错,但p1会被置成nullptr

auto_ptr的赋值操作在编译时不会报错,但p2剥夺了p1的所有权,使得p1成为一个空指针。所以当程序运行时访问p1将会报错。这也是促使auto_ptr的被弃用的原因。

unique_ptr

作为接替auto_ptr的独占资源所有权的智能指针,其特点如下:

  • 独占式拥有或严格拥有, 保证同一时间内只有一个智能指针可以指向该对象;
  • 常见用法:使用 std::unique_ptr 自动管理内存,基本的RAII(Resource Acquisition Is Initialization)思想;
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}
  • 采用独占所有权模式,不允许左值赋值操作,但可以接受临时右值(引用)的赋值,即这里的operator=不是copy赋值操作符,而是move赋值操作符;
std::unique_ptr<string> p1 (new string ("Hello World")); 
std::unique_ptr<string> p2;
p2 = p1; // 编译报错
std::unique_ptr<string> p3;
p3 = unique_ptr<string>(new string ("Hello World")); // 允许
std::unique_ptr<string> p4;
p4 = std::move(p1); // 允许

其内在逻辑是左值(拷贝)赋值操作p2 = p1;后,无法合理的处理左值p1的去留。若采用auto_ptr的思路,将p1置为nullptr,则会重走带来潜在的访问null指针而程序崩溃风险的老路,若置之不理,则不满足独占的属性,所以干脆就不允许这样做。而右值赋值则不存在这样的问题,右值本身就是临时的,在赋值给左值后就会被销毁,左值仍然独占资源。std::move就是将一个左值转换成右值引用,从而实现移动语义。移动语义的存在就是在强调转移所有权,即这时候你是清楚地知道p1对资源的所有权发生了转移,所以即便还是出现了p1已经被销毁,不再可用的情况,那么后面乱用p1而导致程序崩溃的代价也应当由乱用者来承担。

  • 可以指向一个数组;
// 实测C++14标准下,以下两种写法都支持
std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
// or
std::unique_ptr<int[]> uptr(new int[10]);
  • 可以自定义deleter;
{
struct FileCloser {
void operator()(FILE* fp) const {
if (fp != nullptr) {
fclose(fp);
}
}
};
std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}
  • 可以使用lambda函数形式的deleter;
{
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
fclose(fp);
});
}

shared_ptr

std::shared_ptr是引用计数型智慧指针(reference-counting smart pointers,RCSP),其特点如下:

  • 共享式拥有,多个智能指针可以指向相同的对象,该对象和其相关资源会在最后一个指向它的指针被销毁时被释放;
  • 使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数;
{
std::shared_ptr<int> sptr = std::make_shared<int>(200);
assert(sptr.use_count() == 1); // 此时引用计数为 1
{
std::shared_ptr<int> sptr1 = sptr;
assert(sptr.get() == sptr1.get());
assert(sptr.use_count() == 2); // sptr 和 sptr1 共享资源,引用计数为 2
}
assert(sptr.use_count() == 1); // sptr1 已经释放
}
// use_count 为 0 时自动释放内存
  • 可以指向一个数组;
// 实测 std::make_shared<int[]> 这种写法直到C++20标准才支持
std::shared_ptr<int[]> sptr = std::make_shared<int[]>(10);
// or
std::shared_ptr<int[]> sptr(new int[10]);
  • 可以自定义deleter;
{
std::shared_ptr<FILE> sptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});
}
  • 常用的成员函数:
    • use_count:返回引用计数的个数;

    • unique:返回是否为独占所有权( use_count 为 1),C++17标准已弃用;

    • swap:交换两个 shared_ptr 所拥有的对象;

    • reset:放弃拥有对象的所有权或拥有对象的变更(参数为一个新的对象,表示放弃原对象,转而指向新对象),会引起原有对象的引用计数的减少;

    • get:返回内部对象的裸指针,;

  • 出于性能和异常安全的考虑,尽量使用make_shared进行初始化,详细讨论见这里

weak_ptr

share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效(两个指针的引用计数永远不可能下降为0),从而导致内存泄漏。可以看下面这个例子:

class B;	//声明
class A
{
public:
shared_ptr<B> pb_;
~A() { cout << "A delete\n" }
};

class B
{
public:
shared_ptr<A> pa_;
~B() { cout << "B delete\n"; }
};

void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //2
cout << pa.use_count() << endl; //2
}
//函数退出后,pa,pb指针析构,引用计数各自减1,但两者引用计数还是为1,从而导致A,B的析构函数不会被调用,资源没有被释放,造成内存泄漏

weak_ptr 是为了配合shared_ptr而引入的智能指针,它是一种不控制对象生命周期的智能指针, 指向一个 shared_ptr 管理的对象,只可以从一个shared_ptr或另一个weak_ptr对象构造,是shared_ptr的附属。因此,真正管理对象的还是强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段,类似于观察者的角色,weak_ptr的构造和析构不会引起引用记数的增加或减少,weak_ptr也无法直接访问对象的成员方法,除非将其转化成强引用的shared_ptr。其特点还有:

  • weak_ptr和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr可以通过调用lock成员函数来转化为shared_ptr;
  • weak_ptr可以用来解决shared_ptr相互引用的死锁问题;
class B;	//声明
class A
{
public:
weak_ptr<B> pb_;
~A() { cout << "A delete\n" }
};

class B
{
public:
shared_ptr<A> pa_;
~B() { cout << "B delete\n"; }
void print() { cout << "B print\n"; }
};

void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //2
}
// 资源B的引用开始就只有1,当pb指针析构时,B的计数变为0,B对象得到释放,调用B的析构函数(打印B delete),
// B释放的同时pa_指针析构,也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放,调用A的析构函数(打印A delete)。
  • 常用的成员函数:

    • expired:用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false;
    • weak_ptr 没有重载operator*operator->,所以不能通过weak_ptr直接访问对象的方法,比如B类的对象中有一个方法print(),我们不能这样访问:pa->pb_->print()
    • lock:用于获取所管理的对象的强引用shared_ptr. 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象的指向与 weak_ptr 相同。故可以用如下方式访问B类对象的print()方法:
    shared_ptr<B> p = pa->pb_.lock();
    p->print();
    • use_count:返回与 shared_ptr 共享的对象的引用计数;
    • reset:将 weak_ptr 置空;
    • weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数;

参考

[1] 详解C++11智能指针

[2] 现代 C++:一文读懂智能指针