首页 > 编程语言 >C++ 智能指针

C++ 智能指针

时间:2023-05-17 15:11:08浏览次数:40  
标签:std 对象 C++ 智能 shared unique ptr 指针

在介绍智能指针之前,先来看原始指针的一些不便之处:

  • 它的声明不能指示所指到底是单个对象还是数组。

  • 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。

  • 如果你决定你应该销毁指针所指对象,没人告诉你该用delete还是其他析构机制(比如将指针传给专门的销毁函数)。

  • 如果你发现该用delete。第一点说了可能不知道该用单个对象形式(“delete”)还是数组形式(“delete[]”)。如果用错了结果是未定义的。

  • 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了恰为一次销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。

  • 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。

Item 18: Use std::unique_ptr for exclusive-ownership resource management(对于独占资源使用std::unique_ptr)

1. 性能

可以合理假设,默认情况下,std::unique_ptr 大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr一样可以。

2. 用法

2.1 拷贝与移动

std::unique_ptr 体现了专有所有权(exclusive ownership)语义。

  • 移动一个std::unique_ptr将所有权从源指针转移到目的指针。(源指针被设为null。)

  • 拷贝一个std::unique_ptr是不允许的,因为如果你能拷贝一个std::unique_ptr,你会得到指向相同内容的两个std::unique_ptr,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。
    因此,std::unique_ptr 是一种只可移动类型(move-only type)

2.2 构造方式

// 1. 
std::unique_ptr<int> sp1(new int(123));
// 2.
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
// 3. 
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
/* 尝试将原始指针(比如new创建)赋值给std::unique_ptr通不过编译,因为是一种从原始指针到智能指针的隐式转换。
 这种隐式转换会出问题,所以 C++11的智能指针禁止这个行为。这就是通过reset来让up接管通过new创建的对象的所有权的原因。*/
unique_ptr<int> up = nullptr;
int* ip = new int();
// up = ip; // 报错: no operator "=" matches these operands
up.reset(ip);

C++ 14 加入了 std::make_unique,而C++ 11没有

2.3 释放

默认情况下,资源析构通过对std::unique_ptr里原始指针调用delete来实现。

但是在构造过程中,std::unique_ptr对象可以被设置为使用(对资源的)自定义删除器:当资源需要销毁时可调用的任意函数(或者函数对象,包括lambda表达式): std::unique_ptr<T, DeletorFuncPtr>

#include <iostream>
#include <memory>
 
class Socket
{
public:
    Socket() {}
    ~Socket() {}

    //关闭资源句柄
    void close()
    {
	...
    }
};
 
int main()
{
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        delete pSocket;
    };
    std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor); // 返回类型大小是Socket*的大小
    return 0;
}

注意:
上面谈到,当使用默认删除器时(如delete),你可以合理假设std::unique_ptr对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使std::unique_ptr的从一个字(word)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的lambda表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者lambda时,尽量使用lambda:

#include <iostream>
#include <memory>

class Socket
{
public:
    Socket() {}//std::cout << "gouzao" << std::endl;}
    ~Socket() {}//std::cout << "xigou" << std::endl;}

    //关闭资源句柄
    void close()
    {
    }
};

void deletor2(Socket* pSocket) {
    //关闭句柄
    pSocket->close();
    delete pSocket;
}

int main()
{
    Socket* pSocket = new Socket();
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        delete pSocket;
    };
    std::unique_ptr<Socket, decltype(deletor)> spSocket1(new Socket(), deletor); // 返回类型大小是Socket*的大小
    std::unique_ptr<Socket, void(*)(Socket*)> spSocket2(new Socket(), deletor); // 返回类型大小是Socket*的指针加至少一个函数指针大小
    auto spSocket3 = std::make_shared<Socket>();
    std::shared_ptr<Socket> spSocket4(new Socket(),deletor);
    std::weak_ptr<Socket> sp5(spSocket3);
    std::cout << "原始指针大小: " << sizeof(pSocket) << std::endl;
    std::cout << "unique_ptr with lambda 大小:" << sizeof(spSocket1) << std::endl;
    std::cout << "unique_ptr with funcp 大小: " << sizeof(spSocket2) << std::endl;
    std::cout << "shared_ptr 大小:" << sizeof(spSocket3) << std::endl;
    std::cout << "shared_ptr 大小:" << sizeof(spSocket4) << std::endl;
    std::cout << "weak_ptr 大小:" << sizeof(sp5) << std::endl;
}
}
mingyu@ndsl84:~/cudalearn$ ./a.out 
原始指针大小: 8
unique_ptr with lambda 大小:8
unique_ptr with funcp 大小: 16
shared_ptr 大小:16
shared_ptr 大小:16
weak_ptr 大小:16

2.4 两种形式

std::unique_ptr有两种形式:

  • 一种用于单个对象(std::unique_ptr
  • 一种用于数组(std::unique_ptr<T[]>)
    结果就是,指向哪种形式没有歧义。std::unique_ptr的API设计会自动匹配你的用法,比如operator[]就是数组对象,解引用操作符(operator*和operator->)就是单个对象专有。

你应该对数组的std::unique_ptr的存在兴趣泛泛,因为std::array,std::vector,std::string这些更好用的数据容器应该取代原始数组。std::unique_ptr<T[]>有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。

2.5 转换

std::unique_ptr是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr

// std::unique_ptr 禁止复制语义,但存在特例,即可以通过一个函数返回一个std::unique_ptr
#include <memory>
 
std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}
 
int main()
{
    std::shared_ptr<int> sp1 = func(123); // 将返回的unique_ptr 转换成 shared_ptr, 这种可以
    // 但是下面这种转换不允许的
    std::unique_ptr<int> a (new int());
    std::shared_ptr<int> b = a; // no suitable user-defined conversion from "std::unique_ptr<int, std::default_delete<int>>" to "std::shared_ptr<int>" exists
    return 0;
}

由下文的介绍可以了解到: 类似std::unique_ptr,std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是:

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);        //删除器类型不是指针类型的一部分

3. unique_ptr 的总结

  • std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
  • 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
  • std::unique_ptr转化为std::shared_ptr非常简单

Item 19: Use std::shared_ptr for shared-ownership resource management(对于共享资源使用std::shared_ptr)

1. 性能

std::shared_ptr通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。

std::shared_ptr构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。(如果sp1和sp2是std::shared_ptr并且指向不同对象,赋值“sp1 = sp2;”会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一,sp2引用计数加一。)如果std::shared_ptr在计数值递减后发现引用计数值为零,没有其他std::shared_ptr指向该资源,它就会销毁资源。

引用计数暗示着性能问题:

  • std::shared_ptr大小是原始指针的两倍(可见Item18中的2.3的例子),因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。(这种实现法并不是标准要求的,但是我(指原书作者Scott Meyers)熟悉的所有标准库都这样实现。)

  • 引用计数的内存必须动态分配。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由std::shared_ptr管理。)Item21会解释使std::make_shared创建std::shared_ptr可以避免引用计数的动态分配(因此,直接使用new需要为目标对象进行一次内分配,为控制块再进行一次内分配;而使用std::make_shared只有一次分配,因为std::make_shared分配一块内存,同时容纳了目标对象和控制块。),但是还存在一些std::make_shared不能使用的场景,这时候引用计数就会动态分配。

  • 递增递减引用计数必须是原子性的,因为多个reader、writer可能在不同的线程。比如,指向某种资源的std::shared_ptr可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,std::shared_ptr指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个word大小,你也应该假定读写它们是存在开销的。

2. 用法

2.1 拷贝与移动

std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享

  • std::shared_ptr构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。

  • 从一个std::shared_ptr移动构造新std::shared_ptr会将原来的std::shared_ptr设置为null,那意味着老的std::shared_ptr不再指向资源,同时新的std::shared_ptr指向资源。这样的结果就是不需要修改引用计数值。因此移动std::shared_ptr会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。

2.2 构造方式

std::unique_ptr类似

2.3 析构

类似std::unique_ptr(参见Item18),std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);        //删除器类型不是指针类型的一部分

因此,std::shared_ptr的设计更为灵活。考虑有两个std::shared_ptr<Widget>,每个自带不同的删除器(比如通过lambda表达式自定义删除器):

auto customDeleter1 = [](Widget *pw) { … };     //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … };     //每种类型不同
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

因为pw1和pw2有相同的类型(虽然他们的删除其类型不同,但对于shared_ptr而言,删除器类型并不是指针类型的一部分),所以它们都可以放到存放那个类型的对象的容器中:

std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

它们也能相互赋值,也可以传入一个形参为std::shared_ptr<Widget>的函数。但是自定义删除器类型不同的std::unique_ptr就不行,因为std::unique_ptr把删除器视作类型的一部分。

另一个不同于std::unique_ptr的地方是,指定自定义删除器不会改变std::shared_ptr对象的大小。不管删除器是什么,一个std::shared_ptr对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。std::shared_ptr怎么能引用一个任意大的删除器而不使用更多的内存?

它不能。它必须使用更多的内存。然而,那部分内存不是std::shared_ptr对象的一部分。那部分在堆上面,或者std::shared_ptr创建者利用std::shared_ptr对自定义分配器的支持能力,那部分内存随便在哪都行。

2.4 内部实现

前面提到了std::shared_ptr对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块(control block)

每个std::shared_ptr管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如Item21提到的,一个次级引用计数weak count,但是目前我们先忽略它。我们可以想象std::shared_ptr对象在内存中是这样:
image

当指向对象的std::shared_ptr一创建,对象的控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的std::shared_ptr的函数来说不可能知道是否有其他std::shared_ptr早已指向那个对象,所以控制块的创建会遵循下面几条规则:

  • std::make_shared(参见Item21)总是创建一个控制块。它创建一个要指向的新对象,所以可以肯定std::make_shared调用时对象不存在其他控制块。

  • 当从独占指针(即std::unique_ptr)上构造出std::shared_ptr时会创建控制块。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,std::shared_ptr侵占独占指针所指向的对象的独占权,所以独占指针被设置为null)

  • 当从原始指针上构造出std::shared_ptr时会创建控制块。如果你想从一个早已存在控制块的对象上创建std::shared_ptr,你将假定传递一个std::shared_ptr或者std::weak_ptr(参见Item20)作为构造函数实参,而不是原始指针。用std::shared_ptr或者std::weak_ptr作为构造函数实参创建std::shared_ptr不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。

从原始指针上构造超过一个std::shared_ptr就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联(但是所有的shared_ptr都指向同一个T Object)。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。

一个尤其令人意外的地方是使用this指针作为std::shared_ptr构造函数实参的时候可能导致创建多个控制块。https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item19.md

3. shared_ptr的总结

  • std::shared_ptr为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。

  • 较之于std::unique_ptrstd::shared_ptr对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。

  • 默认资源销毁是通过delete,但是也支持自定义删除器。删除器的类型是什么对于std::shared_ptr的类型没有影响。

  • 避免从原始指针变量上创建std::shared_ptr, 通常替代方案是使用std::make_shared,不过当我们要使用自定义删除器,用std::make_shared就没办法做到

Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle(当std::shared_ptr可能悬空时使用std::weak_ptr)

https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item20.md

自相矛盾的是,如果有一个像std::shared_ptr(见Item19)的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似std::shared_ptr但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个在std::shared_ptr中存在的问题:即std::shared_ptr可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(dangle)就是指针指向的对象不再存在。这就是对std::weak_ptr最精确的描述。
请记住:

  • 用std::weak_ptr替代可能会悬空的std::shared_ptr。

  • std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。

参考:
https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item18.md
《Effective Modern Cpp》

标签:std,对象,C++,智能,shared,unique,ptr,指针
From: https://www.cnblogs.com/myrosy/p/17407163.html

相关文章

  • ChatGPT4通道开放接入基于OPEN AI 平台你的任何APP 可一键接入AI 智能
    你一定很好奇什么是OPENAI快速开发平台顾名思义,开放的OPENAI平台。基于这个平台你的上层应用,如何APP,小程序,H5,WEB,公众号,任何一切终端都可以轻松接入,AI智能应用。开发初衷爆肝一周,我开源了ChatGPT中文版接口,官方1:1镜像支持全部官方接口持续熬夜爆肝,炸裂的OPENAI......
  • C++用代码验证“一切函数皆可傅里叶”
    #include<stdio.h>#include<math.h>#definepi3.1415926#definerows3#definecolums5typedefstruct{floatre;//reallyfloatim;//imaginary}complex,*pcomplex;complexcomplexadd(complexa,complexb)//复数加{comple......
  • c++ gdiplus实现屏幕截图
    #include<windows.h>#include<gdiplus.h>#include<iostream>#include<filesystem>#include<chrono>#include<iomanip>#include<sstream>#pragmacomment(lib,"Gdiplus.lib")usingnamespaceGdiplus;U......
  • 【C++ Primer】第二章(2 ~ 6节)
    变量变量提供一个具名的、可供程序操作的存储空间。C++中变量和对象一般可以互换使用。变量定义(define)定义形式:类型说明符(typespecifier)+一个或多个变量名组成的列表。如intsum=0,value,units_sold=0;初始化(initialize):对象在创建时获得了一个特定的值。初......
  • 智能家居生态迎来超强辅助
    在家居领域,中商行业研究所预测,2023年中国智能家居市场可达7157.1亿元。未来5年,中国智能家居产业将继续快速发展。2027年,市场规模预计将超过1.1万亿亿元人民币。那么未来智能家居发展的突破口又在何方?智能终端设备运行小程序的概念在智能终端设备中运行小程序,是指在不需要下载和......
  • 基于云原生的物联大数据智能服务
    摘要:物联大数据已成为当前物联网系统建设的核心,基于物联大数据的涌现智能和应用以及借此对物理世界的反馈和控制是未来物联网系统的建设目标。本文分享自华为云社区《基于云原生的物联大数据智能服务》,作者:赵卓峰、丁维龙、于淇/北方工业大学数据工程研究院、大规模流数据集......
  • 推荐10个AI人工智能技术网站
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生1、AITrendsAITrends(https://www.aitrends.com/)是一个专注于人工智能领域的网站,它提供了最新的AI技术和应用趋势的报道和分析。该网站的内容涵盖了AI技术的各个方面,包括机器学习、深度学习、自然语言处理、......
  • 基于云原生的物联大数据智能服务
    摘要:物联大数据已成为当前物联网系统建设的核心,基于物联大数据的涌现智能和应用以及借此对物理世界的反馈和控制是未来物联网系统的建设目标。本文分享自华为云社区《基于云原生的物联大数据智能服务》,作者:赵卓峰、丁维龙、于淇/北方工业大学数据工程研究院、大规模流数据集成......
  • CS106L: Standard C++ Programming, Special Edition
    课程内容涉及C++五大主题:C++介绍、Stream和Types、STL四大模块、OOP面向对象、高级特性(RAII、多线程、元编程)。本系列整合了CS106L课程公开的资料,系统完整的涵盖了C++核心内容,方便学习。搭配《C++Primer》,一起享用更佳!C++课程自学总结CS106L学习时间:刷课一周,复......
  • 智能排班系统--今日学习总结
    今天我完成了android端连接mysql并且实现增、删、改、查的每个操作,为实现web端和android端的信息互通奠定了基础,在此基础上,能够实现员工安卓端向web管理端的请假信息的传递。明天我要在安卓端实现信息通知推送功能,能够及时提示员工请假的过程以及结果。packagecom.example.pai......