现代 C++ 跨平台开发-内存篇:内存管理、智能指针
本文是整个【现代 C++ 跨平台开发-内存篇】系列的第 2 篇,主要涉及:内存管理、智能指针等内容。
现代 C++ 跨平台开发-内存篇:内存管理、智能指针
C 内存管理
内置的内存管理函数
malloc()
mmap:
内存映射,以页为单位分配;
容易产生缺页中断;
brk:
调整 data segment 后第一个位置(即移动堆顶指针);
容易产生内存碎片;
返回指针前,创建了 metadata header,包含:
size;in-useflag;前/后块指针(用于合并空闲块)
内存对齐信息;
malloc()不会置零,即默认是随机值:如果不能保证后续会完全写入所有单元,必须
memset(),否则读取随机值属于 UB!反之如果确认会全部写入,则不置零反而效率更高;
calloc()
会保证分配的空间全被置为 0;
效果上
malloc()+memset(0)=calloc()基本成立,但calloc()有独特优势:更高效:可直接从全零内存页映射,无需置零;
更安全:
calloc()会检查n * size是否溢出,溢出返回NULL;malloc()空间不够可能会返回较小的内存,后续memset()会越界!
自带
n参数,更适合分配数组。
realloc()
- 重新调整通过
malloc()/calloc()分配的堆内存大小。
free()
- 基于 metadata header,只是做标记,并不保证真正释放。
第三方内存分配
glibc 的 malloc 的问题
跨线程
free()效率低;倾向于不断分配,而不是复用,碎片多;
Windows malloc 的问题
- 分配速度慢。
微软的 mimalloc
基于
thread_local的 heap:更好的缓存局部性;
避免跨线程内存操作的资源竞争,或同步操作的性能;
直接放入当前线程的
free_list;通过无锁队列传递回原 owner 线程;
安全性:
防止缓冲区溢出:通过保护页隔离内存区域,无法跨越页面破坏堆元数据;
防止指针篡改:空闲列表指针使用页面密钥加密存储,无法预测或修改空闲链表;
防止 Use-After-Free:释放的内存会被保护页隔离或填充特定字节,再次访问触发错误;
防止 Double-Free:检测并忽略重复释放操作,避免堆元数据损坏;
增加攻击难度:地址随机化,分配策略随机化,难以预测内存布局。
C++ 内存管理
内存分配/释放/重用
new/delete
不仅分配/释放空间,还自动调用构造/析构函数;
分配失败会抛异常;
std::nothrow new
分配失败不抛异常而返回
nullptr;只能避免内存分配本身的异常,不能避免构造函数内部抛出的异常;
operator new/delete
单纯分配/释放资源;
内部可调用
malloc()/free(),也可直接使用栈内存;
placement new
void* operator new(std::size_t sz, void *ptr);前提:提供的内存必须对齐且足够大;
传入指针,不分配内存,只调用构造函数;不能
delete,但需要调用析构函数;原对象的生命周期会终结,内存被覆盖;如果要继续使用,需通过
std::launder;STL 中的
allocator内部使用placement new复用内存;
std::launder(C++17)
某些场景(内存复用/序列化)需要改变对象表示,之前只能强转或
reinterpret_cast(),但其实属于 UB,需要更安全的机制。
内存布局改变后(如
placement new、reinterpret_cast()),用于获取对象的合法指针;即:安全地销毁旧对象、并复用内存、原地构造新对象;顾名思义,”洗白“:绕过编译器指针溯源所做的优化假设(别管他从哪来的);
不改变内存状态,需要保证已提前构造(
placement new),否则仍是 UB。
auto data = load_file("classes.dex");
auto view = std::span<std::byte>(data);
new (view.data()) dex_header;//确保已构造
auto header = std::launder(reinterpret_cast<dex_header*>(view.data()));//洗白指针
start_lifetime_as(C++23)
不是简单封装
std::launder()+reinterpret_cast(),明确开启生命周期(方便编译器做优化);对于平凡类型,无需再
placement new构造;非平凡类型仍需std::construct_at()或placement new。
auto header = std::start_lifetime_as<dex_header>(view.data());//直接开启生命周期
allocator
提供内存管理接口(嵌入式设备没实现
malloc()/free()),将内存分配和构造隔离开:T* allocate(size_t n):分配空间来存储T的n个实例;void deallocate(T* p, size_t n):释放分配的内存;void construct(T* p, Args ...args):使用参数构造一个对象,C++20 已移除;void destroy(T* p):调用p的析构函数,C++20 已移除;
STL 模板类使用了
allocator;
OOP 内存管理
构造
父类构造(按继承声明顺序);
子类成员变量按类中声明顺序初始化;
子类构造函数体执行;
初始化列表只指定“用什么值初始化”,不改变实际初始化顺序。
析构
与构造顺序正好相反;
多态场景(父类指针指向子类对象),必须将父类析构声明为
virtual,否则会内存泄漏:RTTI 理论上能完整回溯继承树继而安全析构,但 C++ 基于零成本抽象原则,RTTI 默认并未启用,除非真正需要(多态对于继承并不是必须的);
栈上对象,不需要
delete,RAII 自动释放,不涉及到多态;虽然
Base *p = new Derived()创建时能感知子类类型,但delete p可能在很远的地方,单纯父类指针既缺乏完整类型信息,也难以感知完整上下文。
RAII
RAII 即 Resource Acquisition Is Initialization:
资源绑定对象生命周期:构造函数申请资源(不限于内存),析构函数释放资源;
作用域驱动自动清理:基于栈的作用域机制,保证对象离开作用域自动调用析构;
零成本抽象:编译期静态内联调用,无运行时开销。
作用域的本质
每个函数调用会创建一个栈帧,局部变量就分配在这个栈帧上;
所以,“作用域”本质就是“栈帧的生命周期”;“离开作用域”在底层就是“栈指针回退”。
为何不支持堆对象
栈对象地址 = 栈指针偏移 -> 生命周期由函数调用/返回隐式管理;
堆对象地址 = 动态分配 -> 生命周期与任何作用域无关,必须显式释放。
工程实践建议
优先考虑创建栈对象(避免手动管理堆内存);
利用对象封装资源管理逻辑,控制流越复杂,RAII 优势越明显(避免漏掉分支)。
并非万能
new创建的堆内存,仍然需要显式delete;析构函数不能抛异常;
多态场景(基类指针指向子类),基类虚函数必须声明为
virtual,否则基类指针必须手动转换为子类指针去释放,避免内存泄露。
智能指针
智能指针可以理解为:堆内存的 RAII 包装器。
仅限于管理堆内存,某些局部内存指针(如
JNIEnv*)不能用智能指针;内存所有权要清晰,智能指针不是 GC/ARC。
auto_ptr
- 可赋值拷贝,但赋值后原有对象再次访问会异常;已被废弃。
unique_ptr
不能拷贝,只能移动:
一般场景,独占所有权,RAII 自动释放;
作为参数,表示放弃所有权;
作为返回值,用于工厂方法;
真正的零成本开销:
基于模板的编译期 RAII 包装器+类型绑定(
deleter也是类型一部分),此外没有运行时数据结构;尽量使用空
struct或无捕获 lambda 声明deleter,可被优化为内联;移动操作是
constexpr的指针赋值;适合绝大多数场景替换裸指针,结合
deleter尤其适合自动管理 C API 相关资源;
资源释放:
reset():释放并销毁原生指针;
如果参数为一个新指针,将管理这个新指针;
release():仅释放所有权、但不销毁,返回原生指针;
用于和 C 接口交互,避免 double-free;
源码示例:
// 默认删除器,EBO
template<typename T>
struct default_delete {
constexpr default_delete() noexcept = default;
template<typename U, typename = std::enable_if_t<std::is_convertible_v<U*, T*>>>
constexpr default_delete(const default_delete<U>&) noexcept {}
void operator()(T* ptr) const {
delete ptr;
}
};
// 核心实现
template<typename T, typename Deleter = default_delete<T>>
class unique_ptr {
public:
using pointer = T*;
using element_type = T;
using deleter_type = Deleter;
private:
pointer ptr_;
// 实际标准库用 __compressed_pair 做 EBO
[[no_unique_address]] deleter_type del_;
public:
constexpr unique_ptr() noexcept : ptr_(nullptr) {}
constexpr explicit unique_ptr(pointer p) noexcept : ptr_(p) {}
constexpr unique_ptr(pointer p, const deleter_type& d) noexcept : ptr_(p), del_(d) {}
constexpr unique_ptr(pointer p, deleter_type&& d) noexcept : ptr_(p), del_(std::move(d)) {}
// 禁用拷贝
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动
constexpr unique_ptr(unique_ptr&& other) noexcept
: ptr_(other.release())
, del_(std::forward<deleter_type>(other.del_))
{}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(other.release()); // 释放当前资源,接管新指针
del_ = std::forward<deleter_type>(other.del_); // 移动删除器
}
return *this;
}
~unique_ptr() {
if (ptr_ != nullptr) {
del_(ptr_); // 调用删除器(编译期已知类型,可内联)
}
}
pointer release() noexcept {
pointer old = ptr_;
ptr_ = nullptr;
return old; // 转移所有权给调用者
}
void reset(pointer new_ptr = nullptr) noexcept {
pointer old = ptr_;
ptr_ = new_ptr;
if (old != nullptr) {
del_(old); // 销毁旧对象
}
}
explicit operator bool() const noexcept {
return ptr_ != nullptr;
}
typename std::add_lvalue_reference<T>::type operator*() const {
return *ptr_; // 解引用
}
pointer operator->() const noexcept {
return ptr_;
}
};
使用示例:
static sqlite3* allocDB(std::string_view file) {
sqlite3 *db{nullptr};
if (const auto rc = sqlite3_open_v2(std::string(file).c_str(), &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); rc != SQLITE_OK || db == nullptr) [[unlikely]] {
if (db != nullptr) {
sqlite3_close(db);
}
return nullptr;
}
return db;
}
struct DBDeleter {
void operator()(sqlite3 *db) const noexcept {
if (db != nullptr) {
sqlite3_close(db);
}
}
};
std::unique_ptr<sqlite3, DBDeleter> db{allocDB(file), DBDeleter{}};
shared_ptr
共享所有权,基于引用计数;
引入控制块,牺牲一定性能换取灵活性:
采用类型擦除,将
deleter及allocator封装在控制块,通过虚函数或函数指针在运行时调用;此外还维护引用计数、弱引用数;
占用两个指针空间:指针对象、控制块;
std::make_shared():优势:
同时创建对象和控制块,避免两次内存分配;
控制快和对象内存连续,缓存局部性更优;
合并为一次原子化的内存分配,容易保证异常安全;
不适用的场景:
构造函数私有或者声明为
delete;自定义
deleter时,因为它不属于类型的一部分,默认直接delete;重载了
operator new,同上,默认直接调用全局::operator new;
真正原子化的只有引用计数更新,成员访问需要自己处理线程安全(使用 C++20 的
std::atomic<std::shared_ptr<T>>,或通过std::atomic_load()/std::atomic_store()模拟);如果要拿到
this的shared_ptr,需要继承std::enable_shared_from_this:- 避免在构造函数中调用
share_from_this(),因为控制块还未创建。
- 避免在构造函数中调用
切勿滥用:
本质上属于内存所有权不清晰,绝大多数情况可通过调整架构设计来避免;
有一种特例,lambda 需要捕获指针,通过
shared_ptr延长生命周期。
weak_ptr
通过
shared_ptr创建,配合使用,避免循环引用;__shared_weak_count类(就是shared_ptr的控制块)维护强引用和弱引用计数:若强引用计数为 0,释放托管的指针内存;
若弱引用计数为 0,释放控制块本身;
持有回调等场景可考虑;
std::out_ptr(C++23)
- 安全地将输出型(二级)指针绑定到智能指针;
std::unique_ptr<FILE, decltype(&fclose)> file(nullptr, &fclose);
////////// 原始写法:
FILE* raw_ptr;
if (fopen_s(&raw_ptr, "test.txt", "r") == 0) {
file.reset(raw_ptr); // 手动接管
}
////////// 使用 std::out_ptr
// 无需临时裸指针,直接绑定智能指针,安全拿到二级指针
if (fopen_s(std::out_ptr(file), "test.txt", "r") == 0) {
// 无需手动接管
}
// 自动析构,无需手动 fclose
std::inout_ptr(C++23)
- 处理智能指针已有值、需要重新分配或修改内存的情况;
bool safe_realloc_string(char** buf, size_t new_size) {
if (buf == nullptr) return false;
free(*buf); // 先释放旧内存(即使 *buf == NULL 也安全)
*buf = (char*)malloc(new_size);
return (*buf) != nullptr;
}
std::unique_ptr<char[], decltype(&free)> str(nullptr, &free);
str.reset((char*)malloc(100)); // 第一次分配
//////////////////// 原始写法:
// 重分配:需要小心处理
auto temp = str.release(); // 必须用 release 转移所有权,否则 double-free!
if (safe_realloc_string(&temp, 200)) {
str.reset(temp); // 接管新内存
} else {
free(temp); // 失败时手动清理(因为 safe_realloc 可能已 free 旧内存但 malloc 失败)
}
//////////////////// 使用 std::inout_ptr
// 安全重分配,无需临时裸指针,自动 get() 后输入给临时指针
if (safe_realloc_string(std::in_out_ptr(str), 200)) {
// str 自动接管新内存
// 旧内存已被安全释放
} else {
// 失败:str 被置为 nullptr(因为 in_out_ptr 在失败时会 reset)
// 无需手动清理
}
智能指针类型转换
仅支持
std::shared_ptr,用于更新引用计数(虽产生新的对象,但仍共享控制块),避免不安全的裸指针转换导致的双重释放等内存问题。std::unique_ptr和std::weak_ptr不存在控制块和引用计数更新问题:前者可直接转换裸指针,后者可先拿到std::shared_ptr再安全转换。
std::shared_ptr<Base> base = std::make_shared<Derived>();
// 直接拿裸指针转换(危险!)
auto raw = base.get();
auto derivedRaw = static_cast<Derived*>(raw);
std::shared_ptr<Derived> derived0(derivedRaw);
// dynamic_pointer_cast:安全向下转型
auto derived1 = std::dynamic_pointer_cast<Derived>(base);
// static_pointer_cast:假设你知道类型是对的(更快,但不安全)
auto derived2 = std::static_pointer_cast<Derived>(base);
// const_pointer_cast:去除 const
auto cptr = std::make_shared<const Derived>();
auto non_const = std::const_pointer_cast<Derived>(cptr);
// reinterpret_pointer_cast(慎用!)
// auto vptr = std::reinterpret_pointer_cast<void>(base);