C++ 智能指针简介与错误使用情况

3 分钟阅读

智能指针是为了更方便的管理内存而设计的,设计思想就是让使用者不再管理内存,而是由智能指针来进行管理。 换句话说以后不用再考虑new出来的对象什么时候需要delete,智能指针能帮你管理内存。 智能指针分为三种: std::shared_ptrstd::weak_ptrstd::unique_ptr

std::unique_ptr

std::unique_ptr指针拥有其管理对象的所有权,该智能指针不能被复制,只能被移动。当std::unique_ptr智能指针被析构,则其管理的对象也会被析构。 举一个简单的例子,高中宿管大爷,早六点开灯,晚十点关灯,其他人没有办法参与开关灯的事情。这里开关灯指的就是内存的分配与释放。

用法示例

#include <memory>
int main()
{
  {
    int* p = new int(0);  ///< 分配一个int的内存,其值为0
    std::unique_ptr<int> up = std::unique_ptr<int>(p);  ///< 使用指针p来创建一个智能指针对象
    /*
      /// 由于std::unique_ptr对象不可复制,所以下面三句话编译不过
      std::unique_ptr<int> up_copy1(up);
      std::unique_ptr<int> up_copy2;
      up_copy2 = up;
    */
  } ///< 在这个地方智能指针up被析构,up的析构函数对指针p执行delete操作,从而达到智能回收内存的作用

  {
    auto up = std::make_unique<int>(0); ///< 这句话等价于上面的两句话
  }
}

下面看一下std::unique_ptr简单实现

template<typename OBJECT>
class unique_ptr {
  public:
  /// 构造函数
  unique_ptr(OBJECT* p) : p_(p) {}
  /// 析构函数
  ~unique_ptr(){
    if(p_ != nullptr){
      delete p_;
    }
  }
  /// 删除拷贝构造函数, 标识这个类不可拷贝
  unique_ptr(const unique_ptr<OBJECT>& other) = delete;
  /// 删除拷贝构造赋值符, 标识这个类不可拷贝
  unique_ptr<OBJECT>& operator=(const unique_ptr<OBJECT>& other) = delete;
  /// 移动构造函数
  unique_ptr(unique_ptr&& other) {
    p_ = other.p_;
    other.p_ = nullptr;
  }

  /// 移动赋值符
  unique_ptr<OBJECT>& unique_ptr(unique_ptr&& other) {
    if (&other == this) {
      return *this;
    }
    delete p_;
    p_ = other.p_;
    other.p_ = nullptr;
  }
private:
  OBJECT* p_ = nullptr;
};

见示例代码

class object {
public:
  object() {
    std::cout << "object()" << std::endl;
  }
  ~object() {
    std::cout << "~object()" << std::endl;
  }
};

int main() {
  {
     unique_ptr<object> p(new object);
     unique_ptr<object> other(std::move(p));  ///< 调用移动构造函数
     /// 此时other智能指针拥有对象的管理权,而p失去了该对象的管理权
  }
  return 0;
}

运行结果:

Snipaste_2020-12-03_15-42-50

std::shared_ptr

std::shared_ptr使用引用计数的方法来决定是否需要释放掉管理对象的内存。 举一个很简单的例子,办公室中每一个人下班出门前都会看一下还有没有人在办公室中,如果有就直接走掉不关灯,如办公室内没有人了就执行关灯操作。 见下面简单实现

template <typename OBJECT>
class shared_ptr {
  public:
  /// 构造函数
  shared_ptr(OBJECT* object):p_(object), count_(new int(0)) {
    ++(*count_);
  }

  /// 析构函数
  ~shared_ptr() {
    if(--(*count_) == 0) {  ///< 如果自己是最后一个管理该对象的人,自己被析构时负责做善后工作即delete对象
      delete p_;
      delete count_;
    }
  }

  /// 拷贝构造函数
  shared_ptr(const shared_ptr& other) {
    count_ = other.count_;  ///< 把引用计数的指针复制过来
    ++(*count_);  ///< 所有shared_ptr中的count_都自加一下
    p_ = other.p_;  ///< 复制管理对象的指针
  }

  /// 拷贝赋值运算符
  shared_ptr<OBJECT>& operator=(const shared_ptr& other) {
    if (&other == this) {
      return &this; ///< 如果复制的对象是自己,则直接返回
    }
    count_ = other.count_;  ///< 把引用计数的指针复制过来
    ++(*count_);  ///< 所有shared_ptr中的count_都自加一下
    delete p_;
    p_ = other.p_;  ///< 复制管理对象的指针
    return &this;
  }

private:
  OBJECT* p_ = nullptr; ///< 指向管理对象的指针
  int* count_ = nullptr;  ///< 引用计数, 之所以使用指针是为了让管理同一个对象的引用计数
};

运行一下例子

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

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

int main() {
  {
    shared_ptr<object> sp(new object);

    auto sp1(sp);
    auto sp2(sp);
    auto sp3(sp);
    auto sp4(sp);
    auto sp5(sp);
    auto sp6(sp);
  }
  return 0;
}

运行结果:

Snipaste_2020-12-03_10-47-05 \resource\C++智能指针简介与错误使用情况

可以看到该对象被构造了一次,又被析构了一次。

上面的实现虽然让内存管理变得简单,但也带来了一些麻烦。 由于智能指针拥有了对象的管理权,万一两个智能指针管理同一对象,那么这两个智能指针在析构时会对同一对象执行两次delete, 从而造成崩溃。见下面代码举例。

int main() {
  {
    object* p = new object();
    shared_ptr<object> sp1(p);
    shared_ptr<object> sp2(p);
  }
  return 0;
}

上方代码运行会崩溃。因为两个截然不同的智能指针sp1sp2同时管理了同一个object对象, 它们分别析构时会对指针pdelete两次。 所以我们更加推荐使用std::make_shared来代替使用裸指针初始化智能指针。见下面举例。

std::shared_ptr<object> sp = std::make_shared<object>();
/// 代替下面的初始化的方法
/// std::shared_ptr<object> sp = std::shared_ptr<object>(new object);

特殊情况

我们有时会在代码里遇到这样一种情况,见下面代码。

class error_object {
public:
  std::shared_ptr<error_object> get_sp() {
    return std::shared_ptr<error_object>(this);
  }
};

上面代码中get_sp()的函数,目的是想返回一个能够管理自己的智能指针,但是我们可以看到每调用一次get_sp(),我们都会用同一个指针this, 创建一个不同的智能指针。 这意味着我们调用两次get_sp()函数后,程序运行时会崩溃。

为了应对这种情况标准库中设计了一个工具函数std::enable_shared_from_this, 具体用法如下。

class correct_object : public std::enable_shared_from_this<correct_object> {
};

int main() {
  auto instance = std::make_shared<correct_object>();
  std::shared_ptr<correct_object> sp1 = instance.shared_from_this();
  std::shared_ptr<correct_object> sp2 = instance.shared_from_this();
  return 0;
}

上面代码可以正常运行。 关于std::enable_shared_from_this的实现原理,见C++ enable_shared_from_this原理与简单实现

std::weak_ptr

std::weak_ptr不能被称为一个独立的智能指针,它是std::shared_ptr智能指针的一种扩展。

std::weak_ptr的功能是,观察一个被std::shared_ptr管理的对象, 但不会影响std::shared_ptr的引用计数。

std::weak_ptr可以观察一个对象有没有被释放,或是用来防止std::shared_ptr的循环引用问题。

下面代码用于检查资源是否已经释放。用于解决裸指针的野指针的问题。

class object {
public:
  object() {
    std::cout << "object()" << std::endl;
  }
  ~object() {
    std::cout << "~object()" << std::endl;
  }
};

int main()
{
  std::weak_ptr<object> weak;
  {
      auto sp = std::make_shared<object>();
      weak = sp;
      std::shared_ptr<object> sp1 = weak.lock();
      if(sp1){
         sp1->doSomeThing();
      }
  }
  if (weak.expired()) {
    std::cout << "资源没有释放" << std::endl;
  } else {
    std::cout << "资源已经释放" << std::endl;
  }
}

以下代码出现了引用回环,会导致智能指针对象被销毁了,但资源没有被销毁。

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

  ~list_node() {
    std::cout << "~list_node()" << std::endl;
  }
  std::shared_ptr<list_node> next;
};

int main()
{
  {
    auto sp0 = std::make_shared<list_node>();
    auto sp1 = std::make_shared<list_node>();
    auto sp2 = std::make_shared<list_node>();
    sp0->next = sp1;
    sp1->next = sp2;
    sp2->next = sp0;
  }
  return 0;
}

运行结果:

Snipaste_2020-12-03_14-50-26

我们可以看到该对象的析构函数一个也没有执行,说明内存没有释放。

我们现在换成std::weak_ptr来保存指向下一个节点的智能指针。

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

  ~list_node() {
    std::cout << "~list_node()" << std::endl;
  }
  std::weak_ptr<list_node> next;
};

int main()
{
  {
    auto sp0 = std::make_shared<list_node>();
    auto sp1 = std::make_shared<list_node>();
    auto sp2 = std::make_shared<list_node>();
    sp0->next = sp1;
    sp1->next = sp2;
    sp2->next = sp0;
  }
  return 0;
}

运行结果: Snipaste_2020-12-03_15-00-25

现在指针形成的环路被std::weak_ptr完美解决了。内存能够正确释放了。

实际使用用例

#include <iostream>
#include <memory>
#include <string>
#include <vector>

/// 订阅者
class subscriber {
 public:
  /// 订阅者被观察器调用的函数
  void read(const std::string& str) { std::cout << str << std::endl; }
};

/// 广播者
class boardcaster {
 public:
   /// 广播者注册订阅者
  void regist(std::shared_ptr<subscriber> sp) { vec.push_back(sp); }

  /// 广播事件
  void boardcast(const std::string& str) {
    for (auto it = vec.begin(); it != vec.end();) {
      auto sp = it->lock();
      if (sp) {
        /// 让订阅者接受消息
        sp->read(str);
        it++;
      } else {
        std::cout << "subscriber is delete!" << std::endl;
        it = vec.erase(it);  ///< 擦除这个订阅者的指针
      }
    }
  }

 private:
  std::vector<std::weak_ptr<subscriber>> vec; ///< 用于保存订阅者的智能指针
};

int main() {
  boardcaster obj;
  auto reader0 = std::make_shared<subscriber>();
  obj.regist(reader0);
  {
    auto reader1 = std::make_shared<subscriber>();
    obj.regist(reader1);
    auto reader2 = std::make_shared<subscriber>();
    obj.regist(reader2);

    obj.boardcast("start boardcast!");
  }

  obj.boardcast("reader1 and reader2 is deleted, boardcast angin!");
  return 0;
}

智能指针(现代 C++)

C/C++内存泄漏及检测

知乎 C++ 怎么检测内存泄露,怎么定位内存泄露?

#ifdef _WIN32
    #include <crtdbg.h>
    #ifdef _DEBUG

    #define new new(_NORMAL_BLOCK,__FILE__,__LINE__)
    #endif
#endif

#ifdef _WIN32
	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)|_CRTDBG_LEAK_CHECK_DF);
#endif

使用 CRT 库查找内存泄漏