【重学C++】03 | 手撸C++智能指针实战教程
文章首发
前言
大家好,今天是【重学C++】的第三讲,书接上回,第二讲《02 脱离指针陷阱:深入浅出 C++ 智能指针》介绍了C++智能指针的一些使用方法和基本原理。今天,我们自己动手,从0到1实现一下自己的unique_ptr
和shared_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_
指向的资源对象,然后把other
的ptr_
置为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_ptr
和shared_smart_ptr
。相信大家现在对智能指针有一个较为深入的理解了。
【重学C++】03 | 手撸C++智能指针实战教程的更多相关文章
- php手撸轻量级开发(一)
聊聊本文内容 之前讲过php简单的内容,但是原生永远是不够看的,这次用框架做一些功能性的事情. 但是公司用自己的框架不能拿出来,用了用一些流行的框架比如tp,larveral之类的感觉太重,CI也不顺 ...
- 《Spring 手撸专栏》第 3 章:初显身手,运用设计模式,实现 Bean 的定义、注册、获取
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你是否能预见复杂内容的设计问题? 讲道理,无论产品功能是否复杂,都有很大一部分程序员 ...
- 推翻自己和过往,重学自定义View
http://blog.csdn.net/lfdfhl/article/details/51671038 深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 A ...
- Haskell手撸Softmax回归实现MNIST手写识别
Haskell手撸Softmax回归实现MNIST手写识别 前言 初学Haskell,看的书是Learn You a Haskell for Great Good, 才刚看到Making Our Ow ...
- 重学 Java 设计模式:实战抽象工厂模式
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获!
- 重学 Java 设计模式:实战装饰器模式(SSO单点登录功能扩展,增加拦截用户访问方法范围场景)
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 对于代码你有编程感觉吗 很多人写代码往往是没有编程感觉的,也就是除了可以把功能按照固 ...
- 重学 Java 设计模式:实战代理模式「模拟mybatis-spring中定义DAO接口,使用代理类方式操作数据库原理实现场景」
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 难以跨越的瓶颈期,把你拿捏滴死死的! 编程开发学习过程中遇到的瓶颈期,往往是由于看不 ...
- 《Mybatis 手撸专栏》第1章:开篇介绍,我要带你撸 Mybatis 啦!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 1. 为甚,撸Mybatis 我就知道,你会忍不住对它下手! 21年带着粉丝伙伴撸了一遍 Sp ...
- 重学hadoop技术
最近因为做了些和hadoop相关的项目(虽然主要是运维),但是这段经历让我对hadoop的实际运用有了更加深入的理解. 相比以前自学hadoop,因为没有实战场景以及良好的大数据学习氛围,现在回顾下的 ...
- Java集合类简单总结(重学)
java集合类简介(重学) 一.Collection(集合).Map接口两者应该是平行关系吧. 1.Map介绍 Map是以键值(key-value)对来存放的,2个值.通过key来找到value(例: ...
随机推荐
- 迁移学习(CDAN)《Conditional Adversarial Domain Adaptation》(已复现迁移)
论文信息 论文标题:Conditional Adversarial Domain Adaptation论文作者:Yaroslav Ganin, Evgeniya Ustinova, Hana Ajak ...
- 文件的上传&预览&下载学习(四)
0.参考博客 https://blog.csdn.net/Chengzi_comm/article/details/53037967 逻辑清晰 https://blog.csdn.net/alli09 ...
- Activiti7开发(四)-我的待办
目录 1. 查询登录用户的待办任务 2.审批 1. 查询登录用户的待办任务 private List<Task> queryMyTasks(){ String username = Sec ...
- 把 ChatGPT 加入 Flutter 开发,会有怎样的体验?
前言 ChatGPT 最近一直都处于技术圈的讨论焦点.它除了可作为普通用户的日常 AI 助手,还可以帮助开发者加速开发进度.声网社区的一位开发者"小猿"就基于 ChatGPT 做了 ...
- 手把手 Golang 实现静态图像与视频流人脸识别
说起人脸识别,大家首先想到的实现方式应该是 Python 去做相关的处理,因为相关的机器学习框架,库都已经封装得比较好了.但是我们今天讨论的实现方式换成 Golang,利用 Golang 去做静态图像 ...
- pandas之读取文件
当使用 Pandas 做数据分析的时,需要读取事先准备好的数据集,这是做数据分析的第一步.Panda 提供了多种读取数据的方法: read_csv() 用于读取文本文件 read_json() 用于读 ...
- [Android]ADB调试: SecurityException: Injecting to another application requires INJECT_EVENTS permission
问题描述 使用ADB工具调试安卓设备时报此错误: C:\Users\Johnny>adb shell input text "Hello" java.lang.Securit ...
- AndroidBanner - ViewPager 03
AndroidBanner - ViewPager 03 上一篇文章,描述了如何实现自动轮播的,以及手指触摸的时候停止轮播,抬起继续轮播,其实还遗留了一些问题: 当banner不可见的时候,也需要停止 ...
- ChatGPT,我彻彻底底沦陷了!
当谈到人工智能技术的时候,我们会经常听到GPT这个术语.它代表"Generative Pre-trained Transformer",是一种机器学习模型,采用了神经网络来模拟人类 ...
- sql ytd 附python 实现方式
ytd释义 YTD分析属于同比分析类,其特点在于对比汇总值,即从年初第一日值一直至今的值累加.作用在于分析企业中长期的经营绩效. 做法 假定: 有一张销量明细表 date 仓库 sku 销量 2020 ...