C++共享智能指针

news2024/11/25 19:02:46

C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄漏。解决这个问题最有效的方式是使用智能指针。

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动销毁动态分配的对象,防止内存泄漏。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指的堆内存。

C++提供了三种智能指针,使用前需要引用头文件<memory>:

std::shared_ptr:共享的智能指针

std::shared_ptr:独占的智能指针

std::weak_ptr:弱引用的智能指针,不共享指针,不能操作资源,是用来监视shared_ptr的

shared_ptr

计数函数

共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:long use_count() const noexcept;

使用方式如下:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);
    std::shared_ptr<int> sp2 = sp1;
    std::shared_ptr<int> sp3 = sp1;

    std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
    std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;
    std::cout << "sp3 use_count: " << sp3.use_count() << std::endl;

    return 0;
}

首先创建了一个 shared_ptr 对象 sp1,并通过 make_shared 初始化它指向一个值为 10的 int对象。然后 sp2 和 sp3 都赋值为 sp1,此时这三个 shared_ptr 都指向同一个int对象。
当调用 sp1.use_count()sp2.use_count() 和 sp3.use_count() 时,都会返回 ,因为有三个shared_ptr 指向同一个对象。

初始化方式

使用构造函数初始化:std::shared_ptr<T> 智能指针名字(创建堆内存);

使用方式如下:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//输出1
    // 使用智能指针管理一块字符数组对应的堆内存
    shared_ptr<char> ptr2(new char[12]);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//输出1
    // 创建智能指针对象, 不管理任何内存
    shared_ptr<int> ptr3;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//输出0
    // 创建智能指针对象, 初始化为空
    shared_ptr<int> ptr4(nullptr);
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//输出0

    return 0;
}

如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果没有被初始化或者被初始化为nullptr空指针,那么引用计数不会+1。此外,不要使用一个原始指针初始化多个shared_ptr。比如说:

int *p = new int;shared_ptr<int> p1(p);shared_ptr<int> p2(p);//error!会报错


使用拷贝和移动构造函数初始化:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//输出1
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//输出2
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//输出3
    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//输出3
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;//输出3

    return 0;
}

如果使用拷贝的方式初始化共享智能指针对象,这两个对象会同时管理同一块堆内存,堆内存对应的引用计数也会增加;如果使用移动的方式初始智能指针对象,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会变化


通过make_shared初始化:shared_ptr<T> make_shared( Args&&... args)

它可以在一次内存分配中同时完成对象的创建和 shared_ptr 的初始化,提高效率并减少内存泄漏风险。

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Test
{
public:
    Test() 
    {
        cout << "construct Test..." << endl;
    }
    Test(int x) 
    {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) 
    {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test()
    {
        cout << "destruct Test ..." << endl;
    }
};

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;

    shared_ptr<Test> ptr2 = make_shared<Test>();
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;

    shared_ptr<Test> ptr3 = make_shared<Test>(520);
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;

    shared_ptr<Test> ptr4 = make_shared<Test>("我是要成为海贼王的男人!!!");
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    return 0;
}

使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。


通过reset方法初始化

共享智能指针提供std::shared_ptr::reset方法函数原型如下:

ptr:指向要取得所有权的对象的指针

d:指向要取得所有权的对象的指针

aloc:内部存储所用的分配器

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    shared_ptr<int> ptr2 = ptr1;
    shared_ptr<int> ptr3 = ptr1;
    shared_ptr<int> ptr4 = ptr1;
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//4
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//4
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//4
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//4

    ptr4.reset();
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//3
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//3
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//3
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//0

    shared_ptr<int> ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;//1

    return 0;
}

std::shared_ptr 的 reset 函数用于改变 shared_ptr 所管理的对象,它有以下几种作用:

一、释放当前管理的对象(如果有)并接管新对象

  1. 当不带参数调用 reset 函数时(如 ptr4.reset();):
    • 它会将当前 shared_ptr(这里是 ptr4)所管理的对象的引用计数减 1。如果引用计数变为 0,则释放该对象所占用的内存。在这个例子中,ptr4 原本和 ptr1ptr2ptr3 指向同一个对象(值为 520 的 int),调用 reset 后,该对象的引用计数从 4 变为 3,因为 ptr4 不再指向它。
    • 同时,ptr4 被设置为一个空的 shared_ptr,即它不再管理任何对象,其内部的指针被设置为 nullptr

二、接管新分配的对象

  1. 当带参数调用 reset 函数时(如 ptr5.reset(new int(250));):
    • 首先,它会释放 ptr5 当前可能管理的任何对象(在这个例子中,ptr5 最初是空的,所以没有对象需要释放)。
    • 然后,它会使用传入的指针(这里是通过 new int(250) 创建的新的 int 对象的指针)来初始化 ptr5,使 ptr5 管理这个新的对象。此时,新对象的引用计数被初始化为 1,因为只有 ptr5 指向它。

三、注意事项

  1. 内存管理
    • 使用 reset 函数时要小心,特别是在传递指针给 reset 时。确保传递的指针是通过 new 动态分配的内存,或者是与 shared_ptr 兼容的内存分配方式(例如,使用 std::allocate_shared 分配的内存)。如果传递了一个不适当的指针,可能会导致内存泄漏或其他未定义行为。
  2. 引用计数的变化
    • 每次调用 reset 都会影响引用计数,并且可能导致对象的释放。在多线程环境中,如果多个 shared_ptr 同时访问和修改同一个对象,需要注意对 reset 函数的调用顺序和并发访问的安全性,以避免意外的引用计数变化和对象释放。虽然 shared_ptr 的引用计数操作在一定程度上是线程安全的,但复杂的操作序列可能仍然需要额外的同步措施来确保正确性。
  3. 与其他智能指针操作的结合
    • reset 函数通常与 shared_ptr 的其他操作(如赋值、拷贝构造等)结合使用。例如,在函数返回 shared_ptr 时,可以使用 reset 来确保返回的 shared_ptr 正确管理对象的生命周期。同时,在对象的所有权转移场景中,reset 可以用于明确地释放当前的所有权并转移到新的 shared_ptr 上。但要注意在这些操作中保持逻辑的正确性,避免出现悬空指针、内存泄漏或对象被过早释放等问题。

获取原始指针

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。

当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的 get() 方法,其函数原型如下:

T* get() const noexcept;

使用如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // 得到指针的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}
  • memset(add, 0, len);add指向的内存区域(即ptr管理的字符数组)的len个字节设置为0,这是一种初始化内存的操作,确保数组中的内容在复制字符串之前是清空的
  • strcpy(add, "我是要成为海贼王的男人!!!");将字符串常量复制到add指向的字符数组中。这里使用了strcpy函数,它会将源字符串的字符逐个复制到目标数组中,直到遇到字符串结束符'\0'。需要注意的是,strcpy函数在复制字符串时不会检查目标数组的大小,如果源字符串长度超过目标数组长度,会导致缓冲区溢出错误。在实际应用中,更安全的做法是使用strncpy函数,并确保正确处理字符串长度和数组大小的关系。
  • shared_ptr<int> p(new int);创建了一个shared_ptr对象p,它管理一个动态分配的int对象。
  • *p = 100;通过解引用p来访问它所管理的int对象,并将其值设置为100
  • cout << *p.get() << " " << *p << endl;分别通过p.get()获取原始指针并解引用和直接解引用p来输出int对象的值。这两种方式都可以正确获取p所管理的int对象的值,并且输出结果相同,都是100这里展示了两种访问shared_ptr所管理对象的方式,在实际使用中可以根据具体情况选择。

 指定删除器

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

#include <iostream>
#include <memory>
using namespace std;

// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p)
{
    delete p;
    cout << "int 型内存被释放了...";
}

int main()
{
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}

删除器也可以是lambda表达式,因此也可以这么写:

shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });

在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。

在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:

shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:

shared_ptr<int> ptr(new int[10], default_delete<int[]>());

另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:

#include <iostream>
#include <memory>
using namespace std;

template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main()
{
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

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

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

相关文章

python Flask指定IP和端口

from flask import Flask, request import uuidimport json import osapp Flask(__name__)app.route(/) def hello_world():return Hello, World!if __name__ __main__:app.run(host0.0.0.0, port5000)

虚幻引擎---初识篇

一、学习途径 虚幻引擎官方文档&#xff1a;https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-5-5-documentation虚幻引擎在线学习平台&#xff1a;https://dev.epicgames.com/community/unreal-engine/learning哔哩哔哩&#xff1a;https://www.b…

Java开发经验——SpringRestTemplate常见错误

摘要 本文分析了在使用Spring框架的RestTemplate发送表单请求时遇到的常见错误。主要问题在于将表单参数错误地以JSON格式提交&#xff0c;导致服务器无法正确解析参数。文章提供了错误案例的分析&#xff0c;并提出了修正方法。 1. 表单参数类型是MultiValueMap RestControl…

oracle会话追踪

一 跟踪当前会话 1.1 查看当前会话的SID,SERIAL# #在当前会话里执行 示例&#xff1a; SQL> select distinct userenv(sid) from v$mystat; USERENV(SID) -------------- 1945 SQL> select distinct sid,serial# from v$session where sid1945; SID SERIAL# …

数据可视化复习2-绘制折线图+条形图(叠加条形图,并列条形图,水平条形图)+ 饼状图 + 直方图

目录 目录 一、绘制折线图 1.使用pyplot 2.使用numpy ​编辑 3.使用DataFrame ​编辑 二、绘制条形图&#xff08;柱状图&#xff09; 1.简单条形图 2.绘制叠加条形图 3.绘制并列条形图 4.水平条形图 ​编辑 三、绘制饼状图 四、绘制散点图和直方图 1.散点图 2…

postgresql按照年月日统计历史数据

1.按照日 SELECT a.time,COALESCE(b.counts,0) as counts from ( SELECT to_char ( b, YYYY-MM-DD ) AS time FROM generate_series ( to_timestamp ( 2024-06-01, YYYY-MM-DD hh24:mi:ss ), to_timestamp ( 2024-06-30, YYYY-MM-DD hh24:mi:ss ), 1 days ) AS b GROUP BY tim…

【JavaEE初阶 — 多线程】定时器的应用及模拟实现

目录 1. 标准库中的定时器 1.1 Timer 的定义 1.2 Timer 的原理 1.3 Timer 的使用 1.4 Timer 的弊端 1.5 ScheduledExecutorService 2. 模拟实现定时器 2.1 实现定时器的步骤 2.1.1 定义类描述任务 定义类描述任务 第一种定义方法 …

一文学会Golang里拼接字符串的6种方式(性能对比)

g o l a n g golang golang的 s t r i n g string string类型是不可修改的&#xff0c;对于拼接字符串来说&#xff0c;本质上还是创建一个新的对象将数据放进去。主要有以下几种拼接方式 拼接方式介绍 1.使用 s t r i n g string string自带的运算符 ans ans s2. 使用…

LeetCode 3244.新增道路查询后的最短距离 II:贪心(跃迁合并)-9行py(O(n))

【LetMeFly】3244.新增道路查询后的最短距离 II&#xff1a;贪心&#xff08;跃迁合并&#xff09;-9行py&#xff08;O(n)&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/shortest-distance-after-road-addition-queries-ii/ 给你一个整数 n 和一个二维…

MyBatis中特殊SQL的执行

目录 1.模糊查询 2.批量删除 3.动态设置表名 4.添加功能获取自增的主键 1.模糊查询 List<User> getUserByLike(Param("username") String username); <select id"getUserByLike" resultType"com.atguigu.mybatis.pojo.User">&…

ES 基本使用与二次封装

概述 基本了解 Elasticsearch 是一个开源的分布式搜索和分析引擎&#xff0c;基于 Apache Lucene 构建。它提供了对海量数据的快速全文搜索、结构化搜索和分析功能&#xff0c;是目前流行的大数据处理工具之一。主要特点即高效搜索、分布式存储、拓展性强 核心功能 全文搜索:…

Azkaban部署

首先我们需要现在相关的组件&#xff0c;在这里已经给大家准备好了相关的安装包&#xff0c;有需要的可以自行下载。 只需要启动hadoop集群就可以&#xff0c;如果现在你的hive是打开的&#xff0c;那么请你关闭&#xff01;&#xff01;&#xff01; 如果不关会造成证书冲突…

Jmeter中的定时器

4&#xff09;定时器 1--固定定时器 功能特点 固定延迟&#xff1a;在每个请求之间添加固定的延迟时间。精确控制&#xff1a;可以精确控制请求的发送频率。简单易用&#xff1a;配置简单&#xff0c;易于理解和使用。 配置步骤 添加固定定时器 右键点击需要添加定时器的请求…

JavaEE初学07

JavaEE初学07 MybatisORMMybatis一对一结果映射一对多结果映射 Mybatis动态sqlif标签trim标签where标签set标签foreach标签补充 Mybatis Mybatis是一款优秀的持久层框架&#xff0c;他支持自定义SQL、存储过程以及高级映射。Mybatis几乎免除了所有的JDBC代码以及设置参数和获取…

【layui】table的switch、edit修改

<title>简单表格数据</title><div class"layui-card layadmin-header"><div class"layui-breadcrumb" lay-filter"breadcrumb"><a>系统设置</a><a>简单表格数据</a></div> </div>&…

工具使用_docker容器_crossbuild

1. 工具简介 2. 工具使用 拉取 multiarch/crossbuild 镜像&#xff1a; docker pull multiarch/crossbuild 创建工作目录和示例代码&#xff1a; mkdir -p ~/crossbuild-test cd ~/crossbuild-test 创建 helloworld.c &#xff1a; #include <stdio.h>int main() …

Android 天气APP(三十七)新版AS编译、更新镜像源、仓库源、修复部分BUG

上一篇&#xff1a;Android 天气APP&#xff08;三十六&#xff09;运行到本地AS、更新项目版本依赖、去掉ButterKnife 新版AS编译、更新镜像源、仓库源、修复部分BUG 前言正文一、更新镜像源① 腾讯源③ 阿里源 二、更新仓库源三、修复城市重名BUG四、地图加载问题五、源码 前…

基于Java Springboot海洋馆预约系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

采用python3.12 +django5.1 结合 RabbitMQ 和发送邮件功能,实现一个简单的告警系统 前后端分离 vue-element

一、开发环境搭建和配置 #mac环境 brew install python3.12 python3.12 --version python3.12 -m pip install --upgrade pip python3.12 -m pip install Django5.1 python3.12 -m django --version #用于检索系统信息和进程管理 python3.12 -m pip install psutil #集成 pika…

STM32的中断(什么是外部中断和其他中断以及中断号是什么)

一、什么是EXTI 和NVIC EXTI&#xff08;External Interrupt/Event Controller&#xff09;EXTI 是外部中断/事件控制器&#xff0c;它负责处理外部信号变化&#xff0c;并将信号传递给中断控制器&#xff08;如 NVIC&#xff09;。主要负责以下功能&#xff1a; 外部事件检测…