C++智能指针

325 阅读4分钟

C++智能指针就是为应对内存泄露问题,帮助管理new出来的对象,管理内存,C++11 标准在充分借鉴和吸收了 boost 库中智能指针的设计思想,引入了三种类型的智能指针,即 std::unique_ptrstd::shared_ptrstd::weak_ptr。所有的智能指针类(包括 std::unique_ptr)均包含于头文件 中。

std::unique_ptr

std::unique_ptr表示对内存对象的唯一指针,禁止拷贝与赋值语义。std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1, std::unique_ptr 对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个 std::unique_ptr 对象:

std::unique_ptr<int> sp1(new int(123)); // 初始化方式1

std::unique_ptr<int> sp2; // 初始化方式2
sp2.reset(new int(123));

std::unique_ptr<int> sp3 = std::make_unique<int>(123); // 初始化方式3,推荐使用,C++14才引入std::make_unique<>()

std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete

template <class T>
class unique_ptr
{
  //...
  //拷贝构造函数和赋值运算符被标记为delete
  unique_ptr(const unique_ptr&) = delete;
  unique_ptr& operator=(const unique_ptr&) = delete;
};

因此,下列代码是无法通过编译的:

std::unique_ptr<int> sp1(std::make_unique<int>(123));;

std::unique_ptr<int> sp2(sp1);//无法通过编译
std::unique_ptr<int> sp3; 
sp3 = sp1; //无法通过编译

虽然禁止复制语义,但是可以通过一个函数返回一个 std::unique_ptr

#include <memory>

std::unique_ptr<int> func(int val){
  std::unique_ptr<int> up(new int(val));
  return up;
}

int main(){
  std::unique_ptr<int> sp1 = func(123); // 从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 sp1
  return 0;
}

std::unique_ptr 不能复制,但是可以通过移动构造std::move()转移内存对象

#include <memory>

int main(){
  std::unique_ptr<int> sp1(std::make_unique<int>(123));
  std::unique_ptr<int> sp2(std::move(sp1));

  std::unique_ptr<int> sp3;
  sp3 = std::move(sp2);
  
  return 0;
}

std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象:

#include <iostream>
#include <memory>

int main(){

  // 创建10个int类型的堆对象
  std::unique_ptr<int[]> sp1(new int[10]);  // 形式1

  std::unique_ptr<int[]> sp2;  // 形式2
  sp2.reset(new int[10]);

  std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10)); // 形式3
  
  for (int i = 0; i < 10; ++i){
    sp1[i] = i;
    sp2[i] = i;
    sp3[i] = i;
  }

  for (int i = 0; i < 10; ++i){
    std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
  }

  return 0;
}

自定义释放内存

自定义智能指针对象持有的资源的释放函数,默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,可以自定义智能指针对象的资源析构函数

#include <iostream>
#include <memory>

class Socket {
public:
    Socket() {
    }

    ~Socket() {
    }

    void close() {
    }
};

int main() {
    auto deletor = [](Socket *pSocket) {
        pSocket->close(); //关闭句柄
        // ...
        delete pSocket;
    };

    std::unique_ptr<Socket, void (*)(Socket * pSocket)> spSocket(new Socket(), deletor); // 自定义传递销毁指针时需要执行的函数
    return 0;
}

可以使用decltype(deletor)让编译器自己推导 deletor 的类型

std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);

std::shared_ptr

std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 **std::shared_ptr**对象析构时,资源引用计数减 1,最后一个std::shared_ptr对象析构时,发现资源计数为 0,将释放其持有的资源。

多个线程之间递增和减少资源的引用计数是安全的。(这不意味着多个线程同时操作 std::shared_ptr引用的对象是安全的)。 std::shared_ptr 提供了一个**use_count()方法来获取当前持有资源的引用计数。除了上面描述的, std::shared_ptr**用法和 std::unique_ptr 基本相同。

// 初始化方式1
std::shared_ptr<int> sp1(new int(123));

// 初始化方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));

// 初始化方式3
std::shared_ptr<int> sp3;
sp3 = std::make_shared<int>(123);

std::unique_ptr一样,应该优先使用 std::make_shared去初始化一个std::shared_ptr*对象。

#include <iostream>
#include <memory>

class A {
public:
    A() {
        std::cout << "A constructor" << std::endl;
    }

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

int main() {
    {
        // 初始化方式1
        std::shared_ptr<A> sp1(new A());
        std::cout << "use count: " << sp1.use_count() << std::endl;

        // 初始化方式2
        std::shared_ptr<A> sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;
        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;
        {
            std::shared_ptr<A> sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }
        std::cout << "use count: " << sp1.use_count() << std::endl;
    }
    return 0;
}

/**
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor
*/
  1. sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行;此时只有一个 sp1 对象引用 A 对象
  2. 之后利用 sp1 拷贝一份 sp2,引用计数为2,调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,引用计数值再次变为 1
  3. 利用 sp1 再次创建 sp3,引用计数变为 2,之后sp3 出了其作用域被析构,资源 A 的引用计数递减 1,引用计数为 1
  4. 最后sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。

std::enable_shared_from_this

实际开发中,有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr对象给外部使用,有如此需求的类只要继承自std::enable_shared_from_this模板对象即可

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A> {
public:
    A() {
        std::cout << "A constructor" << std::endl;
    }
    ~A() {
        std::cout << "A destructor" << std::endl;
    }

    std::shared_ptr<A> getSelf() {
        return shared_from_this();
    }
};

int main() {
    std::shared_ptr<A> sp1(new A());
    std::shared_ptr<A> sp2 = sp1->getSelf();
    std::cout << "use count: " << sp1.use_count() << std::endl;
    return 0;
}

类 A 的继承std::enable_shared_from_this 并提供一个getSelf()方法返回自身的 std::shared_ptr对象,在 getSelf()中调用shared_from_this()即可。std::enable_shared_from_this用起来比较方便,但是也存在很多不易察觉的陷阱。

不应该共享栈对象的 this 给智能指针对象

假设将上面代码 main 函数生成 A 对象的方式改成一个栈变量,即:

int main() {
    A a;
    std::shared_ptr<A> sp2 = a.getSelf();
    std::cout << "use count: " << sp2.use_count() << std::endl;
    return 0;
}

运行修改后的代码会发现程序在 std::shared_ptr sp2 = a.getSelf(); 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过shared_from_this()将该对象交由智能指针对象管理。切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)

std::weak_ptr

std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。

std::weak_ptr可以从一个std::shared_ptr或另一个std::weak_ptr对象构造, std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptrlock()函数来获得std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。

std::weak_ptr可用来解决 std::shared_ptr相互引用时的死锁问题(即两个std::shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。

#include <iostream>
#include <memory>

int main(){
  //创建一个std::shared_ptr对象
  std::shared_ptr<int> sp1(new int(123));
  std::cout << "use count: " << sp1.use_count() << std::endl;

  //通过构造函数得到一个std::weak_ptr对象
  std::weak_ptr<int> sp2(sp1);
  std::cout << "use count: " << sp1.use_count() << std::endl;

  //通过赋值运算符得到一个std::weak_ptr对象
  std::weak_ptr<int> sp3 = sp1;
  std::cout << "use count: " << sp1.use_count() << std::endl;

  //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
  std::weak_ptr<int> sp4 = sp2;
  std::cout << "use count: " << sp1.use_count() << std::endl;

  return 0;
}
use count: 1
use count: 1
use count: 1
use count: 1

无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。

std::weak_ptr 提供了一个 **expired()方法来检测对象是否被销毁了,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptrlock()**方法得到一个 **std::shared_ptr**对象然后继续操作资源:

//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
  return;
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn){
  //对conn进行操作,省略...
}

std::weak_ptr的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。

std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

class Subscriber{

};
class SubscribeManager{

public:
  void publish(){
    for (const auto& iter : m_subscribers){
      if (!iter.expired()){
        //TODO:给订阅者发送消息
      }
    }
  }

private:
  std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};