C++智能指针(三)——unique_ptr初探

与共享指针shared_ptr用于共享对象的目的不同,unique_ptr是用于独享对象。

文章目录

  • 1. unqiue_ptr的目的
  • 2. 使用 unique_ptr
    • 2.1 初始化 unique_ptr
    • 2.2 访问数据
    • 2.3 作为类的成员
    • 2.4 处理数组
  • 3. 转移所有权
    • 3.1 简单语法
    • 3.2 函数间转移所有权
      • 3.2.1 转移至函数体内
      • 3.2.2 转移出函数体
  • 4. Deleter
    • 4.1 default_delete<>
    • 4.2 其他相关资源的Deleters
  • 5. unique_ptr与shared_ptr性能的简单分析
  • 6. 附录
  • 7. 参考文献


1. unqiue_ptr的目的

首先,如果我们在函数中使用普通指针,会有许多问题,比如下面的函数 void f()

void f()
{ClassA* ptr = new ClassA; // create an object explicitly... // perform some operationsdelete ptr; // clean up(destroy the object explicitly)
}

抛开使用普通指针容易忘记使用 delete 导致内存泄漏不谈,如果我们在执行一些操作的时候,出现错误了,那么也会导致内存泄漏。

那么就要引入异常处理操作,比如 try...catch...,那么程序会看起来冗余且复杂。

之前已经讨论过智能指针中的两个 shared_ptrweak_ptr 带来的便捷性,而与共享对象的场景不同,unique_ptr 主要用于独享对象所有权的场景,即程序只有一个unique_ptr拥有该对象的所有权,只能转移所有权,不能共享所有权。与 shared_ptr 没有拥有该对象的智能指针后,释放资源与空间。

比如我们用 unique_ptr 改写上面的代码:

// header file for unique_ptr
#include <memory>
void f()
{// create and initialize an unique_ptrstd::unique<ClassA> ptr(new ClassA);... // perform some operations
}

that’s all,这样我们就省去了delete 与异常处理部分,是不是非常滴nice~


2. 使用 unique_ptr

2.1 初始化 unique_ptr

unique_ptr 不允许使用赋值语法去使用一个普通指针初始化,只能直接将值代入构造函数进行初始化:

std::unique_ptr<int> up = new int; // ERROR
std::unique_ptr<int> up(new int); // OK

当然,一个unique_ptr可以不用一定拥有一个对象,即可以是空的。那就使用默认构造函数进行初始化:

std::unique_ptr<std::string> up;

可以使用 nullptr 赋值,或调用 reset(),这是等价的:

up = nullptr;
up.reset();

2.2 访问数据

与普通指针类似,可以使用 * 解引用出指向对象,也可以使用 -> 访问一个对象的成员:

// create and initialize (pointer to) string:
std::unique_ptr<std::string> up(new std::string("nico"));
(*up)[0] = ’N’; // replace first character
up->append("lai"); // append some characters
std::cout << *up << std::endl; // print whole string

unique_ptr 不允许指针算术运算,比如 ++,这可以看作是 unique_ptr 的优势,因为指针算术运算容易出错。

如果想在访问数据前,检查一个 unique_ptr 是否拥有一个对象,可以调用操作符 bool()

if (up) { // if up is not empty
std::cout << *up << std::endl;
}

当然也可以将 unique_ptr 与 nullptr 比较,或者将 get() 得到的原生指针与 nullptr 比较,以达到检查的目的:

if (up != nullptr) // if up is not empty
if (up.get() != nullptr) // if up is not empty

2.3 作为类的成员

我们将 unique_ptr 应用到类,也可以避免内存泄漏。并且,如果使用 unique_ptr 而不是普通指针,类将不需要析构器,因为对象删除时,其成员也会被自动删除。

除此之外,正常情况下,析构器只有在构造函数完成的情况下才会调用。如果在一个构造函数内部出现异常,析构器只会给那些完全完成构造的对象调用(所以下面的ClassB在构造函数中出现异常时,此时不会调用析构函数)。这就会导致如果使用原生指针,那么构造函数中第一个new成功,第二个new失败就会产生内存泄漏:

class ClassB {
private:
ClassA* ptr1; // pointer members
ClassA* ptr2;
public:
// constructor that initializes the pointers
// - will cause resource leak if second new throws
ClassB (int val1, int val2)
: ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
}
// copy constructor
// - might cause resource leak if second new throws
ClassB (const ClassB& x)
: ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
}
// assignment operator
const ClassB& operator= (const ClassB& x) {
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
}
~ClassB () {
delete ptr1;
delete ptr2;
}
...
};

可以使用 unique_ptr 替换原生指针来避免上述情况:

class ClassB {
private:
std::unique_ptr<ClassA> ptr1; // unique_ptr members
std::unique_ptr<ClassA> ptr2;
public:
// constructor that initializes the unique_ptrs
// - no resource leak possible
ClassB (int val1, int val2)
: ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
}
// copy constructor
// - no resource leak possible
ClassB (const ClassB& x)
: ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
}
// assignment operator
const ClassB& operator= (const ClassB& x) {
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
}
// no destructor necessary
// (default destructor lets ptr1 and ptr2 delete their objects)
...
};

需要注意,如果不提供拷贝和赋值函数,对于默认的拷贝和赋值拷贝构造函数,显然是不可能的(不可能用一个独享指针拷贝构造一个独享指针),所以,默认只提供移动构造函数。

2.4 处理数组

默认情况下,unique_ptrs 在失去对象的所有权之后,调用 delete,但对于数组,应该是调用 delete[],所以下面的代码是可以编译的,但是有运行时错误的:

std::unique_ptr<std::string> up(new std::string[10]); // runtime ERROR

但其实我们不必像 shared_ptr 一样,在这种情况下,要通过自定义 deleter 来实现 delete[]。C++标准库已经对 unique_ptr 处理数组进行了特化,所以仅需要声明如下就可以:

std::unique_ptr<std::string[]> up(new std::string[10]); // OK

但是需要注意的是,这个特化会导致,我们不能使用 *-> 访问数组,而是要使用 [],这其实就和我们正常访问数组是一样的了:

std::unique_ptr<std::string[]> up(new std::string[10]); // OK
...
std::cout << *up << std::endl; // ERROR: * not defined for arrays
std::cout << up[0] << std::endl; // OK

:最后需要注意,索引的合法性是编码人员需要保证的,错误的索引会带来未定义的结果。


3. 转移所有权

3.1 简单语法

根据前面的讲解,我们知道需要保证没有两个unique_ptrs使用同一个指针初始化:

std::string* sp = new std::string("hello");
std::unique_ptr<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp); // ERROR: up1 and up2 own same data

因为只能独有,不能共享,所以肯定也无法进行一般的拷贝和赋值操作。但C++11有了新的语法——移动语义,这可以让我们使用构造函数和赋值操作来在unique_ptrs中转移对象所有权:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
// copy the unique_ptr
std::unique_ptr<ClassA> up2(up1); // ERROR: not possible
// transfer ownership of the unique_ptr
std::unique_ptr<ClassA> up3(std::move(up1)); // OK

上面是使用移动构造函数,下面使用赋值运算符有类似表现:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2; // create another unique_ptr
up2 = up1; // ERROR: not possible
up2 = std::move(up1); // assign the unique_ptr
// - transfers ownership from up1 to up2

当然,如果up2之前拥有另一个对象的所有权,那么在将up1的所有权转移给up2之后,up2之前拥有的对象就会被释放:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
// initialize another unique_ptr with a new object
std::unique_ptr<ClassA> up2(new ClassA);
up2 = std::move(up1); // move assign the unique_ptr
// - delete object owned by up2
// - transfer ownership from up1 to up2

当然,不能是将普通指针赋值给 unique_ptr,我们可以构造一个新的 unique_ptr来赋值:

std::unique_ptr<ClassA> ptr; // create a unique_ptr
ptr = new ClassA; // ERROR
ptr = std::unique_ptr<ClassA>(new ClassA); // OK, delete old object
// and own new

一个特殊的语法,我们可以使用 release()unique_ptr 拥有的对象所有权还给普通指针,当然也可以用于创建新的智能指针:

std::unique_ptr<std::string> up(new std::string("nico"));
...
std::string* sp = up.release(); // up loses ownership

3.2 函数间转移所有权

分为将所有权转入函数体内,以及从函数内转移出所有权两种情况。

3.2.1 转移至函数体内

下面的代码中,我们使用 sink(std::move(up)) 将函数体外部的up的所有权转移至函数 sink 内:

void sink(std::unique_ptr<ClassA> up) // sink() gets ownership
{
...
}
std::unique_ptr<ClassA> up(new ClassA);
...
sink(std::move(up)); // up loses ownership
...

3.2.2 转移出函数体

比如下面的代码中,source() 返回一个 unique_ptr,我们用 p 接收,就能获取对应对象的所有权:

std::unique_ptr<ClassA> source()
{std::unique_ptr<ClassA> ptr(new ClassA); // ptr owns the new object...return ptr; // transfer ownership to calling function
}
void g()
{std::unique_ptr<ClassA> p;for (int i=0; i<10; ++i) {p = source(); // p gets ownership of the returned object// (previously returned object of f() gets deleted)...}
} // last-owned object of p gets deleted

当然,每一次获取新的对象的所有权,都会把老对象给释放掉,在 g() 函数结束,也会释放最后获得的对象。

这里,不需要再 source() 函数中,使用移动语义,是因为根据C++11语法规则,编译器会自动尝试进行一个移动


4. Deleter

unique_ptr<> 针对初始指针引用对象的类别以及deleter的类型进行模板化:

namespace std {
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
typedef ... pointer; // may be D::pointer
typedef T element_type;
typedef D deleter_type;
...
};
}

对于数组的特化,其有相同的默认deleter,即 default_delete<T[]>

namespace std {
template <typename T, typename D>
class unique_ptr<T[], D>
{
public:typedef ... pointer; // may be D::pointertypedef T element_type;typedef D deleter_type;...};
}

4.1 default_delete<>

下面我们深入研究 unique_ptr 的声明:

namespace std {
// primary template:
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
...
T& operator*() const;
T* operator->() const noexcept;
...
};
// partial specialization for arrays:
template<typename T, typename D>
class unique_ptr<T[], D>
{
public:
...
T& operator[](size_t i) const;
...
}
}

其中,std::default_delete<> 内容如下,对于 T 调用 delete,对于 T[],调用 delete[]

namespace std {
// primary template:
template <typename T> class default_delete {
public:
void operator()(T* p) const; // calls delete p
...
};
// partial specialization for arrays:
template <typename T> class default_delete<T[]> {
public:
void operator()(T* p) const; // calls delete[] p
...
};
}

4.2 其他相关资源的Deleters

这个在 shared_ptr 的讲解中有提到,与 shared_ptr 一致在我们可以自定义 deleter,不同的是,shared_ptr 需要模板中提供deleter的类别。比如使用一个函数对象,传入类的名称:

class ClassADeleter
{
public:
void operator () (ClassA* p) {
std::cout << "call delete for ClassA object" << std::endl;
delete p;
}
};
...
std::unique_ptr<ClassA,ClassADeleter> up(new ClassA());

再比如使用一个函数或lambda表达式,我们可以指定为类似 void(*)(T*)std::function<void(T*)> 或使用 decltype

std::unique_ptr<int,void(*)(int*)> up(new int[10],
[](int* p) {
...
delete[] p;
}); // 1std::unique_ptr<int,std::function<void(int*)>> up(new int[10],
[](int* p) {
...
delete[] p;
}); // 2auto l = [](int* p) {
...
delete[] p;
};
std::unique_ptr<int,decltype(l)>> up(new int[10], l); // 3

还有一个骚操作:使用别名模板以避免指定deleter的类型

template <typename T>
using uniquePtr = std::unique_ptr<T,void(*)(T*)>; // alias template
...
uniquePtr<int> up(new int[10], [](int* p) { // used here
...
delete[] p;
});

5. unique_ptr与shared_ptr性能的简单分析

shared_ptr 类是使用一种非侵入的方式实现的,意味着这个类管理的对象不需要满足一个特定的需求,比如必须一个公共的基类等。这带来的巨大优势就是这个共享指针可以被用于任意类型,包括基础数据类型。因而产生的代价是,shared_ptr 对象内部需要多个成员:

  • 一个指向引用对象的普通指针
  • 一个所有共享指针引用相同的对象的计数器
  • 由于weak_ptr的存在,需要另一个计数器

因此,shared 和 weak 指针内部需要额外的helper对象,内部有指针引用它。这意味着一些特定的优化是不可能的(包括空基类优化,这允许消除任何内存开销)。

unique_ptr 不需要这些开销。它的“智能”是基于特有的构造函数和析构函数,以及拷贝语义的去除。对于一个有着无状态的活空的deleter的unique指针,将会消耗与原生指针相同大小的内存。而且比起使用原生指针和进行手动delete,没有额外的运行时开销。

但是,为了避免不必要开销的引入,你应该使用对于deleters使用函数对象(包括lambda表达式)以带来理想情况下0开销的最佳优化。


6. 附录

A. unique_ptr 操作表


7. 参考文献

《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

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

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

相关文章

软件工程与计算总结(十一)人机交互设计

目录 ​编辑 一.引例 二.目标 三.人类因素 1.精神模型 2.差异性 四.计算机因素 1.可视化设计 2.常见界面类型 五.人机交互设计的交互性 1.导航 2.反馈 3.设计原则 六.设计过程 1.基本过程 2.界面原型化 一.引例 无论软件功能多么出色&#xff0c;亦或内部的构造…

LENOVO联想笔记本小新 Pro-14 2021AMD处理器ACH版(82MS)原厂Win10系统

下载链接&#xff1a;https://pan.baidu.com/s/1-KZ8Y9NmkS7nDXcMbhZLHw?pwdyrkx 系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、lenovo联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#xff1…

深入篇【C++】总结智能指针的使用与应用意义(auto_ptr/unique_ptr/shared_ptr/weak_ptr)底层原理剖析+模拟实现

深入篇【C】总结智能指针的使用与应用意义&&(auto_ptr/unique_ptr/shared_ptr/weak_ptr&#xff09;底层原理剖析模拟实现 智能指针的出现智能指针的使用应用意义/存在问题智能指针原理剖析模拟实现auto_ptrunique_ptrshared_ptrweak_ptr 智能指针的出现 首先我们要理…

vscode ssh linux C++ 程序调试

vscode调试c++程序相比vs2022要复杂很多,vs2022可以"一键运行调试",vscode则需要自己配置。 ​vscode调试程序时,会在当前工作目录产生.vscode 目录, 该目录有两个重要文件launch.json和tasks.json, 下面介绍两种调试方法: 手动调试和自动调试。 手动调试 不管…

VUE树结构实现

实现效果: 数据库表结构如下: 要求:需要有parentId,id。parentId就是父记录的id 表数据要求:一定不要让一条记录的parentid和id相同 前端代码: 注意:el-table标签里面需要加上属性,才可以有下拉箭头的样式 <el-table v-loading="listLoading" :data

Spring Boot中的异步编程:解决的问题与应用场景

Spring Boot中的异步编程&#xff1a;解决的问题与应用场景 在现代Web应用程序中&#xff0c;高并发和性能是至关重要的。为了处理大量的请求和任务&#xff0c;异步编程成为了不可或缺的一部分。Spring Boot提供了强大的异步编程支持&#xff0c;可以显著提高应用程序的吞吐量…

【Java 进阶篇】JavaScript Math对象详解

在JavaScript编程中&#xff0c;Math对象是一个非常有用的工具&#xff0c;用于执行各种数学运算。它提供了许多数学函数和常数&#xff0c;可以用于处理数字、执行几何运算、生成随机数等。在本篇博客中&#xff0c;我们将深入探讨JavaScript中Math对象的各种功能和用法。 什…

LiveMedia视频中间件视频隐私打码直播解决方案

一、方案背景 随着科技的发展&#xff0c;视频监控系统已经成为了我们生活中不可或缺的一部分。无论是在公共区域&#xff0c;还是在私人场所&#xff0c;我们都可以看到各种各样的监控设备。这些设备的出现&#xff0c;无疑提高了我们的生活安全&#xff0c;使得我们可以更好地…

Gin,Gorm实现Web计算器

目录 仓库链接0.PSP表格1. 成品展示1.基础运算2. 清零回退3.错误提示4.历史记录拓展功能1.前端可修改的利率计算器2.科学计算器3. 按钮切换不同计算器模式4.用户在一次运算后不清零继续输入操作符&#xff0c;替换表达式为上次答案 2.设计实现过程3.代码说明4.心路历程和收获 仓…

企业级CI/CD 持续集成/交付/发布

jenkins 安装与使用 nmcli g hostname jenkins 加载缓存 yum makecache fast 上传jdk11、jdk8 获取、上传war包 1、jenkins.io/download 2.4.27 2、老师发的 上传 maven 上传tomcat软件包 &#xff08;apache.org-tomcat8-下载&#xff09; 注意8009端口 /usr... vi /etc/pro…

react中ant.design框架配置动态路由

目录 什么是动态路由&#xff1f; 应用场景&#xff1a; ant.design动态路由如何配置&#xff1a; 首先&#xff1a;找到app.tsx文件 然后&#xff1a;找到menuHeaderRender 其次&#xff1a;修改menuHeaderRender为menuDataRender​编辑 最后&#xff1a;在箭头函数里re…

linux内存、cpu、进程、端口、硬盘管理

这里讲解一下linux内存、cpu、进程、端口、硬盘管理命令操作,更多linux常用命令见:一些经常使用的linux命令 一、内存、cup 管理 top 命令 1、top 命令的第一行 top - 10:11:23 up 12:10, 2 users, load average: 0.00, 0.02, 0.05 依次对应&#xff1a; 系统当前时间 10:11:…

Vite与Webpack谁更胜一筹,谁将引领下一代前端工具的发展

你知道Vite和Webpack吗&#xff1f;也许有不少“程序猿”对它们十分熟悉。 Webpack Webpack是一个JavaScript应用程序的静态模块打包工具&#xff0c;它会对整个应用程序进行依赖关系图构建。而这也会导致一个不可避免的情况&#xff0c;使用Webpack启动应用程序的服务器&…

C++指针解读(5)-- 指针和数组(多维数组)

相比一维数组&#xff0c;二维数组的概念和相关运算要复杂得多。 1、二维数组的存储及访问 假设有这么一个二维数组&#xff1a; int arr[3][4] {{ 10, 11, 12, 13 },{ 20, 21, 22, 23 },{ 30, 31, 32, 33 } }; 我们可以把二维数组看成数组的数组&#xff1a; &#xff…

Arduino驱动BNO055 10轴绝对定向传感器(惯性测量传感器篇)

目录 1、传感器特性 2、控制器和传感器连线图 3、驱动程序 BNO055是实现智能9轴绝对定向的新型传感器IC,它将整个传感器系统级封装在一起,集成了三轴14位加速度计,三轴16位陀螺仪,三轴地磁传感器和一个自带算法处理的32位微控制器。

在 Elasticsearch 中实现自动完成功能 3:completion suggester

在这篇博文中&#xff0c;我们将讨论 complete suggester - 一种针对自动完成功能进行优化的 suggester&#xff0c;并且被认为比我们迄今为止讨论的方法更快。 Completion suggester 使用称为有限状态转换器的数据结构&#xff0c;该结构类似于 Trie 数据结构&#xff0c;并且…

Net6 用imagesharp 实现跨平台图片处理并存入oss

项目要求&#xff1a;生成电子证书 一、模板文件在OSS中&#xff0c;直接加载 二、向模板文件添加二维码 三、向模板文件添加多行文字 四、生成二维码&#xff0c;存入本地&#xff0c; 五、向模板文件添加二维码 代码实现步骤 一、建立.net 6 API项目&#xff0c;安装N…

启航kp OpenHarmony环境搭建

前提 启航kp OpenHarmony环境搭建 搭建好OpenHarmony环境 未搭建好可以参考OpenHarmony docker环境搭建 安装vscode 下载好启航kp所需的开发包和样例 下载地址 搭建过程 进入正确文件夹 首先要进入 /home/openharmony 目录下&#xff0c;如果没有打开在vsc左上角找到文…

JUC并发编程——JUC并发编程概述及Lock锁(重点)(基于狂神说的学习笔记)

基于bilibili狂神说JUC并发编程视频所做笔记 概述 什么是JUC JUC时java.util工具包中的三个包的简称 java.util.concurrent java.util.concurrent.atomic java.util.concurrent.locks 业务&#xff1a;普通的线程代码中&#xff0c;我们常使用Runnable接口 但Runnable没有返…

百度开放平台第三方代小程序开发,授权事件、消息与事件通知总结

大家好&#xff0c;我是小悟 关于百度开放平台第三方代小程序开发的两个事件接收推送通知&#xff0c;是开放平台代小程序实现业务的重要功能。 授权事件推送和消息与事件推送类型都以event的值判断。 授权事件推送通知 授权事件推送包括&#xff1a;推送票据、授权成功、取…