C++ / a working model

41 / 80   ·   C++11   ·   约 11 分钟

malloc 与分配器 Allocator 的职责

先记住这句话

malloc 提供未初始化存储,失败返回空指针,必须与 free 配对;它不运行 C++ 构造函数。分配器可能缓存、分桶或向系统申请映射,具体实现并非语言保证,申请字节与构造对象应当分层理解。

本篇内容
  1. 字节接口不承担构造职责
  2. 分配器实现和系统调用不是同一层
  3. 把 C 资源立即放进拥有者
  4. 运行示例
  5. 动手练习

字节接口不承担构造职责

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> 通常比补齐所有低层步骤更合适。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录