在 C++ 编程中,动态内存管理是绕不开的核心问题。手动使用new/delete管理内存时,一旦出现异常、逻辑疏漏或忘记释放,就极易引发内存泄漏。为了解决这一痛点,C++ 引入了智能指针,它基于 RAII 思想实现了资源的自动管理,让动态内存操作更安全、更简洁。本文将从使用场景、设计原理、标准库智能指针分类、循环引用问题等方面,全面解析智能指针的核心知识。
1. 智能指针的使用场景
我们为什么需要智能指针?手动管理动态内存时,异常场景下的资源释放是一大难点。先看一个典型的内存泄漏案例:
cpp
double Divide(int a, int b)
{
if (b == 0)
throw "Divide by zero condition!";
return (double)a / (double)b;
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 若此处new抛异常,array1无法释放
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...) //..可以在这里捕捉,但毕竟不方便
{
delete[] array1;
delete[] array2;
throw;
}
delete[] array1;
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;
}
这段代码中,new操作、Divide函数都可能抛出异常,一旦异常发生,后续的delete语句可能无法执行,导致内存泄漏。如果嵌套多层new,异常处理的逻辑会变得极其复杂。
而智能指针的出现,让我们无需手动处理异常场景下的资源释放 ------ 它会利用对象的生命周期自动管理内存,从根本上简化问题。
2. 智能指针的设计核心:RAII 思想
智能指针的底层实现依赖于RAII(Resource Acquisition Is Initialization),即资源获取即初始化。这是一种利用对象生命周期管理资源的设计思想,核心逻辑为:
1. 获取资源: 在对象构造时,获取动态资源(如内存、文件句柄)并将其委托给对象管理;
2. 持有资源 :在对象的生命周期内,资源始终有效,可通过对象访问资源;
**3. 释放资源:**在对象析构时,自动释放持有的资源,无需手动调用delete。
除了遵循 RAII,智能指针还需要模拟普通指针的行为,因此会重载operator*、operator->、operator[]等运算符。以下是一个简易智能指针的实现示例:
cpp
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;
};
使用这个简易智能指针重构上述Func函数,代码会变得简洁且安全:
cpp
void Func()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 函数结束时,sp1、sp2自动析构,释放内存
}
这里当new操作或Divide函数出现异常时,会进行栈展开,沿函数调用链向上查找匹配的catch块,沿调用链创建的局部对象sp1、sp2会在栈展开时被析构,避免内存泄漏,这是我们智能指针基于RAII的核心思想,如想详细了解请移步上一篇文章:深入理解 C++ 异常:从概念到实战的全面解析-CSDN博客
3. C++标准库中的智能指针
C++ 标准库在<memory>头文件中提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr(其中weak_ptr不遵循 RAII,仅作为辅助工具)。它们的核心区别在于资源拷贝与管理的逻辑。
3.1 被淘汰的auto_ptr
auto_ptr是 C++98 推出的首个智能指针,其设计存在严重缺陷:拷贝时会转移资源的管理权,导致原对象悬空。
示例:
cpp
struct Date { int _year; };
int main()
{
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // 拷贝后,ap1的资源管理权转移给ap2
// ap1->_year++; // 错误:ap1已悬空,访问空指针
return 0;
}
由于这种设计极易引发空指针访问错误,我们强烈建议不要使用auto_ptr。
3.2 独占式智能指针unique_ptr
unique_ptr是 C++11 推出的独占式智能指针,核心特点是禁止拷贝,仅支持移动,确保同一资源只能被一个unique_ptr管理。
1. 禁止拷贝: 通过将拷贝构造函数和赋值运算符重载声明为delete,杜绝拷贝行为;
2. 支持移动: 通过移动构造和移动赋值,将资源管理权转移给新对象,原对象变为空;
**3. 数组特化:**针对new[]分配的数组,unique_ptr提供了特化版本,析构时自动调用delete[]。
示例:
cpp
struct Date { int _year; };
int main()
{
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 错误:禁止拷贝
unique_ptr<Date> up3(move(up1)); // 移动构造,up1悬空
unique_ptr<Date[]> up4(new Date[5]); // 数组特化版本,析构调用delete[]
return 0;
}
3.3 共享式智能指针shared_ptr
shared_ptr是 C++11 推出的共享式智能指针,支持拷贝和移动,底层通过引用计数实现资源的共享管理:
1. 引用计数: 每一份资源对应一个引用计数,记录当前管理该资源的shared_ptr数量;
2. 拷贝行为: 拷贝shared_ptr时,引用计数加 1;
3. 析构行为: shared_ptr析构时,引用计数减 1;若计数为 0,说明是最后一个管理资源的对象,释放资源;
**4. 数组特化与删除器:**同样提供数组特化版本,也支持自定义删除器(用于释放非new分配的资源,如文件指针,我们会在下面进行讲解)。
**注意:**这里如果不太清楚的话,请先跳到下面的底层原理部分,看完那里后再回来会有更好的体会。
cpp
struct Date
{
int _year, _month, _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;
}
int main()
{
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 拷贝,引用计数变为2,sp1、sp2管理同一份资源
cout << sp1.use_count() << endl; // 输出:2
sp1->_year++;
cout << sp2->_year << endl; // 输出:2,共享资源修改同步
shared_ptr<Date[]> sp3(new Date[2]); // 数组特化版本
shared_ptr<Date> sp4(new Date[2], DeleteArrayFunc<Date>); // 自定义删除器
cout << endl;
return 0;
}
输出结果:

make_shared:更安全的构造方式
shared_ptr还提供了make_shared函数,直接通过参数构造资源对象,相比直接用new构造更安全(避免new抛异常导致的内存泄漏),且效率更高:
cpp
// 推荐方式:make_shared直接构造对象
shared_ptr<Date> sp1 = make_shared<Date>(2024, 9, 11);
// 不推荐:new可能抛异常,导致计数内存泄漏
shared_ptr<Date> sp2(new Date(2024, 9, 11));
3.4 辅助型智能指针weak_ptr
weak_ptr是 C++11 推出的辅助工具,不遵循 RAII,无法直接管理资源,其核心作用是解决shared_ptr的循环引用问题。
(1)shared_ptr的循环引用问题
当两个shared_ptr互相引用时,会形成循环引用,导致引用计数无法归 0,资源无法释放,引发内存泄漏。例如双向链表节点的场景:
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next; // 共享下一个节点
shared_ptr<ListNode> _prev; // 共享上一个节点
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2的引用计数变为2
n2->_prev = n1; // n1的引用计数变为2
// 函数结束时,n1、n2析构,引用计数各减1(变为1)
// 循环引用导致计数无法归0,~ListNode()不会被调用,内存泄漏
return 0;
}
输出结果:



(2)weak_ptr解决循环引用
weak_ptr的特点是:
1. 不增加引用计数: 绑定到shared_ptr时,不会改变其引用计数;
2. 不参与资源管理: 析构时不会释放资源,仅作为 "观察者";
**3. 安全访问资源:**通过lock()函数获取shared_ptr,若资源已释放则返回空对象;通过expired()函数检查资源是否过期。
修改上述链表节点代码,用weak_ptr替代shared_ptr存储节点引用:
cpp
struct ListNode
{
int _data;
weak_ptr<ListNode> _next; // 改为weak_ptr
weak_ptr<ListNode> _prev; // 改为weak_ptr
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // 不增加n2的引用计数
n2->_prev = n1; // 不增加n1的引用计数
// 函数结束时,n1、n2析构,引用计数归0,资源正常释放
return 0;
}
输出结果:

3.5 补充知识点
• shared_ptr和unique_ptr都支持operator bool的类型转换。如果智能指针对象是一个空对象,即没有管理资源,则返回false;否则返回true。这意味着我们可以直接将智能指针对象用于if语句中,以判断其是否为空。
• shared_ptr 和 unique_ptr 的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。
cpp
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 << "sp is not nullptr" << endl;
if (!sp4)
cout << "sp is nullptr" << endl;
// 报错 智能指针的构造函数被 explicit 修饰,禁止隐式类型转换,只能直接初始化构造
shared_ptr<Date> sp5 = new Date(2024, 9, 11); // 错误
unique_ptr<Date> sp6 = new Date(2024, 9, 11); // 错误
//shared_ptr<Date> sp5(new Date(2024, 9, 11)); 正确
return 0;
}
4. 删除器
4.1 删除器概念
智能指针析构时默认是进行delete释放资源,这也就意味着,如果不是new出来的资源交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器,本质就是一个可调用对象(如函数指针、仿函数、Lambda 表达式),在这个可调用对象中实现你想要的释放资源的方式。当构造智能指针时,若给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。
**•**因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本。使用时,unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);就可以管理new[]的资源。
unique_ptr和shared_ptr支持删除器的方式不同:
• unique_ptr: 通过类模板参数指定删除器类型;
**• shared_ptr:**通过构造函数参数传递删除器对象。
4.2 删除器实例
(1)函数指针作为删除器
cpp
// 函数指针:释放数组
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
// unique_ptr需显式指定函数指针类型
unique_ptr<Date, void(*)(Date*)> up1(new Date[5], DeleteArrayFunc<Date>);
// shared_ptr自动推导函数指针类型
shared_ptr<Date> sp1(new Date[5], DeleteArrayFunc<Date>);
return 0;
}
(2)仿函数作为删除器
cpp
// 仿函数:释放数组
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通过类模板参数指定仿函数类型
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr在构造函数传递仿函数对象
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 管理文件指针
shared_ptr<FILE> sp3(fopen("test.txt", "w"), Fclose());
return 0;
}
(3)Lambda作为删除器
cpp
int main()
{
// unique_ptr需用decltype推导Lambda类型,不仅要在类模板参数传,还要在构造函数传
auto delLambda = [](Date* ptr)
{
delete[] ptr;
cout << "Deleted array with Lambda" << endl;
};
unique_ptr<Date, decltype(delLambda)> up3(new Date[5], delLambda);
// shared_ptr直接在构造函数传递Lambda
shared_ptr<Date> sp4(new Date[5], [](Date* ptr)
{
delete[] ptr;
cout << "Deleted array with Lambda" << endl;
});
return 0;
}
4.3 推荐使用场景与区别
推荐使用场景:
unique_ptr相对建议使用仿函数作为删除器(Lambda类型考虑的比较多)。
shared_ptr相对建议使用Lambda作为删除器。
核心区别:
语法形式: Lambda 最简洁,仿函数次之,函数指针最繁琐。
可复用性: 仿函数 > 函数指针 > Lambda。
存储开销: 无状态仿函数(unique_ptr)< Lambda = 函数指针(shared_ptr 有额外开销)。
**使用灵活性:**Lambda 支持捕获上下文(尽管删除器一般用不到),仿函数和函数指针不支持。
5. 智能指针的原理
接下来我们模拟实现几个智能指针的核心功能,其中auto_ptr和unique_ptr这两个智能指针的实现比较简单,我们了解一下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝。而shared_ptr是需要我们重点了解的。
5.1 auto_ptr
auto_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)
{
// 1. 检测是否为自赋值,避免自身资源被释放
if (this != &ap)
{
// 2. 释放当前对象持有的资源
if (_ptr)
delete _ptr;
// 3. 转移源对象的资源所有权
_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; // 唯一成员变量:管理的资源指针
};
5.2 unique_ptr
unique_ptr通过禁止拷贝、支持移动实现独占式资源管理:
cpp
template<class T>
class unique_ptr
{
public:
// 构造函数:explicit防止隐式类型转换,避免意外构造
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;
}
// 关键:禁用拷贝构造函数(=delete),从根源杜绝拷贝
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; // 源对象置空
return *this;
}
private:
T* _ptr; // 管理的资源指针
};
5.3 shared_ptr(重点)
我们要重点看看shared_ptr是如何设计的,尤其是引用计数的设计。主要是这里一份资源就需要一个引用计数,所以采用静态成员的方式是无法实现引用计数的,要使用堆上动态开辟的方式。构造智能指针对象时,针对一份资源,就要new一个引用计数出来。多个shared_ptr指向资源时,就对引用计数++;shared_ptr对象析构时,就对引用计数--。当引用计数减到0时,代表当前析构的shared_ptr是最后一个管理资源的对象,此时则析构资源。

cpp
template<class T>
class shared_ptr
{
public:
// 默认构造函数:初始化资源指针和引用计数
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr) // 管理的资源指针
, _pcount(new int(1)) // 引用计数初始化为1(自身持有)
{}
// 带自定义删除器的构造函数:支持自定义资源释放逻辑
template < class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1)) // 引用计数初始化为1
, _del(del) // 存储自定义删除器
{}
// 拷贝构造:共享资源,引用计数+1
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
++(*_pcount); // 引用计数自增,表示新增一个管理者
}
// 核心:释放资源的逻辑封装
void release()
{
// 引用计数-1后检查是否为0
if (--(*_pcount) == 0)
{
// 最后一个管理者:释放资源和引用计数空间
_del(_ptr); // 使用删除器释放资源
delete _pcount; // 释放引用计数的内存
_ptr = nullptr; // 置空防止野指针
_pcount = nullptr;
}
}
// 赋值运算符重载:先释放当前资源,再共享新资源
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 避免自赋值
// if (this != &sp) 这里这样也不算错,但还是下面的更优
if (_ptr != sp._ptr)
{
release(); // 释放当前对象持有的旧资源
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
// 析构函数:调用release释放资源
~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; // 引用计数指针(多个shared_ptr共享)
// atomic<int>* _pcount; // 线程安全版本需用原子类型
// 默认删除器:lambda表达式,默认用delete释放资源,这里一定要加默认的,不然编不过
//lambda没有类型是个难题,但我们可以用function接收,这样就完美解决了
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
核心机制:
• _pcount是指向引用计数的指针,所有共享同一资源的shared_ptr共用该计数;
• 拷贝 / 赋值时引用计数 + 1,析构时引用计数 - 1;
• 引用计数为 0 时,才真正释放资源(避免提前释放或内存泄漏);
• 支持自定义删除器,灵活处理不同类型资源(如数组、文件句柄)。
在不需要拷贝的场景,我们还是推荐使用unique_ptr,因为在上面的实现中我们可以看到,引用计数还是占了一定资源的,此时使用unique_ptr更优。
5.4 weak_ptr
weak_ptr是辅助智能指针,不管理资源,仅观察shared_ptr的资源状态,用于解决循环引用问题。
cpp
template<class T>
class weak_ptr
{
public:
// 默认构造函数:初始化为空
weak_ptr()
{}
// 构造函数:从shared_ptr构造,不增加引用计数
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get()) // 仅获取资源指针,不影响引用计数
{}
// 赋值运算符重载:从shared_ptr赋值
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都要存储指向这个类的对象才能实现。
结语
好好学习,天天向上!有任何问题请指正,谢谢观看!
