应使用 std::unique_ptr 管理事务日志缓冲区,绑定内存生命周期与事务状态;并发场景下用 std::shared_ptr 安全共享只读快照,杜绝裸指针导致的双重释放、越界写和悬垂指针。

用裸指针管理事务日志容易崩溃,别这么干
直接用 new 分配日志缓冲区、用 char* 指向它、手动 delete[] —— 这类写法在数据库事务日志场景下极大概率触发双重释放、越界写或悬垂指针。日志写入常并发发生,且可能跨线程回滚,裸指针无法表达所有权和生命周期边界。
真正该做的是:把日志数据的内存生命周期和事务状态绑定,而不是和指针变量绑定。
- 事务对象(如
Transaction)应持有日志缓冲区的所有权,用std::unique_ptr<:vector>></:vector>或std::unique_ptr<char></char>管理 - 若需传递日志内容给写入线程,用
std::shared_ptr<const std::vector>></const>安全共享只读快照 - 避免返回
char*或uint8_t*给调用方自行管理——这等于把内存责任踢出去,而日志模块必须对一致性负责
std::unique_ptr 是最贴近“指针语义”的安全选择
如果你确实需要类似原始指针的访问方式(比如对接 legacy 写盘函数),std::unique_ptr<char></char> 提供了 get() 方法返回 char*,同时保有自动析构能力。
示例:
立即学习“C++免费学习笔记(深入)”;
class Transaction {
std::unique_ptr<char[]> log_buffer_;
size_t log_size_ = 0;
<p>public:
void reserve_log(size_t n) {
log<em>buffer</em> = std::make_unique<char[]>(n); // 自动 zero-initialize
log<em>size</em> = n;
}</p><pre class="brush:php;toolbar:false;">char* log_data() { return log_buffer_.get(); }
const char* log_data() const { return log_buffer_.get(); }
size_t log_capacity() const { return log_size_; }};
注意:log_buffer_.get() 返回的指针仅在 Transaction 实例存活期间有效;一旦事务提交/回滚,缓冲区自动释放,任何缓存该指针的行为都属于未定义行为。
日志写入时不能依赖指针值不变
数据库日志常需预分配固定大小缓冲区(如 4KB 对齐),但事务实际日志长度是动态的。若用指针记录“当前写入位置”,必须确保该指针始终在 log_buffer_.get() 到 log_buffer_.get() + log_capacity() 范围内。
- 不要用
char* cursor = log_buffer_.get()后长期持有;应在每次写操作前校验cursor - 写入函数如
pwrite()或自定义 WAL writer 接收的是const void*, size_t,传log_buffer_.get()即可,无需额外指针运算 - 若需多次追加(如先写 header 再写 record),建议用
size_t offset记录位置,比指针算术更清晰、不易溢出
多线程写日志时,指针本身不是同步点
一个 char* 变量不是原子的,也不能防止数据竞争。即使你用 std::atomic<char></char>,也无法保证它指向的内存区域被正确同步。
正确做法是:
- 每个事务独占自己的日志缓冲区(即每个
Transaction实例拥有独立std::unique_ptr) - 日志落盘由专用 WAL writer 线程统一处理,它从线程安全队列(如
moodycamel::ConcurrentQueue或带锁的std::queue)取std::shared_ptr<const std::vector>></const> - 绝对不要让多个线程共用同一块
char[]并靠移动指针来“分配”日志空间——这是典型的 ABA 和覆盖风险源
复杂点在于:日志缓冲区的分配策略(slab / arena / mmap)、刷盘时机控制、以及崩溃恢复时如何校验日志块完整性——这些都远比“怎么用指针”关键得多。指针只是载体,别让它成为问题的起点。


















