目录
[1. 为什么需要智能指针?](#1. 为什么需要智能指针?)
[2. RAII 设计思想](#2. RAII 设计思想)
[3. auto_ptr 独占型智能指针(废弃)](#3. auto_ptr 独占型智能指针(废弃))
[4. unique_ptr(独占型智能指针)](#4. unique_ptr(独占型智能指针))
[4.1 核心特性](#4.1 核心特性)
[4.2 基本使用](#4.2 基本使用)
[4.3 模拟实现](#4.3 模拟实现)
[5. shared_ptr(共享型智能指针)](#5. shared_ptr(共享型智能指针))
[5.1 核心特性](#5.1 核心特性)
[5.2 基本使用](#5.2 基本使用)
[5.3 模拟实现](#5.3 模拟实现)
[6. operator bool()](#6. operator bool())
[7. 删除器](#7. 删除器)
[7.1 默认删除器](#7.1 默认删除器)
[7.2 为什么需要自定义删除器?](#7.2 为什么需要自定义删除器?)
[7.3 unique_ptr 与 shared_ptr 删除器的设计差异](#7.3 unique_ptr 与 shared_ptr 删除器的设计差异)
[unique_ptr 删除器](#unique_ptr 删除器)
[shared_ptr 删除器](#shared_ptr 删除器)
[7.4 自定义删除器的实现及其使用](#7.4 自定义删除器的实现及其使用)
[7.5 小结](#7.5 小结)
[8. make_unique 和 make_shared](#8. make_unique 和 make_shared)
[8.1 基础定义与引入时间](#8.1 基础定义与引入时间)
[8.2 基本用法](#8.2 基本用法)
[8.3 为什么推荐使用它们?](#8.3 为什么推荐使用它们?)
[(3)make_shared 独有优势:一次性内存分配,性能提升](#(3)make_shared 独有优势:一次性内存分配,性能提升)
[8.4 注意事项与局限性](#8.4 注意事项与局限性)
[8.5 最佳实践](#8.5 最佳实践)
[9. shared_ptr 循环引用问题](#9. shared_ptr 循环引用问题)
[10. weak_ptr(弱引用智能指针)](#10. weak_ptr(弱引用智能指针))
[10.1 核心特性](#10.1 核心特性)
[10.2 用 weak_ptr 打破循环引用](#10.2 用 weak_ptr 打破循环引用)
[10.3 常用方法](#10.3 常用方法)
[lock() 函数](#lock() 函数)
[expired() 函数](#expired() 函数)
[10.4 模拟实现](#10.4 模拟实现)
[11. 总结](#11. 总结)
1. 为什么需要智能指针?
使用原始的裸指针(Raw Pointer)和 new/delete 手动管理内存,容易出现以下问题:
- 内存泄漏 :忘记调用
delete释放内存。- 悬空指针 :在
delete之后继续访问指针,导致未定义行为。- 重复释放 :对同一块内存多次调用
delete,可能导致程序崩溃。- 异常不安全 :在函数执行过程中如果抛出异常,可能会跳过
delete语句,导致内存泄漏。
下面代码展示了 C++ 早期(C++98 之前)程序员面临的困境:为了处理异常安全,必须编写大量的样板代码来释放资源。
double Divide(int a, int b)
{
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}
}
void func()
{
//如果array1分配成功了,但array2分配失败抛出异常,那么array1就内存泄漏了,就还需要把分配逻辑包在try_catch块里面,释放array1的资源,代码会变得非常臃肿
int* array1 = new int[10];
int* array2 = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...) //如果发生除0错误,Divide函数抛异常,那么array1和array2没有得到释放,所以这里捕获异常,并不处理异常,
{ //只是手动delete两个数组释放资源,再将异常重新抛出交给上层处理
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1; //释放资源
delete[] array2;
throw;//异常重新抛出
}
delete[] array1;
cout << "delete []" << array1 << endl;
delete[] array2;
cout << "delete []" << array2 << endl;
}
int main()
{
try
{
func();
}
catch (const string& s)
{
cout << s << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
这正是C++引入 智能指针 和 RAII 机制要解决的核心痛点。
2. RAII 设计思想
RAII(Resource Acquisition Is Initialization,资源获取即初始化),RAII核心思想 :把资源的生命周期绑定到对象的生命周期上,对象构造时获取资源,对象析构时释放资源。
RAII 解决了"异常不安全"导致的资源泄漏,这是 RAII 最大的贡献。在传统的 C++ 代码中,如果资源分配和释放之间发生了异常,程序控制流会跳转,导致释放代码(如delete或close)被跳过,从而造成资源泄漏。
- 没有 RAII 时 :你需要编写大量的
try-catch块来捕获异常并释放资源,或者在每个return语句前手动释放资源,代码极其繁琐且容易遗漏。- 使用 RAII 后 :C++ 保证栈上的局部对象在离开作用域时(无论是正常返回还是因异常进行"栈展开"),其析构函数一定会被调用。因此,资源释放代码放在析构函数中是绝对安全的,无需任何额外的异常处理代码。
智能指针是RAII最经典的应用, 也是现代 C++ 管理动态内存的首选方案,它通过封装原生指针并重载运算符,实现了像使用普通指针一样方便,同时又能自动管理。
C++11 标准引入了三种主要的智能指针:std::unique_ptr、std::shared_ptr、std::weak_ptr,并废弃了旧的std::auto_ptr。使用智能指针需要包含 <memory> 头文件。智能指针类还重载 operator*/operator->/operator[] 等运算符,方便访问资源。
3. auto_ptr 独占型智能指针(废弃)
auto_ptr是 C++98 标准中引入的第一代智能指针,旨在通过 RAII 机制自动管理动态内存。然而,由于其设计上存在严重缺陷 ,它在 C++11 标准中被弃用 ,并在 C++17 标准中被正式移除。
auto_ptr号称是 "独占所有权",但受限于 C++98 没有移动语义,它的实现方式是用拷贝 来模拟转移所有权,它的特点是拷贝时把原指针置空,所有权转移给新指针,这导致它极容易写出悬空指针、崩溃代码。
这种设计完全违背了正常拷贝语义,语法语义严重混乱,极易引发难以排查的空指针崩溃。
int main()
{
auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1; // 拷贝!管理权转移,被拷贝对象p1悬空
*p1; // 未定义行为,直接崩溃
return 0;
}
4. unique_ptr(独占型智能指针)
std::unique_ptr在 C++11 中被引入,保留了 "独占所有权" 的核心理念,同时通过显式禁止拷贝、仅支持移动语义 ,彻底解决了auto_ptr的安全隐患,是目前 C++ 中默认首选的零开销智能指针。
4.1 核心特性
- 同一时间只能有一个unique_ptr对象管理这块内存。
unique_ptr禁止拷贝构造和拷贝赋值 ,**只允许移动语义,**这确保了所有权的清晰转移。- 当
unique_ptr离开作用域(或被显式重置)时,它会自动调用删除器释放所管理的对象。
void test()
{
unique_ptr<int> up1(new int(10));
//unique_ptr<int> up2 = up1; // 编译错误!禁止拷贝
unique_ptr<int> up3 = move(up1); // 正确,移动构造,所有权转移给 p3,p1 变为空
//由此,unique_ptr不允许作为容器元素,
//vector<unique_ptr<int>> vec;
//vec.push_back(up1);
}
int main()
{
test();
return 0;
}
4.2 基本使用
#include <iostream>
#include <memory>
using namespace std;
// 创建 unique_ptr
void test1()
{
//管理单个对象
unique_ptr<Person> up1(new Person);
up1->demo();
//管理动态数组
unique_ptr<Person[]> up2(new Person[10]);
//unique_ptr重载了operator* 和 operator->,可以像普通指针一样使用
up2[0].demo();
up2[1].demo();
}
// 常用成员函数的使用
void test2()
{
unique_ptr<Person> up1(new Person);
unique_ptr<Person> up2(new Person);
unique_ptr<Person> up3(new Person);
unique_ptr<Person> up4(new Person);
//get():获取内部的裸指针(不改变所有权,用于与C接口交互)
Person* raw1 = up1.get();
//realease():释放所有权,返回裸指针。注意:调用后unique_ptr变为空,需要手动管理返回的指针
Person* raw2 = up2.release();
//reset():重置指针,有两个重载版本(无参和带参),具体如下:
up3.reset();//无参:释放当前对象,up3指针置空
up3.reset(new Person);//带参:释放旧对象,接管新对象
//swap():交换两个unique_ptr对象所管理的对象
up3.swap(up4);
//operator bool():隐式转换为bool,判断指针是否为空
if (!up2)
{
cout << "up2管理的指针为空" << endl;
}
}
int main()
{
//test1();
test2();
return 0;
}
4.3 模拟实现
//模拟实现unique_ptr
template<class T>
class MyUniquePtr
{
public:
//构造
explicit MyUniquePtr(T* ptr = nullptr) //加explicit防止隐式转换
:_ptr(ptr)
{}
//析构
~MyUniquePtr()
{
if (_ptr)
{
delete _ptr;
}
}
MyUniquePtr(const MyUniquePtr&) = delete; //禁用拷贝构造
MyUniquePtr& operator=(const MyUniquePtr&) = delete; //禁用拷贝赋值
//支持移动构造
MyUniquePtr(MyUniquePtr&& other) noexcept
:_ptr(other._ptr)
{
other._ptr = nullptr; //将对方的指针置空
}
//移动赋值
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept
{
if (this != &other)
{
delete _ptr;//释放自己原本持有的资源(如果有的话)
_ptr = other._ptr; //接管对方的资源
other._ptr = nullptr;//将对方置空
}
return *this;
}
//访问操作符重载
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
//获取原始指针
T* get() const { return _ptr; }
//释放所有权(不销毁资源,只是交出指针)
T* release()
{
T* temp = _ptr;
_ptr = nullptr;
return temp;
}
//重置指针(释放旧资源,指向新资源)
void reset(T* p = nullptr)
{
delete _ptr;
_ptr = p;
}
// 显式转换运算符重载 operator bool()
explicit operator bool() const noexcept
{
return ptr != nullptr; // 检查内部指针是否为空
}
private:
T* _ptr; //持有的原始指针
};
数组特化版本:
// 数组特化版本(管理数组)
// 注意这里:<T[]>,专门匹配 MyUniquePtr<int[]> 这种写法
template<class T>
class MyUniquePtr<T[]>
{
public:
explicit MyUniquePtr(T* ptr=nullptr)
:_ptr(ptr)
{}
~MyUniquePtr()
{
delete[] _ptr; // 关键:使用delete[]
cout << "delete[] _ptr" << endl;
}
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;
MyUniquePtr(MyUniquePtr&& other) noexcept
:_ptr(other._ptr)
{
other._ptr = nullptr;
}
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept
{
if (this != &other)
{
delete[] _ptr; // 关键:使用delete[]
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
//数组特有的访问方式:下标操作符
T& operator[](size_t index)const
{
return _ptr[index];
}
T* get()const
{
return _ptr;
}
T* release()
{
T* temp = _ptr;
_ptr = nullptr;
return temp;
}
void reset(T* p = nullptr)
{
delete[] _ptr;
_ptr = p;
}
private:
T* _ptr;
};
5. shared_ptr(共享型智能指针)
std::shared_ptr实现了共享资源所有权 的语义,允许多个shared_ptr实例共享同一块内存 ,通过引用计数 管理内存,所有共享该对象的指针,共用同一个引用计数。
5.1 核心特性
shared_ptr支持拷贝构造与移动语义:拷贝shared_ptr时,引用计数原子 + 1 ;析构 / 重置shared_ptr时,引用计数原子 - 1;当计数归 0 时,自动调用删除器释放对象。
- 当最后一个指向该对象的
shared_ptr被销毁(即引用计数降为 0)时,对象才会被自动删除。- 引用计数的增减操作是原子性的,因此多线程环境下拷贝和销毁
shared_ptr是线程安全的。但需要注意,它管理的对象本身并不是线程安全的,并发读写仍需加锁。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// 创建 shared_ptr
shared_ptr<int> p1 = make_shared<int>(100);
cout << "引用计数:" << p1.use_count() << endl; // 1
// 拷贝,计数+1
shared_ptr<int> p2 = p1;
cout << "引用计数:" << p1.use_count() << endl; // 2
// 所有指针离开作用域,计数归0,自动释放内存
return 0;
}
5.2 基本使用
#include <iostream>
#include <memory>
using namespace std;
// 创建 shared_ptr
void test1()
{
//管理单个对象
shared_ptr<int> sp1(new int);
*sp1 = 10;
//管理动态数组
shared_ptr<int[]> sp2(new int[10]);
sp2[0] = 1;
}
// 常用成员函数的使用
void test2()
{
shared_ptr<int> sp1; // 默认构造,空指针
//从unique_ptr转换构造:从unique_ptr隐式转换为shared_ptr,unique_ptr自动置空,shared_ptr接管对象所有权和删除器
unique_ptr<int> up(new int(6));
shared_ptr<int> sp2(std::move(up));
//get():返回裸指针(仅用于与C接口交互)
int* raw = sp1.get();
//reset():重置指针,释放当前管理的对象(引用计数-1,归0则释放),并可选择接管新对象
sp1.reset();//无参:释放当前对象,sp1置空
sp2.reset(new int(8));//带参:释放旧对象,接管新对象
//use_count():获取引用计数
cout << sp1.use_count() << endl;
//swap():交换两个shared_ptr管理的对象和控制块
sp1.swap(sp2);
//operator bool():判断shared_ptr是否管理有效对象(非空),支持隐式转换,常用于 if 条件判断
if (sp1)
{
cout << "sp1管理的指针不为空" << endl;
}
}
int main()
{
//test1();
test2();
return 0;
}
5.3 模拟实现
简单实现:
//模拟实现shared_ptr
template<class T>
class MySharedPtr
{
public:
//构造函数
explicit MySharedPtr(T* ptr = nullptr) //使用explicit防止隐式类型转换
:_ptr(ptr)
, _refCount(ptr ? new int(1) : nullptr) //指针为空,计数也为空
{}
//拷贝构造 核心:引用计数+1
MySharedPtr(const MySharedPtr& other)
:_ptr(other._ptr)
, _refCount(other._refCount)
{
if (_refCount)
++(*_refCount);
}
//析构函数
~MySharedPtr()
{
release();
}
//拷贝赋值
MySharedPtr& operator=(const MySharedPtr& other)
{
//if (this != &other)
if (_ptr != other._ptr) //更好的判断方式 两个不同的智能指针对象可能指向同一块内存,防止做无用功
{
//释放旧资源
release();
// 共享对方的资源
_ptr = other._ptr;
_refCount = other._refCount;
// 引用计数+1
if (_refCount)
++(*_refCount);
}
return *this;
}
//移动构造
MySharedPtr(MySharedPtr&& other) noexcept
:_ptr(other._ptr)
, _refCount(other._refCount)
{
other._ptr = nullptr;
other._refCount = nullptr;
}
//移动赋值
MySharedPtr& operator=(MySharedPtr&& other) noexcept
{
//if (this != &other)
if (_ptr != other._ptr)
{
release();
_ptr = other._ptr;
_refCount = other._refCount;
other._ptr = nullptr;
other._refCount = nullptr;
}
return *this;
}
//获取原始指针
T* get() const { return _ptr; }
//获取引用计数
int use_count() const { return _refCount ? *_refCount : 0; }
//重置指针
void reset(T* p = nullptr)
{
release();
_ptr = p;
_refCount = p ? new int(1) : nullptr;
}
//访问操作符重载
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
//显式转换运算符
explicit operator bool()const
{
return _ptr != nullptr;
}
private:
//辅助函数
//释放资源
void release()
{
if (_refCount)//先判空,否则指针解引用会崩溃
{
if (--(*_refCount) == 0)
{
delete(_ptr);
delete _refCount;
}
}
_ptr = nullptr;
_refCount = nullptr;
}
T* _ptr; //指向管理的资源
int* _refCount; //引用计数
};
带自定义删除器:
//模拟实现shared_ptr(带自定义删除器)
template<class T>
class MySharedPtr
{
public:
//构造函数
explicit MySharedPtr(T* ptr = nullptr) //即使不传删除器,也会调用其默认构造
:_ptr(ptr)
, _refCount(ptr ? new int(1) : nullptr)//指针为空,计数也为空
{}
//带自定义删除器的构造函数
//函数模版,这里接收一个删除器模板参数,自动推导D的类型
template<class D>
explicit MySharedPtr(T* ptr, D del)
:_ptr(ptr)
, _refCount(ptr ? new int(1) : nullptr)
, _del(del)
{}
//拷贝构造
MySharedPtr(const MySharedPtr& other)
:_ptr(other._ptr)
, _refCount(other._refCount)
, _del(other._del)
{
if (_refCount)
++(*_refCount);
}
//析构函数
~MySharedPtr()
{
release();
}
//拷贝赋值
MySharedPtr& operator=(const MySharedPtr& other)
{
if (_ptr != other._ptr)
{
//释放旧资源
release();
_ptr = other._ptr;
_refCount = other._refCount;
_del = other._del;
if (_refCount)
++(*_refCount);
}
return *this;
}
//移动构造
MySharedPtr(MySharedPtr&& other) noexcept
:_ptr(other._ptr)
, _refCount(other._refCount)
, _del(std::move(other._del))//移动删除器
{
other._ptr = nullptr;
other._refCount = nullptr;
//other._del保持默认或原样即可,反正它不再管理资源
}
//移动赋值
MySharedPtr& operator=(MySharedPtr&& other) noexcept
{
if (_ptr != other._ptr)
{
release();
_ptr = other._ptr;
_refCount = other._refCount;
_del = std::move(other._del);
other._ptr = nullptr;
other._refCount = nullptr;
}
return *this;
}
//获取原始指针
T* get() const { return _ptr; }
//获取引用计数
int use_count() const { return _refCount ? *_refCount : 0; }
//重置指针
void reset(T* p = nullptr)
{
release();
_ptr = p;
_refCount = p ? new int(1) : nullptr;
}
//访问操作符重载
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
//显式转换运算符
explicit operator bool()const
{
return _ptr != nullptr;
}
private:
//辅助函数
//释放资源
void release()
{
if (_refCount)// 先判断指针是否为空,避免空指针解引用;
{
if (--(*_refCount) == 0)
{
_del(_ptr); //调用删除器
delete _refCount;
}
}
_ptr = nullptr;
_refCount = nullptr;
}
T* _ptr; //指向管理的资源
int* _refCount; //引用计数
function<void(T*)> _del = [](T* ptr) {delete ptr; };//删除器
};
6. operator bool()
operator bool()是 C++ 中的类型转换运算符 ,作用是:让一个对象可以直接用在if、while等布尔判断的位置,自动转换成true或false。
std::unique_ptr 和 std::shared_ptr 都重载了 operator bool() ,这使得它们可以像普通指针一样,直接在 if、while 等条件语句中使用,极大地简化了代码,提高了可读性。
模拟实现
template <typename T>
class shared_ptr
{
T* ptr; // 内部管理的原始指针
// ...引用计数等其他成员
public:
// 重载 operator bool()
explicit operator bool() const noexcept {
return ptr != nullptr; // 检查内部指针是否为空
}
// ... 其他成员函数
};
使用
当写下 if (ptr) 时,编译器会自动调用这个 operator bool() 函数。如果内部的原始指针 ptr 不为 nullptr,函数返回 true,if 条件成立;反之则返回 false。
shared_ptr<int> ptr = make_shared<int>(10);
if (ptr) // 这里会自动调用 ptr.operator bool()
{
// ...
}
explicit 关键字 防止了智能指针被隐式地转换为 bool 或其他整数类型,保证了类型安全。
shared_ptr<int> ptr = make_shared<int>(5);
// bool m = ptr; // 编译错误!禁止隐式转换
7. 删除器
7.1 默认删除器
C++ 标准库为智能指针提供了默认删除器std::default_delete<T>,核心行为分为两类:
- 普通版本
std::default_delete<T>:内部调用delete释放单个对象,适用于unique_ptr<T>/shared_ptr<T>。- 数组特化版本
std::default_delete<T[]>:内部调用delete[]释放动态数组,适用于unique_ptr<T[]>(C++11 起原生支持)、shared_ptr<T[]>(C++17 起原生支持)。
// 主模版:用于普通对象(模拟 std::default_delete<T>)
template<class T>
struct DefaultDelete
{
void operator()(T* ptr) const
{
if (ptr == nullptr) return; // 空指针防护,避免野指针释放
cout << "[DefaultDelete] 普通对象:调用 delete" << endl;
delete ptr; // 单个对象使用 delete 释放
}
};
// 偏特化:针对数组对象(模拟 std::default_delete<T[]>)
template<class T>
struct DefaultDelete<T[]>
{
void operator()(T* ptr) const
{
if (ptr == nullptr) return; // 空指针防护
cout << "[DefaultDelete] 数组对象:调用 delete[]" << endl;
delete[] ptr; // 数组对象使用 delete[] 释放
}
};
7.2 为什么需要自定义删除器?
默认情况下,智能指针释放资源只会调用 delete/delete[ ],但在很多场景下资源不一定是new出来的,比如:
FILE*文件句柄 → 要用fclosemalloc内存 → 要用free这时候默认删除器就不管用了,如果只用默认删除器就会导致资源泄漏或未定义行为。
自定义删除器作用:智能指针析构时,不再执行默认 delete,转而执行自定义的释放逻辑,安全管理所有非标资源。
7.3 unique_ptr 与 shared_ptr 删除器的设计差异
unique_ptr 删除器
unique_ptr:删除器是**"模板参数"** (指针类型的一部分 )
模版定义:
template <class T,class Deleter = std::default_delete<T>>
class unique_ptr;
unique_ptr的设计追求零开销,所以它把删除器作为类型的一部分。
- 语法 :std::unique_ptr<管理类型, 删除器类型> 指针名(资源,删除器对象);
- 特点:删除器的类型直接写在尖括号里,参与类型定义。
- 类型影响: 删除器类型直接决定
unique_ptr的最终类型,只要T或Deleter有一个不同,就是不同类型,无法互相赋值、存入同一个容器。- 零开销设计(EBO 空基类优化) :
unique_ptr会根据删除器是否为无状态空类 ,自动选择最优存储策略:
- 无状态空类删除器(空仿函数、无捕获 Lambda):通过私有继承删除器触发空基类优化(EBO) ,编译器不会为空类分配额外空间,最终sizeof(unique_ptr) = 裸指针大小,完全零开销。
- 有状态删除器(函数指针、带捕获 Lambda、带成员变量的仿函数):EBO 优化失效,删除器作为成员变量存储,最终sizeof(unique_ptr) = 裸指针大小 + 删除器实例大小。
- 构造规则 :是否需要手动传入删除器对象,完全取决于删除器类型是否支持默认构造:
- 仿函数空类 :支持默认构造→ 可以不传入删除器对象
- Lambda、函数指针 :不支持默认构造→ 必须手动传入删除器实例
注:如果一个类继承自一个"空类",编译器可以不为这个基类分配内存空间,这叫空基类优化。(了解)
内存布局

shared_ptr 删除器
shared_ptr:删除器是"构造函数参数"(不参与指针类型定义 )
模版定义:
template <class T> class shared_ptr;
shared_ptr将删除器存储在堆上的控制块中。
- 语法 :std::shared_ptr<管理类型> 指针名(资源, 删除器对象);
- 特点:删除器作为第二个参数传给构造函数,不参与类型定义。
- 类型影响: 删除器仅在构造时传入,不影响
shared_ptr<T>的主类型。无论删除器是函数指针、Lambda 还是仿函数,只要管理的对象类型T一致,就是完全相同的类型,可互相赋值、存入同一个容器。- 控制块存储 :
shared_ptr本身固定为2 个指针大小(64 位系统 16 字节),一个指向资源,一个指向堆上的控制块;删除器、强引用计数、弱引用计数、分配器等元数据,全部存储在控制块中。- 构造规则: 仅需在构造智能指针时传入删除器实例即可。
内存布局:

7.4 自定义删除器的实现及其使用
我们可以用函数指针、仿函数或Lambda表达式来实现删除器。
Lambda表达式删除器
int main()
{
// 自定义Lambda删除器
auto my_deleter = [](int* p) {
cout << "Lambda 删除器:释放int对象" << endl;
delete p;
};
//使用 decltype 推导类型
//unique_ptr<int, decltype(my_deleter)> ptr(new int); //错误写法! Lambda类型没有默认构造函数,必须手动把Lambda对象传给构造函数
unique_ptr<int, decltype(my_deleter)> ptr(new int, my_deleter); //正确写法 构造时传入Lambda对象
shared_ptr<int> sp1(new int, my_deleter);
shared_ptr<int> sp2(new int, [](int* p) {delete p; }); //直接内联Lambda
return 0;
}
仿函数删除器
// 仿函数删除器
struct FileDeleter
{
void operator()(FILE* fp)const
{
if (fp)
{
fclose(fp);
cout << "仿函数删除器:关闭文件\n";
}
}
};
int main()
{
unique_ptr<FILE, FileDeleter> up1(fopen("test.txt", "w"));//空仿函数有默认构造,可以不传删除器
unique_ptr<FILE, FileDeleter> up2(fopen("test.txt", "w"), FileDeleter());//手动传临时仿函数对象
shared_ptr<FILE> sp(fopen("test.txt", "w"), FileDeleter());
return 0;
}
函数指针删除器
// 函数指针删除器
void free_malloc(void* p)
{
cout << "函数指针删除器:释放malloc申请的内存" << endl;
free(p);
}
int main()
{
//unique_ptr<void, void (*)(void*)> up1(malloc(100)); //错误写法! 函数指针没有默认构造,必须传删除器
unique_ptr<void, void(*)(void*)> up2(malloc(100), free_malloc);//正确写法!必须传入函数地址
unique_ptr<void, decltype(&free_malloc)> up3(malloc(100), free_malloc);//正确写法!使用decltype自动推导类型(更简洁)
shared_ptr<void> sp(malloc(100), free_malloc);
return 0;
}
7.5 小结
| 特性 | unique_ptr |
shared_ptr |
|---|---|---|
| 删除器位置 | 模板参数 <T, Del> |
构造函数参数 (ptr, del) |
| 类型影响 | 删除器不同,类型就不同(UP<T, D1> 和 UP<T, D2> 是不同类型)。 |
删除器不同,类型依然相同(都是 shared_ptr<T>)。 |
| 内存开销 | 无状态删除器零开销;有状态额外占用 | 固定 2 个指针大小,删除器额外占用控制块 |
| 主要用途 | 独占资源管理,追求极致性能。 | 共享资源管理,需要灵活指定不同的销毁策略。 |
实战建议 :尽量使用无状态的仿函数 作为 unique_ptr 的删除器,这样既保持了代码整洁,又确保了零内存开销。对于 shared_ptr,Lambda 是最方便的写法。
8. make_unique 和 make_shared
8.1 基础定义与引入时间
std::make_unique和std::make_shared是 C++ 标准库提供的创建智能指针的推荐方式 ,核心目标是更安全、更高效地创建智能指针对象 。它们通过 "一次性分配内存" 和 "隐藏裸指针" 的设计,从根源解决了直接使用new构造智能指针带来的安全隐患与性能问题。
| 函数 | 引入标准 | 头文件 | 核心功能 |
|---|---|---|---|
std::make_shared<T> |
C++11 | <memory> |
安全、高效地创建 std::shared_ptr<T> |
std::make_unique<T> |
C++14 | <memory> |
安全地创建 std::unique_ptr<T>(C++11 遗漏,C++14 补全) |
8.2 基本用法
语法:std::make_unique<T>(构造参数...),参数完美转发给 T 的构造函数。
int main()
{
// 1. 管理单个对象
auto up1 = make_unique<int>(10); // 等价于 unique_ptr<int>(new int(10))
auto up2 = make_unique<std::string>("hello");
// 2. 管理动态数组(C++14+)
auto up3 = make_unique<int[]>(10); // 等价于 unique_ptr<int[]>(new int[10])
up3[0] = 100; // 支持operator[]下标访问
return 0;
}
语法:std::make_shared<T>(构造参数...),括号内的参数会完美转发给 T 的构造函数。
int main()
{
// 1. 管理单个对象
auto sp1 = make_shared<int>(10); // 等价于 shared_ptr<int>(new int(10))
auto sp2 = make_shared<std::string>("world");
// 2. 管理动态数组(注意C++20 起)
auto sp3 = make_shared<int[]>(10); // 自动匹配 delete[] 释放
sp3[0] = 200; //支持operator[]下标访问
return 0;
}
8.3 为什么推荐使用它们?
make_unique与make_shared可从根源上杜绝裸指针误用、保证异常安全,其中make_shared还额外具备内存分配的性能优势,是现代 C++ 创建智能指针的首选方案。
(1)杜绝裸指针误用
直接用
new创建智能指针时,裸指针会短暂暴露在代码中,可能被误操作,导致:
- 手动
delete裸指针,智能指针析构时触发双重释放,程序直接崩溃;- 裸指针被多个主体管理,所有权彻底混乱。
代码示例:双重释放(运行时崩溃)
class MyClass
{
public:
MyClass() { std::cout << "MyClass 构造\n"; }
~MyClass() { std::cout << "MyClass 析构\n"; }
};
int main()
{
// 手动 new,拿到裸指针
MyClass* p = new MyClass();
// 把裸指针传给 shared_ptr,完成所有权接管
std::shared_ptr<MyClass> sp(p);
delete p;// 致命错误:手动delete裸指针,此时p已经被shared_ptr接管
// 导致后续shared_ptr离开作用域,再次析构同一块内存,双重释放,程序直接崩溃(触发未定义行为)
return 0;
}
安全写法
// ✅ 安全写法:使用make_shared,全程不暴露裸指针
auto sp = std::make_shared<MyClass>();
(2)保证异常安全(最重要的原因)
这是 C++ 标准推荐使用make_xxx函数的主要原因,它彻底解决了直接使用new构造智能指针时的内存泄漏隐患。
C++ 标准未规定函数参数的求值顺序 ,编译器可以自由决定先计算哪个参数的值。直接用
new构造智能指针时,"new分配内存" 和 "智能指针接管所有权" 是两个独立步骤,中间可能被异常打断,导致内存泄漏。
代码示例(异常安全隐患导致内存泄漏)
// 业务函数
void process(std::shared_ptr<int> sp, int param)
{
if (param < 0)
throw std::runtime_error("参数非法");
}
// 一个可能抛出异常的函数
int risky_func()
{
throw std::runtime_error("函数执行失败");
return 0;
}
int main()
{
try {
// ❌ 危险写法:直接用new构造智能指针
//编译器的执行顺序可能是: 1.new int(42) -> 2.调用risky_func() -> 3.构造shared_ptr
//如果第 2 步抛异常,程序直接跳转至异常处理逻辑,shared_ptr还没构造出来,没人接管new出来的裸指针,内存就泄漏了!
process(std::shared_ptr<int>(new int(42)), risky_func());
}
catch (...) {}
return 0;
}
安全写法
// ✅ 安全写法:用 make_shared 创建
process(std::make_shared<int>(42), risky_func());
此时,无论编译器先计算哪个参数,都不会发生内存泄漏:
- 如果先执行
make_shared:函数内部一次性完成**「new 内存」和「构造智能指针」** ,返回一个完整的shared_ptr。哪怕后续risky_func()抛异常,这个临时智能指针会被自动析构,内存被正确释放,不会泄漏。- 如果先执行
risky_func():抛异常后,make_shared根本不会执行,没有内存分配,自然也不会泄漏。
底层核心原理
//简易版 make_unique 实现(仅单个对象)
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
make_xxx把「new 内存」和「构造智能指针」封装在函数内部, 相当于 "绑定成了一步",中间不会插入其它代码,从根源上解决了所有场景的异常安全问题。
(3)make_shared 独有优势:一次性内存分配,性能提升
make_shared除了解决异常安全问题,还额外优化了「对象 + 控制块」的内存分配 ,效率更高,是创建shared_ptr的首选方式。
shared_ptr 的底层依赖控制块 (存储引用计数、弱引用计数、删除器等),手动写法和 make_shared 的内存分配差异巨大:
shared_ptr<T>(new T):需要两次独立的内存分配。
- 第一次
new T:分配对象本身的内存- 第二次
shared_ptr内部:分配控制块的内存make_shared<T>():仅需**一次内存分配,**一次性分配一块连续内存,同时容纳对象本身和控制块。
优势:
- 性能更高 :减少一次 new/delete 调用, 降低内存分配开销,同时减少堆内存碎片。
- 缓存更友好 :对象和控制块在连续内存中,CPU 只需加载一次缓存行,即可同时访问对象和引用计数,大幅提升缓存命中率。
8.4 注意事项与局限性
1. 无法使用自定义删除器
make_unique和make_shared均不支持传入自定义删除器。如果需要自定义释放逻辑,必须放弃工厂函数,手动使用「new + 智能指针构造函数」。
2. make_shared 内存延迟释放问题
这是make_shared 性能优化带来的"副作用",在特定场景下需要注意:
由于
make_shared将对象和控制块分配在同一块内存上 ,即使强引用计数归 0(对象已析构) ,只要还有std::weak_ptr观察(弱引用计数不为 0),整块内存就无法完全释放,只有当强、弱引用计数全部归 0 时,整块内存才会被释放,导致 "对象已析构但内存未归还" 的伪内存泄漏现象。
此类场景避免使用make_shared,直接用裸指针构造shared_ptr,让对象和控制块分开分配,对象析构后可立即释放对象内存,仅保留控制块内存。
8.5 最佳实践
优先使用 make_unique 和 make_shared, 除非有特殊需求(如自定义删除器、处理 make_shared 的控制块延迟问题),否则永远优先用它们创建智能指针。
9. shared_ptr 循环引用问题
std::shared_ptr通过强引用计数 管理对象生命周期,只有当强引用计数归 0 时,才会析构对象并释放内存。 但当两个或多个shared_ptr管理的对象,互相持有对方的shared_ptr成员 ,形成一个 "引用闭环 ",每个对象的强引用计数都至少为 1,永远无法归 0,导致对象永远不会被释放,内存泄漏。
代码示例(循环引用导致内存泄漏):
//双向链表节点类
class ListNode
{
public:
int _data;
// ❌ 致命设计:next 和 prev 都用 shared_ptr,会形成循环引用
std::shared_ptr<ListNode> _next; // 强引用:持有下一个节点,增加其强引用计数
std::shared_ptr<ListNode> _prev; // 强引用:持有上一个节点,增加其强引用计数
ListNode(int data = 0)
: _data(data)
{
//打印,方便追踪具体节点的构造
cout << "ListNode: " << _data << " 构造" << endl;
}
~ListNode()
{
//打印,方便追踪具体节点的析构
cout << "ListNode: " << _data << " 析构" << endl;
}
};
int main()
{
// 创建两个节点,shared_ptr变量 n1 和 n2 分别持有节点的所有权
std::shared_ptr<ListNode> n1(new ListNode(10));
std::shared_ptr<ListNode> n2(new ListNode(20));
// 构建双向链表,形成循环引用
n1->_next = n2; // n1 的 _next 强引用 n2 → n2 的强引用计数 +1(变为 2)
n2->_prev = n1; // n2 的 _prev 强引用 n1 → n1 的强引用计数 +1(变为 2)
cout << n1.use_count() << endl; // 输出 2(n1自身 + n2->prev)
cout << n2.use_count() << endl; // 输出 2(n2自身 + n1->next)
return 0;
}
// 离开main作用域后的释放流程:
// 1. n2 析构:
// - n2 释放对节点 20 的所有权 → 节点 20 的强引用计数 -1 → 变为 1(仍被 n1->_next 持有)
// - 节点 20 暂不析构//
// 2. n1 析构:
// - n1 释放对节点 10 的所有权 → 节点 10 的强引用计数 -1 → 变为 1(仍被 n2->_prev 持有)
// - 节点 10 暂不析构
//
// 最终结果:节点 10 和节点 20 互相持有对方的强引用,强引用计数永远为 1,
// 两个节点都无法被析构,内存彻底泄漏!
运行结果: 可以看到,两个节点的析构函数从未被调用,内存彻底泄漏!

循环引用内存布局图解:

10. weak_ptr(弱引用智能指针)
std::weak_ptr是 C++ 标准专为辅助shared_ptr设计的弱引用智能指针 ,主要目的是**解决shared_ptr可能导致的循环引用问题,**核心定位是「观察对象,而非拥有对象」。
10.1 核心特性
- 不增加强引用计数 :仅 "观察 " 对象,不影响
shared_ptr管理的对象的生命周期,这是打破循环的核心;- 无对象所有权 :无法直接解引用(
*wp)、箭头访问(wp->)对象,必须通过lock()方法获取有效的shared_ptr才能访问;- 共享控制块 :与关联的
shared_ptr共享同一个控制块,可安全检测对象是否已被释放;- 线程安全:所有状态检查与转换操作均为原子操作,适配多线程场景。
10.2 用 weak_ptr 打破循环引用
我们将其中一个方向的引用(比如 prev)从 shared_ptr 改为 weak_ptr,即可彻底打破循环:
//双向链表节点类
class ListNode
{
public:
int _data;
// ✅ 修正设计:_prev用 weak_ptr(仅观察)
std::shared_ptr<ListNode> _next;//强引用:持有下一个节点的所有权,增加其强引用计数
std::weak_ptr<ListNode> _prev; //弱引用:仅观察前驱节点,不增加其强引用计数
ListNode(int data = 0)
: _data(data)
{
cout << "ListNode " << " 构造" << endl;
}
~ListNode()
{
cout << "ListNode " << " 析构" << endl;
}
};
int main()
{
// 创建两个节点,shared_ptr变量 n1 和 n2 分别持有节点的所有权
std::shared_ptr<ListNode> n1(new ListNode(10));
std::shared_ptr<ListNode> n2(new ListNode(20));
// 构建双向链表关系
n1->_next = n2; // n1 的 _next 强引用 n2 → n2 的强引用计数 +1(变为 2)
n2->_prev = n1; // n2 的 _prev 弱引用 n1 → n1 的强引用计数不变(仍为 1)
//打印当前强引用计数
cout << n1.use_count() << endl; // 输出 1(仅n1持有)
cout << n2.use_count() << endl; // 输出 2(n2 + n1->next)
return 0;
}
// 离开main作用域后的释放流程:
// 1. n2 析构:
// - n2 释放对节点 20 的所有权 → 节点 20 的强引用计数 - 1 → 变为 1(仍被 n1->_next 持有)
// - 此时节点 20 暂不析构
// 2. n1 析构:
// - n1 释放对节点 10 的所有权 → 节点 10 的强引用计数 -1 → 变为 0
// - 节点 10 开始析构,其成员 _next(shared_ptr)也随之析构
// - _next 析构 → 释放对节点 20 的所有权 → 节点 20 的强引用计数 -1 → 变为 0
// - 节点 20 析构
//
// 最终结果:节点 10 和节点 20 均被正确析构,无内存泄漏!
运行结果: 可以看到两个节点的析构函数都被正确调用,内存泄漏问题彻底解决。

修复后内存布局图解:

10.3 常用方法
| 操作 | 功能 | 说明 |
|---|---|---|
weak_ptr<T> wp(sp) |
构造 | 从 shared_ptr 构造 weak_ptr,不增加强引用计数 |
wp = sp |
赋值 | 从 shared_ptr 赋值给 weak_ptr,不增加强引用计数 |
wp.expired() |
判断过期 | 检测观察的对象是否已释放 (等价于 use_count() == 0),返回 true 表示对象已析构 |
wp.lock() |
获取对象 | 获取观察对象的 shared_ptr,若对象存在,返回有效的 shared_ptr;否则返回空 |
wp.reset() |
重置 | 清空 weak_ptr,停止观察,不影响对象生命周期 |
lock() 函数
lock() 是 weak_ptr 唯一能安全访问对象的方式
weak_ptr不能直接解引用(*wp或wp->),必须通过lock()获取shared_ptr后才能访问,且访问前必须判空,避免对象已释放。使用lock()访问对象必须遵循**"先转换,再判空,后访问"**的三步流程,缺一不可:
- 调用
wp.lock()获取shared_ptr;- 检查返回的
shared_ptr是否为空(if (sp)或if (sp != nullptr));- 只有判空通过后,才能通过
shared_ptr访问对象。
函数行为
lock函数会检查weak_ptr指向的对象是否存活(即检查强引用计数),如果>0,对象存活,它会立刻将引用计数+1 ,并返回一个有效的shared_ptr,如果=0,对象死了,返回一个空的shared_ptr。
基本使用:
int main()
{
weak_ptr<int> wp;
{
auto sp = make_shared<int>(40);
wp = sp; // weak_ptr观察sp
//对象存在时lock()返回有效shared_ptr
if (shared_ptr<int> temp = wp.lock())
{
cout << "对象存在,值为: " << *temp << endl;
}
}//sp析构,对象释放
//对象释放后,lock()返回空的shared_ptr
if (shared_ptr<int> temp = wp.lock()) //先调用lock()赋值给sp,然后if(...)会判断temp是否为空
{
cout << "对象存在,值为: " << *temp << endl;
}
else
{
cout << "对象已释放" << endl;
}
return 0;
}
expired() 函数
expired()的唯一作用是快速判断对象是否已释放 ,它等价于wp.use_count() == 0,但更高效,返回true表示对象已销毁,返回false,表示对象可能还存活。
如果只是想判断对象是否存活,建议使用 wp.expired(),如果既要判断存活又要安全访问对象,直接使用 wp.lock()。
10.4 模拟实现

//weak_ptr模拟实现
template<class T>
class weak_ptr
{
public:
//从shared_ptr构造
weak_ptr(const shared_ptr<T>& sp)
{
cb = sp.cb;
if (cb)
cb->weak_count++;
}
//拷贝构造
weak_ptr(const weak_ptr<T>& wp)
{
cb = wp.cb;
if (cb)
cb->weak_count++;
}
//析构函数
~weak_ptr()
{
if (--cb->weak_count == 0 && cb->strong_count == 0)
delete cb;
}
shared_ptr<T> lock()const
{
if (!cb || cb->strong_count == 0)
return shared_ptr<T>();//返回空shared_ptr
shared_ptr<T> sp;
sp.ptr = cb->ptr;
sp.cb = cb;
cb->strong_count++; //引用计数+1
return sp; //返回有效shared_ptr
}
private:
control_block<T>* cb; //控制块指针
};
11. 总结
选择原则
- 优先用
unique_ptr:若对象不需要共享所有权,unique_ptr零开销、更安全,是默认选择。- 仅在需要共享时用
shared_ptr:当对象有多个明确的所有者时,才使用shared_ptr,接受其固定开销。- 用
weak_ptr打破循环引用 :shared_ptr循环引用会导致内存泄漏,需用weak_ptr(弱引用,不增加计数)解决。
避坑点
- 禁止用同一个裸指针初始化多个智能指针:会导致双重释放。
- 禁止
deleteget()/release()以外的裸指针 :get()返回的裸指针仅用于接口交互,不要手动释放。unique_ptr转移所有权后不要访问原指针 :move()后原unique_ptr置空,访问会导致未定义行为。
