1. 智能指针
1.1 智能指针的使用场景
来看以下场景:
cpp
double Divide(int a, int b)
{
// 当 b == 0时抛出异常
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;
}
如果上述代码发生除 0 错误抛出异常,另外下面的 array1 和 array2 没有得到释放。所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。但是如果 array2 new 的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是使用智能指针。
1.2 RAII
RAII 是 Resource Acquisition Is Initialization(资源获取即初始化 )的缩写,它是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。
RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
智能指针的基本设计,核心就是利用析构函数自动释放资源。
示例代码:
cpp
template <typename T>
class SmartPointer
{
public:
SmartPointer(T* ptr) : m_ptr(ptr) {}
~SmartPointer()
{
cout << "delete [] " << m_ptr << endl;
delete[] m_ptr;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
T& operator[](size_t index) { return m_ptr[index]; }
private:
T* m_ptr;
};
之前申请的资源,都需要手动释放。有了智能指针,可以将资源交给智能指针来管理。

通过调用SmartPointer的析构函数的来释放资源。因此之后申请资源都通过智能指针来申请:SmartPointer<int> sp = new int[10],将 p 传递给智能指针,智能指针将负责释放内存。
通过智能指针可以解决开头出现的问题:
cpp
double Divide(int a, int b)
{
// 当 b == 0时抛出异常
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]; // 抛异常呢
SmartPointer<int> array1(new int[10]);
SmartPointer<int> array2(new int[10]);
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
throw; // 异常重新抛出,捕获到什么抛出什么
}
}
使用智能指针来管理申请的资源。抛出异常,清理调用链上创建的对象,其中就包括 array1 和 array2,调用析构函数,释放自己。最终,才跳转到catch子句。无论是正常结束还是异常结束,都可以保障 new 的资源正常释放。
示例结果:

1.3 三种智能指针
智能指针核心的问题:所有权管理与拷贝语义。
cpp
SmartPointer<int> sp1 = new int[10];
SmartPointer<int> sp2 = new int[10];
SmartPointer<int> sp3(sp2); // 错误:智能指针不允许复制构造,同一份资源会释放两次
这里sp2和sp3指向同一份资源,智能指针模拟的是指针的行为,托管资源,资源并不属于它。使用一个智能指针去拷贝另一个智能指针,期望的是两个指针指向同一份资源,但核心难点在于谁来负责释放、何时释放。如何解决智能指针的拷贝问题,这就牵扯到了智能指针的不同设计策略。
C++标准库中的智能指针都在**<memory>** 这个头文件下面,我们包含<memory>就可以使用了。智能指针有好几种,除了weak_ptr之外,其它的都符合RAII且支持像指针一样访问资源 ,weak_ptr同样符合RAII但不直接提供资源访问(没有operator*/operator->),它只是一个观察者,需要lock()提升为shared_ptr才能访问。原理上而言,不同的智能指针主要是解决所有权共享与拷贝时的策略不同。
auto_ptr 是C++98时设计出来的智能指针,特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,原对象被置为null。这是一个非常糟糕的设计,因为它会将被拷贝对象悬空,访问报错的问题。强烈建议不要使用auto_ptr,其他C++11出来之前很多公司也是明令禁止使用这种智能指针的。
示例代码:
cpp
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // ap1 的所有权转移给 ap2,ap1 不再拥有 Date 对象
ap1->_year = 2024; // 错误:ap1 不再拥有 Date 对象,访问 ap1 是未定义行为
ap2->_year = 2025; // 正确:ap2 拥有 Date 对象,可以访问和修改它
报错显示:


unique_ptr是C++11设计出来的智能指针,它的名字翻译出来是唯一指针,它的特点的不支持拷贝(拷贝构造和拷贝赋值被delete) ,只支持移动(移动构造和移动赋值)。如果不需要拷贝的场景就非常建议使用它。
示例代码:
cpp
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up2(up1); // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up2 = up1; // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up3 = move(up1); // 将 up1 的所有权转移给 up3,up1 不再拥有 Date 对象
// up1->_year = 2024; // 错误:up1 不再拥有 Date 对象,访问 up1 是未定义行为
up3->_year = 2025; // 正确:up3 拥有 Date 对象,可以访问和修改它
示例结果:

shared_ptr 是C++11设计出来的智能指针,它的名字翻译出来是共享指针,它的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用它了。shared_ptr 内部通过引用计数管理资源,引用计数的增减是原子操作(线程安全),但shared_ptr管理的对象本身不是线程安全的。
示例代码:
cpp
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up2(up1); // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up2 = up1; // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up3 = move(up1); // 将 up1 的所有权转移给 up3,up1 不再拥有 Date 对象
// up1->_year = 2024; // 错误:up1 不再拥有 Date 对象,访问 up1 是未定义行为
up3->_year = 2025; // 正确:up3 拥有 Date 对象,可以访问和修改它
sp1,sp2,sp3都指向同一份资源,但是却只会析构一次:

这是因为智能指针的底层是用引用计数的方式实现的。当计数器为0时,才调用析构函数释放指向的资源。计数器的变量名为 use_count。

move 会导致 sp1 的资源被转移,导致sp1指针悬空。
auto_ptr,unique_ptr,shared_ptr 这三个智能指针都重载了 operator->,operator*,operator bool(检查指针是否为空)。它俩支持了operator bool ,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

shared_ptr 实现了 reset 接口,功能为:手动释放指向资源的shared_ptr指针。
示例代码:
cpp
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // sp1 和 sp2 共享 Date 对象的所有权
shared_ptr<Date> sp3 = sp1; // sp1 和 sp3 也共享 Date 对象的所有权
if (sp1) // 检查 sp1 是否为空
{
cout << "sp1.use_count() = " << sp1.use_count() << endl;
sp1.reset(); // sp1 释放对 Date 对象的所有权,sp2 和 sp3 仍然拥有 Date 对象
cout << "sp1.use_count() = " << sp1.use_count() << endl;
}
if (sp2) // 检查 sp2 是否为空
{
cout << "sp2.use_count() = " << sp2.use_count() << endl;
sp2.reset(); // sp2 释放对 Date 对象的所有权,sp3 仍然拥有 Date 对象
cout << "sp2.use_count() = " << sp2.use_count() << endl;
}
if (sp3) // 检查 sp3 是否为空
{
cout << "sp3.use_count() = " << sp3.use_count() << endl;
sp3.reset(); // sp3 释放对 Date 对象的所有权,Date 对象被销毁
cout << "sp3.use_count() = " << sp3.use_count() << endl;
}
示例结果:

get 接口 ------ 功能:获取底层的指针。
示例代码:
cpp
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // sp1 和 sp2 共享 Date 对象的所有权
shared_ptr<Date> sp3 = sp1; // sp1 和 sp3 也共享 Date 对象的所有权
cout << sp1.get() << endl; // 获取 sp1 底层的指针,输出 Date 对象的地址
cout << sp2.get() << endl; // 获取 sp2 底层的指针,输出 Date 对象的地址,与 sp1 相同
cout << sp3.get() << endl; // 获取 sp3 底层的指针,输出 Date 对象的地址,与 sp1 相同
示例结果:

shared_ptr 中有一种构造函数:别名构造函数。
cpp
int* p = new int(10);
shared_ptr<int> a(new int(20));
shared_ptr<int> b(a, p); // b 共享 a 的所有权,但管理一个不同的指针 p
只是存储指针为 p。b 不拥有 p,也不会管理其存储。相反,它共同拥有 a 的托管对象,并将其视为 a 的一个额外用途。
可以打印看看,a,b,p三者的地址:

b 的存储指针 (get()返回)是p的地址,但是b和a共享同一个控制块和引用计数,b通过别名指针p来访问资源。
可以看看a,b的引用计数:由此也可以看出 a,b 指向的是同一份资源。

weak_ptr 是 C++11 设计出来的智能指针,它的名字翻译出来是弱指针,它完全不同于上面的智能指针,它不拥有目标资源的所有权 (不增加引用计数),也就意味着不能直接用它来管理资源的释放。weak_ptr的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。weak_ptr本身符合RAII设计,其构造和析构会正确管理控制块中的弱引用计数。
智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。
智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。
因为new[ ]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[ ]的版本,使用时 unique_ptr<Date[ ]> up1(new Date[5]),shared_ptr<Date[ ]> sp1(new Date[5]) 就可以管理new[ ]的资源。

示例:

资源不一定是new出来的,在早期不支持上述的方式时,是通过定制删除器来解决的。在构造智能指针时,作为参数传递。


D del 就是删除器。
shared_ptr 的删除器是在构造时传递:
cpp
shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>()); // 使用 DeleteArray 作为自定义删除器
shared_ptr<Date> sp3(new Date[10], [](Date* p) { // 使用 lambda 表达式作为自定义删除器
cout << "delete [] " << p << endl;
delete[] p;
});
unique_ptr 的删除器是在模板参数处指定类型,构造函数处可以传入具体的删除器对象:
cpp
unique_ptr<Date[], DeleteArray<Date>> up2(new Date[10]); // unique_ptr 需要指定删除器类型
unique_ptr<Date[], void(*)(Date*)> up3(new Date[10], [](Date* p) {
cout << "delete [] " << p << endl;
delete[] p;
}); // unique_ptr 需要指定删除器类型
unique_ptr<Date, DeleteArray<Date>> up2(new Date[10]); // unique_ptr 需要指定删除器类型
unique_ptr<Date, void(*)(Date*)> up3(new Date[10], [](Date* p) {
cout << "delete [] " << p << endl;
delete[] p;
}); // unique_ptr 需要指定删除器类型
shared_ptr 除了支持用指向资源的指针构造,还支持直接构造。make_shared 与 make_pair 有些类似,但是它与make_pair 有所不同,make_pair 的原型:template <class T1, class T2> pair<T1,T2> make_pair (T1 x, T2 y)。使用模板构造一个对象。make_shared 的原型:template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args)。将参数 args 传递给其构造函数,并返回一个 shared_ptr<T> 类型的对象,该对象永远并存储指向它的指针。make_shared 的类型需要显式指定模板参数。
cpp
shared_ptr<Date> sp1(new Date(2026, 4, 17));
// 使用 make_shared 创建 shared_ptr, 效率更高, 代码更简洁
shared_ptr<Date> sp2 = make_shared<Date>(2026, 4, 17);
auto sp3 = make_shared<Date>(2026, 4, 17); // 使用 auto 自动推导类型, 代码更简洁
在后期的标准中增加了 make_unique,在C++11中还不存在。
shared_ptr 和 unique_ptr 的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。
cpp
// shared_ptr<Date> sp1 = new Date; // 不支持隐式转换,编译错误: 无法将 Date* 转为 shared_ptr<Date>
shared_ptr<Date> sp2(new Date);
总结:shared_ptr 和 unique_ptr 的区别
| 特性 | unique_ptr | shared_ptr |
|---|---|---|
| 所有权语义 | 独占所有权 | 共享所有权 |
| 拷贝语义 | 禁止拷贝,只允许移动 | 支持拷贝(引用计数+1),支持移动 |
| 内存开销 | 一个指针(零开销) | 两个指针 + 控制块 |
| 性能 | 无额外开销 | 引用计数原子操作有开销 |
| 删除器 | 模板参数,编译期确定 | 构造函数参数,运行时类型擦除 |
| 使用场景 | 明确资源唯一归属,默认首选 | 需要资源共享,注意循环引用 |
| 相互转换 | 可隐式转为shared_ptr | 不可转为unique_ptr |
1.4 智能指针的原理
auto_ptr 的核心原理:拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。
模拟实现 auto_ptr:
cpp
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr) : m_ptr(ptr) {} // 构造函数,接受一个原始指针
~auto_ptr() { delete m_ptr; } // 析构函数,释放资源
auto_ptr(auto_ptr& other) : m_ptr(other.m_ptr) { other.m_ptr = nullptr; } // 拷贝构造函数,转移所有权
auto_ptr& operator=(auto_ptr& other) // 赋值运算符,转移所有权
{
if (this != &other)
{
if(m_ptr) delete m_ptr; // 释放当前对象拥有的资源
m_ptr = other.m_ptr; // 转移所有权
other.m_ptr = nullptr; // 将其他对象的指针置空,防止重复删除
}
return *this;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
private:
T* m_ptr;
};
unique_ptr 的核心原理:不支持拷贝。
模拟实现 unique_ptr:
cpp
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr) : m_ptr(ptr) {} // 构造函数,接受一个原始指针
~unique_ptr() { delete m_ptr; } // 析构函数,释放资源
unique_ptr(const unique_ptr&) = delete; // 禁止拷贝构造
unique_ptr& operator=(const unique_ptr&) = delete; // 禁止赋值运算符
// 移动构造函数,转移所有权
unique_ptr(unique_ptr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
unique_ptr& operator=(unique_ptr&& other) noexcept // 移动赋值运算符,转移所有权
{
if (this != &other)
{
if (m_ptr) delete m_ptr; // 释放当前对象拥有的资源, 前提是当前对象必须拥有资源
m_ptr = other.m_ptr; // 转移所有权
other.m_ptr = nullptr; // 将其他对象的指针置空,防止重复删除
}
return *this;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
private:
T* m_ptr;
};
C++98不支持拷贝和赋值重载的方式为:只声明,不实现,声明成私有。
shared_ptr 需要重点学习,它是如何设计的,尤其是引用计数的设计。
主要这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式。每构造一个智能指针对象就需要来一份资源,就要new一个引用计数出来。
若是采用 static 静态成员变量,所有创建出来的智能指针对象使用的都是同一个计数器,这表明着这些的智能指针指向的都是同一份资源。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。
模拟实现 shared_ptr:
cpp
namespace AY
{
template <class T>
class shared_ptr
{
public:
// 构造函数,接受一个原始指针,并初始化引用计数为 1
shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new int(1)) {}
void release() // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
{
if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
{
cout << "delete " << m_ptr << endl;
delete m_ptr; // 删除资源,前提是 new 出来的对象
delete m_count; // 删除引用计数,前提是 new 出来的对象
}
}
// 析构函数,释放资源,如果引用计数为 0 则删除资源
~shared_ptr()
{
release(); // 调用 release 函数来释放资源
}
// 拷贝构造函数,增加引用计数
shared_ptr(const shared_ptr<T>& other) : m_ptr(other.m_ptr), m_count(other.m_count)
{
++(*m_count); // 增加引用计数
}
// 赋值运算符,先释放当前资源,再增加引用计数
shared_ptr<T>& operator=(const shared_ptr<T>& other)
{
// 自赋值检查:sp1 = sp1, sp1 = sp2
if (m_ptr != other.m_ptr) // 检查自赋值
{
release(); // 判断是否需要释放当前资源
m_ptr = other.m_ptr; // 复制指针和引用计数
m_count = other.m_count;
++(*m_count); // 增加引用计数
}
return *this;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
T& operator[](size_t index) { return m_ptr[index]; }
int use_count() const { return *m_count; } // 获取引用计数
private:
T* m_ptr; // 原始指针,指向共享的资源
int* m_count; // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
};
}
使用模拟实现的 shared_ptr。
示例代码:
cpp
AY::shared_ptr<Date> sp1(new Date(2026, 4, 17));
AY::shared_ptr<Date> sp2(sp1); // sp1 和 sp2 共享 Date 对象的所有权
cout << "sp1.use_count() = " << sp1.use_count() << endl; // 输出 2,sp1 和 sp2 共享 Date 对象的所有权
cout << "sp2.use_count() = " << sp2.use_count() << endl; // 输出 2,sp1 和 sp2 共享 Date 对象的所有权
AY::shared_ptr<Date> sp3(new Date); // sp3 拥有一个新的 Date 对象,与 sp1 和 sp2 不共享
sp1 = sp3; // sp1 释放对原来 Date 对象的所有权,增加对 sp3 的 Date 对象的所有权,sp2 仍然拥有原来 Date 对象的所有权
cout << "sp1.use_count() = " << sp1.use_count() << endl; // 输出 2,sp1 和 sp3 共享 Date 对象的所有权
cout << "sp2.use_count() = " << sp2.use_count() << endl; // 输出 1,sp2 仍然拥有原来 Date 对象的所有权
cout << "sp3.use_count() = " << sp3.use_count() << endl; // 输出 2,sp1 和 sp3 共享 Date 对象的所有权
示例结果:

实现定制删除器版本的 shared_ptr:
cpp
namespace AY
{
template <class T>
class shared_ptr
{
public:
// 构造函数,接受一个原始指针,并初始化引用计数为 1
shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new int(1)) {}
// 定制删除的构造函数,使用一个成员来保存删除器
template <class D>
shared_ptr(T* ptr, D del) : m_ptr(ptr), m_count(new int(1)), m_del(del) {}
void release() // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
{
if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
{
cout << "delete " << m_ptr << endl;
m_del(m_ptr); // 释放资源时,使用删除器去释放,调用operator()
delete m_count;
}
}
// 析构函数,释放资源,如果引用计数为 0 则删除资源
~shared_ptr()
{
release(); // 调用 release 函数来释放资源
}
// 拷贝构造函数,增加引用计数
shared_ptr(const shared_ptr<T>& other)
: m_ptr(other.m_ptr), m_count(other.m_count), m_del(other.m_del)
{
++(*m_count); // 增加引用计数
}
// 赋值运算符,先释放当前资源,再增加引用计数
shared_ptr<T>& operator=(const shared_ptr<T>& other)
{
// 自赋值检查:sp1 = sp1, sp1 = sp2
if (m_ptr != other.m_ptr) // 检查自赋值
{
release(); // 判断是否需要释放当前资源
m_ptr = other.m_ptr; // 复制指针和引用计数
m_count = other.m_count;
++(*m_count); // 增加引用计数
m_del = other.m_del; // 复制删除器
}
return *this;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
T& operator[](size_t index) { return m_ptr[index]; }
int use_count() const { return *m_count; } // 获取引用计数
private:
T* m_ptr; // 原始指针,指向共享的资源
int* m_count; // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
// 为了能够在这里也使用上模板参数 D,可以定义在类模板中
// 或者直接使用 std::function<void(T*)> 来保存删除器,这样就不需要模板参数 D 了
// 默认删除器,使用 lambda 表达式来定义一个默认的删除器,调用 delete 来释放资源
function<void(T*)> m_del = [](T* ptr) { delete ptr; };
};
}
示例代码及其结果:

shared_ptr 的循环引用的问题
shared_ptr 大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这种问题。weak_ptr 就是用来专门解决 shared_ptr 的循环引用问题。
循环引用场景:
cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode(int data = 0) : _data(data), _next(nullptr), _prev(nullptr) {}
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
/*ListNode* node1 = new ListNode{ 1, nullptr, nullptr };
ListNode* node2 = new ListNode{ 2, nullptr, nullptr };*/
// 既然如此,我们可以使用 shared_ptr 来管理 ListNode 对象的生命周期,避免内存泄漏
std::shared_ptr<ListNode> node1(new ListNode{ 1 });
std::shared_ptr<ListNode> node2(new ListNode{ 2 });
// 但是,如果我们让 node1 和 node2 互相持有对方的 shared_ptr,就会导致循环引用,最终导致内存泄漏
node1->_next = node2; // node1 持有 node2 的原始指针
node2->_prev = node1; // node2 持有 node1 的原始指针
return 0;
}
分析出现循环引用的原因,如下图所示:

n1和n2析构后,管理两个节点的引用计数减到1,
右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了
_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了
这就构成了循环引用,_next 和 _prev 释放前提都是对方释放,导致内存泄漏。
解决方案:把 ListNode 结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用。
改进后的代码:
cpp
int* p = new int(10);
shared_ptr<int> a(new int(10));
shared_ptr<int> b(a, p); // a 和 b 共享 int 对象的所有权,但 b 使用 p 作为底层指针
测试代码及其结果:

shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原⼦操作保证线程安全的。
shared_ptr 智能指针的引用计数操作是线程安全的 (原子操作),但shared_ptr对象本身的并发读写(多个线程同时操作同一个shared_ptr对象)需要外部同步。它所指向的资源不是线程安全的,这个对象的线程安全问题不归shared_ptr管,应该由外层使用 shared_ptr 的人进行线程安全的控制。
shared_ptr引用计数从 int* 改成 atomic* 就可以保证引用计数的线程安全问题 ,或者使用互斥锁加锁也可以。它是C++库中的。
改进后,模拟实现的 shared_ptr 如下所示:
cpp
template <class T>
class shared_ptr
{
public:
// 构造函数,接受一个原始指针,并初始化引用计数为 1
shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new atomic<int>(1)) {}
// 定制删除的构造函数,使用一个成员来保存删除器
template <class D>
shared_ptr(T* ptr, D del) : m_ptr(ptr), m_count(new atomic<int>(1)), m_del(del) {}
void release() // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
{
if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
{
cout << "delete " << m_ptr << endl;
m_del(m_ptr); // 释放资源时,使用删除器去释放,调用operator()
delete m_count;
}
}
// 析构函数,释放资源,如果引用计数为 0 则删除资源
~shared_ptr()
{
release(); // 调用 release 函数来释放资源
}
// 拷贝构造函数,增加引用计数
shared_ptr(const shared_ptr<T>& other)
: m_ptr(other.m_ptr), m_count(other.m_count), m_del(other.m_del)
{
++(*m_count); // 增加引用计数
}
// 赋值运算符,先释放当前资源,再增加引用计数
shared_ptr<T>& operator=(const shared_ptr<T>& other)
{
// 自赋值检查:sp1 = sp1, sp1 = sp2
if (m_ptr != other.m_ptr) // 检查自赋值
{
release(); // 判断是否需要释放当前资源
m_ptr = other.m_ptr; // 复制指针和引用计数
m_count = other.m_count;
++(*m_count); // 增加引用计数
m_del = other.m_del; // 复制删除器
}
return *this;
}
T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }
T& operator[](size_t index) { return m_ptr[index]; }
int use_count() const { return *m_count; } // 获取引用计数
private:
T* m_ptr; // 原始指针,指向共享的资源
atomic<int>* m_count; // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
// 为了能够在这里也使用上模板参数 D,可以定义在类模板中
// 或者直接使用 std::function<void(T*)> 来保存删除器,这样就不需要模板参数 D 了
// 默认删除器,使用 lambda 表达式来定义一个默认的删除器,调用 delete 来释放资源
function<void(T*)> m_del = [](T* ptr) { delete ptr; };
};
1.5 认识 weak_ptr
weak_ptr不拥有目标资源的所有权 ,也不直接支持访问资源(没有operator*和operator->),所以可以发现weak_ptr构造时不支持直接绑定到原始资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数。weak_ptr符合RAII设计,其构造和析构会正确管理控制块中的弱引用计数(weak_count)。

weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,那么如果它绑定的 shared_ptr 已经释放了资源,那么它去访问资源就是很危险的。
它只有以下接口:

weak_ptr 支持expired 检查指向的资源是否过期 ,use_count 也可获取shared_ptr的引用计数,weak_ptr 想访问资源时,可以调用 lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
示例代码:
cpp
std::shared_ptr<string> sp1(new string("string"));
std::shared_ptr<string> sp2(sp1); // sp1 和 sp2 共享 string 对象的所有权
std::weak_ptr<string> wp = sp1; // wp 是 sp1 的弱引用,不增加 string 对象的引用计数
cout << wp.use_count() << endl; // 输出 2,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl; // 输出 0,wp 没有过期,仍然可以访问 string 对象
// sp1 释放对原来 string 对象的所有权,增加对新 string 对象的所有权,sp2 仍然拥有原来 string 对象的所有权
sp1 = make_shared<string>("new string");
cout << wp.use_count() << endl; // 输出 1,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl; // 输出 0,wp 没有过期,仍然可以访问原来 string 对象
// sp2 释放对原来 string 对象的所有权,增加对新 string 对象的所有权
sp2 = make_shared<string>("another string");
cout << wp.use_count() << endl; // 输出 0,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl; // 输出 1,wp 已经过期,无法访问原来 string 对象
示例结果

weak_ptr 要访问资源,一定是 lock 出一个新的 shared_ptr 对象。

模拟实现 weak_ptr:
cpp
template <class T>
class weak_ptr
{
public:
weak_ptr() : m_ptr(nullptr) {}
weak_ptr(const shared_ptr<T>& sp) : m_ptr(sp.m_ptr) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
m_ptr = sp.m_ptr;
return *this;
}
private:
T* m_ptr; // 原始指针,指向共享的资源
};
1.6 总结
什么是RAII?
RAII是Resource Acquisition Is Initialization(资源获取即初始化)的缩写,是一种管理资源的类设计思想。
核心机制 :利用对象的生命周期来管理获取到的动态资源。资源在对象构造时获取并委托给对象,在对象析构时自动释放。
解决的问题:
- 避免资源泄漏(内存、文件句柄、网络连接、锁等)
- 异常安全问题:异常发生时栈展开,局部对象析构,资源自动释放
智能指针是RAII的典型应用:将原始指针封装为对象,析构时自动delete,无需手动释放。
auto_ptr,unique_ptr,shared_ptr 各自的特点。
| 特性 | auto_ptr (C++98) | unique_ptr (C++11) | shared_ptr (C++11) |
|---|---|---|---|
| 所有权语义 | 独占,但支持拷贝 | 独占 | 共享 |
| 拷贝语义 | 拷贝时转移所有权,原对象置null | 禁止拷贝 ,只允许移动 | 拷贝时引用计数+1,共享所有权 |
| 核心问题 | 转移所有权后原对象悬空,误访问导致UB | 安全,明确表达独占语义 | 循环引用问题 |
| 使用建议 | 已废弃,禁止使用 | 不需要共享时使用 | 需要共享所有权时使用 |
unique_ptr vs shared_ptr关键区别:
- unique_ptr:零开销,只有一个指针开销,明确资源归属
- shared_ptr:有引用计数开销(控制块),支持共享但需注意循环引用
shared_ptr 常见的问题以及怎么解决的。
引用计数设计
为什么不能是static成员**?所有对象共享同一个计数,无法区分不同资源** 。正确设计:堆上动态分配的控制块。
循环引用问题
场景:双向链表、树结构中的父子互相引用。
死锁逻辑:n1释放需要n2的_prev析构 → n2释放需要n1的_next析构 → 互相等待,无法释放。
解决原理:weak_ptr打破循环。
核心机制 :weak_ptr只观察,不拥有 ,绑定到shared_ptr时不增加引用计数。
2. 内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。最危险的一般是慢泄漏。
如何避免内存泄漏?内存泄漏非常常键,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
内存泄漏检测的原理:定义一个容器,申请资源时,将指针存在容器中;释放资源时,在容器中查找对应的指针,并删除。程序结束时,记录资源的各种属性,什么时候申请的等等,哪些资源还没有释放等等。