智能指针

简介

有关内存的分配与释放。我们往往会遇到以下的问题。

  1. 由于疏忽忘记释放 new 出来的堆空间。
  2. 由于代码逻辑的原因,在 if 判断, try-catch 异常中直接退出函数块,导致并没有执行函数末尾的 delete 逻辑。

智能指针的解决方案 :分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存。

c++11 提供了 unique_ptr、shared_ptr 和 weak_ptr 方便我们去管理内存。

auto_ptr

在这里先说一下 c++98 提供的 auto_ptr :

  • 与 unique_ptr 类似,都是对资源的独占性。
  • 但是在 C++11 后 auto_ptr 已经被“抛弃”,具体的原因有以下几点 :
    1. 复制或者赋值都会改变资源的所有权
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      auto_ptr<string> p1(new string("Hello"));
      auto_ptr<string> p2(new string("World"));

      cout << "p1:" << p1.get() << endl;
      cout << "p2:" << p2.get() << endl;

      p1 = p2; // p2赋值给p1,p1先释放自身管理的资源,然后接收p2所管理的指针,

      cout << "p1:" << p1.get() << endl;
      cout << "p2:" << p2.get() << endl; // p2 : 00000000 已经被释放
    2. STL容器要求元素必须支持可复制和可赋值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      vector<auto_ptr<string>> vec;
      auto_ptr<string> p3(new string("Tiny"));
      auto_ptr<string> p4(new string("Sky"));

      // 必须使用std::move修饰成右值,才可以进行插入容器中
      vec.push_back(std::move(p3));
      vec.push_back(std::move(p4));

      vec[0] = vec[1]; // 如果进行赋值,vec[1]的资源会被释放
      cout << "vec[1]:" << *vec[1] << endl; // 此时访问 vec[1] 会访问越界
    3. 不支持对象数组的内存管理
      1
      auto_ptr<int[]> array(new int[10]);	// 定义出错

unique_ptr

unique_ptr 相较于 shared_ptr 是对资源的独占。

  • 由于 unique_ptr 的独占性,无法进行左值的复制构造与赋值,但是运行右值的复制构造与赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unique_ptr<string> p1(new string("Tiny"));
unique_ptr<string> p2(new string("Sky"));

cout << "p1: " << p1.get() << endl;
cout << "p2: " << p2.get() << endl;

p1 = p2; // 禁止左值赋值
unique_ptr<string> p3(p2); // 禁止左值赋值构造

unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2); // 使用move可以把左值转成右值

cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl; // p2.get() -> 00000000 资源已被转移
  • 在 STL 容器中使用unique_ptr,不允许直接赋值,同 auto_ptr 一致,资源被释放,导致访问越界。

  • 支持对象数组的内存管理

    1
    unique_ptr<int[]> array(new int[10]);
  • reset()

智能指针的 reset 方法用于重置一个新的管理对象,原先的管理对象会被释放。

1
2
3
4
5
6
7
8
9
10
auto_ptr<string> p1;
string *str = new string("Error");
p1.reset(str); // p1托管str指针
{
auto_ptr<string> p2;
p2.reset(str); // p2接管str指针时,会先取消p1的托管,然后再对str的托管
}

// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;

shared_ptr

shared_ptr 相对于 unique_ptr 则是对资源的共享,通过引用计数来管理,如果计数为0则释放该内存。

  • 构造

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    shared_ptr<User> sp1;
    Person *user = new User();
    sp1.reset(user); // 托管 user ,引用计数为1.

    shared_ptr<User> sp2(new user());
    shared_ptr<User> sp3(sp1); // sp2的资源交给sp3,sp2的引用计数-1,sp3的计数+1。

    shared_ptr<User[]> sp4; // C++17 后支持可以指向类型为T[]的数组对象。
    shared_ptr<User[]> sp5(new User[5] { 3, 4, 5, 6, 7 });

    // 使用 make_shared 初始化对象
    shared_ptr<string> sp6 = make_shared<string>("Tiny-Sky");
    shared_ptr<User> sp7 = make_shared<User>(9);
  • 陷阱

shared_ptr作为被管控的对象的成员时,会因循环引用造成无法释放资源!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
shared_ptr<B> b; // A中有B的智能指针
};

class B {
public:
shared_ptr<A> a; // B中有A的智能指针
};

// 初始化两个 shared_ptr
shared_ptr<A> ptrA(new A());
shared_ptr<B> ptrB(new B());

// 循环依赖, 导致引用计数都减不到0
ptrA->b = ptrB;
ptrB->a = ptrA;

weak_ptr

为解决在使用shared_ptr智能指针时,造成的循环依赖问题,可以引用一种弱引用 weak_ptr 来解决。

weak_ptr 更多的是为配合 shared_ptr 而进行管理工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少

weak_ptr 没有重载 * 和 -> 但可以使用 lock 获得一个可用的 shared_ptr 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shared_ptr<A> ptrA(new A());
shared_ptr<B> ptrB(new B());

weak_ptr<Girl> Weak; // 定义空的弱指针
weak_ptr<Girl> Weak_1(ptrA); // 使用共享指针构造
weak = ptrB // 共享指针直接赋值给弱指针

// 弱指针不支持 * 和 -> 对指针的访问
*(Weak_1) // Worry!!
Weak_1->XXX() // Worry!!

// weak_ptr 可以转换成 shared_ptr
shared_ptr<A> ptrA_1;
ptrA_1 = weak.lock();

总结

在 c++ 中但凡是需要用到指针的地方都想用智能指针而抛弃了传统的裸指针,是很愚蠢的。

对于智能指针的使用要基于对管理对象的考量,而这种考量更多的从所有权生命周期方面出发。

  • 将由智能指针管理的数据的所有权交给另一个对象,该对象释放,则指针指针管理的数据释放。
  • 将由指针指针管理的数据的生命周期管理到一个代码块,该代码块结束,则指针指针管理的数据释放。