首页 > 编程语言 >【C++】智能指针的正确使用方式

【C++】智能指针的正确使用方式

时间:2024-11-01 10:33:41浏览次数:1  
标签:std C++ 智能 内存 shared unique ptr 指针

本文将从这几方面讲解智能指针:

  • 智能指针的应用场景分析
  • 智能指针的性能分析: 为什么 shared_ptr 性能比 unique_ptr 差
  • 指针作为函数参数时应该传,传值、传引用,还是裸指针?

对于智能指针的使用,实际上是对所有权和生命周期的思考

1.unique_ptr:专属所有权

1.1 unique_ptr介绍

我们大多数场景下用到的应该都是 unique_ptr 。

unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。

所以, unique_ptr 不支持复制和赋值 ,如下:

auto w = std::make_unique<Widget>();   
auto w2 = w;     // 编译错误

如果想要把 w 复制给 w2, 是不可以的。因为复制从语义上来说,两个对象将共享同一块内存。

因此, unique_ptr 只支持移动 , 即如下:

auto w = std::make_unique<Widget>();
auto w2 = std::move(w);    // w2获得内存所有权,w此时等于nullptr

unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。 只能使用 std::move 转移当前对象的所有权 。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。

如上代码中,将 w 对象的所有权转移给 w2 后,w 此时等于 nullptr,而 w2 获得了专属所有权。

1.2 unique_ptr的性能

因为 C++ 的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的。
所以 内存上没有任何的额外消耗,性能是最优的。

1.3 unique_ptr的使用场景

1.3.1 忘记delete

unique_ptr 一个最简单的使用场景是用于类属性。代码如下:

class Box
{
public:
    Box() : w(new Widget())
	{}

    ~Box()
	{}
private:
	Widget* w;
};

如果因为一些原因,w 必须建立在堆上。如果用裸指针管理 w,那么需要在析构函数中 delete w;
这种写法虽然没什么问题,但是容易漏写 delete 语句,造成内存泄漏。

如果按照 unique_ptr 的写法,不用在析构函数手动 delete 属性,当对象析构时,属性 w 将会自动释放内存。

1.3.2 异常安全

假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:

void process()
{
	Widget *w = new Widget();
	w->do_something();        // 可能会发生异常
	delete w;
}

在正常流程下,我们会在函数末尾 delete 创建的对象 w,正常调用析构函数,释放内存。

但是如果 w->do_something() 发生了异常,那么 delete w 将不会被执行。此时就会发生 内存泄漏
我们当然可以使用 try…catch 捕捉异常,在 catch 里面执行 delete,但是这样代码上并不美观,也容易漏写。

如果我们用 std::unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在 unique_ptr 离开函数作用域的时候,内存就将会自动释放。

2.shared_ptr:共享所有权

2.1 shared_ptr简介

在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
因此,从语义上来看, shared_ptr 是支持复制的 。如下:

auto w = std::make_shared<Widget>();
{
	auto w2 = w;
	cout << w.use_count() << endl;    // 2
} 
cout << w.use_count() << endl;    // 1

shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。

同时, shared_ptr 也支持移动 。从语义上来看,移动指的是所有权的传递。如下:

auto w = std::make_shared<Widget>();
auto w2 = std::move(w);          // 此时w等于nullptr,w2.use_count()等于1

我们将 w 对象 move 给 w2,意味着 w 放弃了对内存的所有权和管理,此时 w 对象等于 nullptr。

而 w2 获得了对象所有权,但因为此时 w 已不再持有对象,因此 w2 的引用计数为 1。

用法:

std::shared_ptr<类型> 变量名称{};
std::shared_ptr<int> ptrA{};
std::shared_ptr<int> ptrB{std::make_shared<int>(5)};

注意:std::make_shared不支持数组。

std::shared_ptr<int[]> ptrC{ new int[5]{1, 2, 3, 4, 5} };

2.2 shared_ptr性能

1.内存占用高

shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
因此相比于 unique_ptr, shared_ptr 的内存占用更高

2.原子操作性能低

考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

3.使能移动优化性能

shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。

如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。

对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

2.3 shared_ptr的使用场景

1.shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。

2.对象的延迟销毁。陈硕在《Linux 多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用 BlockingQueue<std::shared_ptr<void>> 将对象转移到另外一个线程中释放,从而解放关键线程。

3.选择哪种指针作为函数的参数?

很多时候,函数的参数是个指针。这个时候就会面临选择困难症,这个参数应该怎么传,应该是 shared_ptr,还是 const shared_ptr&,还是直接 raw pointer 更合适。

1.只在函数使用指针,但并不保存对象内容

假如我们只需要在函数中,用这个对象处理一些事情,但不打算涉及其生命周期的管理,也不打算通过函数传参延长 shared_ptr 的生命周期。

对于这种情况,可以使用 raw pointer 或者 const shared_ptr&。

void func(Widget *);
void func(const std::shared_ptr<Widget>&);

实际上第一种裸指针的方式可能更好,从语义上更加清楚,函数也不用关心智能指针的类型。

2.在函数中保存智能指针

假如我们需要在函数中把这个智能指针保存起来,这个时候建议直接传值。

void func(std::shared_ptr<Widget> ptr);

这样的话,外部传过来值的时候,可以选择 move 或者赋值。函数内部直接把这个对象通过 move 的方式保存起来。
这样性能更好,而且外部调用也有多种选择。










参考文章:

C++ 智能指针的正确使用方式:unique_ptr VS shared_ptr_unique shared区别-CSDN博客

标签:std,C++,智能,内存,shared,unique,ptr,指针
From: https://www.cnblogs.com/Wangzx000/p/18456480

相关文章

  • 什么是指针数组 和 数组指针
    什么是指针数组答:就是一个数组,里面存的是指针而已它的写法可以如下:int*a[10];看看,它就是一个指针数组,数组名字当然是a,里面有10个元素,每个元素都是一个int*类型(即存放整型地址的指针)的指针。我们可以这样用,比如: #include<stdio.h>Cintmain(){intx=10,y=20,z=3......
  • C++之OpenCV入门到提高003:矩阵的掩膜(Mask)处理
    一、介绍今天是这个系列《C++之Opencv入门到提高》得第三篇文章。今天这篇文章也不难,主要介绍如何使用Opencv对图像进行掩膜处理,提高图像的对比度。在这个过程中,我们可以学到如何获取图像指针、如何处理像素值越界等问题。我们一步一个脚印的走,收获就会越来越多。虽然......
  • 道品智能科技与系统集成:迈向未来的科技之路
    在当今快速发展的科技时代,系统集成与智能科技已经成为推动各行各业创新与发展的重要动力。随着信息技术的不断进步,各类智能设备和系统逐渐融入我们的日常生活,改变了人们的工作方式、生活习惯以及思维模式。本文将探讨科技、系统集成和智能科技的内涵、发展现状及其在未来的趋势......
  • PCL 法线微分(DoN)分割(C++详细过程版)
    目录一、概述二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接,首发于:2024年11月1日。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的抄袭狗。一、概述  法线微分(DoN)分割在PCL里有现成的调用函数,具体算法原理和实现代码见:PCL基于法线微分(D......
  • C++《list》
    在本篇当中我们将学习STL中的list,在此list就是我们之前在数据结构学习过的链表,在本篇中我们要来了解list当中的成员函数该如何使用,由于list各个函数的接口和之前学习过的vector类型,因此在学习list的使用就较为轻松。在lis篇章中我们要重点了解的是在下一个篇章当中的list模拟实......
  • [智能自动编曲软件 ]band in a box 2024 中文汉化完整版+安装方法 [WiN](201GB+)
    智能自动编曲软件2024有50多项新功能!其中包括许多重要的新功能。首先是新的音轨窗口。与大多数DAW音轨窗口类似,它显示所有音轨,允许在DAW用户熟悉的环境中对音轨进行无损数据操作。新的音轨窗口包括使用RealTracks内容创建循环和乐句的特定支持。现在,浮动窗口无处不......
  • 人工智能生产力悖论:为什么大多数人没用上AI工具?
    真正的障碍不是技术,而是思考的时间:虽然像ChatGPT这样的工具确实有潜力带来变革,但我接触的大多数人几乎不用它。就算用的,也只是用来做些总结之类的小活儿。而在这些用户里,只有大约5%的人订阅了高级版,这意味着真正用AI来搞复杂任务的“高手”少得可怜。下面的内容主要来自Shopify......
  • C++ 手撕--基本数据结构的简单实现
    C++面试手撕代码----基本数据结构的简单实现1.String数据结构的简单实现:#include<iostream>#include<cstring>//forstrcpystrlenmethodsusingnamespacestd;classString{private: char*data; size_tlength;public: String():data(nullptr),length(0)......
  • 华为OD机试-(E卷,100分) - 补种未成活胡杨(Java & Python& JS & C++ & C )
    最新华为OD机试题目描述近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1-N),排成一排。一个月后,有M棵胡杨未能成活。现可补种胡杨K棵,请问如何补种(只能补种,不能新种),可以得到最多的连续胡杨树?输入描述N总种植数量,1<=N<=100000M未成活胡杨数量,M个空格......
  • C++对象优化4条原则
    1、函数参数传递优先使用传引用,而不是传值①、函数参数传递的过程是赋值的过程,对象之间赋值是会产生赋值运算符的重载调用,退出函数时还会再调用一次析构函数,传引用就不存在上述函数调用2、函数返回一个临时对象时,应该直接返回,而不应该先定义一个临时对象,然后返回定义的临......