C++手撸智能指针的教程分享

 更新时间:2023年05月21日 16:49:57   作者:会玩code  
在前文中小编为大家介绍了C++智能指针的一些使用方法和基本原理,所以本文就来自己动手,从0到1实现一下自己的unique_ptr和shared_ptr吧

前言

大家好,今天是【重学C++】的第三讲,书接上回,第二讲《02 脱离指针陷阱:深入浅出 C++ 智能指针》介绍了C++智能指针的一些使用方法和基本原理。今天,我们自己动手,从0到1实现一下自己的unique_ptrshared_ptr

回顾

智能指针的基本原理是基于RAII设计理论,自动回收内存资源,从根本上避免内存泄漏。在第一讲《01 C++ 如何进行内存资源管理?》介绍RAII的时候,就已经给了一个用于封装int类型指针,实现自动回收资源的代码实例:

class AutoIntPtr {
public:
    AutoIntPtr(int* p = nullptr) : ptr(p) {}
    ~AutoIntPtr() { delete ptr; }
    int& operator*() const { return *ptr; }
    int* operator->() const { return ptr; }
private:
    int* ptr;
};

我们从这个示例出发,一步步完善我们自己的智能指针。

模版化

这个类有个明显的问题:只能适用于int类指针。所以我们第一步要做的,就是把它改造成一个类模版,让这个类适用于任何类型的指针资源。code show time

template <typename T>
class smart_ptr {
public:
 explicit smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
 ~smart_ptr() {
  delete ptr_;
 }
 T& operator*() const { return *ptr_; }
 T* operator->() const { return ptr_; }
private:
 T* ptr_;
}

我给我们的智能指针类用了一个更抽象,更切合的类名:smart_ptr

AutoIntPtr相比,我们把smart_ptr设计成一个类模版,原来代码中的int改成模版参数T,非常简单。使用时也只要把AutoIntPtr(new int(9)) 改成smart_ptr<int>(new int(9))即可。

另外,有一点值得注意,smart_ptr的构造函数使用了explicit, explicit关键字主要用于防止隐式的类型转换。代码中,如果原生指针隐式地转换为智能指针类型可能会导致一些潜在的问题。至于会有什么问题,你那聪明的小脑瓜看完下面的代码肯定能理解了:

void foo(smart_ptr<int> int_ptr) {
    // ...
}
int main() {
    int* raw_ptr = new int(42);
    foo(raw_ptr);  // 隐式转换为 smart_ptr<int>
    std::cout << *raw_ptr << std::endl;   // error: raw_ptr已经被回收了
    // ...
}

假设我们没有为smart_ptr构造函数加上explicit,原生指针raw_ptr在传给foo函数后,会被隐形转换为smart_ptr<int>, foo函数调用结束后,栖构入参的smart_ptr<int>时会把raw_ptr给回收掉了,所以后续对raw_ptr的调用都会失败。

拷贝还是移动

当前我们没有为smart_ptr自定义拷贝构造函数/移动构造函数,C++会为smart_ptr生成默认的拷贝/移动构造函数。默认的拷贝/移动构造函数逻辑很简单:把每个成员变量拷贝/移动到目标对象中。

按当前smart_ptr的实现,我们假设有以下代码:

smart_ptr<int> ptr1{new int(10)};
smart_ptr<int> ptr2 = ptr1;

这段代码在编译时不会出错,问题在运行时才会暴露出来:第二行将ptr1管理的指针复制给了ptr2,所以会重复释放内存,导致程序奔溃。

为了避免同一块内存被重复释放。解决办法也很简单:

  • 独占资源所有权,每时每刻一个内存对象(资源)只能有一个smart_ptr占有它。
  • 一个内存对象(资源)只有在最后一个拥有它的smart_ptr析构时才会进行资源回收。

独占所有权 - unique_smart_ptr

独占资源的所有权,并不是指禁用掉smart_ptr的拷贝/移动函数(当然这也是一种简单的避免重复释放内存的方法)。而是smart_ptr在拷贝时,代表资源对象的指针不是复制到另外一个smart_ptr,而是"移动"到新smart_ptr。移动后,原来的smart_ptr.ptr_ == nullptr, 这样就完成了资源所有权的转移。这也是C++ unique_ptr的基本行为。我们在这里先把它命名为unique_smart_ptr,代码完整实现如下:

template <typename T>
class unique_smart_ptr {
public:
 explicit unique_smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
 ~unique_smart_ptr() {
  delete ptr_;
 }
 // 1. 自定义移动构造函数
 unique_smart_ptr(unique_smart_ptr&& other) {
  // 1.1 把other.ptr_ 赋值到this->ptr_
  ptr_ = other.ptr_;
  // 1.2 把other.ptr_指为nullptr,other不再拥有资源指针
  other.ptr_ = nullptr;
 }
 // 2. 自定义赋值行为
 unique_smart_ptr& operator = (unique_smart_ptr rhs) {
  // 2.1 交换rhs.ptr_和this->ptr_
  std::swap(rhs.ptr_, this->ptr_);
  return *this;
 }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
 T* ptr_;
};

自定义移动构造函数。在移动构造函数中,我们先是接管了other.ptr_指向的资源对象,然后把otherptr_置为nullptr,这样在other析构时就不会错误释放资源内存。

同时,根据C++的规则,手动提供移动构造函数后,就会自动禁用拷贝构造函数。也就是我们能得到以下效果:

unique_smart_ptr<int> ptr1{new int(10)};
unique_smart_ptr<int> ptr2 = ptr1; // error
unique_smart_ptr<int> ptr3 = std::move(ptr1); // ok
unique_smart_ptr<int> ptr4{ptr1} // error
unique_smart_ptr<int> ptr5{std::move(ptr1)} // ok

自定义赋值函数。在赋值函数中,我们使用std::swap交换了 rhs.ptr_this->ptr_,注意,这里不能简单的将rhs.ptr_设置为nullptr,因为this->ptr_可能有指向一个堆对象,该对象需要转给rhs,在赋值函数调用结束,rhs析构时顺便释放掉。避免内存泄漏。

注意赋值函数的入参rhs的类型是unique_smart_ptr而不是unique_smart_ptr&&,这样创建rhs使用移动构造函数还是拷贝构造函数完全取决于unique_smart_ptr的定义。因为unique_smart_ptr当前只保留了移动构造函数,所以rhs是通过移动构造函数创建的。

多个智能指针共享对象 - shared_smart_ptr

学过第二讲的shared_ptr, 我们知道它是利用计数引用的方式,实现了多个智能指针共享同一个对象。当最后一个持有对象的智能指针析构时,计数器减为0,这个时候才会回收资源对象。

我们先给出shared_smart_ptr的类定义

template <typename T>
class shared_smart_ptr {
public:
 // 构造函数
 explicit shared_smart_ptr(T* ptr = nullptr)
 // 析构函数
 ~shared_smart_ptr()
 // 移动构造函数
 shared_smart_ptr(shared_smart_ptr&& other)
 // 拷贝构造函数
 shared_smart_ptr(const shared_smart_ptr& other)
 // 赋值函数
 shared_smart_ptr& operator = (shared_smart_ptr rhs)
 // 返回当前引用次数
 int use_count() const { return *count_; }
 T& operator*() const { return *ptr_; }
 T* operator->() const { return ptr_; }
private:
 T* ptr_;
 int* count_;
}

暂时不考虑多线程并发安全的问题,我们简单在堆上创建一个int类型的计数器count_。下面详细展开各个函数的实现。

为了避免对count_的重复删除,我们保持:只有当ptr_ != nullptr时,才对count_进行赋值。

构造函数

同样的,使用explicit避免隐式转换。除了赋值ptr_, 还需要在堆上创建一个计数器。

explicit shared_smart_ptr(T* ptr = nullptr){
 ptr_ = ptr;
 if (ptr_) {
  count_ = new int(1);
 }
}

析构函数

在析构函数中,需要根据计数器的引用数判断是否需要回收对象。

~shared_smart_ptr() {
 // ptr_为nullptr,不需要做任何处理
 if (ptr_) {
  return;
 }
 // 计数器减一
 --(*count_);
 // 计数器减为0,回收对象
 if (*count_ == 0) {
  delete ptr_;
  delete count_;
  return;
 }
}

移动构造函数

添加对count_的处理

shared_smart_ptr(shared_smart_ptr&& other) {
 ptr_ = other.ptr_;
 count_ = other.count_;
 other.ptr_ = nullptr;
 other.count_ = nullptr;
}

赋值构造函数

添加交换count_

shared_smart_ptr& operator = (shared_smart_ptr rhs) {
 std::swap(rhs.ptr_, this->ptr_);
 std::swap(rhs.count_, this->count_);
 return *this;
}

拷贝构造函数

对于shared_smart_ptr,我们需要手动支持拷贝构造函数。主要处理逻辑是赋值ptr_和增加计数器的引用数。

shared_smart_ptr(const shared_smart_ptr& other) {
 ptr_ = other.ptr_;
 count_ = other.count_;
 if (ptr_) {
  (*count_)++;
 }
}

这样,我们就实现了一个自己的共享智能指针,贴一下完整代码

template <typename T>
class shared_smart_ptr {
public:
 explicit shared_smart_ptr(T* ptr = nullptr){
  ptr_ = ptr;
  if (ptr_) {
   count_ = new int(1);
  }
 }
 ~shared_smart_ptr() {
  // ptr_为nullptr,不需要做任何处理
  if (ptr_ == nullptr) {
   return;
  }
  // 计数器减一
  --(*count_);
  // 计数器减为0,回收对象
  if (*count_ == 0) {
   delete ptr_;
   delete count_;
  }
 }
 shared_smart_ptr(shared_smart_ptr&& other) {
  ptr_ = other.ptr_;
  count_ = other.count_;
  other.ptr_ = nullptr;
  other.count_ = nullptr;
 }
 shared_smart_ptr(const shared_smart_ptr& other) {
  ptr_ = other.ptr_;
  count_ = other.count_;
  if (ptr_) {
   (*count_)++;
  }
 }
 shared_smart_ptr& operator = (shared_smart_ptr rhs) {
  std::swap(rhs.ptr_, this->ptr_);
  std::swap(rhs.count_, this->count_);
  return *this;
 }
 int use_count() const { return *count_; };
 T& operator*() const { return *ptr_; };
 T* operator->() const { return ptr_; };
private:
 T* ptr_;
 int* count_;
};

使用下面代码进行验证:

int main(int argc, const char** argv) {
 shared_smart_ptr<int> ptr1(new int(1));
 std::cout << "[初始化ptr1] use count of ptr1: " << ptr1.use_count() << std::endl;
 {
  // 赋值使用拷贝构造函数
  shared_smart_ptr<int> ptr2 = ptr1;
  std::cout << "[使用拷贝构造函数将ptr1赋值给ptr2] use count of ptr1: " << ptr1.use_count() << std::endl;
  // 赋值使用移动构造函数
  shared_smart_ptr<int> ptr3 = std::move(ptr2);
  std::cout << "[使用移动构造函数将ptr2赋值给ptr3] use count of ptr1: " << ptr1.use_count() << std::endl;
 }
 std::cout << "[ptr2和ptr3析构后] use count of ptr1: " << ptr1.use_count() << std::endl;
}

运行结果:

[初始化ptr1] use count of ptr1: 1
[使用拷贝构造函数将ptr1赋值给ptr2] use count of ptr1: 2
[使用移动构造函数将ptr2赋值给ptr3] use count of ptr1: 2
[ptr2和ptr3析构后] use count of ptr1: 1

总结

这一讲我们从AutoIntPtr出发,先是将类进行模版化,使其能够管理任何类型的指针对象,并给该类起了一个更抽象、更贴切的名称——smart_ptr

接着围绕着「如何正确释放资源对象指针」的问题,一步步手撸了两个智能指针 ——unique_smart_ptrshared_smart_ptr。相信大家现在对智能指针有一个较为深入的理解了。

以上就是C++手撸智能指针的教程分享的详细内容,更多关于C++智能指针的资料请关注脚本之家其它相关文章!

相关文章

  • C++实现模拟shell命令行(代码解析)

    C++实现模拟shell命令行(代码解析)

    这篇文章主要介绍了C++实现模拟shell命令行,本文通过实例代码进行命令行解析,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • 北邮计算机考研复试题的C语言解答精选

    北邮计算机考研复试题的C语言解答精选

    这篇文章主要介绍了北邮计算机考研复试题目的C语言解答精选,选自2012年的一些基础的上机题目,需要的朋友可以参考下
    2015-08-08
  • C语言轻松实现扫雷小游戏

    C语言轻松实现扫雷小游戏

    扫雷是一款经典的小游戏,这篇文章主要为大家详细介绍了C语言轻松实现扫雷小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • 一文带你探索C++中类型转换的奥秘

    一文带你探索C++中类型转换的奥秘

    C++ 提供了四种类型转换方式,帮助我们在不同数据类型之间进行有效的数据传递和操作,这些类型转换方式在不同的场景下有各自的优势和适用性,下面我们就来深入了解一下吧
    2023-10-10
  • 详解C++ Qt中堆叠窗体的使用案例

    详解C++ Qt中堆叠窗体的使用案例

    这篇文章主要为大家详细介绍了C++ Qt中堆叠窗体的使用案例,文中的示例代码讲解详细,对我们学习QT有一定的帮助,感兴趣的小伙伴可以了解一下
    2023-08-08
  • boost.asio框架系列之socket编程

    boost.asio框架系列之socket编程

    这篇文章介绍了boost.asio框架系列之socket编程,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • 浅谈C语言=与==的区别详解

    浅谈C语言=与==的区别详解

    这篇文章主要介绍了浅谈C语言=与==的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • 浅谈C++内存分配及变长数组的动态分配

    浅谈C++内存分配及变长数组的动态分配

    下面小编就为大家带来一篇浅谈C++内存分配及变长数组的动态分配。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-09-09
  • c++中关于int、long、long long等取值范围

    c++中关于int、long、long long等取值范围

    这篇文章主要介绍了c++中关于int、long、long long等取值范围,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-02-02
  • C语言版停车位管理系统

    C语言版停车位管理系统

    这篇文章主要为大家详细介绍了C语言版停车位管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03

最新评论