
◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。
⭐️现代C++系列个人专栏: 插曲:现代C++
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- 一,探索_智能指针因何而生
- 二,RAII和智能指针的设计思路
- 三,C++库中的智能指针历史演进与使用
- [<font color ="red">四,share_ptr的底层原理与实现](#四,share_ptr的底层原理与实现)
- 五,shared_ptr的三大问题
- 六,C++11和boost中智能指针的关系
- [七, 内存泄漏](#七, 内存泄漏)
-
- [7.1 什么是内存泄漏,内存泄漏的危害](#7.1 什么是内存泄漏,内存泄漏的危害)
- [7.2 如何检测内存泄漏(了解)](#7.2 如何检测内存泄漏(了解))
- [7.3 如何避免内存泄漏](#7.3 如何避免内存泄漏)
概要&序論
这里是此方,好久不见。 本专栏是【主题曲:C++程序设计】专栏的补充篇【插曲:现代C++】。本系列将优先深度解析C++11标准,力求内容详实,无微不至。C++14~C++20的进阶内容将在后续间隔一段时间后连载。本期将重点讲解:智能指针的底层原理、RAII 设计思路、循环引用痛点及其解决方案等内容。
一,探索_智能指针因何而生
下面程序中我们可以看到,new了以后,我们也delete了 ,但是因为抛异常导致后面的delete没有得到执行,所以就内存泄漏了,在异常章节,我们讲了一种解决方案:捕获到异常后delete内存,再把异常抛出。
但是这种方法有两个缺陷:
- 不方便。
- 一种场景:new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。
cpp
double Divide(int a, int b){
if (b == 0){ throw "Divide by zero condition!";}
else{ return (double)a / (double)b;}
}
void Func(){
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();
}
catch (const char* errmsg){
cout << errmsg << endl;
}
catch (const exception& e){
cout << e.what() << endl;
}
catch (...){
cout << "未知异常" << endl;
}
return 0;
}
于是,为了解决这个问题,科学家们发明了RAII设计方法和智能指针。
二,RAII和智能指针的设计思路
2.1RAII设计思想
RAII,就是Resource Acquisition Is Initialization,翻译过来就叫:"资源获得即初始化"------显然是一个老外起的名字。
什么叫"资源获得即初始化 "呢?他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源(这是核心),避免资源泄漏,(这里的资源可以是内存、文件指针、网络连接、互斥锁等等)的设计方法。
有人说,哎,此方呀,我还是听不懂,什么是"利用对象生命周期来管理获取到的动态资源"呢?
RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
就是说:把资源释放的工作托管给一个对象,对象析构了,资源也就释放了。
科普:RAII设计思想在Java中也有使用有人看到后一定要说了,哎呀此方啊,Java里面资源释放有gc了,怎么还要RAII呢?Java的RAII在线程安全中使用的非常广泛 ,而不是资源释放,我来举一个例子:
在多线程环境下执行 ++i 操作时,如果多个线程同时操作,会产生竞态条件 。为了保证操作的原子性 ,我们需要通过加锁来确保线程同步。
然而,手动加锁存在风险:**如果锁定的代码块 func() 抛出未捕获的异常,程序会直接跳出当前逻辑。**若解锁操作在异常点之后,锁将永远无法释放,从而导致死锁(坏锁)。
为了规避风险,Java采用了类似于RAII的 思想(C++这块也是一样的),将锁封装在局部对象的生命周期中,利用析构函数在作用域结束时自动解锁。
2.2智能指针是如何利用RAII设计思想的
我们用一个代码来帮助大家理解一下:
cpp
//智能指针类除了满足RAII的设计思路,还要方便资源的访问,
//所以智能指针类还会想迭代器类一样,
//重载 operator*/operator->/operator[] 等
//运算符,方便访问资源。
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;
}
智能指针也有两种写法,我们更加倾向于写法二:
cpp
//写法一:
int* array4 = new int[10];
SmartPtr<int> sp1(array1);
//写法二:
SmartPtr<int> sp4 = new int[10];
那么怎么使用智能指针呢?我们有一种和迭代器高度相似的方法去使用他们。
cpp
sp1[5] = 50;
sp5->first = 1;
sp5->second = 2;
cout << sp1[5] << endl;
三,C++库中的智能指针历史演进与使用
C++标准库中的智能指针都在 <memory> 这个头文件下 面,我们包含 <memory> 就可以使用了,智能指针有好几种,除了 weak_ptr 他们都符合RAII和像指针一样访问的行为。
Tips:智能指针设计上首要解决的问题------拷贝问题
- 智能指针的设计初衷 是"让委托方对象指向的资源和原指针指向的资源相同"------
代理管理(代理而非拥有 ),于是必须实现成浅拷贝。 - 但是浅拷贝又会带来一个问题 :按照 C++ 对象的生命周期,每个指针对象析构时都会调一次 delete, 对同一块内存 delete 两次,程序直接报 Double Free 错误并挂掉。
面试常考 :这四种智能指针的根本区别?解决拷贝问题的不同方法。
3.2auto_ptr------C++98的设计尝试
3.2.1auto_ptr的使用
auto_ptr 是C++98设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象。
这是一个非常糟糕的设计,因为他会被拷贝对象悬空 ,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用 auto_ptr。 其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。(部分主流编译器如VS2026在C++17及以上标准中将auto_ptr移除。你得调整到c++14及以下才能使用)

如上,直接将p1的资源转移给p2,p1访问时报错。
3.2.2auto_ptr的原理
cpp
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;
};
3.3unique_ptr------对auto_ptr的补救
3.3.1unique_ptr的使用
unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。

如上,直接在编译时告知:不可以使用unique_ptr进行拷贝。 支持移动,移动后的数据就不可以访问了。会报出警告。

auto_ptr和unique_ptr都悬空了为什么后者更好?
因为后者是程序员心甘情愿地去悬空 up1(指通过 move 显式转移);
前者是库里面的败笔,"隐式" 导致的悬空。
3.3.2unique_ptr的原理
cpp
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)
{
if (this != &sp)
{
if (_ptr)
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
3.4shared_ptr------C++11的全新设计思路
shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动 。底层是用引用计数的方式实现的。如果需要拷贝的场景就需要使用他了。

如上,拷贝正常,析构正常。两个智能指针共同管理同一个资源。
(小的注意事项 )shared_ptr的构造函数加了一个explicit,意思是这个构造函数不支持隐式类型转换(我在C++加强专栏里面会讲)。所以
- 我们这样写是不可以的:shared_ptr<Date> ptr=new Date(2021,2,5);
(构造一个对象在拷贝构造的过程中发生了隐式类型转换)- 必须这么写: shared_ptr<Date> ptr(new Date(2021,2,5));(直接调用构造没有触发隐式类型转换 )
四,share_ptr的底层原理与实现
(面试高频考点·本文重点内容)
建议每一位学习智能指针的求职者都能够做到手撕智能指针,而选择那一个智能指针,建议选择难度最高,最实用的share_ptr。
4.1手写一个最简洁的share_ptr
4.1.1引用计数的复习
引用计数的设计,一份资源就需要一个引用计数 ,所以引用计数才用静态成员的方式是无法实现的,要使用堆上动态开辟的方式 ,构造智能指针对象时来一份资源,就要new一个引用计数出来。
多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。
引用计数我在string中讲过,这里直接拷贝过来:


4.1.2构造与拷贝构造
cpp
//SharePtr.h
//SharePtr.h
#pragma once
#include<iostream>
#include<functional>
using namespace std;
namespace MySharePtr
{
template <class T>
class Share_ptr
{
private:
T* _ptr; // 实际管理的资源指针
int* _pcount; // 引用计数:必须是指针,才能让多个对象共享同一个计数器
//在堆上开辟而不是全局静态
public:
// 构造函数 A:接收资源指针和自定义删除器
template<class K>
Share_ptr( T* ptr, const K& Delete)
: _ptr(ptr)
, _pcount(new int(1)) // 核心:在堆上开辟计数器,
//初始值为 1,每创建第一个对象,就会开辟一个int的空间,来作为引用计数器
, _Delete(Delete) // 初始化删除器
{}
// 构造函数 B:普通构造,只接收资源指针
Share_ptr( T* ptr)
: _ptr(ptr)
, _pcount(new int(1)) // 同上,新资源计数为 1
{}
// 拷贝构造函数:实现"资源共享"
// 逻辑:将新对象的指针指向现有资源,并让引用计数自增
Share_ptr(const Share_ptr& SharePtr)
: _ptr(SharePtr._ptr) // 浅拷贝:指向同一块资源
, _pcount(SharePtr._pcount) // 浅拷贝:指向同一个计数器地址
, _Delete(SharePtr._Delete) // 复制删除逻辑
{
(*_pcount)++; // 关键:由于多了一个管理者,计数器内容 +1,
//拷贝构造意味着一块资源被多个人管理,
}
};
}
4.1.3赋值运算符重载

cpp
// 逻辑:处理"先解绑旧资源,再绑定新资源"的过程
const Share_ptr& operator=(Share_ptr<T>& SharePtr)
{
// 检查是否为"指向同一资源"的赋值(不仅仅是自赋值,相同资源的两个对象赋值也应跳过)
//自己给自己赋值不行,如果引用计数不为1还好,
//引用计数为1的时候直接提前释放了,释放完之后两个指针都悬空了
//自赋值检查不仅仅要看对象地址是否相同(this != &sp),
//更要看它们管理的资源是否相同。
if (_ptr != SharePtr._ptr)
{
// 2. 减扣当前对象原本管理的资源计数
// 如果当前对象是该资源的最后一个管理者,则负责释放它
if (--(*_pcount) == 0)
{
_Delete(_ptr); // 使用定制删除器释放资源
delete(_pcount); // 释放堆上的计数器变量
}
// 3. 接管新资源
_ptr = SharePtr._ptr; // 指向新资源
_pcount = SharePtr._pcount; // 指向新计数器
_Delete = SharePtr._Delete; // 同步删除器逻辑
// 4. 新资源计数自增
(*_pcount)++;
}
return *this; // 支持 a = b = c 的连续赋值
}
4.1.5完整代码示例
cpp
//SharePtr.h
#pragma once
#include<iostream>
using namespace std;
namespace MySharePtr
{
template <class T>
class Share_ptr
{
private:
T* _ptr;
int* _pcount;
public:
Share_ptr(T* ptr)
: _ptr(ptr)
, _pcount(new int(1))
{
}
Share_ptr(const Share_ptr& SharePtr)
: _ptr(SharePtr._ptr)
, _pcount(SharePtr._pcount)
{
(*_pcount)++;
}
~Share_ptr()
{
if (--(*_pcount) == 0)
{
cout << "Success Delete" << endl;
delete _ptr;
delete _pcount;
}
}
T* operator->()
{
return _ptr;
}
T operator*()
{
return *_ptr;
}
const Share_ptr& operator=(Share_ptr<T>& SharePtr)
{
if (_ptr != SharePtr._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_pcount = SharePtr._pcount;
_ptr = SharePtr._ptr;
(*_pcount)++;
}
return *this;
}
};
}
有细心的小伙伴在官方文档中发现了这个接口,这接口我决定放在加强内容种讲。加强内容在哪里?我目前连专栏还没有创建,不过总会有的。

4.2定制删除器
面试官让你写一个智能指针,你写不写定制删除器?你不要写定制删除器,除非让你写你才写。
4.2.1什么是定制删除器
为什么要用定制删除器
你交给智能指针什么,它的底层就必须知道如何销毁它。

智能指针默认仅用 delete 释放内存。但在处理 new[] 申请的数组(需 delete[])、fopen 打开的文件(需 fclose)或数据库连接等特殊资源时,默认行为会导致内存泄漏或系统崩溃。定制删除器赋予了智能指针通用性,使其能管理任何形式的资源回收。
什么是定制删除器及可传内容
它是资源引用计数归零时自动触发的可调用对象。代码层面,它可以是:
- Lambda 表达式:最简洁,直接内联释放逻辑。
- 仿函数:重载了 operator() 的类对象,适合复杂复用。
- 函数指针:传统的 C 风格回调接口。
- std::function:统一包装上述所有类型,增加灵活性。
4.2.2如何使用定制删除器
针对不同的智能指针:
对于普通的 unique_ptr ,默认的删除器是 std::default_delete,其内部执行的是 delete ptr。
而对于 unique_ptr<T[]>,它使用的默认删除器是 std::default_delete<T[]>,其内部执行的是 delete[] ptr。
对于普通的 shared_ptr ,其默认删除逻辑是通过内部模板实例化的,执行的是 delete ptr。
而对于 shared_ptr<T[ ]>(C++17引入),它在内部进行了数组特化,默认执行的是 delete[] ptr。
cpp
struct Fclose {
void operator()(FILE* ptr) {
std::cout << "fclose: " << ptr << std::endl;
fclose(ptr);
}
};
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
//普通shared_ptr与shared_ptr<T[]>不需要传递定制删除器
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date[]> sp2(new Date[10]);
std::unique_ptr<Date> up1(new Date);
std::unique_ptr<Date[]> up2(new Date[10]);
//传递lambda,最好用
std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) { delete[] ptr; });
//传递函数指针,最不好用
std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);
//传递函数对象,也可以
std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
return 0;
}
4.2.3unique_ptr和shared_ptr的定制删除器区别
4.2.3.1设计思路的核心差异



4.2.3.2unique_ptr为什么不建议传递lambda作为定制删除器
为什么不能直接把 lambda 传递给模板参数?
-
模板参数只能传递类型不能传递对象实例。
-
lambda 的类型是在运行时由编译器确定的一个 lambda + uuid 的一个类型。
-
所以得先运行时推导出类型名称给 fcloseFunc,再用对象推导类型接口来推导得到。
-
为什么不能用typeid?typeid接口返回的是一个字符串,不是类型
这指的就是通过 decltype 关键字,从一个已有的对象中"推导出"它的类型,从而满足模板对类型的严格要求。
cpp
auto fcloseFunc = [](FILE* ptr) { fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"));
cpp
auto fcloseFunc = [](FILE* ptr) { fclose(ptr); };
// 补全代码:注意构造函数的第二个参数必须传入 fcloseFunc 对象
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);
但是传递 lambda 又在底层有限制 。lambda 是不支持默认构造的 。unique_ptr 本来想要用这个类型去在底层去构造一个对象来删除,但是由于 lambda 的底层禁止了默认构造函数。所以我们这个时候必须再传递一个 lambda 的对象。 这样 unique_ptr 的底层会用拷贝构造的方式来生成删除器。
总之,不建议用lambda在unique里面
顺带一提,函数指针也不建议这么传递:
4.2.3给我们手写的shared_ptr添加定制删除器
cpp
template <class T>
class Share_ptr
{
private:
T* _ptr;
int* _pcount;
//这里也是无奈之举:
//1.首先定制删除器要在对象中存储,但是类型不知啊于是应该是模板参数类型,
//但是构造函数的模板参数不能被搞过来。如果在类的模板参数中增加又会变成
//unique_ptr的设计,于是使用包装器进行自由推导。
//2.使用function后,这个定制删除器在不传递定制删除器的时候走构造函数列表
//但是他不知道应该被初始化为什么类型,于是必须加上缺省值。
function<void(T*)> _Delete = [](T* ptr) {delete ptr;};
public:
// 带删除器的构造:通过模板 K 支持 lambda、函数指针或仿函数
template<class K>
Share_ptr( T* ptr, const K& Delete)
: _ptr(ptr)
, _pcount(new int(1))
, _Delete(Delete)
{}
Share_ptr( T* ptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
// 拷贝构造:保证删除器也能正确传递
Share_ptr(const Share_ptr& SharePtr)
: _ptr(SharePtr._ptr)
, _pcount(SharePtr._pcount)
, _Delete(SharePtr._Delete)
{
(*_pcount)++;
}
// 析构函数:智能释放的核心
~Share_ptr()
{
// 逻辑:每次销毁一个对象,计数减 1
// 只有当计数器归零时,才证明没有其他对象在用了
if (--(*_pcount) == 0)
{
cout << "Success Delete" << endl;
_Delete(_ptr); // 执行具体的删除动作(可能是 delete,也可能是 delete[])
delete(_pcount); // 必须手动释放堆上的 int 计数器,防止内存泄漏
}
// 如果计数不为 0,说明还有其他 Share_ptr 指向这块内存,当前对象只需静静消失
}
五,shared_ptr的三大问题
5.1内存碎片化问题与make_shared
shared_ptr 虽然好用,但无法替代 unique_ptr 的原因:
-
引用计数的消耗:shared_ptr 每一次拷贝都要原子性地增减引用计数,在多线程环境下这种同步操作是有性能开销的。
-
内存碎片化问题:shared_ptr 除了管理资源本身,还要额外在堆上分配一个"控制块"来存计数器和删除器。如果你频繁创建大量小对象,这种额外的堆分配会导致严重的内存碎片。
-
独占语义的需求:在很多场景(如 RAII 包装文件句柄)下,资源必须由唯一的主人管理,unique_ptr 的强制不可拷贝性在编译器层面就杜绝了逻辑错误。
这里我们谈一谈内存碎片化问题 ,这个问题我们会在内存池中详细讲解。简单来说,内存碎片化 是指系统内存虽然总量充足,但由于分配不连续,导致无法凑出足够大的连续空间来满足程序的需求。
它主要分为两种形态:
- 外碎片:大量散落在堆内存中的微小空闲块,每个都太小,无法装下任何新对象。
- 内碎片:为了对齐(Alignment)或最小分配限制,分配给你的内存比你实际申请的要多,多出来的部分被浪费了。
1,为什么日常代码"问题不大"?
在写小程序或短时间运行的脚本时,碎片化几乎是"隐身"的:
- 生命周期短:程序运行几秒钟就结束了,操作系统会直接回收整块进程内存,碎片还没来得及堆积。
- 规模小:申请的对象数量级有限,现代内存分配器(如 ptmalloc)完全压得住。
2,为什么在工程中"极具破坏性"?
在长期运行(7x24小时)的高并发服务器、嵌入式系统或大型游戏引擎中,它会变成一颗"慢速炸弹":
-
虚假的"内存不足" :
你的监控显示还有 2GB 剩余内存,但当你尝试申请一个连续的 50MB 缓冲区时,程序竟然直接抛出
bad_alloc崩溃了。原因就是没有一块连续的 50MB 空间,内存被切得太碎了。 -
性能阴跌 :
碎片化严重意味着数据在物理内存中分布极其凌乱。这会导致 CPU 缓存命中率(Cache Hit Rate) 大幅下降,原本顺序读取的操作变成了频繁的随机访问,程序运行速度会随着时间推移越来越慢。
-
内存膨胀 :
为了寻找合适的空闲块,内存分配器不得不向操作系统申请更多的新页面。最终导致程序占用的虚拟内存远超其实际存储的数据量,引发系统级的页面置换,导致整机卡死。
工程中的生存法则 :为了对抗碎片化,大型工程通常会引入内存池或专门的分配器,核心思想就是"自己管理,减少频繁向系统讨要零碎内存"。
3,解决方案:make_shared
cpp
int main()
{
std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
//内存开辟好了之后,再把这个开辟好的内存的指针传递给智能指针,
//再智能指针内部再开辟一个引用计数.
//这就导致了引用计数和维护内存的位置分离 + 内存分散问题(一共开了两次)
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
//make_shared是直接把这个数据的内存和这个引用计数的内存放在一块儿一起开。
//只开一次内存不分离。就会更好一点。
//它和make_pair很相似,但是它不只是方便那么简单
return 0;
}
5.2循环引用问题与weak_ptr
循环引用一般碰不到,但是碰到了就是大问题,必须识别出来然后尽可能避免
5.2.1循环引用问题
shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。(这个问题出现的可能性不大,但是一旦出现了你不会解决就要完蛋)
cpp
int main(){
// 循环引用 -- 内存泄漏
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}

场景解析: 如上图示,经过这离奇的三步之后,我们成功触发了循环引用场景,于是:
-
右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
-
_next什么时候析构呢,_next是左边节点的成员,左边节点释放,_next就析构了。
-
左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
-
_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
-
至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
5.2.2循环引用问题的解决方案:weak_ptr
5.2.2.1weak_ptr的介绍与使用
weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数 ,那么就可以解决上述的循环引用问题。

cpp
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;
};

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>
int main() {
// 1. 初始化资源
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 2. 绑定 weak_ptr (不增加引用计数)
std::weak_ptr<int> wp = sp;
// 查看引用计数: 此时只有 sp 在管理资源
std::cout << "1. 引用计数: " << wp.use_count() << std::endl; // 输出 1
// 3. 模拟访问资源:检查是否过期
if (!wp.expired()) {
// 使用 lock() 获取 shared_ptr
std::shared_ptr<int> temp_sp = wp.lock();
if (temp_sp) {
std::cout << "2. 访问资源成功: " << *temp_sp << std::endl;
std::cout << "3. 临时引用计数: " << wp.use_count() << std::endl; // 输出 2
}
}
// 4. 释放资源
sp.reset();
// 5. 再次检查
if (wp.expired()) {
std::cout << "4. 资源已过期,无法访问。" << std::endl;
// 即使调用 lock(),返回的也是空对象
std::shared_ptr<int> empty_sp = wp.lock();
if (empty_sp == nullptr) {
std::cout << "5. lock() 返回了一个空的 shared_ptr。" << std::endl;
}
}
return 0;
}
lock"资源锁" weak_ptr虽然不指向引用计数,也不管理资源,但是它必须指向引用计数来保证自己的安全 。
锁住资源的方式很简单:在里面新建一个shared_ptr用这个shared_ptr去指向这个资源,让weak_ptr和shared_ptr的声明周期保持一致。(也算是一种间接增加引用计数的方法了)
cpp
std::shared_ptr<std::string> sp1 = std::make_shared<std::string>("hello");
std::weak_ptr<std::string> wp = sp1;
// 在大哥还在的时候,找一个新大哥
auto sp3 = wp.lock(); // sp3 就是新大哥
// 原来的大哥走了
sp1.reset();
// 新大哥还在,资源安全
std::cout << *sp3 << std::endl; // "hello"
5.2.2.2weak_ptr解决循环引用问题
把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
cpp
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;

从shareptr的实现到shareptr的缺陷再到weakptr的解决方案最后到weakptr的底层的设计------面试会步步深挖,
5.3.shared_ptr的线程安全问题
- shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数 ,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
- shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
- 下面的程序会崩溃或者A资源没释放,bit::shared_ptr引用计数从int*改成atomic*就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
智能指针本身是线程安全的,但是他指向的资源不是线程安全的。
cpp
struct AA{
int _a1 = 0;
int _a2 = 0;
~AA(){ cout << "~AA()" << endl;}
};
int main(){
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&](){
for (size_t i = 0; i < n; ++i){
// 这里智能指针拷贝++计数
bit::shared_ptr<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;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
六,C++11和boost中智能指针的关系
6.1什么是boost社区
- Boost库是为C++语言标准库提供扩展的一些C++程序库的总称 ,Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
Scott Meyers的EffectiveC++最后一条款特地讲述了boost社区和boost库,非常推荐大家去看一看。
6.2boost社区为C++智能指针做了哪些贡献
-
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中的实现的。
七, 内存泄漏
7.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的 。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断减少,各种功能响应越来越慢,最终卡死。
cpp
int main(){
// 申请一个1G未释放,这个程序多次运行也没啥危害
// 因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
7.2 如何检测内存泄漏(了解)
7.3 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
- 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
- 总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
【主题曲:C++程序设计】完结撒花 ✿ 感谢您一直以来的收看,主线故事迎来了尾声,但是C++的路还有很长,很长很长。未来让我们共同进步,走的更远。バイバイ!



