C++:一文读懂智能指针

C++11 引入了 3 个智能指针类型:
当使用智能指针时,我们首先需要包含 memory头文件,这个头文件包含了 C++ 标准库中智能指针的定义。
1.std::unique_ptr<T> :独占资源所有权的指针。
2.std::shared_ptr<T> :共享资源所有权的指针。
3.std::weak_ptr<T> :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。
std::auto_ptr 已被废弃。

std::unique_ptr 的使用

#include <iostream>
#include <memory>int main() {// 创建一个 std::unique_ptr,指向一个动态分配的整数std::unique_ptr<int> ptr(new int(42));// 使用智能指针访问其所管理的对象std::cout << "值:" << *ptr << std::endl;// 不需要手动释放内存,当 std::unique_ptr 离开作用域时会自动释放内存return 0;
}

在这个示例中,std::unique_ptr ptr(new int(42)); 创建了一个 std::unique_ptr,并将其指向动态分配的整数。当 ptr 离开作用域时,它所管理的整数会自动被释放,无需手动调用 delete。

std::unique_ptr 的原理是基于资源获取即初始化(RAII)的概念。它在构造时接管了动态分配的内存,然后在析构时自动释放该内存。由于 std::unique_ptr 不能进行拷贝或赋值,因此保证了独占所有权的原则。

std::unique_ptr

简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。

std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。

1.使用裸指针时,要记得释放内存。

{int* p = new int(100);// ...delete p;  // 要记得释放内存
}

1.使用 std::unique_ptr 自动管理内存。

{std::unique_ptr<int> uptr = std::make_unique<int>(200);//...// 离开 uptr 的作用域的时候自动释放内存
}

1.std::unique_ptr 是 move-only 的。

{std::unique_ptr<int> uptr = std::make_unique<int>(200);std::unique_ptr<int> uptr1 = uptr;  // 编译错误,std::unique_ptr<T> 是 move-only 的std::unique_ptr<int> uptr2 = std::move(uptr);assert(uptr == nullptr);
}

1.std::unique_ptr 可以指向一个数组。

{std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);for (int i = 0; i < 10; i++) {uptr[i] = i * i;}   for (int i = 0; i < 10; i++) {std::cout << uptr[i] << std::endl;}   
}

1.自定义 deleter。

{struct FileCloser {void operator()(FILE* fp) const {if (fp != nullptr) {fclose(fp);}}   };  std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}

1.使用 Lambda 的 deleter。

{std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(fopen("test_file.txt", "w"), [](FILE* fp) {fclose(fp);});
}

std::shared_ptr 的使用

#include <iostream>
#include <memory>int main() {// 创建一个 std::shared_ptr,指向一个动态分配的整数std::shared_ptr<int> ptr1(new int(42));// 创建另一个 std::shared_ptr 指向同一块内存std::shared_ptr<int> ptr2 = ptr1;// 使用智能指针访问其所管理的对象std::cout << "ptr1 的值:" << *ptr1 << std::endl;std::cout << "ptr2 的值:" << *ptr2 << std::endl;// 不需要手动释放内存,当最后一个指向该内存的 std::shared_ptr 离开作用域时会自动释放内存return 0;
}

在这个示例中,std::shared_ptr ptr1(new int(42)); 创建了一个 std::shared_ptr,并将其指向动态分配的整数。然后 std::shared_ptr ptr2 = ptr1; 创建了另一个 std::shared_ptr 指向同一块内存。由于 std::shared_ptr 使用引用计数来管理内存,因此当最后一个指向该内存的 std::shared_ptr 离开作用域时,内存会被自动释放。

std::shared_ptr 的原理是基于引用计数的概念。每个 std::shared_ptr 都会维护一个引用计数,当有新的 std::shared_ptr 指向同一块内存时,引用计数会增加,当 std::shared_ptr 离开作用域时,引用计数会减少,当引用计数为 0 时,内存会被释放。这样可以确保在不再需要时释放内存,避免内存泄漏。

std::shared_ptr

std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。

{std::shared_ptr<int> sptr = std::make_shared<int>(200);assert(sptr.use_count() == 1);  // 此时引用计数为 1{   std::shared_ptr<int> sptr1 = sptr;assert(sptr.get() == sptr1.get());assert(sptr.use_count() == 2);   // sptr 和 sptr1 共享资源,引用计数为 2}   assert(sptr.use_count() == 1);   // sptr1 已经释放
}
// use_count 为 0 时自动释放内存

和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。

{// C++20 才支持 std::make_shared<int[]>// std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);std::shared_ptr<int[]> sptr(new int[10]);for (int i = 0; i < 10; i++) {sptr[i] = i * i;}   for (int i = 0; i < 10; i++) {std::cout << sptr[i] << std::endl;}   
}{std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {std::cout << "close " << fp << std::endl;fclose(fp);});
}

std::shared_ptr 的实现原理

一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。

  std::cout << sizeof(int*) << std::endl;  // 输出 8std::cout << sizeof(std::unique_ptr<int>) << std::endl;  // 输出 8std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)<< std::endl;  // 输出 40std::cout << sizeof(std::shared_ptr<int>) << std::endl;  // 输出 16std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {std::cout << "close " << fp << std::endl;fclose(fp);}); std::cout << sizeof(sptr) << std::endl;  // 输出 16

无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。

shared_ptr 需要维护的信息有两部分:

指向共享资源的指针。
引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。
所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。

当我们创建一个 shared_ptr 时,其实现一般如下:

std::shared_ptr<T> sptr1(new T);

在这里插入图片描述
复制一个 shared_ptr :

std::shared_ptr<T> sptr2 = sptr1;

在这里插入图片描述
为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared_ptr 对象中指向共享资源的指针,以节省内存开销?

答案是:不能。 因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。

来看一个例子。

struct Fruit {int juice;
};struct Vegetable {int fiber;
};struct Tomato : public Fruit, Vegetable {int sauce;
};// 由于继承的存在,shared_ptr 可能指向基类对象
std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> vegetable = tomato;

在这里插入图片描述
另外,std::shared_ptr 支持 aliasing constructor。

template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

Aliasing constructor,简单说就是构造出来的 shared_ptr 对象和参数 r 指向同一个控制块(会影响 r 指向的资源的生命周期),但是指向共享资源的指针是参数 ptr。看下面这个例子。

using Vec = std::vector<int>;
std::shared_ptr<int> GetSPtr() {auto elts = {0, 1, 2, 3, 4};std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);return std::shared_ptr<int>(pvec, &(*pvec)[2]);
}std::shared_ptr<int> sptr = GetSPtr();
for (int i = -2; i < 3; ++i) {printf("%d\n", sptr.get()[i]);
}

在这里插入图片描述
看上面的例子,使用 std::shared_ptr 时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了 std::make_shared 函数来创建一个 shared_ptr 对象,只需要一次内存分配。
在这里插入图片描述

这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。
在这里插入图片描述

std::weak_ptr

std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:

1.如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
2.当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。

void Observe(std::weak_ptr<int> wptr) {if (auto sptr = wptr.lock()) {std::cout << "value: " << *sptr << std::endl;} else {std::cout << "wptr lock fail" << std::endl;}
}std::weak_ptr<int> wptr;
{auto sptr = std::make_shared<int>(111);wptr = sptr;Observe(wptr);  // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr
}
Observe(wptr);  // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr

在这里插入图片描述
当 shared_ptr 析构并释放共享资源的时候,只要 weak_ptr 对象还存在,控制块就会保留,weak_ptr 可以通过控制块观察到对象是否存活。
在这里插入图片描述

enable_shared_from_this

一个类的成员函数如何获得指向自身(this)的 shared_ptr? 看看下面这个例子有没有问题?

class Foo {public:std::shared_ptr<Foo> GetSPtr() {return std::shared_ptr<Foo>(this);}
};auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);

在这里插入图片描述
成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this。

class Bar : public std::enable_shared_from_this<Bar> {public:std::shared_ptr<Bar> GetSPtr() {return shared_from_this();}
};auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);

一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

似乎继承了 std::enable_shared_from_this 的类都被强制必须通过 shared_ptr 进行管理。

auto b = new Bar;
auto sptr = b->shared_from_this();

小结

智能指针,本质上是对资源所有权和生命周期管理的抽象:

1.当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
2.当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
3.使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
4.通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。

参考资料
1.智能指针

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/215144.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

「Verilog学习笔记」信号发生器

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 方波的实现&#xff0c;较为简单&#xff0c;只需要设置一个计数器&#xff0c;使输出保持10个时钟为0&#xff0c;跳变为20&#xff0c;再保持10个时钟。依次循环。可以按…

RTMDet原理与代码解析

paper&#xff1a;RTMDet: An Empirical Study of Designing Real-Time Object Detectors official implementation&#xff1a;https://github.com/open-mmlab/mmdetection/tree/main/configs/rtmdet 本文的创新点 Backbone and Neck 在backbone的basic building block中采…

2023年【R1快开门式压力容器操作】考试资料及R1快开门式压力容器操作复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 R1快开门式压力容器操作考试资料参考答案及R1快开门式压力容器操作考试试题解析是安全生产模拟考试一点通题库老师及R1快开门式压力容器操作操作证已考过的学员汇总&#xff0c;相对有效帮助R1快开门式压力容器操作复…

2023年【危险化学品经营单位安全管理人员】考试内容及危险化学品经营单位安全管理人员最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位安全管理人员考试内容是安全生产模拟考试一点通生成的&#xff0c;危险化学品经营单位安全管理人员证模拟考试题库是根据危险化学品经营单位安全管理人员最新版教材汇编出危险化学品经营单位安全管…

Java核心知识点整理大全12-笔记

Java核心知识点整理大全-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全2-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全3-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全4-笔记-CSDN博客 Java核心知识点整理大全5-笔记-CSDN博客 Java核心知识点整理大全6…

【多线程】Thread类的使用

目录 1.概述 2.Thread的常见构造方法 3.Thread的几个常见属性 4.启动一个线程-start() 5.中断一个线程 5.1通过共享的标记来进行沟通 5.2 调用 interrupt() 方法来通知 6.等待一个进程 7.获取当前线程引用 8.线程的状态 8.1所有状态 8.2线程状态和转移的意义 1.概述 …

使用向日葵开机棒进行远程开机

文章目录 1\. 前言2\. 说明3\. 开机棒设置4\. 电脑端设置4.1. 电脑端允许网卡唤醒4.1.1. 关闭设备节能4.2. 将电脑端设备加入设备列表 5\. 手机端5.1. 添加开机棒5.2. 绑定主机5.2.1. 添加成功的主机 6\. 唤醒 1. 前言 如果我们出差在外或者人不在实验室&#xff0c;如果可以使…

【极客技术】真假GPT-4?微调 Llama 2 以替代 GPT-3.5/4 已然可行!

近日小编在使用最新版GPT-4-Turbo模型&#xff08;主要特点是支持128k输入和知识库截止日期是2023年4月&#xff09;时&#xff0c;发现不同商家提供的模型回复出现不一致的情况&#xff0c;尤其是模型均承认自己知识库达到2023年4月&#xff0c;但当我们细问时&#xff0c;Fak…

【Spring篇】JDK动态代理

目录 什么是代理&#xff1f; 代理模式 动态代理 Java中常用的代理模式 问题来了&#xff0c;如何动态生成代理类&#xff1f; 动态代理底层实现 什么是代理&#xff1f; 顾名思义&#xff0c;代替某个对象去处理一些问题&#xff0c;谓之代理&#xff0c;那么何为动态&a…

Zabbix-Liunx服务器内存使用率测试

要在Python 2.7中运行内存消耗脚本并安装psutil&#xff0c;您需要先安装pip。以下是完整的步骤&#xff0c;包括如何在Python 2.7环境中安装pip&#xff0c;然后安装psutil&#xff0c;以及最后如何运行内存消耗脚本。 步骤1: 安装pip 在Python 2.7中安装pip&#xff1a; 首先…

积分球吸收光谱测量的领域有哪些?

积分球吸收光谱测量是一种常用的吸收光谱测量方法&#xff0c;它通过将样品放置在积分球的入口处&#xff0c;球内的光线经过多次反射后形成均匀的照度分布&#xff0c;然后使用光度计或光谱仪对光线进行测量&#xff0c;可以获得样品的相关参数。 在积分球吸收光谱测量中&…

Python入门03变量

目录 1 什么是变量2 变量声明3 变量命名规则4 变量类型5 类型转换总结 1 什么是变量 编程语言中变量就像容器一样&#xff0c;可以用来存放东西 我的变量就像杯子一样&#xff0c;可以用来盛放各种饮料。在Python中变量用来存放各种各样的数据&#xff0c;比如整数、浮点数、…