41 / 80 · C++11 · 约 11 分钟
malloc 与分配器 Allocator 的职责
malloc 提供未初始化存储,失败返回空指针,必须与 free 配对;它不运行 C++ 构造函数。分配器可能缓存、分桶或向系统申请映射,具体实现并非语言保证,申请字节与构造对象应当分层理解。
字节接口不承担构造职责
malloc 按字节数申请存储,成功返回 void*,失败返回空指针。它不会调用构造函数,也不会替字符串建立有效内部状态。现代标准对某些隐式生存期类型有对象创建规则,但这不是允许跳过任意类构造的理由。显式 placement new 能清楚表达在既有存储中创建对象。
malloc 提供的对齐足以满足基本对齐要求,不能泛化成满足所有扩展对齐类型。超对齐类型应使用支持其对齐的接口。零字节请求的结果具有实现相关性,不能据非空返回值推断可以访问一个字节;最简单的业务策略是单独处理空请求。
分配器实现和系统调用不是同一层
通用分配器通常把系统取得的大块存储拆分、复用,并维护尺寸类别或空闲块。一次 malloc 不一定发起系统调用,一次 free 也不一定立即让进程驻留内存降低。缓存、碎片和操作系统提交策略都可能影响监控数字。
这些是理解性能的常见模型,不是“所有 malloc 都使用某个固定算法”的断言。内部碎片来自块大小取整等开销,外部碎片来自空闲区域难以组合。C++ allocator 则是容器可替换的分配协议,使存储来源与元素构造管理分离,默认分配器并不承诺直接调用 malloc。
把 C 资源立即放进拥有者
示例用自定义删除器把 malloc 的存储交给 unique_ptr<void, Free> 管理,在检查失败后构造一个不抛异常的 Record。先显式析构 Record,再由拥有者 free 存储;对象与存储的清理顺序因此可见,而且不会误用 delete。
需要分配 n 个元素时,先检查 n 与 sizeof(T) 相乘是否溢出,否则可能申请过小的块。调用 realloc 也要通过临时变量接收结果,失败时原块仍需保留。对普通 C++ 容器或非平凡对象,优先用 vector,别用 realloc 搬运对象表示来模拟移动。
容易答错的地方
- malloc/free 与 new/delete 不能混用;释放函数必须匹配获得资源的协议。
- free 后驻留内存未立即下降不等于泄漏;先区分仍被业务拥有、分配器缓存和真正失去释放路径。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <cstdlib>
#include <memory>
#include <new>
struct Free {
void operator()(void* p) const noexcept { std::free(p); }
};
struct Record {
int value;
explicit Record(int n) noexcept : value(n) {}
~Record() noexcept {}
};
int main() {
std::unique_ptr<void, Free> storage(std::malloc(sizeof(Record)));
if (!storage) throw std::bad_alloc();
Record* record = ::new (storage.get()) Record(42);
assert(record->value == 42);
record->~Record();
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-malloc.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为 n 个 T 计算 malloc 请求字节数,怎样避免无符号乘法回绕?
查看参考答案
先单独处理 n 为零;对非零请求检查 n > std::numeric_limits<std::size_t>::max() / sizeof(T),成立就报告长度错误,否则才计算 n * sizeof(T)。这只解决大小计算,不解决对齐、元素构造和失败回收。需要完整 C++ 元素序列时,用 vector<T> 通常比补齐所有低层步骤更合适。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。