以下程序示例中,虽然我们在 new 操作后进行了 delete 处理,但由于异常抛出导致后续的 delete 未能执行,从而引发内存泄漏。理论上,我们可以在 new 操作后捕获异常,并在异常处理中执行 delete 后再重新抛出异常。然而,new 操作本身可能抛出异常,且连续多个 new 操作和后续的 Divide 函数调用都可能产生异常,这使得异常处理变得异常复杂。在这种情况下,使用智能指针可以显著简化问题的处理流程。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array1和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();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}运行结果:

但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是智能指针,否则代码太挫了
• RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种重要的资源管理范式。它的核心思想是将资源的生命周期与对象的生命周期绑定,通过对象的构造和析构来管理资源的获取和释放。具体实现包含三个关键步骤:
这种机制确保了即使在异常发生时,资源也能被正确释放。例如:
class FileHandle {
public:
FileHandle(const char* filename) {
file = fopen(filename, "r");
}
~FileHandle() {
if(file) fclose(file);
}
private:
FILE* file;
};• 智能指针是RAII思想的典型应用,它不仅管理资源生命周期,还提供便捷的访问接口。常见的智能指针类型包括:
智能指针通过运算符重载提供自然的使用方式:
std::unique_ptr<Object> ptr(new Object);
(*ptr).method(); // 重载operator*
ptr->method(); // 重载operator->
ptr[0].method(); // 重载operator[] (仅适用于数组)这种设计既保证了资源安全,又保持了原生指针的使用体验,是现代C++开发中的重要工具。在实际应用中,智能指针可以显著减少内存泄漏和资源管理错误,特别是在多线程环境和复杂对象关系中。
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;
}运行结果:

代码中将申请到的内存空间交给了SmartPtr对象进行管理,这样即使代码抛异常了,我们申请的资源也能让SmartPtr对象帮我们释放,避免了内存泄漏
• C++标准库中的智能指针均定义在<memory>头文件中,包含该头文件即可使用。智能指针家族包括auto_ptr(C++98)、unique_ptr、shared_ptr和weak_ptr(C++11)等类型。除weak_ptr外都严格遵循RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化,通过构造函数获取资源,析构函数释放资源。所有智能指针都支持指针式访问,通过operator*和operator->来访问资源。它们的主要区别在于处理拷贝行为的方式不同,这是选择使用哪种智能指针的关键考量因素。
• auto_ptr是C++98引入的最初版本智能指针,其特点是拷贝时会转移资源管理权给拷贝对象。这种行为是通过拷贝构造函数和赋值运算符实现的,转移后原auto_ptr变为nullptr。这种设计存在严重缺陷,会导致被拷贝对象变为悬空指针,进而引发访问错误。例如:
auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2 = ap1; // 所有权转移
*ap1; // 运行时错误,ap1已为空C++11推出新智能指针后,强烈建议避免使用auto_ptr。事实上在C++11之前,许多公司(如Google、Microsoft)就已明文禁止使用该类型,并在编码规范中将其列为禁用项。
• unique_ptr是C++11引入的智能指针,其名称意为"唯一指针"。特点是禁止拷贝操作(拷贝构造函数和赋值运算符被删除),仅支持移动语义(通过std::move转移所有权)。这种设计确保了资源所有权的唯一性,避免了auto_ptr的问题。在不需要拷贝的场景中,推荐优先使用unique_ptr,因为它的开销比shared_ptr更小。典型使用场景包括:
unique_ptr<File> file(new File("test.txt"));
// 转移所有权
unique_ptr<File> file2 = std::move(file);• shared_ptr同样是C++11引入的智能指针,名为"共享指针"。它支持拷贝和移动操作,适用于需要共享所有权的场景。其内部采用引用计数机制实现资源管理,每拷贝一次引用计数加1,析构时引用计数减1,当计数为0时释放资源。shared_ptr是线程安全的,但仅针对控制块(引用计数)的操作,不保证所管理资源的线程安全。典型使用场景:
shared_ptr<Connection> conn(new Connection);
shared_ptr<Connection> conn2 = conn; // 引用计数+1• weak_ptr是C++11提供的特殊智能指针,称为"弱指针"。与其他类型不同,它不遵循RAII原则,不能直接管理资源。weak_ptr主要用于解决shared_ptr循环引用导致的内存泄漏问题。它不增加引用计数,需要通过lock()方法获取可用的shared_ptr。典型使用场景:
class Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // 避免循环引用
};• 智能指针默认在析构时调用delete释放资源。这意味着若将非new分配的资源交给智能指针管理,析构时会导致程序崩溃。例如:
int x;
unique_ptr<int> up(&x); // 错误,会尝试delete栈变量智能指针支持在构造时指定删除器——一个可调用对象,用于自定义资源释放方式。例如:
void fileDeleter(FILE* fp) { fclose(fp); }
unique_ptr<FILE, decltype(&fileDeleter)> fp(fopen("a.txt","r"), fileDeleter);为简化new[]操作,unique_ptr和shared_ptr都提供了特化版本:
unique_ptr<Date[]> up1(new Date[5]); // 会调用delete[]
shared_ptr<Date[]> sp1(new Date[5]); // C++17支持•
template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);make_shared函数是创建shared_ptr的推荐方式,它比直接new更高效,因为能一次性分配控制块和对象所需内存。使用示例:
auto sp = make_shared<Widget>(10, "hello"); // 相当于new Widget(10, "hello")• shared_ptr支持两种构造方式:通过资源指针构造,或使用make_shared函数直接初始化资源对象。前者需要两次内存分配(对象和控制块),后者只需一次。但某些情况下必须使用前者,例如:
// 需要自定义删除器时
shared_ptr<FILE> sp(fopen("a.txt","r"), fclose);• shared_ptr和unique_ptr都重载了operator bool,可将智能指针对象直接用于if条件判断:空指针返回false,非空返回true。例如:
if (sp) { // 等价于sp.get() != nullptr
// 使用sp
}• shared_ptr和unique_ptr的构造函数均使用explicit修饰,防止普通指针隐式转换为智能指针对象。这意味着必须显式构造智能指针:
void foo(shared_ptr<int> sp);
int* p = new int;
foo(p); // 错误,不能隐式转换
foo(shared_ptr<int>(p)); // 正确具体示例如下:
下面统一使用Date对象来演示,方便析构函数打印查看
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;
}
};auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象ap1悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;

通过调试和运行结果可以看出,拷贝时,管理权限发生了转移,并且ap1对象也被悬空,容易引发空指针访问
unique_ptr<Date> up1(new Date);
// 不支持拷贝
//unique_ptr<Date> up2(up1);
// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎
unique_ptr<Date> up3(move(up1));使用拷贝会编译报错

但是支持移动,但是移动后,原来的资源是会被“窃取”的,所以也会产生悬空,但是这里我们自己move的时候心里是有数的
运行结果:

调用一次析构函数
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));注意:use_count成员函数是用来获取当前对象管理的资源所对应的引用计数
运行结果:

智能指针默认在析构时调用delete释放资源。这意味着若将非new分配的资源(如malloc分配的内存或文件描述符)交给智能指针管理,析构时会导致程序崩溃。智能指针支持在构造时指定删除器——一个可调用对象,用于自定义资源释放方式。例如:
// 默认删除器使用delete,但数组需要delete[]
unique_ptr<Date> up1(new Date[10]); // 程序崩溃
shared_ptr<Date> sp1(new Date[10]); // 程序崩溃问题分析:
delete 释放资源
delete[] 释放
// unique_ptr和shared_ptr提供的数组特化版本
unique_ptr<Date[]> up1(new Date[5]); // 使用delete[]
shared_ptr<Date[]> sp1(new Date[5]); // 使用delete[]特点:
delete[]
shared_ptr 的数组特化需要C++17支持
// 方式1:函数模板
template<class T>
void DeleteArrayFunc(T* ptr) {
delete[] ptr;
}
// 方式2:仿函数模板
template<class T>
class DeleteArray {
public:
void operator()(T* ptr) {
delete[] ptr;
}
};
// 方式3:特定资源仿函数
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};// unique_ptr:模板参数指定删除器类型
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr:构造函数参数传递删除器实例
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
关键区别:
unique_ptr 的删除器是类型的一部分(模板参数)
shared_ptr 的删除器是运行时绑定(构造参数)
unique_ptr(无需构造时传递)
// unique_ptr:需指定函数指针类型
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
// shared_ptr:直接传递函数指针
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);注意事项:
unique_ptr 需要显式声明函数指针类型
shared_ptr 自动推导函数指针类型
// 定义lambda删除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
// unique_ptr:使用decltype推导lambda类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
// shared_ptr:直接传递lambda对象
shared_ptr<Date> sp4(new Date[5], delArrOBJ);Lambda优势:
// 使用仿函数管理文件
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
// 使用lambda管理文件
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});应用场景:
fclose)
closesocket)
disconnect)
特性 | unique_ptr | shared_ptr |
|---|---|---|
删除器位置 | 模板参数(编译时绑定) | 构造参数(运行时绑定) |
类型影响 | 影响智能指针类型 | 不影响智能指针类型 |
仿函数使用 | 可不传实例(直接使用默认构造) | 必须传递实例 |
函数指针使用 | 需显式指定指针类型 | 自动类型推导 |
Lambda使用 | 需用decltype推导类型 | 直接传递lambda对象 |
性能 | 无额外开销(可能编译期优化) | 有运行时开销(类型擦除) |
优先使用特化版本:
unique_ptr<Date[]> arr(new Date[5]); // 首选方案资源类型匹配原则:
工厂函数推荐:
auto sp = make_shared<Date>(...); // 单个对象
auto up = make_unique<Date[]>(5); // C++20起支持数组删除器实现选择:
unique_ptr删除器技巧:
// 使用类型别名简化复杂声明
using FilePtr = unique_ptr<FILE, decltype([](FILE* f){
fclose(f);
})>;
FilePtr fp(fopen("a.txt", "r"));由于Lambda 表达式本质上是一个匿名函数对象,没有显式类型,所以这里需要用到decltype来推导
decltype:
decltype 获取
decltype 中的 lambda 仅用于类型推导,不会被执行
以上示例完整展示了智能指针删除器的各种用法,并且强调了unique_ptr和shared_ptr在删除器实现上的重要差异,以及如何优雅地管理各种类型的资源。
shared_ptr支持两种构造方式:通过资源指针构造,或使用make_shared函数直接初始化资源对象。前者需要两次内存分配(对象和控制块),后者只需一次。
shared_ptr和unique_ptr都重载了operator bool,可将智能指针对象直接用于if条件判断:空指针返回false,非空返回true。
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = 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和unique_ptr的构造函数均使用explicit修饰,
// 防止普通指针隐式转换为智能指针对象。这意味着必须显式构造智能指针
//shared_ptr<Date> sp5 = new Date(2024, 9, 11);
//unique_ptr<Date> sp6 = new Date(2024, 9, 11);运行结果:

• 下面我们模拟实现了auto_ptr和unique_ptr的核心功能,这两个智能指针的实现相对简单,主要目的是帮助理解原理。auto_ptr的实现思路是在拷贝时将资源管理权转移给被拷贝对象,但这种设计存在缺陷,已不被推荐使用。unique_ptr则通过禁止拷贝来实现资源管理。
namespace RO
{
template<class T>
class auto_ptr
{
public:
auto_ptr() = default;
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr& ap) // 注意:非 const 引用,需要修改ap对象
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
if(_ptr) delete _ptr;
// 转移ap的资源
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}测试一下:
RO::auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象ap1悬空
RO::auto_ptr<Date> ap2(ap1);
RO::auto_ptr<Date> ap3;
ap3 = ap2;

可以看到没有问题,最后资源都转移给了ap3
namespace RO
{
template<class T>
class unique_ptr
{
public:
unique_ptr() = default;
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr& up) = delete;
unique_ptr& operator=(unique_ptr& up) = delete;
unique_ptr(unique_ptr&& up) noexcept
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr& operator=(unique_ptr&& up) noexcept
{
if (this != &up)
{
if (_ptr) delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
return *this;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}测试一下:
RO::unique_ptr<Date> up1(new Date);
RO::unique_ptr<Date> up2(new Date);
up1 = move(up1);
RO::unique_ptr<Date> up3(move(up2));

• shared_ptr的设计是重点研究对象,其核心在于引用计数机制的设计。需要注意以下几点:
shared_ptr 不使用成员变量或静态成员变量来存储引用计数,原因如下:
所有权不共享:如果引用计数是对象的成员变量(即每个对象自带一个计数),则:
class BadSharedPtr {
T* ptr;
int count; // 成员变量计数
};当多个智能指针指向同一个对象时,每个智能指针需要共享同一个计数器。但成员变量属于对象本身,不同智能指针无法共享同一个计数器(每个智能指针有自己的 count 副本)。
示例错误场景:
T* obj = new T;
BadSharedPtr p1(obj); // p1.count = 1
BadSharedPtr p2(obj); // p2.count = 1(独立计数,无法共享)当 p1 析构时,count 减为 0 会删除 obj,但 p2 仍指向已释放的内存(悬空指针)。
全局共享计数:如果引用计数是静态成员变量(所有智能指针共享一个计数器):
class BadSharedPtr {
T* ptr;
static int count; // 静态计数
};所有智能指针实例共享同一个计数器,导致:
BadSharedPtr p1(new T); // 静态 count = 1
BadSharedPtr p2(new T); // 静态 count = 2(错误!两个对象共享计数)当 p1 析构时,count 减为 1,但此时本应释放 p1 的对象,却因 count > 0 未释放。而 p2 析构时,count 减为 0 会错误地释放 p2 的对象两次(或释放未分配的内存)。
std::shared_ptr 的解决方案是:
独立控制块:在堆上动态分配一个控制块(包含引用计数、弱引用计数等)。
共享计数:所有指向同一对象的 shared_ptr 共享同一个控制块。
std::shared_ptr<T> p1(new T); // 创建控制块,计数=1
std::shared_ptr<T> p2 = p1; // 共享控制块,计数=2线程安全:控制块的引用计数操作是原子的,保证多线程安全。
方案 | 问题 | 后果 |
|---|---|---|
成员变量计数 | 不同智能指针无法共享同一对象的计数 | 重复释放或内存泄漏 |
静态成员计数 | 所有对象共享全局计数,无法区分不同对象 | 错误释放/未释放对象 |
动态控制块 | 每个对象独立控制块,同一对象的智能指针共享计数 | 安全且正确 |
std::shared_ptr 必须使用堆分配的控制块(而非成员/静态变量)来确保:
下面我们只是简单模拟实现shared_ptr,所以我们在堆上动态分配引用计数
namespace RO
{
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& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount);
}
shared_ptr& operator=(const shared_ptr& sp)
{
// 不同对象共享同一份资源,不能通过 this != &sp 来判断
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = _pcount = nullptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
shared_ptr(shared_ptr&& sp) noexcept
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(move(sp._del))
{
sp._ptr = nullptr;
sp._pcount = nullptr;
}
shared_ptr& operator=(shared_ptr&& sp) noexcept
{
// 检查自赋值
if (this != &sp) // 这里不能使用_ptr != sp._ptr来判断,会导致指向相同资源的对象之 间进行移动赋值时不能进入下面代码,导致移动后的对象未被悬空
{
if (_pcount && --(*_pcount) == 0) // 先判空避免对移动后的对象重新移动赋值,出现空指针解引用的问题
{
_del(_ptr);
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = move(sp._del);
sp._ptr = nullptr;
sp._pcount = nullptr;
}
return *this;
}
~shared_ptr()
{
// 检查计数指针是否为空
if (_pcount == nullptr)
{
return; // 已被移动过的对象,无需操作
}
if (--(*_pcount) == 0)
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
}_pcount 指针动态分配引用计数
std::function 包装器function<void(T*)> _del = [](T* ptr) { delete ptr; };这种设计提供了三大优势:
std::function<void(T*)> 类型
[](T* p) { delete[] p; }
[](FILE* f) { fclose(f); }
delete ptr 释放资源
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del) // 存储自定义删除器
{}这个模板构造函数的设计亮点:
D:自动推导删除器类型
void(T*)
测试一下:
RO::shared_ptr<Date> sp1(new Date);
// 支持拷贝
RO::shared_ptr<Date> sp2(sp1);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎
RO::shared_ptr<Date> sp4(move(sp1));
sp2 = move(sp4);
sp1 = move(sp2);
sp1 = move(sp1);
// 自定义文件删除器
//auto file_deleter = [](FILE* ptr) {
// cout << "fclose:" << ptr << endl;
// fclose(ptr);
// };
//// 使用自定义删除器
//FILE* fp = fopen("data.txt", "w");
//RO::shared_ptr<FILE> file_ptr(fp, file_deleter);
// 等价于上面
RO::shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
// 数组需要特殊删除器
auto array_deleter = [](int* p) { delete[] p; };
RO::shared_ptr<int> arr(new int[10], array_deleter);运行结果:


• shared_ptr在大多数情况下非常适合管理资源,既支持RAII机制,也支持拷贝操作。但在循环引用场景中会导致资源无法释放,造成内存泄漏。我们需要理解循环引用的产生原因,并掌握使用weak_ptr解决此类问题的方法。
• 如图所示场景中,当n1和n2析构后,两个节点的引用计数仅降至1:
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_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;
return 0;
}
• 这样就形成了逻辑上的循环依赖关系,导致双方都无法释放,最终造成内存泄漏。
运行结果:

并没有调用析构函数,造成内存泄漏。
• 解决方案是将ListNode结构体中的_next和_prev改为weak_ptr。weak_ptr在绑定到shared_ptr时不会增加引用计数,使得_next和_prev不再参与资源管理,从而有效打破循环引用。
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;
return 0;
}运行结果:

可以看到使用weak_ptr后,没有增加引用计数,最后也正确调用了2次析构函数
不过需要注意weak_ptr的用法
// weak_ptr不支持管理资源,不支持RAII
// weak_ptr是专门绑定shared_ptr,不增加他的引用计数,作为一些场景的辅助管理
std::weak_ptr<ListNode> wp(new ListNode);例如这样使用是会报错的

• weak_ptr不具备RAII特性,也无法直接访问资源。根据文档说明,weak_ptr在构造时只能绑定到shared_ptr对象,而不能直接绑定到资源。这种绑定方式不会增加shared_ptr的引用计数,从而有效解决了循环引用问题。
• weak_ptr没有重载operator*和operator->等操作符,因为它不参与资源管理。如果其绑定的shared_ptr已经释放资源,weak_ptr访问资源将存在安全隐患。weak_ptr提供expired()方法检查资源是否过期,use_count()方法获取shared_ptr的引用计数。当需要访问资源时,可以调用lock()方法返回一个管理该资源的shared_ptr对象:若资源已释放则返回空shared_ptr;若资源未释放,则通过返回的shared_ptr安全地访问资源。
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;
}运行结果:

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;
};需要注意的是我们这里实现的shared_ptr和weak_ptr都是以最简洁的方式实现的,只能满足基本的功能,这里的weak_ptr lock等功能是无法实现的,想要实现就要把shared_ptr和weak_ptr一起改了,把引用计数拿出来放到一个单独类型,shared_ptr和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
• shared_ptr的引用计数对象位于堆上。当多个线程同时对shared_ptr对象进行拷贝或析构操作时,会访问和修改同一个引用计数,从而产生线程安全问题。因此,shared_ptr的引用计数需要通过加锁或原子操作来保证线程安全性。
• shared_ptr管理的对象本身也存在线程安全问题,但这不属于shared_ptr的管理范畴,应由使用shared_ptr的上层代码负责控制对象的线程安全。
• 以下程序可能导致崩溃或资源泄漏(A资源未释放)。
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
RO::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数
RO::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;
}shared_ptr<AA> 对象 p 指向一个 AA 实例
t1 和 t2,每个线程执行:
p 的拷贝 copy
copy 指向对象的成员变量
_a1 和 _a2 的值(预期为 200,000)
use_count()(预期为 1)
问题主要出现在引用计数操作上:
// 拷贝构造函数中的非原子操作
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount); // 非原子操作,线程不安全!
}
// 析构函数中的非原子操作
~shared_ptr()
{
if (--(*_pcount) == 0) // 非原子操作,线程不安全!
{
// ...
}
}++(*_pcount) 或 --(*_pcount)
use_count() 输出可能不等于 1
AA 对象
_a1 和 _a2 的修改有互斥锁保护
运行此代码时,可能出现以下情况:
p->_a1 时访问已释放内存
_a1 和 _a2 小于 200,000(计数错误导致部分操作未执行)
运行结果:

可以通过以下方案解决:
namespace RO
{
template<class T>
class shared_ptr
{
private:
T* _ptr;
std::atomic<int>* _pcount; // 改为原子指针
function<void(T*)> _del = [](T* ptr) { delete ptr; };
public:
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new std::atomic<int>(1)) // 初始化原子计数器
{}
// 拷贝构造(使用原子操作)
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount); // 原子增加
}
// 析构函数(使用原子操作)
~shared_ptr()
{
if (_pcount == nullptr) return;
// 原子减少并获取结果
if (--(*_pcount) == 0) // 原子操作
{
if (_ptr)
{
_del(_ptr);
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
// 其他成员函数保持不变...
};
}使用 std::atomic<int>:
std::atomic<int>* _pcount;++ 和 -- 操作是线程安全的
正确的初始化:
_pcount(new std::atomic<int>(1))new 分配原子计数器
保持其他逻辑不变:
++(*_pcount) 和 --(*_pcount) 成为单指令操作
memory_order_seq_cst
std::shared_ptr 使用原子操作管理计数
• Boost库作为C++标准库的重要补充,是一个由全球C++开发者共同维护的开源项目。它最初由Beman Dawes于1998年发起,旨在为C++标准化工作提供实践参考。Boost库中超过80%的组件最终都被纳入了C++标准,其中智能指针的发展历程尤为典型。Dawes本人不仅是Boost社区的创始人,还担任C++标准委员会库工作组的负责人,这为Boost与标准C++的协同演进提供了制度保障。
• C++98标准首次尝试引入智能指针概念,提供了auto_ptr。但它在所有权转移时采用移动语义的设计存在严重缺陷:
std::auto_ptr<int> p1(new int(10));
std::auto_ptr<int> p2 = p1; // p1变为NULL• Boost库针对auto_ptr的缺陷进行了全面改进:
boost::shared_ptr<Resource> res1(new Resource);
boost::shared_ptr<Resource> res2 = res1; // 引用计数+1• 2005年的C++技术报告TR1(Technical Report 1)作为标准化的过渡方案,首次在标准文档中引入shared_ptr等组件。但需要注意的是:
• C++11正式将智能指针纳入标准:
std::unique_ptr<Object> obj(new Object);
std::unique_ptr<Object> obj2 = std::move(obj); // 合法所有权转移
// 自定义删除器
auto deleter = [](FILE* fp){ fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> file(fopen("data.txt","r"), deleter);标准库智能指针的实现直接参考了Boost的成熟设计,但做了以下优化:
定义:内存泄漏是指由于程序设计疏忽或错误导致程序无法释放已不再使用的内存。常见原因包括忘记释放内存或异常情况下未能执行释放操作。需要注意的是,内存泄漏并非物理内存的消失,而是应用程序在分配内存后,因设计缺陷失去对该内存的控制权,从而造成内存资源的浪费。
危害:对于短期运行的程序,内存泄漏的影响相对有限,因为进程终止时会自动解除内存映射关系并释放物理内存。然而,对于长期运行的程序(如操作系统、后台服务或持续运行的客户端),内存泄漏会导致严重后果:随着时间推移,可用内存不断减少,系统响应速度逐渐变慢,最终可能导致程序崩溃或系统卡死。
int main()
{
// 申请一个1G未释放,这个程序多次运行也没啥危害
// 因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}• Linux平台:Linux下几款C++程序中的内存泄露检查工具
• Windows平台:windows下的内存泄露检测工具VLD使用
• 规范开发流程:
• 资源管理建议:
• 定期检测:
总结: 内存泄漏常见解决方案分为两类: