【C++进阶篇】智能指针

news2025/6/13 9:30:26

C++内存管理终极指南:智能指针从入门到源码剖析

  • 一. 智能指针
    • 1.1 auto_ptr
    • 1.2 unique_ptr
    • 1.3 shared_ptr
    • 1.4 make_shared
  • 二. 原理
  • 三. shared_ptr循环引用问题
  • 三. 线程安全问题
  • 四. 内存泄漏
    • 4.1 什么是内存泄漏
    • 4.2 危害
    • 4.3 避免内存泄漏
  • 五. 最后

一. 智能指针

智能指针通过RAII(Resource Acquisition Is Initialization)机制,将内存管理封装为类生命周期行为,其核心价值体现在:

  • 自动内存回收:通过析构函数自动释放资源,避免忘记delete导致的内存泄漏
  • 异常安全性:在异常抛出时仍能保证资源释放
  • 所有权语义:明确资源归属关系,减少悬垂指针风险

1.1 auto_ptr

特点:拷贝时将被拷贝对象的资源转移给拷贝对象,会导致被拷贝对象悬空问题,访问会崩溃。**建议:**坚决不要使用该智能指针。

1.2 unique_ptr

特点:见名知意,不支持拷贝,只支持移动。
使用场景:

  • 当某种特定场景不需要拷贝,强烈建议使用它。
int main()
{
	unique_ptr<int> sp(new int[10]);
	//unique_ptr<int> sp1 = sp;
	int* fp = sp.get();
	//对该指针进行操作
	for (size_t i = 0; i < 10; i++)
	{
		fp[i] = i + 1;
	}

	for (size_t i = 0; i < 10; i++)
	{
		cout << fp[i] << " ";
	}
	cout << endl;
	cout << "sp交换前: ";
	cout << "sp _ptr:" << sp.get() << endl;
	unique_ptr<int> sp1;
	sp1.swap(sp);
	cout << "sp交换后: ";
	cout << "sp _ptr:" << sp.get() << endl;
	cout << "sp1 _ptr:" << sp1.get() << endl;

	sp1.release();//将指针置空
	cout << "调用release()后: ";
	cout << "sp1 _ptr:" << sp1.get() << endl;
	return 0;
}
  • 输出结果:
    在这里插入图片描述
    从结果可以看出调用swap后资源的管理权转移给调用者对象,自己置空。

1.3 shared_ptr

特点:支持拷贝也支持移动。底层是使用计数方式来看看是哪个对象来释放和清理资源。

  • 使用场景:

当某种场景需要拷贝时,推荐使用它。

struct Date
{
	int _year;
	int _month;
	int _day;

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "_year = " << _year << " _month = " << _month << " _day = " << _day << endl;
	}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	shared_ptr<Date> sp(new Date(2025, 6, 9));
	cout << sp.use_count()<<endl;
	shared_ptr<Date> sp1 = sp;//赋值
	cout << sp.use_count() << endl;//引用计数
	cout << sp.get() << endl;//获取原生指针
	cout << sp1.get() << endl;
	cout << sp.operator->() << endl;
	return 0;
}
  • 输出结果:
    在这里插入图片描述

从结果可以看出创建出的新对象与拷贝对象指向的资源一致。

sp->_year;

上面语句访问是允许的。因为智能指针里面重载operator ->() 同时返回的是管理对象的类的对象。
上述语句等价于:shared_ptr通过重载operator->,使得sp->_day的语法等价于**(sp)._day*。

下面再看其它问题:

 shared_ptr<Date> sp1(new Date[10]);

程序会崩溃!!!
输出结果:

在这里插入图片描述

  • 原因分析:
  1. shared_ptr默认使用delete释放内存(针对单个对象)但new Date[ ],必须使用delete [ ] 释放资源。错误类型匹配会导致未定义行为。
    在这里插入图片描述
    这里的T是Date实例化时,而构造时Date[ ],导致析构时资源不匹配。

解决办法:

使用自定义(推荐):说白了就是让智能指针管理资源的对象类型与构造时的对象数据类型一致。就可以解决了。

shared_ptr<Date[]> sp1(new Date[10]);

输出结果:
在这里插入图片描述
从结果可以看出构造时的10个Date对象都被正确清理了。
其它的方法可以使用仿函数对象,lambda表达式,函数指针对象等构造时将删除器传递给智能指针整个类,删除器在类内部初始化了,因为这个类内部包含指向对象指针,然后释放和清理资源。

// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 函数指针做删除器
template<class T>
void DeleteArrayFunc(T* ptr)
{
	delete[] ptr;
}
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

1.4 make_shared

std::make_shared 是 C++11 引入的工厂函数,用于高效、安全地创建 shared_ptr 智能指针。
基本用法:

#include <memory>

// 创建 shared_ptr<int>
auto sp1 = std::make_shared<int>(42);

// 创建 shared_ptr<Date>
class Date { /* ... */ };
auto sp2 = std::make_shared<Date>(2025, 6, 9);

性能优化:一次内存分配
传统方式:

shared_ptr sp(new Date(2025,6,9)); // 两次内存分配

  • 第一次分配:为 Date 对象分配内存
  • 第二次分配:为引用计数控制块分配内存
    make_shared 方式:

auto sp = std::make_shared(2025,6,9); // 一次内存分配

  • 底层原理:内存分配策略
  1. 调用 ::operator new(sizeof(T) + sizeof(ControlBlock))
  2. 将对象和控制块放置在同一块连续内存中

二. 原理

auto_ptr 转移资源,思路不被认可,而unique_ptr 不支持拷贝,思路较简单,下面重点看看shared_ptr 设计思路原理

  • 思路

主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就–引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。
在这里插入图片描述

  • 问题:为啥shared_ptr计数需要再堆上开空间,静态方式行不行???

不行,因为需要特定的实例对象指向资源时,计数器才+1,因为需要一个资源共享一个计数器。
假如使用静态方式:

static int static_counter = 0; // 错误!所有对象共享同一个计数器

导致虽然是类对象实例,但未指向该资源,导致计数器逻辑错误。
模拟实现shrared_ptr智能指针:

namespace W
{
	template<class T>
	class shared_ptr
	{
	public:
		explicit shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		template<class D>
		explicit shared_ptr(T* ptr = nullptr,D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			,_del(del)
		{}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			,_del(sp._del)
		{
			++(*_pcount);
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//delete _ptr;
				_del(_ptr);
				delete _pcount;

				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		shared_ptr<T> operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				release();

				_ptr = sp._ptr;
				_pcout = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get()const
		{
			return _ptr;
		}
		int use_count()const
		{
			return *_pcount;
		}
		operator bool()
		{
			return _ptr != nullptr;
		}

	private:
		T* _ptr;
		int* _pcount;

		std::function<void(T*)> _del = [](T* ptr) {delete ptr; };//默认删除器
	};
}

三. shared_ptr循环引用问题

循环引用导致资源未被释放,从而导致内存泄漏,使用weak_ptr可以解决该问题,减少引用个数。

  • 下面以一个场景来看看循环引用导致内存泄漏场景:
    在这里插入图片描述
    当n1和n2析构时,计数器分别减1,节点中的指针分别指向对方,导致每个资源引用计数器增加1。
    n1节点中的next指针指向n2,n2节点中的prev指针指向n1,next什么时候析构呢,等着n2的prev指针不再指向是就析构了,n2的prev指针什么时候析构呢,等着n1的next不在指向时,就析构了。又回到原始问题,这就导致内存泄漏。
    使用 weak_ptr 可以解决问题。
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;

下面详细介绍一下weak_ptr原理

  • 功能:

weak_ptr是C++11引入的智能指针,主要用于解决shared_ptr的循环引用问题,并提供对共享资源的非拥有式观察。

  • 非拥有式观察

weak_ptr不增加对象的引用计数,仅观察由shared_ptr管理的资源。它通过共享控制块(Control Block)跟踪对象状态,但不会影响对象生命周期。

  • 解决循环引用

当两个对象通过shared_ptr互相引用时,引用计数无法归零,导致内存泄漏。将其中一个引用改为weak_ptr可打破循环。例如:

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
};
class B {
public:
    std::weak_ptr<A> a_ptr; // 使用weak_ptr打破循环
};
  • expired():快速检查对象是否存活(无需创建shared_ptr)。

总结:

weak_ptr通过非拥有式观察机制,有效解决了shared_ptr的循环引用问题,并支持缓存、观察者模式等场景。理解其控制块共享、引用计数管理及安全访问方法,能帮助开发者编写更健壮的C++代码。

三. 线程安全问题

如果多个线程在堆上同时进行对该计数器进行操作,就会导致线程安全问题。
解决办法:

  1. 加互斥锁。
  2. 将计数器不设置为int* 类型,而设置为atomic*。
atmoic<int>* _pcount;

四. 内存泄漏

4.1 什么是内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释
放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分
配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

4.2 危害

普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射
关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服
务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越
慢,最终卡死。

4.3 避免内存泄漏

⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理
想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理
才有保证。

  • 尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。
  • 定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。
  • 总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测⼯具。

五. 最后

本文深入探讨了C++智能指针(auto_ptr、unique_ptr、shared_ptr、weak_ptr)的原理与应用。智能指针通过RAII机制实现自动内存管理,提升代码健壮性。unique_ptr独占资源,shared_ptr共享资源并通过引用计数管理生命周期,weak_ptr则提供非拥有式观察以解决循环引用问题。文章还介绍了make_shared的高效内存分配策略,并强调了线程安全与内存泄漏防范的重要性,是C++开发者掌握现代内存管理的实用指南。关于C++全内容到此结束了,下面将进入Linux网络篇的学习征程。

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

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

相关文章

接口自动化测试:HttpRunner基础

相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具&#xff0c;支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议&#xff0c;涵盖接口测试、性能测试、数字体验监测等测试类型…

STM32HAL库USART源代码解析及应用

STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…

【JVM】Java虚拟机(二)——垃圾回收

目录 一、如何判断对象可以回收 &#xff08;一&#xff09;引用计数法 &#xff08;二&#xff09;可达性分析算法 二、垃圾回收算法 &#xff08;一&#xff09;标记清除 &#xff08;二&#xff09;标记整理 &#xff08;三&#xff09;复制 &#xff08;四&#xff…

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…

免费数学几何作图web平台

光锐软件免费数学工具&#xff0c;maths,数学制图&#xff0c;数学作图&#xff0c;几何作图&#xff0c;几何&#xff0c;AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…

基于Springboot+Vue的办公管理系统

角色&#xff1a; 管理员、员工 技术&#xff1a; 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能&#xff1a; 该办公管理系统是一个综合性的企业内部管理平台&#xff0c;旨在提升企业运营效率和员工管理水…

Linux nano命令的基本使用

参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时&#xff0c;显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …

Razor编程中@Html的方法使用大全

文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …

Windows安装Miniconda

一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客

iview框架主题色的应用

1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题&#xff0c;无需引入&#xff0c;直接可…

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…

【网络安全】开源系统getshell漏洞挖掘

审计过程&#xff1a; 在入口文件admin/index.php中&#xff1a; 用户可以通过m,c,a等参数控制加载的文件和方法&#xff0c;在app/system/entrance.php中存在重点代码&#xff1a; 当M_TYPE system并且M_MODULE include时&#xff0c;会设置常量PATH_OWN_FILE为PATH_APP.M_T…

uniapp 开发ios, xcode 提交app store connect 和 testflight内测

uniapp 中配置 配置manifest 文档&#xff1a;manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号&#xff1a;4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…

C++ 设计模式 《小明的奶茶加料风波》

&#x1f468;‍&#x1f393; 模式名称&#xff1a;装饰器模式&#xff08;Decorator Pattern&#xff09; &#x1f466; 小明最近上线了校园奶茶配送功能&#xff0c;业务火爆&#xff0c;大家都在加料&#xff1a; 有的同学要加波霸 &#x1f7e4;&#xff0c;有的要加椰果…

【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看

文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…

代码规范和架构【立芯理论一】(2025.06.08)

1、代码规范的目标 代码简洁精炼、美观&#xff0c;可持续性好高效率高复用&#xff0c;可移植性好高内聚&#xff0c;低耦合没有冗余规范性&#xff0c;代码有规可循&#xff0c;可以看出自己当时的思考过程特殊排版&#xff0c;特殊语法&#xff0c;特殊指令&#xff0c;必须…