本篇文章主要讲解 C++ 中的智能指针的使用及其原理
目录
[1 什么是智能指针](#1 什么是智能指针)
[2 智能指针的优点](#2 智能指针的优点)
[3 智能指针的使用](#3 智能指针的使用)
[3.1 库中提供的智能指针](#3.1 库中提供的智能指针)
[3.2 定制删除器](#3.2 定制删除器)
[3.3 智能指针的其他特性](#3.3 智能指针的其他特性)
[4 智能指针的原理](#4 智能指针的原理)
[5 weak_ptr](#5 weak_ptr)
[5.1 shared_ptr 循环引用问题](#5.1 shared_ptr 循环引用问题)
[5.2 weak_ptr 解决循环引用问题](#5.2 weak_ptr 解决循环引用问题)
1 什么是智能指针
智能指针是RAII设计思想的一种具体实现。RAII 是 Resource Acquisition Is Initialization 的缩写,是一种管理资源的类的设计思想,本质上就是将动态获取到的资源交给类对象来进行管理,避免资源泄露。这里的资源可以是内存指针、网络连接、文件指针、互斥锁等等。将资源交给一个对象管理之后,在对象的生命周期内部我们可以正常使用动态开辟的资源,出了对象的生命周期,对象就会调用析构函数释放资源,这样就避免了内存泄露问题。
智能指针就是借用了 RAII 的设计思想,实现了一个具体的类来管理动态开辟的内存资源。但是只管理资源是不够的,用户还是想要通过智能指针来访问资源,所以在智能指针中还必须实现 operator*、operator->、operator\[\] 等接口。
以下是一个智能指针的简单实现:
cpp
template<class T>
class SmartPtr
{
public:
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;
};
2 智能指针的优点
看下面这个动态申请资源的场景:
cpp
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <cstring>
using namespace std;
class Logger
{
public:
Logger(const char* filename)
{
//C++ 打开文件
fout.open(filename);
if (!fout.is_open())
{
throw runtime_error("日志文件打开失败");
}
cout << "Logger 构造:日志文件打开成功" << endl;
}
void Write(const char* msg)
{
fout << msg << endl;
}
~Logger()
{
cout << "Logger 析构:关闭日志文件" << endl;
if (fout.is_open())
{
fout.close();
}
}
private:
ofstream fout;
};
void ProcessData()
{
int* data = new int[100];
cout << "data 数组申请成功" << endl;
char* buffer = new char[256];
cout << "buffer 缓冲区申请成功" << endl;
Logger* logger = new Logger("log.txt");
cout << "logger 对象创建成功" << endl;
strcpy(buffer, "开始处理数据");
logger->Write(buffer);
//假设这里中间发生了异常
throw runtime_error("处理中途发生异常");
//后面的代码都不会执行,资源不释放,造成了资源泄露
delete[] data;
delete[] buffer;
delete logger;
cout << "资源释放完成" << endl;
}
int main()
{
try
{
ProcessData();
}
catch (const exception& e)
{
cout << "捕获异常:" << e.what() << endl;
}
return 0;
}

可以看到上面的代码,在 ProcessData 函数中申请了资源,但是由于中间执行过程中出错,抛出了异常,所以后续释放资源的代码没有执行,而且该资源是在函数内部申请的,后续再也无法拿到指向这些资源的指针,这些资源就永久资源泄露了。
那么如果我们将这些资源交给智能指针管理,就不会发生上述资源泄露的问题:
cpp
#include <iostream>
#include <string>
#include <fstream>
#include <stdexcept>
#include <cstring>
#include <functional>
using namespace std;
template<class T>
class SmartPtr
{
using del_t = function<void(T* ptr)>;
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
SmartPtr(T* ptr, del_t del)
:_ptr(ptr)
,_del(del)
{}
~SmartPtr()
{
//析构时自动释放资源
cout << "delete[] ptr" << endl;
_del(_ptr);
}
//重载对应过的访问运算符
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
T* Ptr()
{
return _ptr;
}
private:
T* _ptr;
del_t _del = [](T* ptr) { delete ptr; };
};
class Logger
{
public:
Logger(const char* filename)
{
//C++ 打开文件
fout.open(filename);
if (!fout.is_open())
{
throw runtime_error("日志文件打开失败");
}
cout << "Logger 构造:日志文件打开成功" << endl;
}
void Write(const char* msg)
{
fout << msg << endl;
}
~Logger()
{
cout << "Logger 析构:关闭日志文件" << endl;
if (fout.is_open())
{
fout.close();
}
}
private:
ofstream fout;
};
void ProcessData()
{
SmartPtr<int> sp1 = { new int[100], [](int* ptr) { delete[] ptr; } };
cout << "data 数组申请成功" << endl;
SmartPtr<char> sp2 = { new char[256], [](char* ptr) { delete[] ptr; } };
cout << "buffer 缓冲区申请成功" << endl;
SmartPtr<Logger> logger = new Logger("log.txt");
cout << "logger 对象创建成功" << endl;
strcpy(sp2.Ptr(), "开始处理数据");
logger->Write(sp2.Ptr());
//假设这里中间发生了异常
throw runtime_error("处理中途发生异常");
//不再需要资源释放代码了
cout << "资源释放完成" << endl;
}
int main()
{
try
{
ProcessData();
}
catch (const exception& e)
{
cout << "捕获异常:" << e.what() << endl;
}
return 0;
}

在智能指针中,我新添加了一个定制删除器的功能,就是可以在智能指针构造时传入一个 function<void (T* ptr)> 可以封装的可调用对象,这样就可以实现传入 delete 与 delete\[\] 了,而不是限制写死 delete 了。这里我们也可以看到包装器和 lambda 匿名函数的价值。
所以比起手动 delete 或者 delete\[\] 释放资源,智能指针会更加安全,即使出现一些不可预知的情况,智能指针的使用也能保证资源的正确释放,不至于造成内存泄露情况。
3 智能指针的使用
3.1 库中提供的智能指针
在 C++98 及 C++11 中一共支持 4 种常用的智能指针:




这几个智能指针都在 memory头文件下,之所以有这么多智能指针,还是为了解决各自的问题而产生的。
auto_ptr
auto_ptr 是 C++98 就存在的智能指针,auto_ptr 的特点就是将被拷贝对象的资源管理权转移给拷贝对象,这样就会导致被拷贝对象被悬空,此时再访问被拷贝对象就会报错:
cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
auto_ptr<int> ap1(new int(10));
cout << *ap1 << endl;
auto_ptr<int> ap2 = ap1;
cout << *ap2 << endl;
//ap1 被悬空,再次访问会报错
//cout << *ap1 << endl;
return 0;
}
我们可以调试一下看看:

可以看到将 ap1 拷贝给 ap2 之后,ap1 的指针就为空了,所以说 auto_ptr 拷贝之后会将被拷贝对象置空,再访问就会出错。
正是由于 auto_ptr 会出现上述的拷贝问题,所以不建议使用 auto_ptr 智能指针。
unique_ptr
unique_ptr 是 C++11 新添加的智能指针,从其名字看来,该智能指针为唯一指针。unique_ptr 的特性就是其支持移动,但是不支持拷贝,其将拷贝构造函数设为了 delete 函数。如果智能指针不需要拷贝,那就推荐使用 unique_ptr。
cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> up1(new int(10));
cout << *up1 << endl;
//无法进行拷贝,拷贝构造函数直接被 delete 掉了
//unique_ptr<int> up2 = up1;
//但是可以被移动
unique_ptr<int> up2 = move(up1);
cout << *up2 << endl;
return 0;
}

移动之后,原对象就会被置空。
shared_ptr
shared_ptr 也是 C++11 新添加的智能指针,其中文名称为共享指针。其特点就是既支持拷贝,又支持移动,其拷贝是使用引用计数的方式实现的。如果智能指针对象需要被拷贝,那就推荐使用 shared_ptr。
cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1(new int(10));
cout << *sp1 << endl;
// shared_ptr 支持拷贝
shared_ptr<int> sp2 = sp1;
//也支持移动
shared_ptr<int> sp3 = move(sp1);
cout << *sp3 << endl;
return 0;
}


可以看到 shared_ptr 移动并不是将所有拷贝与被拷贝对象都置空,而只是将被移动对象的指针置空。
weak_ptr
weak_ptr 是 C++11 提供的另一种指针类型,但是其不是智能指针,不是按照 RAII 设计思想设计出来的。所以并不能直接用 weak_ptr 来管理动态资源,其就是为了解决 shared_ptr 循环引用问题而存在的,至于 shared_ptr 的循环引用问题会在 weak_ptr 中进行讲解。
3.2 定制删除器
- unique_ptr 与 shared_ptr 析构函数默认都是使用 delete 释放资源,所以如果你开辟资源时,如果不是使用的 new,而是 new\[\],那就可能会出错。所以 unique_ptr 与 shared_ptr 支持我们传入定制删除器,实现定制化的析构逻辑。
- 定制删除器就是一个具体的可调用对象 。shared_ptr 与 unique_ptr 传入定制删除器的位置不同,shared_ptr 是在构造时传入具体的对象,unique_ptr 是在模板参数中传入具体的类型。
- 由于 new\[\] 特别常用,所以 unique_ptr 与 shared_ptr 都支持了一份特化的版本,只要使用 T\[\] 作为模板参数,就可以管理 new\[\] new 出来的资源,比如:
cpp
std::unique_ptr<std::string[]> up(new std::string[10]);
std::shared_ptr<std::string[]> sp(new std::string[10]);
cpp
#include <iostream>
#include <memory>
#include <string>
struct Date
{
int day;
Date(int d) : day(d) { std::cout << "Date(" << day << ") 构造\n"; }
~Date() { std::cout << "Date(" << day << ") 析构\n"; }
};
// 自定义删除器示例:适用于单个对象
auto singleDateDeleter = [](Date* p) {
std::cout << "自定义删除单个Date\n";
delete p; // 必须对应 new
};
// 自定义删除器示例:适用于数组
auto arrayDateDeleter = [](Date* p) {
std::cout << "自定义删除 Date 数组\n";
delete[] p; // 必须对应 new[]
};
int main()
{
std::cout << "--- unique_ptr 单个对象 ---\n";
{
//可以使用 decltype 推导出匿名函数的类型
std::unique_ptr<Date, decltype(singleDateDeleter)> up1(new Date(1), singleDateDeleter);
std::cout << "unique_ptr up1 使用中\n";
}
std::cout << "\n--- unique_ptr 数组 ---\n";
{
std::unique_ptr<Date[], decltype(arrayDateDeleter)> up2(new Date[3]{ Date(2), Date(3), Date(4) }, arrayDateDeleter);
std::cout << "unique_ptr 数组 up2 使用中\n";
}
std::cout << "\n--- shared_ptr 单个对象 ---\n";
{
std::shared_ptr<Date> sp1(new Date(5), singleDateDeleter);
std::cout << "shared_ptr sp1 使用中\n";
}
std::cout << "\n--- shared_ptr 数组 ---\n";
{
std::shared_ptr<Date> sp2(new Date[2]{ Date(6), Date(7) }, arrayDateDeleter);
std::cout << "shared_ptr 数组 sp2 使用中\n";
}
return 0;
}
3.3 智能指针的其他特性
- 我们不仅可以使用 new 或者 new\[\] 来构造 shared_ptr 或者 unique_ptr 对象,C++ 也支持使用 make_shared(C++11) 与 make_unique(C++14) 来构造一个 shared_ptr 或者 unique_ptr 对象:

cpp
#include <iostream>
#include <memory>
#include <string>
using namespace std;
int main()
{
//使用 make_shared 创建 sp 对象
shared_ptr<string> sp1 = make_shared<string>("11111");
//make_shared 无法创建 new[] 对象
//shared_ptr<string[]> sp2 = make_shared<string[]>(10);
//使用 make_uniqe 创建 up1 对象
unique_ptr<string> up1 = make_unique<string>("222222");
//使用 make_unique 创建 new[] 对象
unique_ptr<string[]> up2 = make_unique<string[]>(10);
return 0;
}
- shared_ptr 与 unique_ptr 都实现了 operator bool,也就是可以支持逻辑判断,当 shared_ptr 与 unique_ptr 为空时,都返回的是 false,不为空就返回 true。


- shared_ptr 与 unique_ptr 中构造函数都添加了 explicit 关键字,不允许普通指针隐式类型转换为智能指针对象。


4 智能指针的原理
这里我们主要实现 shared_ptr,至于 auto_ptr 和 unique_ptr 在实现了 shared_ptr 之后就很好实现了。shared_ptr 的特点就是既支持拷贝也支持移动,那么底层是如何实现的呢?其实底层是通过一个引用计数实现的,拷贝改变的引用计数,而移动则是转移资源管理权。开始状态为 sp1 与 sp2 管理同一块资源,sp3 自己管理一块资源,此时 sp1 与 sp2 的引用计数为 2,sp3 引用计数为1:

然后将 sp3 拷贝赋值给 sp1,首先 sp1 会先将引用计数--,如果引用计数减到0,那么就是释放 sp1 管理的资源;然后将 sp3 的指针以及引用计数赋值给 sp1,并将 sp1 与 sp3 的引用计数++:

之后将 sp1 移动赋值给 sp3,sp3 会先先处理自己的资源,将引用计数--,如果引用计数变为0,那么就将资源释放;然后直接将 sp1 的资源移动给 sp3,再将 sp1 置空:

所以 shared_ptr 的核心实现就在于引用计数如何实现,要想实现引用计数,我们就要想想引用计数有哪些特点。引用计数最核心的特点就是指向一块资源的对象之间共享,那么要使用静态成员变量吗?但是静态成员变量是所有成员之间共享,引用计数需要一份资源就有一个引用计数,所有成员共享就意味着所有成员的引用计数都是相同的,所以这里是不行的。这里我们选择使用 int* 指针变量来作为引用计数,第一次申请时就开辟一块 int 的堆空间;析构时也别忘记释放这块堆空间就可以了。
这里我们主要讲解拷贝构造、拷贝复制、移动构造、移动赋值、定制删除器的实现,其他函数比较简单,就不讲解了,可以看下面的具体实现代码。
拷贝构造
既然有了引用计数的具体实现方案,那么拷贝构造其实就很简单了。假设是 sp1 = sp3,只需要将 sp3 内部的 _ptr、_pcount(开辟引用计数空间的指针)赋值给 sp1 就可以了。最后也是最重要的一步,别忘记让引用计数++。
拷贝赋值
拷贝构造的时候我们首先要避免自己给自己赋值,那么这里要采用 this != &sp 吗?在智能指针这里是不行的,因为虽然两个智能指针对象的地址不同,但是其管理的资源却是相同的,此时也认为他们是同一个对象,所以这里的核心是判断管理的资源是否是同一个,所以应该采用 _ptr != sp._ptr。
这里我们假设是 sp1 = sp3,这个语句的意思是让 sp1 与 sp3 共同管理一块资源,所以我们需要先对 sp1 管理的资源进行处理,也就是让 sp1 的引用计数 --,如果为0,那就释放资源;之后再将 sp3 的 _ptr、_pcount 赋值给 sp1,再将引用计数++即可。
移动构造
移动构造也很简单,只需要转移资源管理权,也就是 swap 底层资源的 _ptr 与 _pcount 就可以。
移动赋值
对于移动赋值,我们不需要判断是否指向了同一块资源,因为不管是否指向同一块资源,最后都会将被移动对象的资源管理权交给移动对象,将被移动对象的 _ptr 与 _pcount 置空。假设是 sp3 = move(sp1),只需要先对 sp3 的资源进行管理,就是引用计数--,如果减到了0就释放资源;然后交换 sp3 与 sp1 的资源管理权即可。
定制删除器
这里的定制删除器主要是指我们在构造 shared_ptr 时可以传入一个可调用对象完成一些自定义类型的资源清理工作,定制删除器我们可以使用 C++11 中的包装器实现。在 shared_ptr 类中我们添加一个成员变量,该成员变量为 function<void (T*)> 类型的可调用对象,默认为 delete 版本,然后再构造时就可以传入该类型的可调用对象,析构时调用定制删除器析构 _ptr 即可。
代码实现
cpp
//SmartPtr.hpp
#pragma once
#include <iostream>
#include <functional>
using namespace std;
namespace LTL
{
template<class T>
class shared_ptr
{
using del_t = function<void(T*)>;
void release()
{
if (_pcount && --(*_pcount) == 0)
{
//释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_del = nullptr;
}
}
public:
//默认构造
shared_ptr()
{}
//避免进行隐式类型转换
explicit shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1)) //第一次构造时开辟引用计数的空间
{}
//支持定制删除器版本
explicit shared_ptr(T* ptr, del_t del)
:_ptr(ptr)
, _pcount(new int(1)) //第一次构造时开辟引用计数的空间
,_del(del)
{}
//析构函数
~shared_ptr()
{
release();
}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
//别忘记让引用计数++
if (_pcount)
++(*_pcount);
}
//拷贝赋值
//sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//一定要判断资源的相同,_pcount 也可以
if (_ptr != sp._ptr)
{
//先对 sp1 的资源进行管理
release();
//进行资源的拷贝
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
//别忘记进行引用计数的 ++
if (_pcount)
++(*_pcount);
}
return *this;
}
void swap(shared_ptr<T>& sp)
{
std::swap(_ptr, sp._ptr);
std::swap(_pcount, sp._pcount);
std::swap(_del, sp._del);
}
//移动构造
shared_ptr(shared_ptr<T>&& sp)
{
//直接转移资源的管理权
swap(sp);
}
//移动赋值
//sp3 = move(sp1)
shared_ptr<T>& operator=(shared_ptr<T>&& sp)
{
//先对 sp3 的资源进行管理
release();
//转移资源管理权
swap(sp);
//将 sp 的指针置空
sp._ptr = nullptr;
sp._pcount = nullptr;
return *this;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return _pcount ? *_pcount : 0;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr = nullptr;
int* _pcount = nullptr; // 引用计数
del_t _del = [](T* ptr) { delete ptr; }; // 定制删除器
};
}
//Main.cc -- 测试用例
#include "SmartPtr.hpp"
using namespace std;
using namespace LTL;
// 简单类用于测试
class Test
{
public:
Test(int v = 0) : val(v) { cout << "Test构造: " << val << endl; }
~Test() { cout << "Test析构: " << val << endl; }
void show() { cout << "Test值: " << val << endl; }
private:
int val;
};
// 测试默认构造和空指针
void test_default()
{
cout << "\n=== test_default ===" << endl;
LTL::shared_ptr<int> sp;
cout << "use_count: " << sp.use_count() << endl;
}
// 测试拷贝构造
void test_copy()
{
cout << "\n=== test_copy ===" << endl;
LTL::shared_ptr<Test> sp1(new Test(10));
cout << "sp1 use_count: " << sp1.use_count() << endl;
LTL::shared_ptr<Test> sp2(sp1);
cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
sp2->show();
}
// 测试拷贝赋值
void test_copy_assign()
{
cout << "\n=== test_copy_assign ===" << endl;
LTL::shared_ptr<Test> sp1(new Test(20));
LTL::shared_ptr<Test> sp2;
sp2 = sp1;
cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
sp2->show();
}
// 测试移动构造
void test_move()
{
cout << "\n=== test_move ===" << endl;
LTL::shared_ptr<Test> sp1(new Test(30));
LTL::shared_ptr<Test> sp2(std::move(sp1));
cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
if (sp2.get()) sp2->show();
}
// 测试移动赋值
void test_move_assign()
{
cout << "\n=== test_move_assign ===" << endl;
LTL::shared_ptr<Test> sp1(new Test(40));
LTL::shared_ptr<Test> sp2;
sp2 = std::move(sp1);
cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
if (sp2.get()) sp2->show();
}
// 测试自定义删除器
void test_custom_deleter()
{
cout << "\n=== test_custom_deleter ===" << endl;
auto del = [](Test* t) {
cout << "自定义删除器释放对象" << endl;
delete t;
};
LTL::shared_ptr<Test> sp(new Test(50), del);
cout << "sp use_count: " << sp.use_count() << endl;
}
// 测试异常安全
void test_exception()
{
cout << "\n=== test_exception ===" << endl;
try
{
LTL::shared_ptr<Test> sp(new Test(60));
cout << "sp use_count: " << sp.use_count() << endl;
throw runtime_error("模拟异常");
}
catch (const exception& e)
{
cout << "捕获异常: " << e.what() << endl;
}
// 此时 shared_ptr 自动析构,不会泄漏资源
}
int main()
{
test_default();
test_copy();
test_copy_assign();
test_move();
test_move_assign();
test_custom_deleter();
test_exception();
return 0;
}
5 weak_ptr
5.1 shared_ptr 循环引用问题
shared_ptr 在一般情况下是没有什么问题的,适合管理资源,也支持拷贝与移动,但是 shared_ptr 一旦发生了循环引用,就会产生资源泄露问题。
那么什么是循环引用问题呢?简单来说,就是 两个 shared_ptr 对象内部又各自有 shared_ptr 指向对方,导致引用计数永远减不到0,就会导致内存泄露。比如以下的这个场景,就会发生循环引用问题:
cpp
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
ListNode(int data = 0)
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode(1));
shared_ptr<ListNode> n2(new ListNode(2));
n1->_next = n2;
n2->_prev = n1;
return 0;
}
在 ListNode 结构体中,_next 与 _prev 对象分别都是 shared_ptr 类型;然后在 main 函数中开辟了 n1 与 n2 两个节点,然后 n1 的 _next 指向了 n2,n2 的 _prev 指向了 n1,这样就造成了 shared_ptr 的循环引用:



以上的循环引用问题其实就是一个逻辑上的死循环:n1 中的 _next 释放之后,n2 节点的引用计数减为 0,n2 节点就释放,那么 n1 的 _next 什么时候释放呢?n1 节点释放时,_next 就释放了,n1 节点什么时候释放呢?n1 的引用计数减为 0 时,那么 n1 的引用计数什么时候减为 0 呢?n2 的 _prev 释放时,n1 的引用减到 0,n2 的 _prev 什么时候释放呢?n2 节点释放时,也就是 n2 节点的引用计数减为 0,n2 的引用计数什么时候减到 0 呢?n1 的 next 释放之后,至此就完成了逻辑上的死循环,也就是循环引用问题。
5.2 weak_ptr 解决循环引用问题
weak_ptr 并不是像 unique_ptr 与 shared_ptr 一样,他不符合 RAII 设计思想,也不能管理资源,所以就不能用一个具体的指针来构造 weak_ptr,即使是空指针也不可以,只支持使用 shared_ptr 对象来构造 weak_ptr 对象:

weak_ptr 在用 shared_ptr 构造之后,并不会增加 shared_ptr 的引用计数,所以就可以解决循环引用的问题:
cpp
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _data;
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
ListNode(int data = 0)
:_data(data)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode(1));
shared_ptr<ListNode> n2(new ListNode(2));
n1->_next = n2;
n2->_prev = n1;
return 0;
}

由于 weak_ptr 不参与资源管理,所以 weak_ptr 中也没有对应的 operator*,operator-> 等访问接口,但是可以通过其他接口来间接实现资源管理:

其中可以使用 expired 函数来查看绑定的 shared_ptr 有没有过期,也就是有没有释放资源;可以使用 use_count 来查看绑定的 shared_ptr 的引用计数;也可以使用 lock 函数返回一个 shared_ptr 对象,但是如果 weak_ptr 绑定的 shared_ptr 已经过期了,那么 lock 函数返回的是一个空指针。
总结
在现代 C++ 编程中,智能指针是一种用于自动管理动态分配内存的工具,它能显著降低内存泄漏和野指针的风险。unique_ptr 提供对象的独占所有权,生命周期结束时自动释放资源;shared_ptr 支持多个指针共享同一对象,并通过引用计数在最后一个拥有者销毁时释放资源,但是存在循环引用问题;weak_ptr 则用作观察者,避免循环引用问题。通过 make_unique 和 make_shared 工厂函数,可以安全、高效地创建智能指针实例,同时保证异常安全性。智能指针的使用不仅提高了代码的安全性,也让资源管理更直观、简洁,是现代 C++ 程序设计的推荐实践。
但是在 shared_ptr 中容易发生线程安全问题,引用计数作为共享资源,需要加锁或者保证其原子性来保证是线程安全的。