C++学习之旅【智能指针的使⽤及其原理】


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》

《C++知识内容》《Linux系统知识》

✨逆境不吐心中苦,顺境不忘来时路! 🎬 博主简介:

引言:前篇文章,小编已经介绍了关于C++异常相关内容以及类型转换介绍!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于C++智能指针的使⽤及其原理!在C++开发中,手动管理动态内存是绕不开的核心环节,而裸指针带来的内存泄漏、野指针、重复释放、资源未释放等问题,一直是程序稳定性的最大隐患.为了从根本上解决手动内存管理的痛点,C++11引入了智能指针这一核心工具,它依托RAII机制实现了内存的自动管理,让开发者无需手动调用new/delete.本文将从智能指针的基础使用、底层实现原理、常用智能指针(unique_ptr/shared_ptr/weak_ptr)的区别与场景三个维度,全面拆解智能指针,帮助你彻底掌握这一C++进阶必备知识点.废话不多说,带着这些疑问,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.智能指针的使⽤场景分析

下⾯程序中我们可以看到new了以后,我们也delete了,但是因为抛异常导致后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦.智能指针放到这样的场景⾥⾯就让问题简单多了.

cpp 复制代码
#include<iostream>
using namespace std;
double Divide(int a, int b)
{
//当除数为0时,抛出字符串类型异常
if (b == 0)
{
throw "Divide by zero condition!";
}
//正常情况返回除法结果
else
{
return (double)a / (double)b;
}
}
void Func()
{
//这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
//所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
//但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案是智能指针,否则代码太戳了
//动态申请两个整型数组(堆内存)
int* array1 = new int[10];
int* array2 = new int[10]; 
try
{
//读取用户输入,调用除法函数(可能抛异常)
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
//捕获所有类型的异常(万能捕获)
catch (...)
{
//异常发生时,手动释放堆内存,避免泄漏
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
//正常执行路径:无异常时,手动释放内存
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();//调用可能抛异常的函数
}
//捕获Divide 抛出的 const char* 类型异常
catch (const char* errmsg)
{
cout << errmsg << endl;
}
//捕获 C++ 标准库异常(如内存分配失败 bad_alloc)
catch (const exception& e)
{
cout << e.what() << endl;
}
//捕获其他所有未知异常
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}



场景1:正常输入(无异常)
输入:10 2
执行流程:
申请array1/array2
计算10/2=5,正常输出
执行delete[] 释放内存
程序正常结束
场景2:输入除数为0(抛异常)
输入:10 0
执行流程:
Divide抛出字符串异常
Func的 catch 捕获 → 释放两个数组
throw;重新抛出异常
main捕获字符串异常 → 打印:Divide by zero condition!


2.RAII和智能指针的设计思路

1️⃣RAII设计思路
①全称与定义
RAII = Resource Acquisition Is Initialization
资源获取即初始化---是C++独有的、最核心的资源管理设计思想.
②核心痛点
你之前的代码问题:
堆内存/文件/锁等资源,需要手动申请、手动释放.
一旦发生异常跳转、函数提前返回,delete 永远执行不到→内存泄漏
手动释放代码冗余、极易出错
③RAII 三大核心设计思路
资源与对象生命周期强绑定
把需要管理的资源(内存、文件、锁)交给一个栈上对象管理.
构造函数 = 获取资源
对象创建时,自动调用构造函数,完成资源申请(new).
析构函数 = 释放资源
利用C++ 栈对象出作用域自动析构的特性:
✅正常执行 → 析构释放
✅异常跳转 → 栈展开,依然自动析构释放
资源绝对不会泄漏.
④RAII本质总结
让编译器自动管理资源生命周期,彻底摆脱手动释放!
2️⃣智能指针的设计思路(基于RAII)
智能指针 = RAII 思想 + 指针行为模拟
专门用来解决 **堆内存(new)*的自动管理问题.
①智能指针设计目标
自动管理裸指针,离开作用域自动delete
用起来和普通指针一样(支持 * / ->)
控制指针所有权,避免重复释放、野指针.
保证异常安全

②智能指针通用设计步骤

RAII是Resource Acquisition Is Initialization的缩写,它是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等.RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题.
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀样,重载operator/operator->/operator[] 等运算符,⽅便访问资源.

cpp 复制代码
#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
//这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}

3.C++标准库智能指针的使⽤

①C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同.
auto_ptr是C++98时设计出来的智能指针,它的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr.其他C++11出来之前很多公司也是明令禁⽌使⽤这个智能指针的.
unique_ptr是C++11设计出来的智能指针,它的名字翻译出来是唯⼀指针,它的特点的不⽀持拷⻉,只⽀持移动.如果不需要拷⻉的场景就⾮常建议使⽤它.
shared_ptr是C++11设计出来的智能指针,它的名字翻译出来是共享指针,它的特点是⽀持拷⻉,也⽀持移动.如果需要拷⻉的场景就需要使⽤它了.底层是⽤引⽤计数的⽅式实现的.
weak_ptr是C++11设计出来的智能指针,它的名字翻译出来是弱指针,它完全不同于上⾯的智能指
针,它不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题.
⑥智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃.智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源.因为new[]经常使⽤,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(newDate[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源.template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
⑦shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值直接构造.
⑧shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空.
⑨shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换成智能指针对象.
小编自己简短总结
C++11标准库在``头文件中提供了三种核心智能指针,基于RAII 机制实现,彻底解决裸指针的内存泄漏、异常安全、重复释放等问题,是现代 C++ 开发必须掌握的知识点.
1️⃣std::unique_ptr(独占式智能指针)⭐最常用
核心特性
同一时间,只有一个unique_ptr管理对象.
禁止拷贝、赋值,只支持移动(转移所有权).
效率极高,无额外开销.
支持管理普通对象和数组对象.

常用API
reset():释放当前对象,置空指针
release():释放所有权,返回裸指针(不会释放内存)
get():获取底层裸指针
2️⃣std::shared_ptr (共享式智能指针)⭐高频使用
核心特性
共享所有权,多个指针可以管理同一个对象.
底层采用引用计数机制.
拷贝/赋值时,计数 + 1;析构时,计数 - 1.
计数变为0时,自动释放内存.
有轻微性能开销(维护引用计数)

常用API
use_count():获取当前引用计数
unique():判断是否独占对象(计数=1返回 true)
reset():释放/重置托管对象
get():获取底层裸指针
3️⃣std::weak_ptr (辅助智能指针)⭐解决循环引用
核心特性
不拥有对象所有权,不增加引用计数
专为解决shared_ptr 循环引用导致的内存泄漏问题
不能直接解引用,必须通过 lock() 转为shared_ptr 使用

常用API
lock():将weak_ptr 转为shared_ptr
expired():判断托管对象是否已释放
use_count():查看关联的shared_ptr 计数
4️⃣std::auto_ptr
C++98 产物,C++17 已移除
支持拷贝,但会转移所有权,极易造成悬空指针
绝对不要使用,直接用 unique_ptr 替代

cpp 复制代码
#include<iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
//拷⻉时,管理权限转移,被拷⻉对象ap1悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
unique_ptr<Date> up1(new Date);
//不⽀持拷⻉
//unique_ptr<Date> up2(up1);
//⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
//⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
//⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include <memory>
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
};
template<class T>
void DeleteArrayFunc(T* ptr)
{
	delete[] ptr;
}
template<class T>
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
class Fclose
{
public:
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	}
};
int main()
{
	//这样实现程序会崩溃
	//unique_ptr<Date> up1(new Date[10]);
	//shared_ptr<Date> sp1(new Date[10]);
	//解决⽅案1
	//因为new[]经常使⽤,所以unique_ptr和shared_ptr
	//实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
	unique_ptr<Date[]> up1(new Date[5]);
	shared_ptr<Date[]> sp1(new Date[5]);
	//解决⽅案2
	//仿函数对象做删除器
	//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
	//unique_ptr和shared_ptr⽀持删除器的⽅式有所不同
	//unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的
	//这⾥没有使⽤相同的⽅式还是挺坑的
	//使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤
	//但是下⾯的函数指针和lambda的类型不可以
	unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
	shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
	//函数指针做删除器
	unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
	shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
	//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);
	//实现其他资源管理的删除器
	shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
	shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
		});
	return 0;
}
cpp 复制代码
#include<iostream>
#include <memory>
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
};
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
//if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
//报错
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
//shared_ptr<Date> sp5 = make_shared<Date>(2024, 9, 11);正常运行
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
//unique_ptr<Date> sp6(new Date(2024, 9, 11)); 正常运行
return 0;
}

4.智能指针的原理

①下⾯我们模拟实现了auto_ptr和unique_ptr的核⼼功能,这两个智能指针的实现⽐较简单,⼤家了解⼀下原理即可.auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可的,也不建议使⽤.unique_ptr的思路是不⽀持拷⻉.
②⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来.多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源.

cpp 复制代码
#include<iostream>
#include <memory>
#include<functional>
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
};
namespace lcz
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//检测是否为⾃⼰给⾃⼰赋值
if (this != &ap)
{
//释放当前对象中资源
if (_ptr)
delete _ptr;
//转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, 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)
{
//最后⼀个管理的对象,释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
//atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
//需要注意的是我们这⾥实现的shared_ptr和weak_ptr都是以最简洁的⽅式实现的,
//只能满⾜基本的功能,这⾥的weak_ptr lock等功能是⽆法实现的,想要实现就要
//把shared_ptr和weak_ptr⼀起改了,把引⽤计数拿出来放到⼀个单独类型,shared_ptr
//和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
}
int main()
{
lcz::auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
lcz::auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
lcz::unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
lcz::unique_ptr<Date> up3(move(up1));
lcz::shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
lcz::shared_ptr<Date> sp2(sp1);
lcz::shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}

5.shared_ptr和weak_ptr

5.1shared_ptr循环引⽤问题

①shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉.但是在循环引⽤的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放原因,并且学会使⽤weak_ptr解决这种问题.
②如下图所述场景,n1和n2析构后,管理两个节点的引⽤计数减到1
(1)右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了.
(2)_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了.
(3)左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了.
(4)_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了.
③⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏.
④把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引⽤计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题.

cpp 复制代码
#include<iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
//这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
//不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了
/*std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;*/
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
//循环引⽤ -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
//weak_ptr不⽀持管理资源,不⽀持RAII
//weak_ptr是专⻔绑定shared_ptr,不增加他的引⽤计数,作为⼀些场景的辅助管理
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}

5.2weak_ptr

①weak_ptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题.
②weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,那么如果它绑定的shared_ptr已经释放了资源,那么它去访问资源就是很危险的.weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的.

cpp 复制代码
#include<iostream>
#include <memory>
using namespace std;
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}

6.shared_ptr的线程安全问题

①shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷⻉析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者原⼦操作保证线程安全.
②shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使⽤shared_ptr的⼈进⾏线程安全的控制.
③下⾯的程序会崩溃或者A资源没释放,lcz::shared_ptr引⽤计数从int*改成atomic<int>*就可以保证
引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以.

cpp 复制代码
#include<iostream>
#include <memory>
#include <thread>   //新增:多线程相关
#include <mutex>    //新增:互斥锁相关
using namespace std;

namespace lcz
{
    template<class T>
    class auto_ptr
    {
    private:
        T* _ptr;  
    public:
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {
        }
        auto_ptr(auto_ptr<T>& sp)
            :_ptr(sp._ptr)
        {
            sp._ptr = nullptr;  
        }
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                if (_ptr)
                    delete _ptr;
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }
        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        explicit operator bool() const { return _ptr != nullptr; }
    };

    struct AA
    {
        int _a1 = 0;
        int _a2 = 0;
        ~AA()
        {
            cout << "~AA()" << endl;
        }
    };

    template<class T>
    class shared_ptr
    {
    private:
        T* _ptr;
        int* _use_count;  //引用计数指针
        mutex* _mtx;      //保护引用计数的互斥锁

        //释放资源的辅助函数
        void release() {
            if (_ptr) {
                lock_guard<mutex> lk(*_mtx);
                if (--(*_use_count) == 0) {
                    delete _ptr;
                    delete _use_count;
                    delete _mtx;
                }
            }
            _ptr = nullptr;
            _use_count = nullptr;
            _mtx = nullptr;
        }

    public:
        //构造函数
        explicit shared_ptr(T* ptr = nullptr)
            :_ptr(ptr), _use_count(new int(1)), _mtx(new mutex)
        {
        }

        //拷贝构造(线程安全增加引用计数)
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr), _use_count(sp._use_count), _mtx(sp._mtx)
        {
            if (_ptr) {
                lock_guard<mutex> lk(*_mtx);
                ++(*_use_count);
            }
        }

        //赋值运算符
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            if (this != &sp) {
                release();
                _ptr = sp._ptr;
                _use_count = sp._use_count;
                _mtx = sp._mtx;
                if (_ptr) {
                    lock_guard<mutex> lk(*_mtx);
                    ++(*_use_count);
                }
            }
            return *this;
        }

        //析构函数
        ~shared_ptr() { release(); }

        //指针行为重载
        T& operator*()  const { return *_ptr; }
        T* operator->() const { return _ptr; }

        //获取引用计数
        int use_count() const {
            if (!_use_count) return 0;
            lock_guard<mutex> lk(*_mtx);
            return *_use_count;
        }

        //判空
        explicit operator bool() const { return _ptr != nullptr; }
    };
}

int main()
{
    lcz::shared_ptr<lcz::AA> p(new lcz::AA);
    const size_t n = 100000;
    mutex mtx;

    auto func = [&]() {
        for (size_t i = 0; i < n; ++i)
        {
            lcz::shared_ptr<lcz::AA> copy(p);
            {
                unique_lock<mutex> lk(mtx);
                copy->_a1++;
                copy->_a2++;
            }
        }
        };

    thread t1(func);
    thread t2(func);
    t1.join();
    t2.join();

    cout << p->_a1 << endl;  // 预期输出 200000
    cout << p->_a2 << endl;  // 预期输出 200000
    cout << p.use_count() << endl;  // 预期输出 1

    return 0;
}

7.C++11和boost中智能指针的关系

①Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员之⼀.在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来.
②C++98中产⽣了第⼀个智能指针auto_ptr.
③C++boost给出了更实⽤的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.
④C++TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版.
⑤C++11,引⼊了unique_ptr和shared_ptr和weak_ptr.需要注意的是unique_ptr对应boost的scoped_ptr.并且这些智能指针的实现原理是参考boost中的实现的.


8.内存泄漏

8.1什么是内存泄漏?内存泄漏的危害有哪些?

①什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的.内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费.
②内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放.⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死.

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
// 申请⼀个1G未释放,这个程序多次运⾏也没啥危害
// 因为程序⻢上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}

8.2如何检测内存泄漏

VS 编译器内置检测(零依赖,Windows 直接用)
AddressSanitizer(ASAN)(编译器自带,速度快、精度高)
Valgrind(服务器/后端开发必备)
手动重载new/delete(底层原理,适合教学)


8.3如何避免内存泄漏

①⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放.但是如果碰上异常时,就算注意释放了,还是可能会出问题.需要下⼀条智能指针来管理才有保证.
②尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理.
③定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费.
④总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:事前预防型,如智能指针等.事后查错型,如泄漏检测⼯具.


9.智能指针完整代码

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

namespace lcz
{
    // 独占式智能指针
    template<class T>
    class unique_ptr
    {
    private:
        T* _ptr;

    public:
        unique_ptr(T* ptr = nullptr)
            :_ptr(ptr)
        {
        }

        ~unique_ptr()
        {
            if (_ptr)
            {
                delete _ptr;
                _ptr = nullptr;
            }
        }

        unique_ptr(const unique_ptr<T>&) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

        unique_ptr(unique_ptr<T>&& u) noexcept
        {
            _ptr = u._ptr;
            u._ptr = nullptr;
        }

        unique_ptr<T>& operator=(unique_ptr<T>&& u) noexcept
        {
            if (this != &u)
            {
                delete _ptr;
                _ptr = u._ptr;
                u._ptr = nullptr;
            }
            return *this;
        }

        // 指针行为
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        explicit operator bool() const { return _ptr != nullptr; }

        void reset() noexcept
        {
            if (_ptr)
            {
                delete _ptr;
                _ptr = nullptr;
            }
        }

        void reset(T* ptr) noexcept
        {
            if (_ptr != ptr)
            {
                delete _ptr;
                _ptr = ptr;
            }
        }
    };

    template<class T>
    class shared_ptr
    {
        template<class>
        friend class weak_ptr;

    private:
        T* _ptr;
        int* _use_count;
        mutex* _mtx;

        void release()
        {
            bool del = false;
            {
                lock_guard<mutex> lock(*_mtx);
                if (_ptr && --(*_use_count) == 0)
                {
                    delete _ptr;
                    delete _use_count;
                    delete _mtx;
                    del = true;
                }
            }
            _ptr = nullptr;
            _use_count = nullptr;
            _mtx = nullptr;
        }

    public:
        explicit shared_ptr(T* ptr = nullptr)
            :_ptr(ptr), _use_count(new int(1)), _mtx(new mutex)
        {
        }

        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr), _use_count(sp._use_count), _mtx(sp._mtx)
        {
            if (_ptr)
            {
                lock_guard<mutex> lock(*_mtx);
                ++(*_use_count);
            }
        }

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

                if (_ptr)
                {
                    lock_guard<mutex> lock(*_mtx);
                    ++(*_use_count);
                }
            }
            return *this;
        }

        ~shared_ptr()
        {
            release();
        }

        //指针行为
        T& operator*()  const { return *_ptr; }
        T* operator->() const { return _ptr; }
        explicit operator bool() const { return _ptr != nullptr; }

        //获取引用计数
        int use_count() const
        {
            if (!_use_count) return 0;
            lock_guard<mutex> lock(*_mtx);
            return *_use_count;
        }

        void reset() noexcept
        {
            release();
        }

        void reset(T* ptr)
        {
            if (_ptr != ptr)
            {
                release();
                if (ptr)
                {
                    _ptr = ptr;
                    _use_count = new int(1);
                    _mtx = new mutex;
                }
            }
        }
    };

    template<class T>
    class weak_ptr
    {
    private:
        T* _ptr;
        int* _use_count;
        mutex* _mtx;

    public:
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr), _use_count(sp._use_count), _mtx(sp._mtx)
        {
        }

        bool expired() const
        {
            if (!_use_count) return true;
            lock_guard<mutex> lock(*_mtx);
            return *_use_count == 0;
        }

        shared_ptr<T> lock() const
        {
            if (expired())
                return shared_ptr<T>(nullptr);

            lock_guard<mutex> lock(*_mtx);
            shared_ptr<T> sp(_ptr);
            sp._use_count = _use_count;
            sp._mtx = _mtx;
            ++(*_use_count);
            return sp;
        }
    };
}

struct Date
{
    int _year, _month, _day;
    Date(int y = 1, int m = 1, int d = 1) :_year(y), _month(m), _day(d) {}
    ~Date() { cout << "~Date() 析构" << endl; }
};

int main()
{
    // 测试 unique_ptr + reset
    cout << "测试 unique_ptr " << endl;
    lcz::unique_ptr<Date> up(new Date(2025, 3, 15));
    cout << up->_year << endl;
    up.reset();
    cout << "reset 后是否为空:" << boolalpha << !up << endl;

    cout << "\n测试 shared_ptr" << endl;
    lcz::shared_ptr<Date> sp1(new Date(2026, 1, 1));
    cout << "计数:" << sp1.use_count() << endl;

    sp1.reset(); 
    cout << "reset 后是否为空:" << boolalpha << !sp1 << endl;

    sp1.reset(new Date(2024, 9, 11));
    cout << "新对象年份:" << sp1->_year << endl;

    cout << "\n测试 weak_ptr" << endl;
    lcz::shared_ptr<Date> sp2 = sp1;
    lcz::weak_ptr<Date> wp = sp2;
    auto lock_sp = wp.lock();
    if (lock_sp)
    {
        cout << "对象存活,计数:" << lock_sp.use_count() << endl;
    }

    return 0;
}

敬请期待下一篇文章内容:C++IO库相关内容介绍!


每日心灵鸡汤:不是先决定,而是先思考!
人生路上,很多人急于求成,遇事跟着情绪匆匆"拍板",结果往往事与愿违.真正厉害的人,懂得"磨刀不误砍柴工"的道理.在做决定之前,他们往往会先给自己一段沉下心来充分思考的时间.他们明白,与其不动脑子做十件半途而废的事,不如下功夫把一件事想清楚、办利索.做事前,他们会梳理前因后果,预判可能出现的问题,制定周全的计划;遇到突发状况,也不会慌不择路,而是先冷静分析利弊.遇事只有充分思考,做足准备,才能稳扎稳打地走好脚下的每一步.

相关推荐
ん贤2 小时前
一文读懂 Go-Eino 的 Embedding
开发语言·golang·embedding
爆更小哇2 小时前
JMeter配置和使用入门指南
java·开发语言·测试工具·jmeter·自动化
一叶落4382 小时前
LeetCode 380. O(1) 时间插入、删除和获取随机元素【哈希表 + 动态数组 | C语言详解】
c语言·数据结构·c++·算法·leetcode·散列表
sycmancia2 小时前
C++——类模板的概念和意义
c++
xixixiLucky2 小时前
TreeSet |TreeMap|jar包|web包易混淆解答
java·开发语言
xiaoye-duck2 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--8.二叉树剪枝,9.验证二叉搜索树
c++·算法·深度优先·递归
●VON2 小时前
2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线
服务器·开发语言·spring boot·mysql·ui·von
承渊政道2 小时前
C++学习之旅【异常相关内容以及类型转换介绍】
c语言·c++·笔记·vscode·学习·macos·visual studio
飞Link2 小时前
洞察数据的“分寸感”:深度解析对比学习(Contrastive Learning)
开发语言·python·学习·数据挖掘