服务器设计 之 【定时器、时间轮、智能指针的简介与使用】
timerfd_create —— 创建定时器文件描述符项目说明函数原型头文件功能创建一个定时器对象,返回一个关联的文件描述符,用于后续的定时设置和事件监听返回值成功返回文件描述符(非负整数),失败返回-1并设置errno参数1:clockid指定时间基准源:•:系统实时时间,受系统时间修改影响•:单调时间,从开机开始计时,不受系统时间修改影响参数2:flags设置文件描述符属性(可位或组合):•
目录
1.定时器接口介绍
timerfd 是 Linux 提供的一种定时器文件描述符,它把定时器抽象成了一个可读的文件描述符
核心思想:当定时器超时时,对应的文件描述符变为“可读”,配合 epoll/select/poll 等 I/O 多路复用机制,可以将定时器事件统一到事件循环中处理,避免了传统 signal 或独立线程定时方式的复杂性
- timerfd_create —— 创建定时器文件描述符
| 项目 | 说明 |
|---|---|
| 函数原型 | int timerfd_create(int clockid, int flags); |
| 头文件 | <sys/timerfd.h> |
| 功能 | 创建一个定时器对象,返回一个关联的文件描述符,用于后续的定时设置和事件监听 |
| 返回值 | 成功返回文件描述符(非负整数),失败返回 -1 并设置 errno |
参数1: clockid |
指定时间基准源: • CLOCK_REALTIME:系统实时时间,受系统时间修改影响• CLOCK_MONOTONIC:单调时间,从开机开始计时,不受系统时间修改影响 |
参数2: flags |
设置文件描述符属性(可位或组合): • 0:默认阻塞模式• TFD_NONBLOCK:非阻塞模式• TFD_CLOEXEC:exec 时自动关闭 |
| 使用示例 | int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); |
| 注意事项 | 创建后 fd 需要像普通文件一样用 close() 关闭,否则会泄露 |
- timerfd_settime —— 设置/启动定时器
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};
struct itimerspec {
struct timespec it_interval; /* Interval for periodic timer */
struct timespec it_value; /* Initial expiration */
};
| 项目 | 说明 |
|---|---|
| 函数原型 | int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); |
| 头文件 | <sys/timerfd.h> |
| 功能 | 启动(或修改)由 fd 指定的定时器,设置首次超时时间和周期性间隔 |
| 返回值 | 成功返回 0,失败返回 -1 并设置 errno |
参数1: fd |
timerfd_create 返回的文件描述符 |
参数2: flags |
时间解释方式: • 0:相对时间,new_value 相对于当前时刻• TFD_TIMER_ABSTIME:绝对时间,new_value 是绝对时间戳 |
参数3: new_value |
struct itimerspec 结构,指定新的超时设置:• it_value:首次超时时间(设为 {0,0} 表示停止定时器)• it_interval:后续周期性超时间隔(设为 {0,0} 表示单次触发) |
参数4: old_value |
若非 NULL,返回之前的定时设置,可用于恢复;不需要则传 NULL |
| 使用示例 | struct itimerspec ts = { .it_value = {2,0}, .it_interval = {1,0} };timerfd_settime(fd, 0, &ts, NULL); // 2秒后首次,之后每秒一次 |
| 注意事项 | 如果 fd 已经在 epoll 中监听,调用此函数修改定时时间不会影响 epoll 注册状态 |
- read —— 读取超时事件(定时器专用语义)
| 项目 | 说明 |
|---|---|
| 函数原型 | ssize_t read(int fd, void *buf, size_t count);(标准 POSIX 接口,此处专用于 timerfd) |
| 头文件 | <unistd.h> |
| 功能 |
从 timerfd 文件描述符读取超时事件 若定时器尚未超时,调用将阻塞(取决于是否设为非阻塞);超时后读取成功 |
| 返回值 | 成功返回读取的字节数(固定为 8,每次超时都自增),失败返回 -1 并设置 errno |
参数1: fd |
timerfd_create 返回的文件描述符 |
参数2: buf |
必须指向一个 uint64_t 类型的变量地址 |
参数3: count |
必须为 sizeof(uint64_t),即 8 字节 |
| 读取内容的含义 | 返回的 uint64_t 数值表示:自上次成功 read 以来,该定时器累计超时的次数 |
| 阻塞行为 | • 默认(未设 TFD_NONBLOCK):无超时事件时阻塞当前线程• 非阻塞(设 TFD_NONBLOCK):无事件时返回 -1,errno 为 EAGAIN |
| 使用示例 | uint64_t exp;ssize_t n = read(fd, &exp, sizeof(exp));if (n == sizeof(uint64_t)) { /* 处理 exp 次超时 */ } |
| 与 epoll 配合 | 当定时器超时时,fd 变为可读状态,触发 epoll 的 EPOLLIN 事件 |
| 注意事项 | 必须使用 uint64_t 缓冲区,且必须读取完整的 8 字节;读取后内核会清零累计计数 |
2.时间轮
- 问题起源:O(n) 遍历的痛点
在管理大量长连接时,若每次定时检查都遍历所有连接判断是否超时,绝大部分未超时的连接会被无意义地重复检查,当连接数达到十万、百万级别时,这种 O(n) 的线性扫描会严重消耗 CPU 资源,成为系统的性能瓶颈
- 小根堆方案
将所有连接按最近通信时间戳构建一个小根堆,每次只需检查堆顶元素是否到期即可,插入和删除的时间复杂度为 O(log n),相比全量遍历大幅提升了效率,但在超高频的定时刷新场景下,堆的调整开销仍然存在
- 时间轮核心思想

时间轮借鉴钟表原理,将一个环形数组划分为若干槽位,每个槽位挂载一个任务数组或链表(支持同一时刻添加多个定时任务),指针每秒前进一格,走到哪个槽就执行该槽上的所有到期任务,添加任务时根据超时时间直接 O(1) 定位到对应槽位,无需遍历也无需堆调整
- 单层时间轮的局限
单层时间轮的槽位数量决定了能表示的最大定时范围,如果需要支持 1 小时的定时,就需要 3600 个槽位,因此在超时范围较大时单层结构内存占用会线性膨胀
- 多层时间轮
多层时间轮通过"时针轮→分针轮→秒针轮"的层级结构,将大范围定时任务先存入上层轮,随时间推移逐级降级到下层轮,最终在秒针轮执行,这样只需 24 + 60 + 60 = 144 个槽位即可覆盖 24 小时的定时范围,兼顾了内存效率和 O(1) 操作复杂度
-
智能指针自动取消机制
将定时任务封装成一个类,在其析构函数中编写实际的超时处理逻辑(如关闭连接),每次设置或刷新超时时向时间轮投递一个 shared_ptr 指向该任务对象,由于多个 shared_ptr 共享同一对象,只有当最后一个 shared_ptr 被时间轮释放时引用计数归零,析构函数才会真正执行,从而自动实现了"旧任务被新任务覆盖即失效"的效果,无需手动删除或标记
- 总结
时间轮配合智能指针析构的定时任务方案,既利用时间轮的 O(1) 插入和检查特性保证了高并发下的性能,又借助 shared_ptr 的引用计数机制巧妙地解决了超时刷新时旧任务自动失效的问题
#include <iostream>
#include <cstdint>
#include <functional>
#include <vector>
#include <memory>
#include <unordered_map>
#include <unistd.h>
//将定时任务封装成一个类
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:
uint64_t _id; //定时任务编号
uint32_t _timeout; //超时时间
TaskFunc _task_cb; //定时任务函数
ReleaseFunc _release;//销毁映射关系
bool _canceled; //false表示没有取消任务
public:
//传引用传参:降低拷贝开销
TimerTask(uint64_t id, uint32_t timeout, const TaskFunc& cb) :_id(id), _timeout(timeout), _task_cb(cb), _canceled(false) {}
//对象析构时执行定时任务,同时销毁映射关系
~TimerTask()
{
//未被取消才执行任务
if(!_canceled) _task_cb();
//都要执行消除操作
_release();
}
void SetRelease(const ReleaseFunc& cb) { _release = cb; }
uint32_t GetTimeout() { return _timeout; }
void Cancel() { _canceled = true; }
};
class TimeWheel
{
private:
using WeakPtr = std::weak_ptr<TimerTask>; //防止后续裸构造shared_ptr,导致资源被多次释放
using TaskPtr = std::shared_ptr<TimerTask>; //管理定时任务对象
int _tick; //秒针
int _capacity; //总容量
std::vector<std::vector<TaskPtr>> _wheel; //二维数组的元素是定时任务对象的智能指针
std::unordered_map<uint64_t, WeakPtr> _timers;//根据编号查找定时任务是否存在,同时使用weakptr保证任务对象可被析构
private:
void RemoveTimer(uint64_t id)
{
if(_timers.count(id))
_timers.erase(id);
}
public:
TimeWheel():_tick(0), _capacity(60), _wheel(_capacity){}
~TimeWheel(){}
//添加任务
void TaskAdd(uint64_t id, uint32_t timeout, const TaskFunc& cb)
{
//构建智能指针,添加进时间轮中
TaskPtr pt(new TimerTask(id, timeout, cb));
pt->SetRelease(std::bind(&TimeWheel::RemoveTimer, this, id));
uint32_t pos = (_tick + timeout) % _capacity;
_wheel[pos].push_back(pt);
//建立映射关系
_timers[id] = WeakPtr(pt);
}
//刷新任务
void TaskRefresh(uint64_t id)
{
if(_timers.count(id))
{
TaskPtr pt = _timers[id].lock();//weak进行构造
uint32_t pos = (pt->GetTimeout() + _tick) % _capacity;
_wheel[pos].push_back(pt);
}
}
//秒针移动
void RunTimerTask()
{
_wheel[_tick].clear();
_tick = (_tick + 1) % _capacity;
}
void Cancel(uint64_t id)
{
if(_timers.count(id))
{
TaskPtr pt = _timers[id].lock();
pt->Cancel();
}
}
};
3.智能指针的使用及起细节
3.1.总览
| 特性 | std::unique_ptr |
std::shared_ptr |
std::weak_ptr |
|---|---|---|---|
| 所有权模型 | 独占所有权 | 共享所有权 | 非拥有(弱)引用 |
| 核心机制 | 禁止拷贝,仅支持移动 | 引用计数 | 观察shared_ptr管理的对象 |
| 内存与性能开销 | 零开销(仅封装一个裸指针) | 有开销(分配控制块,维护原子操作的引用计数) | 极小开销(仅存储一个指向控制块的指针) |
| 主要用途 | 管理单一所有者、PImpl惯用法、工厂函数返回值 | 多组件共享资源、跨线程共享数据 | 打破循环引用、缓存观察者、观察对象生命周期 |
3.2.std::unique_ptr
std::unique_ptr的核心是“独占”,它不允许被拷贝,但所有权可通过std::move转移。其大小通常与裸指针相同,没有额外开销
- 构造与析构
| 接口 | 说明 |
|---|---|
constexpr unique_ptr() noexcept; |
默认构造,创建空的unique_ptr |
explicit unique_ptr(pointer p) noexcept; |
从裸指针构造,接管所有权 |
unique_ptr(unique_ptr&& u) noexcept; |
移动构造,转移所有权,源指针变空 |
~unique_ptr(); |
若非空,调用删除器释放资源 |
注意:拷贝构造和拷贝赋值均被删除(
= delete),确保独占所有权
-
赋值操作符
| 接口 | 说明 |
|---|---|
unique_ptr& operator=(unique_ptr&& u) noexcept; |
移动赋值,先释放当前资源,再接管u的资源 |
unique_ptr& operator=(nullptr_t) noexcept; |
等价于reset(),释放当前资源 |
-
修改器
| 接口 | 说明 |
|---|---|
pointer release() noexcept; |
放弃所有权,返回裸指针并将自身置空 调用者需手动管理返回的指针 |
void reset(pointer p = pointer()) noexcept; |
替换管理的对象 先用删除器销毁当前对象(如果有),然后接管 |
void swap(unique_ptr& other) noexcept; |
交换两个unique_ptr管理的指针和删除器 |
-
观察器
| 接口 | 说明 |
|---|---|
pointer get() const noexcept; |
返回存储的裸指针,不放弃所有权 |
explicit operator bool() const noexcept; |
检查是否非空(是否有管理对象) |
T& operator*() const; |
解引用,获得对象引用(要求非空,否则行为未定义) |
pointer operator->() const noexcept; |
返回裸指针,用于成员访问 |
T& operator[](size_t i) const; |
仅对数组特化有效,访问数组第i个元素 |
- 数组特化 (
std::unique_ptr<T[]>)
当模板参数为T[]时,提供operator[]代替operator*和operator->,且删除器默认调用delete[]
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
arr[0] = 10; // 使用 operator[]
- 自定义删除器
删除器作为第二个模板参数,类型需为可调用对象
// 函数指针类型
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("a.txt", "r"), &fclose);
- 非成员函数
| 接口 | 说明 |
|---|---|
std::make_unique<T>(Args&&... args) |
(C++14) 创建 异常安全,推荐使用 |
std::make_unique<T[]>(size_t size) |
(C++14) 创建管理数组的unique_ptr<T[]> |
bool operator==(const unique_ptr& x, nullptr_t) 等 |
与nullptr比较 |
void swap(unique_ptr& a, unique_ptr& b) noexcept; |
特化的std::swap |
3.3.std::shared_ptr
std::shared_ptr允许多个智能指针共享同一对象,通过“引用计数”来管理生命周期
- 构造与析构
| 接口 | 说明 |
|---|---|
constexpr shared_ptr() noexcept; |
默认构造,空指针 |
explicit shared_ptr(Y* p); |
从裸指针构造,创建控制块,引用计数初始为1 |
shared_ptr(const shared_ptr& r) noexcept; |
拷贝构造,引用计数+1 |
shared_ptr(shared_ptr&& r) noexcept; |
移动构造,转移所有权,r变空,引用计数不变 |
template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept; |
从派生类指针构造(协变) |
template<class Y> shared_ptr(const weak_ptr<Y>& r); |
从weak_ptr提升构造,若weak_ptr已过期则抛出std::bad_weak_ptr |
template<class Y, class D> shared_ptr(Y* p, D d); |
使用自定义删除器d |
shared_ptr(std::nullptr_t) noexcept; |
构造空shared_ptr |
~shared_ptr(); |
引用计数-1,若变为0则释放资源 |
只有从已有 shared_ptr / weak_ptr 间接构造才共享计数;
直接包裸指针、新建、移动均为独立控制块
| 构造方式 | 是否共享控制块 & 引用计数 | 说明 |
|---|---|---|
拷贝已有的 shared_ptr |
共享 | 计数 + 1,同一块资源 |
weak_ptr::lock() 提升 |
共享 | 安全观测,同一块资源 |
| 别名构造(alias 构造) | 共享 | 共享生命周期,指向不同子对象 |
| 从裸指针直接构造 | 独立 | 新建控制块,易造成重复释放 |
单独 make_shared |
独立 | 全新对象 + 全新控制块 |
移动构造(std::move) |
不增不减 | 所有权转移,计数不变 |
从 unique_ptr 移动构造 |
新建 | 转移所有权并新建控制块 |
-
赋值操作符
| 接口 | 说明 |
|---|---|
shared_ptr& operator=(const shared_ptr& r) noexcept; |
拷贝赋值,先递减当前计数,再递增r的计数 |
shared_ptr& operator=(shared_ptr&& r) noexcept; |
移动赋值,所有权转移 |
shared_ptr& operator=(std::nullptr_t) noexcept; |
释放当前资源(reset()) |
-
修改器
| 接口 | 说明 |
|---|---|
void reset() noexcept; |
释放所有权,引用计数-1 |
template<class Y> void reset(Y* p); |
接管新指针p,释放旧资源 |
template<class Y, class D> void reset(Y* p, D d); |
带删除器接管新指针 |
void swap(shared_ptr& other) noexcept; |
交换两个shared_ptr(包括指针和控制块) |
-
观察器
| 接口 | 说明 |
|---|---|
element_type* get() const noexcept; |
返回裸指针 |
long use_count() const noexcept; |
返回强引用计数(即当前shared_ptr的副本数) |
explicit operator bool() const noexcept; |
是否非空 |
T& operator*() const noexcept; |
解引用(要求非空) |
T* operator->() const noexcept; |
成员访问 |
-
别名构造函数
template<class Y> shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;
功能:创建一个与
r共享控制块(即生命周期与r相同),但指向不同对象p的shared_ptr常用于指向某个对象的成员变量,并保证该对象在成员被访问时仍存活
struct Bar { int data; };
auto foo = std::make_shared<Bar>();
std::shared_ptr<int> alias(foo, &foo->data); // 生命周期与foo绑定
-
非成员函数
| 接口 | 说明 |
|---|---|
std::make_shared<T>(Args&&... args) |
创建shared_ptr<T>,一次分配对象和控制块,高效且异常安全 |
std::allocate_shared<T>(Alloc alloc, Args&&... args) |
使用自定义分配器创建 |
bool operator== 等比较运算符 |
支持与nullptr、其他shared_ptr比较 |
template<class T, class U> shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r); |
静态转换智能指针类型 |
template<class T, class U> shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r); |
动态转换(运行时类型检查) |
template<class T, class U> shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r); |
常量性转换 |
template<class T, class U> shared_ptr<T> reinterpret_pointer_cast(const shared_ptr<U>& r); |
重新解释转换(C++17) |
3.4.std::weak_ptr
std::weak_ptr是shared_ptr的“观察者”,它不增加引用计数,专门用来打破shared_ptr间的循环引用
-
构造与赋值
| 接口 | 说明 |
|---|---|
constexpr weak_ptr() noexcept; |
默认构造,空指针 |
weak_ptr(const weak_ptr& r) noexcept; |
拷贝构造,弱引用计数+1 |
weak_ptr(const shared_ptr<Y>& r) noexcept; |
从shared_ptr构造,弱引用计数+1,强引用计数不变 |
weak_ptr(weak_ptr&& r) noexcept; |
移动构造 |
~weak_ptr(); |
弱引用计数-1,若控制块的强、弱计数均为0,则销毁控制块 |
-
赋值操作符
| 赋值操作符 | 行为说明 |
|---|---|
weak_ptr& operator=(const weak_ptr& r) noexcept; |
拷贝赋值 先减少当前指针指向控制块的弱引用计数 再增加 r 指向控制块的弱引用计数更新内部指针指向 r 所观察的对象 |
weak_ptr& operator=(weak_ptr&& r) noexcept; |
移动赋值 先减少当前控制块的弱引用计数 将 r 的内部状态直接转移给 *this,弱引用计数不变操作后 r 变为空(expired() 为 true) |
weak_ptr& operator=(const shared_ptr<Y>& r) noexcept; |
从 shared_ptr 赋值先减少当前控制块的弱引用计数 增加 r 指向控制块的弱引用计数开始观察 r 管理的对象,强引用计数不变 |
weak_ptr& operator=(std::nullptr_t) |
置空赋值 效果同 reset(),释放当前观察,弱引用计数减一 |
注:当控制块的强引用计数和弱引用计数均归零时,控制块所占内存会被自动释放(由 shared_ptr 或最后一个 weak_ptr 的析构触发)
-
修改器
| 接口 | 说明 |
|---|---|
void reset() noexcept; |
清空weak_ptr,弱引用计数-1 |
void swap(weak_ptr& r) noexcept; |
交换 |
-
观察器
| 接口 | 说明 |
|---|---|
long use_count() const noexcept; |
返回强引用计数(管理对象的shared_ptr数量) |
bool expired() const noexcept; |
检查对象是否已销毁(即use_count() == 0) |
std::shared_ptr<T> lock() const noexcept; |
最关键的方法。尝试提升为shared_ptr。若expired()为true,返回空shared_ptr;否则返回一个共享所有权的shared_ptr |
-
非成员函数
std::weak_ptr支持与shared_ptr、nullptr的比较运算符(==、!=),以及std::swap特化。
-
辅助接口:
std::enable_shared_from_this<T>
当一个类需要安全地获取自身的
shared_ptr时(例如在成员函数中返回一个指向自己的shared_ptr),应公有继承此类
| 成员函数 | 说明 |
|---|---|
shared_ptr<T> shared_from_this(); |
返回一个与当前对象共享所有权的shared_ptr<T>。前提:该对象必须已被某个shared_ptr管理,否则行为未定义(C++17前抛出std::bad_weak_ptr) |
weak_ptr<T> weak_from_this() noexcept; |
(C++17) 返回一个weak_ptr,效果同shared_from_this()但无需立即提升 |
- enable_shared_from_this 通过在对象内部存储一个指向控制块的弱指针来工作
- 当调用 shared_from_this() 时,从该弱指针提升(lock),返回一个新的 shared_ptr
- 由于所有提升操作都指向同一个控制块,因此只会增加引用计数,不会创建新的控制块
- 这保证了对象的所有权管理是统一且正确的
shared_from_this() 的工作原理是:通过一个内部弱引用 weak_ptr 来生成 shared_ptr
但这个弱引用必须等到至少有一个 shared_ptr 管理该对象时才会被初始化
使用示例:
class Good : public std::enable_shared_from_this<Good> {
public:
std::shared_ptr<Good> getShared() {
return shared_from_this(); // 安全
}
// 错误做法: return std::shared_ptr<Good>(this);
//用同一个 this 构造了独立的智能指针,产生多个控制块,导致双重释放
};
3.5.注意事项
- unique_ptr 没有 use_count(),因为它独占,计数恒为1或0
- weak_ptr::lock() 是原子操作,开销与shared_ptr拷贝相当,但它是安全访问对象的唯一途径
- 避免从this直接构造shared_ptr,除非使用了enable_shared_from_this
3.6.三大智能指针的控制块
控制块是 shared_ptr 和 weak_ptr 实现共享所有权与弱观察的核心数据结构,它独立于被管理对象,存储生命周期管理所需信息(指向相同的两种指针共用同一个控制块)
unique_ptr 没有控制块,因为它独占且无引用计数
-
控制块的结构与内容
一个典型的控制块包含以下字段(具体实现可不同):
| 字段 | 说明 |
|---|---|
| 强引用计数 | 当前指向该对象的 shared_ptr 实例数量。归零时销毁被管理对象。 |
| 弱引用计数 | 当前指向该对象的 weak_ptr 实例数量。注意:弱引用计数通常包含强引用计数的一个隐含份额(如强弱计数总和 = shared_count + weak_count + 1 标记位),以确保控制块自身在最后一个 shared_ptr 析构后仍能为 weak_ptr 提供 expired() 查询,直到所有 weak_ptr 也析构。 |
| 删除器 | 可调用对象,用于销毁被管理对象(类型擦除存储)。 |
| 分配器 | 用于分配/释放控制块内存(若使用 allocate_shared)。 |
| 被管理对象的指针 | 可能不同于 shared_ptr::get() 返回的指针(例如存在虚继承或别名构造时)。 |
-
控制块的创建时机
| 创建方式 | 控制块分配次数 | 特点 |
|---|---|---|
std::make_shared<T>(...) |
一次分配 | 将对象内存和控制块内存在一块连续内存中分配,效率高、缓存友好。但对象内存与控制块绑定,即使强引用计数归零、弱引用计数仍存在时,对象所占内存也无法释放(对象已析构,但内存块保留)。 |
std::shared_ptr<T>(new T(...)) |
两次分配 | 先 new 分配对象,再分配控制块。效率稍低,但对象内存可在强引用计数归零时立即释放,不受弱引用计数影响。 |
从 weak_ptr 拷贝构造/赋值 shared_ptr |
不创建新控制块 | 复用已有控制块,增加强引用计数。 |
别名构造函数 (shared_ptr(const shared_ptr<Y>&, T*)) |
不创建新控制块 | 复用源 shared_ptr 的控制块,仅改变内部存储的指针。 |
- 当强引用计数归零时:调用删除器销毁被管理对象(执行 T::~T() 或自定义删除器)
- 当弱引用计数也归零时:释放控制块自身占用的内存(调用 delete 或自定义分配器的释放操作)
- 若使用 make_shared,对象内存与控制块一同分配,因此即使强引用计数归零、对象已析构,只要还存在任何 weak_ptr,整个内存块就不会归还操作系统(但对象本身已不可访问)。对于大对象或内存敏感场景,可考虑使用 new 方式以允许对象内存在强计数归零时立即释放。
3.7.智能指针的删除器
删除器是一个可调用对象,用于在资源需要释放时执行自定义清理逻辑
默认删除器为 std::default_delete<T>,其行为是 delete ptr 或 delete[] ptr
| 智能指针 | 删除器存储位置 | 类型擦除 | 对指针类型的影响 |
|---|---|---|---|
std::unique_ptr |
智能指针对象内部(作为数据成员) | 无 | 删除器类型不同 → 不同的 unique_ptr 类型 |
std::shared_ptr |
控制块内部 | 有 | 删除器不同但 shared_ptr<T> 类型相同 |
std::weak_ptr |
不适用(无所有权) | - | - |
-
std::unique_ptr 的删除器
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr;
删除器作为 unique_ptr 的第二个模板参数,是类型的一部分
| 特性 | 说明 |
|---|---|
| 默认行为 | delete 单个对象(对 T[] 特化调用 delete[]) |
| 自定义删除器语法 | 必须显式指定删除器类型,并在构造时传入可调用对象实例 |
| 类型擦除 | 无。删除器类型是编译期固定的,不同删除器的 unique_ptr 是不同类型,不能相互赋值 |
| 常见用法 | 管理 FILE*、socket、内存池分配的内存等 |
自定义删除器示例:
// 1. 函数指针类型
void close_file(FILE* fp) { if (fp) fclose(fp); }
std::unique_ptr<FILE, decltype(&close_file)> fp1(fopen("a.txt", "r"), close_file);
// 2. 使用 Lambda(C++11 后推荐)
auto file_deleter = [](FILE* fp) { if (fp) fclose(fp); };
std::unique_ptr<FILE, decltype(file_deleter)> fp2(fopen("b.txt", "r"), file_deleter);
// 3. 函数对象
struct FileCloser { void operator()(FILE* fp) const { fclose(fp); } };
std::unique_ptr<FILE, FileCloser> fp3(fopen("c.txt", "r"));
| 写法 | 删除器类型 | 构造时是否传参 | 为什么? |
|---|---|---|---|
| 函数指针 | decltype(&close_file) |
必须传 | 类型是 void (*)(FILE*),默认构造值为 nullptr |
| Lambda | decltype(file_deleter) |
必须传 | Lambda 闭包类型默认构造被 =delete |
| 函数对象 | FileCloser |
不需要传 | 类型自身是可默认构造的空类 |
-
std::shared_ptr 的删除器
删除器不作为模板参数,而是存储在控制块中进行类型擦除
| 特性 | 说明 |
|---|---|
| 默认行为 |
注意: (C++17 后引入 |
| 自定义删除器语法 | 构造函数参数中直接传入可调用对象,无需指定模板类型 |
| 类型擦除 | 有。不同删除器的 shared_ptr 具有相同类型 shared_ptr<T>,可以互相赋值和存储在同一容器中 |
| 存储位置 | 删除器作为控制块的一部分(通常使用虚函数或函数指针实现类型擦除) |
自定义删除器示例:
// 1. Lambda(最常用)
auto deleter = [](FILE* fp) { if (fp) fclose(fp); };
std::shared_ptr<FILE> sp1(fopen("d.txt", "r"), deleter);
// 2. 函数指针
std::shared_ptr<FILE> sp2(fopen("e.txt", "r"), &fclose);
// 3. 管理动态数组(C++17 之前)
std::shared_ptr<int> sp3(new int[10], [](int* p) { delete[] p; });
// C++17 后支持数组特化
std::shared_ptr<int[]> sp4(new int[10]); // 默认调用 delete[]
-
std::weak_ptr 与删除器
weak_ptr 不拥有对象,因此不涉及删除器
当它通过 lock() 提升为 shared_ptr 时,提升后的 shared_ptr 会沿用控制块中存储的删除器
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)