C++ 内存与性能优化:语言特性的开销分析与替代方案

在内存和性能敏感的 C++ 系统开发中,语言特性的选择直接决定程序的内存占用和运行效率。本文针对 RTTI、异常处理、虚函数、泛型编程、值语义、动态内存分配等关键语言特性,从底层实现机制出发,深入分析其内存开销和性能代价,并提供多种高性能替代方案的对比。通过理解这些特性的权衡取舍,帮助开发者在内存效率、运行性能和代码质量之间做出明智的技术选择。

禁用 RTTI(运行时类型识别)

RTTI 提供运行时类型识别能力,但依赖 type_info 表和虚函数表,增加内存占用和运行时开销。使用编译选项 -fno-rtti 禁用 RTTI 后,typeiddynamic_caststd::any 等特性无法使用。本章介绍 RTTI 的基本概念和用法,分析其底层实现机制和性能代价,并提供编译期类型识别、自定义类型 ID、std::variant 等替代方案。

RTTI 简述

RTTI(Run-Time Type Information) 是 C++ 提供的运行时类型识别机制,允许程序在运行时查询对象的实际类型。RTTI 主要包含三个核心功能:typeid 运算符、dynamic_cast 运算符和 std::any 类型。

typeid 运算符

typeid 返回对象的类型信息,结果是 std::type_info 对象的引用:

cpp 复制代码
#include <iostream>
#include <typeinfo>
#include <memory>

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {};

int main() {
    std::unique_ptr<Base> ptr = std::make_unique<Derived>();

    // 获取类型信息
    const std::type_info& info = typeid(*ptr);
    std::cout << info.name() << std::endl;  // 输出:Derived

    // 类型比较
    if (typeid(*ptr) == typeid(Derived)) {
        std::cout << "ptr points to Derived" << std::endl;
    }

    // 自动释放,无需手动 delete
}

dynamic_cast 运算符

dynamic_cast 用于在继承层次中进行安全的向下转换(Downcast),如果转换失败返回 nullptr(指针)或抛出异常(引用):

cpp 复制代码
#include <iostream>
#include <memory>

class Base {
public:
    virtual ~Base() = default;
};

class Derived1 : public Base {
public:
    void derived1Method() {
        std::cout << "Derived1 method" << std::endl;
    }
};

class Derived2 : public Base {
public:
    void derived2Method() {
        std::cout << "Derived2 method" << std::endl;
    }
};

void process(Base* base) {
    // 尝试向下转换
    if (Derived1* d1 = dynamic_cast<Derived1*>(base)) {
        d1->derived1Method();
    } else if (Derived2* d2 = dynamic_cast<Derived2*>(base)) {
        d2->derived2Method();
    }
}

int main() {
    std::unique_ptr<Base> ptr1 = std::make_unique<Derived1>();
    std::unique_ptr<Base> ptr2 = std::make_unique<Derived2>();

    process(ptr1.get());  // 输出:Derived1 method
    process(ptr2.get());  // 输出:Derived2 method

    // 自动释放,无需手动 delete
}

std::any

std::any(C++17) 可以存储任意类型的值,使用 RTTI 在运行时识别存储的类型:

cpp 复制代码
#include <any>
#include <iostream>
#include <string>

int main() {
    // 存储不同类型的值
    std::any value;

    value = 42;
    std::cout << std::any_cast<int>(value) << std::endl;  // 输出:42

    value = 3.14;
    std::cout << std::any_cast<double>(value) << std::endl;  // 输出:3.14

    value = std::string("hello");
    std::cout << std::any_cast<std::string>(value) << std::endl;  // 输出:hello

    // 类型检查
    if (value.type() == typeid(std::string)) {
        std::cout << "value contains a string" << std::endl;
    }

    // 类型错误时抛出异常
    try {
        int x = std::any_cast<int>(value);  // value 当前是 string
    } catch (const std::bad_any_cast& e) {
        std::cout << "Bad cast: " << e.what() << std::endl;
    }
}

这些 RTTI 特性提供了灵活的运行时类型处理能力,但都依赖编译器生成的类型信息表,会带来额外的内存和性能开销。

底层实现与开销分析

RTTI 的实现依赖编译器生成的元数据。理解底层实现机制有助于量化 RTTI 的内存和性能代价。

typeid

typeid 的行为取决于类是否包含虚函数。

没有虚函数的类typeid编译期确定类型,无运行时开销:

cpp 复制代码
class SimpleClass {
    int data_;
public:
    SimpleClass(int d) : data_(d) {}
};

static_assert(sizeof(SimpleClass) == 4);

int main() {
    SimpleClass obj(42);
    const std::type_info& info = typeid(obj);

    // 编译期确定类型,等价于:
    // const std::type_info& info = typeid(SimpleClass);

    std::cout << info.name() << std::endl;  // 输出:SimpleClass
}

有虚函数的类 :对于有虚函数的类,typeid 需要在运行时通过 vptr 查找类型信息:

cpp 复制代码
class Base {
    int data_;
public:
    virtual ~Base() = default;
};

class Derived : public Base {};

int main() {
    std::unique_ptr<Base> ptr = std::make_unique<Derived>();

    // 运行时确定类型
    const std::type_info& info = typeid(*ptr);
    std::cout << info.name() << std::endl;  // 输出:Derived

    // 自动释放
}

底层实现(概念性说明,实际实现依编译器而异):

cpp 复制代码
// 编译器生成的伪代码(概念性说明):
// 1. 读取对象的 vptr
void** vptr = *(void***)ptr;

// 2. 从 vtable 读取 type_info 指针
const std::type_info* type_info_ptr = (const std::type_info*)vptr[-1];

// 3. 返回 type_info 引用
return *type_info_ptr;

内存布局:对象有 vptr,额外占用指针大小的开销(64 位系统 8 字节,32 位系统 4 字节):

cpp 复制代码
// Base 对象内存布局(64位系统):
// [vptr: 8字节][data_: 4字节][padding: 4字节]
sizeof(Base) = 16  // 8 (vptr) + 4 (data_) + 4 (padding)

// 内存布局可视化:
// +--------+--------+--------+--------+
// |  vptr  |  vptr  | data_  | data_  |
// | (低4B) | (高4B) | (4B)   | padding|
// +--------+--------+--------+--------+
// 地址: 0x00   0x04   0x08   0x0C

内存与性能对比

情况 对象内存开销 性能开销 类型确定时机
无虚函数的类 无额外开销 无开销 编译期
有虚函数的类 +8 字节(64 位系统 vptr) 2 次内存访问 运行时

dynamic_cast

dynamic_cast 只能用于有虚函数的多态类型

继承无虚函数的类(编译错误) :如果基类没有虚函数,无法使用 dynamic_cast

cpp 复制代码
class Base {};  // 没有虚函数
class Derived : public Base {};

int main() {
    std::unique_ptr<Base> ptr = std::make_unique<Derived>();
    Derived* d = dynamic_cast<Derived*>(ptr.get());  // 编译错误:Base 不是多态类型
}

继承有虚函数的类(正常工作)

cpp 复制代码
class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void derivedMethod() {}
};

int main() {
    std::unique_ptr<Base> ptr = std::make_unique<Derived>();

    // 向下转换成功
    Derived* d = dynamic_cast<Derived*>(ptr.get());  // 返回 Derived*
    if (d) {
        d->derivedMethod();
    }

    // 自动释放
}

底层实现:

cpp 复制代码
// 编译器生成的伪代码:
// 1. 获取源对象的 type_info
const std::type_info* src_type = get_type_info(ptr);

// 2. 获取目标类型的 type_info
const std::type_info* dst_type = &typeid(Derived);

// 3. 调用运行时库函数
void* result = __dynamic_cast(
    ptr,          // 源指针
    src_type,     // 源类型
    dst_type,     // 目标类型
    -1            // 偏移量
);

// 4. 比较类型是否匹配,计算指针偏移

内存与性能对比

情况 对象内存开销 性能开销 能否使用 dynamic_cast
基类无虚函数 无额外开销 - 否(编译错误)
单继承、浅层次 +8 字节(vptr) 2-3 次内存访问(vptr → type_info → 比较)
多重继承、深层继承 +8 字节(vptr) 多次内存访问(需遍历继承链、计算偏移)

多重继承和深层继承的性能开销更高的原因:

  • 深层继承:需要遍历从子类到基类的继承链,逐层比较类型信息
  • 多重继承:需要计算不同基类子对象的指针偏移量,可能涉及多个 vtable 查找

std::any

std::any 通过类型擦除技术实现类型识别,内部通过函数指针和 RTTI 配合工作,无论存储的类型是否有虚函数。

实现机制

cpp 复制代码
// std::any 的简化实现
class any {
    union Storage {
        void* heap_ptr;              // 大对象使用堆分配
        alignas(8) char buffer[16];  // 小对象使用栈缓冲区(SBO)
    } storage_;

    // 操作函数指针(通过这些函数间接访问 type_info)
    void (*destroy_)(Storage&);
    void (*copy_)(Storage&, const Storage&);
    const std::type_info& (*get_type_)();  // 获取类型信息的函数指针

public:
    template<typename T>
    any(T&& value) {
        // 为每个类型 T 生成特化的函数
        get_type_ = []() -> const std::type_info& {
            return typeid(T);  // 依赖 RTTI
        };
        // ... 其他初始化
    }

    const std::type_info& type() const {
        return get_type_();  // 通过函数指针获取 type_info
    }
};

使用示例

cpp 复制代码
std::any value = 42;  // 存储 int

// any_cast 实现
template<typename T>
T any_cast(const any& operand) {
    // 比较 type_info
    if (operand.type() != typeid(T)) {
        throw std::bad_any_cast();
    }
    return *reinterpret_cast<const T*>(&operand.storage_);
}

int x = any_cast<int>(value);  // OK
double d = any_cast<double>(value);  // 抛出 bad_any_cast

内存开销

std::any 对象本身的大小固定为 32-48 字节,包含:

  • type_info 指针(8 字节)
  • 存储空间(16-24 字节,用于小对象优化)
  • 函数指针(8-16 字节,用于析构、拷贝等操作)

存储数据时的内存分配:

  • 小对象(≤16 字节):使用内部栈缓冲区,无额外堆分配
  • 大对象(>16 字节):使用堆分配,需要额外内存
cpp 复制代码
std::any a1 = 42;                    // int (4字节),栈缓冲区
std::any a2 = std::string("hello");  // string (>16字节),堆分配

性能开销

类型检查:比较 type_info 指针(1 次指针比较)

cpp 复制代码
if (operand.type_ != &typeid(T)) {  // 指针比较
    throw std::bad_any_cast();
}

替代方案一:编译期类型识别(替代 typeid 和 dynamic_cast)

编译期类型识别通过模板和 if constexpr 在编译期确定类型,完全消除运行时开销。适合类型在编译期已知的场景。

使用 if constexpr 进行类型分发

if constexpr(C++17) 根据编译期条件选择代码分支,未选中的分支不会生成代码:

cpp 复制代码
#include <type_traits>
#include <iostream>

template<typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "Integer: " << value << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Float: " << value << std::endl;
    } else {
        std::cout << "Other type" << std::endl;
    }
}

int main() {
    process(42);      // 编译期选择第一个分支
    process(3.14);    // 编译期选择第二个分支
    process("hello"); // 编译期选择第三个分支
}

编译器为每个类型生成特化代码:

cpp 复制代码
// process<int> 生成的代码(简化)
void process_int(int value) {
    std::cout << "Integer: " << value << std::endl;
    // 其他分支的代码被完全丢弃
}

// process<double> 生成的代码(简化)
void process_double(double value) {
    std::cout << "Float: " << value << std::endl;
    // 其他分支的代码被完全丢弃
}

使用 CRTP 实现编译期多态

CRTP(Curiously Recurring Template Pattern) 通过模板继承实现编译期多态,替代虚函数:

cpp 复制代码
// CRTP 基类
template<typename Derived>
class Shape {
public:
    double area() const {
        // 编译期转换为派生类,静态分发
        return static_cast<const Derived*>(this)->areaImpl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double areaImpl() const {
        return 3.14159 * radius_ * radius_;
    }
};

class Rectangle : public Shape<Rectangle> {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double areaImpl() const {
        return width_ * height_;
    }
};

// 模板函数处理不同类型
template<typename ShapeType>
void printArea(const Shape<ShapeType>& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
    // 编译期确定调用 Circle::areaImpl 或 Rectangle::areaImpl
}

int main() {
    Circle c(5.0);
    Rectangle r(3.0, 4.0);

    printArea(c);  // 编译器实例化 printArea<Circle>
    printArea(r);  // 编译器实例化 printArea<Rectangle>
}

内存与性能对比

方案 对象内存开销 性能开销 运行时多态
RTTI (dynamic_cast) +8 字节 vptr 多次内存访问 支持
编译期类型识别 无额外开销 零开销(直接调用) 不支持

替代方案二:自定义类型 ID 系统(替代 dynamic_cast)

自定义类型 ID 系统通过为每个类分配唯一标识符,在运行时进行类型匹配,性能优于 dynamic_cast

基础实现

cpp 复制代码
// 类型 ID 枚举
enum class TypeID {
    Base,
    Derived1,
    Derived2,
};

class Base {
protected:
    TypeID type_id_;

    explicit Base(TypeID id) : type_id_(id) {}

public:
    virtual ~Base() = default;

    TypeID getTypeID() const { return type_id_; }
};

class Derived1 : public Base {
public:
    Derived1() : Base(TypeID::Derived1) {}

    void derived1Method() {
        std::cout << "Derived1 method" << std::endl;
    }
};

class Derived2 : public Base {
public:
    Derived2() : Base(TypeID::Derived2) {}

    void derived2Method() {
        std::cout << "Derived2 method" << std::endl;
    }
};

// 类型安全的向下转换
template<typename Derived, TypeID ID>
Derived* cast(Base* base) {
    if (base && base->getTypeID() == ID) {
        return static_cast<Derived*>(base);
    }
    return nullptr;
}

// 使用示例
void process(Base* base) {
    if (auto* d1 = cast<Derived1, TypeID::Derived1>(base)) {
        d1->derived1Method();
    } else if (auto* d2 = cast<Derived2, TypeID::Derived2>(base)) {
        d2->derived2Method();
    }
}

int main() {
    std::unique_ptr<Base> ptr = std::make_unique<Derived1>();
    process(ptr.get());  // 输出:Derived1 method
}

自动生成类型 ID

使用静态计数器自动为每个类分配唯一 ID,避免手动维护枚举:

cpp 复制代码
class Base {
    using TypeIDValue = uint32_t;

protected:
    TypeIDValue type_id_;

    explicit Base(TypeIDValue id) : type_id_(id) {}

    // 静态类型 ID 生成器
    static TypeIDValue nextTypeID() {
        static TypeIDValue next_id = 0;
        return next_id++;
    }

public:
    virtual ~Base() = default;

    TypeIDValue getTypeID() const { return type_id_; }

    // 获取类型的静态 ID
    template<typename T>
    static TypeIDValue getStaticTypeID() {
        static TypeIDValue id = nextTypeID();
        return id;
    }
};

class Derived1 : public Base {
public:
    Derived1() : Base(getStaticTypeID<Derived1>()) {}
};

class Derived2 : public Base {
public:
    Derived2() : Base(getStaticTypeID<Derived2>()) {}
};

// 类型安全的向下转换
template<typename Derived>
Derived* cast(Base* base) {
    if (base && base->getTypeID() == Base::getStaticTypeID<Derived>()) {
        return static_cast<Derived*>(base);
    }
    return nullptr;
}

// 使用示例
void process(Base* base) {
    if (auto* d1 = cast<Derived1>(base)) {
        d1->derived1Method();
    }
}

内存与性能对比

方案 对象内存开销 性能开销 运行时多态
dynamic_cast +8 字节 vptr 多次内存访问 支持
自定义类型 ID +4 字节 type_id_ 1 次整数比较 支持

替代方案三:std::variant(替代 std::any 和运行时类型切换)

std::variant(C++17) 是类型安全的联合体,可以在编译期确定的类型集合中存储任意一个,性能优于 std::any

基础用法

cpp 复制代码
#include <variant>
#include <string>
#include <iostream>

// 定义可能的类型集合
using Data = std::variant<int, double, std::string>;

int main() {
    Data value;

    value = 42;
    std::cout << std::get<int>(value) << std::endl;  // 输出:42

    value = 3.14;
    std::cout << std::get<double>(value) << std::endl;  // 输出:3.14

    value = std::string("hello");
    std::cout << std::get<std::string>(value) << std::endl;  // 输出:hello
}

内存布局

std::variant 的大小等于最大类型的大小加上索引字节:

cpp 复制代码
struct Small { char c; };          // 1 字节
struct Medium { int i; };          // 4 字节
struct Large { double d[10]; };    // 80 字节

using V = std::variant<Small, Medium, Large>;

// sizeof(V) = 80 (Large) + padding + 索引
// 通常是 88 字节

内存与性能对比

方案 对象内存开销 性能开销 类型集合
std::any 32-48 字节(固定) 指针比较 任意类型
std::variant 最大类型大小 + 索引 整数比较(索引) 编译期确定

禁用异常捕获

C++ 异常捕获涉及栈展开(Stack Unwinding)、RTTI 查询、内存分配等操作,在高性能系统中带来显著运行时开销。使用编译选项 -fno-exceptions 禁用异常处理后,不能使用 try-catch 语句,从而消除异常捕获的运行时代价。

异常捕获简述

C++ 异常处理(Exception Handling)允许在检测到错误的位置抛出异常(throw),在调用栈的上层捕获(catch)并处理。异常会自动沿调用栈向上传播,直到被捕获或导致程序终止。

cpp 复制代码
#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::runtime_error("Division by zero");  // 抛出异常
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10.0, 0.0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {  // 捕获异常
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

异常传播过程中,编译器会自动调用栈上所有局部对象的析构函数(Stack Unwinding),确保资源正确释放。

底层实现与开销分析

异常处理的性能开销主要来自三个方面:栈展开(Stack Unwinding)、RTTI 类型匹配和异常对象分配。

栈展开机制

当异常抛出后,运行时系统需要沿调用栈向上查找匹配的 catch 块,同时调用栈上所有局部对象的析构函数:

cpp 复制代码
#include <iostream>

struct Resource {
    int id_;
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " acquired\n";
    }
    ~Resource() {
        std::cout << "Resource " << id_ << " released\n";
    }
};

void level3() {
    Resource r3(3);
    throw std::runtime_error("Error");
    // r3 的析构函数会被调用
}

void level2() {
    Resource r2(2);
    level3();
    // r2 的析构函数会被调用
}

void level1() {
    Resource r1(1);
    try {
        level2();
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
    // r1 正常析构
}

int main() {
    level1();
    // 输出顺序:
    // Resource 1 acquired
    // Resource 2 acquired
    // Resource 3 acquired
    // Resource 3 released  (栈展开)
    // Resource 2 released  (栈展开)
    // Caught: Error
    // Resource 1 released  (正常析构)
}

栈展开的底层实现依赖编译器生成的元数据表:

cpp 复制代码
// 编译器生成的栈展开表(伪代码)
struct UnwindEntry {
    void* function_start;    // 函数起始地址
    void* function_end;      // 函数结束地址
    void* lsda;              // Language Specific Data Area(析构函数列表)
    void* personality;       // 异常处理函数指针
};

当异常抛出时,运行时系统执行以下步骤:

  1. 查找栈展开表:根据当前指令地址查找对应的 UnwindEntry
  2. 调用析构函数:按 LSDA 记录的顺序调用局部对象的析构函数
  3. 恢复栈帧:恢复寄存器状态,跳转到上一层调用者
  4. 重复步骤 1-3:直到找到匹配的 catch 块

RTTI 类型匹配

catch 块需要在运行时判断异常对象是否匹配:

cpp 复制代码
try {
    throw DerivedError();
} catch (const BaseError& e) {     // 匹配(派生类 → 基类)
    // 处理异常
} catch (const OtherError& e) {    // 不匹配
    // 不执行
}

类型匹配依赖 RTTI,运行时系统需要:

  1. 获取异常对象的 type_info(通过 vptr)
  2. 逐个比较 catch 块的目标类型
  3. 检查继承关系(如果目标是基类)

异常对象分配

抛出异常时,运行时系统需要在堆上分配异常对象的副本:

cpp 复制代码
void throwException() {
    LargeException ex(/* 大量数据 */);
    throw ex;  // 复制 ex 到堆上
    // ex 的栈内存会被释放(栈展开)
}

异常对象分配的开销:

  • 内存分配 :调用 __cxa_allocate_exception(类似 malloc)
  • 对象构造:调用拷贝构造函数或移动构造函数
  • 内存释放 :异常处理完成后调用 __cxa_free_exception

性能开销总结

操作 开销类型 影响范围
栈展开 多次内存访问(查表、调用析构函数) 所有调用层级
类型匹配 RTTI 查询、类型比较 每个 catch 块
对象分配 堆分配、拷贝构造、释放 每次 throw
二进制膨胀 栈展开表、LSDA 元数据 增加 5-15% 体积

即使没有抛出异常,编译器也会生成栈展开表和异常处理代码,增加二进制体积和指令缓存压力。

替代方案:直接终止程序

检测到错误时直接调用 std::abort() 终止程序,避免异常捕获的栈展开、RTTI 查询和内存分配开销。

cpp 复制代码
#include <cstdlib>
#include <iostream>

double divide(double a, double b) {
    if (b == 0.0) {
        std::cerr << "Fatal error: Division by zero\n";
        std::abort();  // 立即终止程序
    }
    return a / b;
}

int main() {
    double result = divide(10.0, 0.0);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

这种策略将错误视为致命错误(Fatal Error),适合高性能计算、嵌入式系统等对性能极度敏感的场景。

避免使用虚函数

虚函数通过虚函数表(vtable)实现运行时多态,但每个对象需要额外存储 vptr 指针(8 字节),每次虚函数调用需要两次内存间接访问,且阻止编译器内联优化。本章分析虚函数的底层实现和性能开销,并提供 CRTP、模板、std::variant、std::function 等替代方案。

虚函数简述

虚函数(Virtual Function)允许通过基类指针或引用调用派生类的重写函数,实现运行时多态(Runtime Polymorphism)。

cpp 复制代码
#include <iostream>

class Shape {
public:
    virtual double area() const = 0;  // 纯虚函数
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
};

class Rectangle : public Shape {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const override {
        return width_ * height_;
    }
};

void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << std::endl;  // 运行时确定调用哪个函数
}

int main() {
    Circle c(5.0);
    Rectangle r(3.0, 4.0);

    printArea(c);  // 输出:Area: 78.5398
    printArea(r);  // 输出:Area: 12

    return 0;
}

虚函数的关键特性:

  • 通过基类指针或引用统一管理不同派生类对象
  • 运行时根据对象的实际类型选择调用的函数
  • 依赖虚函数表(vtable)和虚表指针(vptr)实现

底层实现与开销分析

虚函数的实现依赖虚函数表(vtable)和虚表指针(vptr)。

vtable 和 vptr

编译器为每个包含虚函数的类生成一个 vtable,存储该类所有虚函数的地址。每个对象包含一个 vptr,指向其类的 vtable。

cpp 复制代码
class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

class Derived : public Base {
public:
    void func1() override {}  // 重写 func1
};

内存布局:

cpp 复制代码
// Base vtable
Base::vtable = { &Base::func1, &Base::func2 };

// Derived vtable
Derived::vtable = { &Derived::func1, &Base::func2 };

// 对象内存布局
Base obj1:     [vptr → Base::vtable]
Derived obj2:  [vptr → Derived::vtable]

虚函数调用过程:虚函数调用涉及两次内存间接访问,普通函数调用是直接调用。

汇编代码对比

cpp 复制代码
// 普通函数调用
class Simple {
public:
    void func() { /* ... */ }
};

Simple obj;
obj.func();

// 生成的汇编(简化):
// call Simple::func  // 直接调用,地址在编译期确定
cpp 复制代码
// 虚函数调用
class Base {
public:
    virtual void func() { /* ... */ }
};

Base* ptr = new Derived();
ptr->func();

// 生成的汇编(简化):
// mov rax, [ptr]        ; 1. 读取 vptr(第一次内存访问)
// mov rax, [rax]        ; 2. 读取 vtable[0](第二次内存访问)
// call rax              ; 3. 间接调用

性能开销总结

项目 开销
对象内存 +8 字节 vptr(64 位系统)
调用开销 2 次内存间接访问
内联优化 通常无法内联(除非编译器能去虚化)
缓存友好性 vtable 可能不在缓存中,增加缓存未命中
二进制体积 每个类增加 vtable(N 个虚函数 × 8 字节)

替代方案一:编译期多态(CRTP 和策略模式)

编译期多态通过模板在编译期确定调用的函数,消除虚函数的运行时开销。

CRTP(奇异递归模板模式)

CRTP 通过模板继承实现编译期多态,基类模板参数是派生类本身。

cpp 复制代码
// CRTP 基类
template<typename Derived>
class Shape {
public:
    double area() const {
        // 编译期转换为派生类,静态分发
        return static_cast<const Derived*>(this)->areaImpl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double areaImpl() const {
        return 3.14159 * radius_ * radius_;
    }
};

class Rectangle : public Shape<Rectangle> {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double areaImpl() const {
        return width_ * height_;
    }
};

// 模板函数处理不同类型
template<typename ShapeType>
void printArea(const Shape<ShapeType>& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
    // 编译期确定调用 Circle::areaImpl 或 Rectangle::areaImpl
}

int main() {
    Circle c(5.0);
    Rectangle r(3.0, 4.0);

    printArea(c);  // 编译器实例化 printArea<Circle>
    printArea(r);  // 编译器实例化 printArea<Rectangle>
}

底层实现:编译器为每个派生类生成特化代码,area() 调用在编译期解析为直接函数调用,可以内联。

策略模式(模板参数)

将行为作为模板参数传递,实现编译期策略选择。

cpp 复制代码
// 策略接口(编译期)
struct FastStrategy {
    static int compute(int x) { return x * 2; }
};

struct PreciseStrategy {
    static int compute(int x) { return x * 2 + 1; }
};

// 使用策略
template<typename Strategy>
class Processor {
public:
    int process(int value) {
        return Strategy::compute(value);  // 编译期确定调用哪个函数
    }
};

int main() {
    Processor<FastStrategy> p1;
    Processor<PreciseStrategy> p2;

    std::cout << p1.process(10) << std::endl;  // 输出:20
    std::cout << p2.process(10) << std::endl;  // 输出:21
}

编译期多态的限制:

  • 无法用基类指针统一管理Shape<Circle>*Shape<Rectangle>* 是不同类型,无法存储在同一个容器中
  • 无运行时多态:类型必须在编译期确定,无法根据运行时条件选择类型
cpp 复制代码
// 编译错误:Shape<Circle> 和 Shape<Rectangle> 是不同类型
Shape<Circle>* ptr = new Rectangle(3.0, 4.0);  // 错误

// 无法使用基类指针容器
std::vector<Shape<???>*> shapes;  // 无法表达

性能对比

方案 对象内存 调用开销 内联优化 运行时多态
虚函数 +8 字节 2 次内存间接访问 不可内联 支持
CRTP/策略模式 无额外 直接调用 可内联 不支持

替代方案二:std::variant

std::variant(C++17) 是类型安全的联合体,可以在编译期确定的类型集合中存储任意一个,结合 std::visit 实现类型安全的多态。

基础用法

cpp 复制代码
#include <variant>
#include <vector>
#include <iostream>

class Circle {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area() const {
        return 3.14159 * radius_ * radius_;
    }
};

class Rectangle {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const {
        return width_ * height_;
    }
};

// 定义可能的类型集合
using Shape = std::variant<Circle, Rectangle>;

int main() {
    // 统一存储不同类型
    std::vector<Shape> shapes;
    shapes.push_back(Circle(5.0));
    shapes.push_back(Rectangle(3.0, 4.0));

    // 使用 std::visit 处理
    for (const auto& shape : shapes) {
        std::visit([](const auto& s) {
            std::cout << "Area: " << s.area() << std::endl;
        }, shape);
    }
}

底层实现(简化说明)

std::variant 内部使用 union 存储数据,并维护一个索引标识当前存储的类型:

cpp 复制代码
// 简化的 variant 实现(概念性说明)
template<typename... Types>
class variant {
    union Storage {
        // 存储所有可能的类型
    } storage_;

    size_t index_;  // 当前类型的索引(0, 1, 2...)
};

类型分发通过索引进行跳转。实际的 std::visit 实现远比这里展示的复杂,涉及跳转表优化、编译期分支消除等技术,以下是概念性说明:

cpp 复制代码
// std::visit 的简化实现(概念性说明)
switch (variant.index_) {
    case 0: return visitor(std::get<0>(variant));  // Circle
    case 1: return visitor(std::get<1>(variant));  // Rectangle
}

开销对比

方案 对象内存 调用开销 内联优化 运行时多态
虚函数 +8 字节 vptr 2 次内存访问 不可内联 支持
std::variant 最大类型大小 + 索引(8B) switch 跳转,编译器优化 可能内联 支持

替代方案三:std::function

std::function(C++11) 是通用函数包装器,可以存储任意可调用对象(函数指针、lambda、函数对象),实现行为级别的多态。

基础用法

cpp 复制代码
#include <functional>
#include <vector>
#include <iostream>

double circleArea(double radius) {
    return 3.14159 * radius * radius;
}

double rectangleArea(double width, double height) {
    return width * height;
}

int main() {
    using AreaCalculator = std::function<double()>;
    std::vector<AreaCalculator> calculators;

    // 使用 lambda 捕获参数
    calculators.push_back([]() { return circleArea(5.0); });
    calculators.push_back([]() { return rectangleArea(3.0, 4.0); });

    for (const auto& calc : calculators) {
        std::cout << "Area: " << calc() << std::endl;
    }
}

底层实现

std::function 使用类型擦除(Type Erasure),内部存储函数指针和可调用对象:

cpp 复制代码
// 简化的 std::function 实现
template<typename R, typename... Args>
class function<R(Args...)> {
    void* callable_;  // 指向可调用对象
    R (*invoker_)(void*, Args...);  // 调用适配器

public:
    R operator()(Args... args) {
        return invoker_(callable_, std::forward<Args>(args)...);
    }
};

开销对比

方案 对象内存 调用开销 内联优化
虚函数 +8 字节 2 次内存访问 不可内联
std::function 32-48 字节 1 次函数指针间接调用 不可内联
函数指针 8 字节 1 次函数指针间接调用 不可内联

std::function 适合回调、事件处理等场景,但内存开销大,不适合性能关键路径。

推荐使用泛型编程

泛型编程通过模板在编译期生成特化代码,实现零运行时开销的类型抽象。编译器可以内联、常量折叠和死代码消除等优化,性能等同甚至优于手写代码。本章介绍模板的性能优势、编译期计算、constexpr 和模板元编程等技术。

泛型编程简述

泛型编程(Generic Programming)通过模板参数化类型,编写与类型无关的代码。编译器为每个使用的类型生成特化代码。

cpp 复制代码
// 函数模板
template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    int i = max(10, 20);        // 编译器生成 max<int>
    double d = max(3.14, 2.71); // 编译器生成 max<double>
}

编译器生成的代码:

cpp 复制代码
// 编译器自动生成(简化)
int max_int(int a, int b) {
    return (a > b) ? a : b;
}

double max_double(double a, double b) {
    return (a > b) ? a : b;
}

性能优势

模板代码在编译期完全展开,编译器可以进行激进优化。

内联优化

模板函数自动内联,消除函数调用开销:

cpp 复制代码
template<typename T>
inline T square(T x) {
    return x * x;
}

int result = square(5);  // 编译器直接生成:int result = 25;

常量折叠

编译期计算常量表达式:

cpp 复制代码
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

int x = Factorial<5>::value;  // 编译期计算为 120

死代码消除

编译器移除未使用的分支:

cpp 复制代码
template<bool UseCache>
int compute(int x) {
    if constexpr (UseCache) {
        return cachedValue;  // 使用缓存
    } else {
        return x * x;        // 直接计算
    }
}

int a = compute<false>(10);  // 编译器只生成 return x * x

constexpr 和编译期计算

constexpr(C++11) 标记的函数和变量在编译期计算,生成常量,运行时零开销。

constexpr 函数

cpp 复制代码
#include <array>

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int result = factorial(5);  // 编译期计算为 120
    std::array<int, factorial(4)> arr;    // 使用 std::array,编译期计算大小
}

编译器生成的代码:

cpp 复制代码
int main() {
    const int result = 120;         // 直接常量
    std::array<int, 24> arr;        // 直接常量
}

constexpr 类

cpp 复制代码
class Point {
    int x_, y_;
public:
    constexpr Point(int x, int y) : x_(x), y_(y) {}
    constexpr int getX() const { return x_; }
    constexpr int distance() const { return x_ * x_ + y_ * y_; }
};

constexpr Point p(3, 4);
constexpr int d = p.distance();  // 编译期计算为 25

constexpr vs 运行时计算

方案 计算时机 运行时开销
运行时计算 运行时 函数调用
constexpr 计算 编译期 零开销

constexpr 将计算提前到编译期,运行时直接使用常量,是零成本抽象的典范。

推荐使用值语义与移动语义

值语义(Value Semantics)通过直接存储对象避免指针间接访问,提高缓存局部性和访问速度。移动语义(Move Semantics)通过转移资源所有权避免深拷贝开销。两者结合是 C++ 高性能编程的核心实践:按值传递配合移动语义,既保持代码简洁,又实现零拷贝性能。

值语义

值语义直接存储对象本身,而非指针或引用。对象拷贝时复制整个内容。

值语义 vs 引用语义

cpp 复制代码
// 值语义:直接存储对象
struct Point {
    int x, y;
};

std::vector<Point> points;
points.push_back(Point{1, 2});  // 对象直接存储在 vector 内存中

// 引用语义:存储指针
std::vector<Point*> pointPtrs;
pointPtrs.push_back(new Point{1, 2});  // 只存储指针,对象在堆上

内存布局对比:

cpp 复制代码
// 值语义:连续内存
points: [Point{1,2}][Point{3,4}][Point{5,6}]
        ↑ 直接访问,缓存友好

// 引用语义:间接访问
pointPtrs: [ptr1][ptr2][ptr3]
            ↓     ↓     ↓
          堆内存分散,缓存未命中

性能优势

值语义的缓存局部性:

cpp 复制代码
// 值语义:连续访问,缓存命中率高
std::vector<Point> points(1000);
for (const auto& p : points) {
    process(p);  // 顺序访问连续内存,利用 CPU 缓存
}

// 引用语义:随机访问,缓存未命中
std::vector<Point*> pointPtrs(1000);
for (const auto* p : pointPtrs) {
    process(*p);  // 每次解引用可能缓存未命中
}

值语义优势:

  • 无指针间接访问,减少内存访问次数
  • 内存连续,提高缓存命中率
  • 无需手动管理生命周期

移动语义

移动语义(C++11)通过转移资源所有权避免深拷贝,使用右值引用(&&)和 std::move 实现。

拷贝 vs 移动

cpp 复制代码
#include <vector>
#include <string>

std::vector<int> createVector() {
    std::vector<int> v(1000000);
    return v;  // 返回临时对象
}

int main() {
    // 拷贝:深拷贝所有元素(慢)
    std::vector<int> v1 = createVector();  // C++11 之前:拷贝 100 万个元素

    // 移动:转移内存所有权(快)
    std::vector<int> v2 = createVector();  // C++11 之后:移动构造,只转移指针
}

移动构造函数实现:

cpp 复制代码
class Buffer {
    int* data_;
    size_t size_;
public:
    // 拷贝构造:深拷贝
    Buffer(const Buffer& other) : size_(other.size_) {
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);  // 拷贝所有元素
    }

    // 移动构造:转移所有权
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;  // 清空源对象
        other.size_ = 0;
    }
};

std::move

std::move 将左值转换为右值引用,触发移动语义:

cpp 复制代码
std::vector<int> v1(1000000);
std::vector<int> v2 = std::move(v1);  // 移动,v1 被清空

性能优势

移动语义实现按值传递的零拷贝:

cpp 复制代码
// 按值返回 + 移动语义 = 零拷贝
std::vector<int> createLargeVector() {
    std::vector<int> v(1000000);
    return v;  // 自动移动,无拷贝
}

void processVector(std::vector<int> v) {  // 按值接收
    // 处理 v
}

int main() {
    auto v = createLargeVector();  // 移动构造
    processVector(std::move(v));   // 移动传递
}

移动语义优势:

  • 避免深拷贝,只转移指针(O(1) vs O(n))
  • 配合值语义,实现简洁语法和极致性能

避免使用动态内存分配

动态内存分配通过 new/deletemalloc/free 在堆上分配内存,但涉及系统调用、内存碎片和缓存未命中等开销。本章分析动态内存分配的底层实现和性能代价,并提供栈分配、内存池、自定义分配器等替代方案。

动态内存分配简述

动态内存分配(Dynamic Memory Allocation)在运行时从堆(Heap)上分配内存,生命周期由程序员控制。C++ 提供 new/delete 运算符,C 提供 malloc/free 函数。

基础用法

cpp 复制代码
#include <iostream>

class Resource {
    int* data_;
public:
    Resource(int size) : data_(new int[size]) {
        std::cout << "Resource allocated\n";
    }
    ~Resource() {
        delete[] data_;
        std::cout << "Resource released\n";
    }
};

int main() {
    // new:分配内存 + 调用构造函数
    Resource* r = new Resource(100);
    delete r;  // 调用析构函数 + 释放内存

    // malloc:只分配原始内存,不调用构造函数
    int* arr = (int*)malloc(1000 * sizeof(int));
    free(arr);  // 只释放内存,不调用析构函数

    return 0;
}

new vs malloc

特性 new/delete malloc/free
类型 C++ 运算符 C 函数
调用构造/析构函数
类型安全 是(自动推导类型) 否(需要强制转换)
失败处理 抛出 std::bad_alloc 返回 nullptr

动态内存分配的典型使用场景包括大小不确定(运行时才知道需要多少内存)、生命周期跨越作用域(对象需要在函数返回后继续存在)、多态对象管理(使用基类指针管理不同派生类对象)。虽然动态内存分配提供了灵活性,但带来了显著的性能开销和管理复杂度。

底层实现与开销分析

动态内存分配的性能开销主要来自四个方面:系统调用、内存管理器维护、内存碎片和缓存未命中。

系统调用开销

频繁的小内存分配会触发系统调用,陷入内核态,开销远大于用户态操作。现代内存分配器(如 ptmalloc、jemalloc)通过内存池缓解这一问题:

cpp 复制代码
// 内存分配器的简化工作流程
void* malloc(size_t size) {
    // 1. 检查内存池是否有空闲块
    if (has_free_block_in_pool(size)) {
        return allocate_from_pool(size);  // 用户态,快速
    }

    // 2. 内存池不足,向操作系统申请大块内存
    void* ptr = sbrk(BIG_SIZE);  // 系统调用,慢
    add_to_pool(ptr);
    return allocate_from_pool(size);
}

内存池分配从用户态完成,而系统调用需要陷入内核态,两者的性能差距巨大。

内存管理器维护开销

内存分配器需要维护元数据(空闲链表、大小信息等),每次分配/释放都需要查找和更新:

cpp 复制代码
// 典型的内存块结构
struct MemoryBlock {
    size_t size;           // 块大小(8 字节)
    bool is_free;          // 是否空闲(1 字节)
    MemoryBlock* next;     // 下一个空闲块(8 字节)
    char padding[7];       // 对齐填充
    // 总开销:24 字节
    char user_data[];      // 用户数据
};

对于小对象,元数据开销占比显著:

cpp 复制代码
// 分配 8 字节对象
int* ptr = new int[2];  // 用户数据:8 字节
// 实际占用:8 (数据) + 24 (元数据) = 32 字节
// 开销:300%

内存碎片

频繁分配/释放不同大小的内存导致碎片,降低内存利用率:

cpp 复制代码
// 分配和释放导致碎片
int* a = new int[100];   // 400 字节
int* b = new int[200];   // 800 字节
int* c = new int[100];   // 400 字节

delete[] b;              // 释放 b,中间留下 800 字节空洞

// 尝试分配 1000 字节,虽然总空闲内存足够,但无连续空间
int* d = new int[250];   // 可能失败或触发系统调用

内存布局:

css 复制代码
[a: 400B][空洞: 800B][c: 400B]
          ↑ 无法分配 1000B

缓存未命中

堆上分配的对象地址分散,相比栈上连续分配,缓存命中率低:

cpp 复制代码
// 堆分配:对象地址分散
std::vector<int*> heap_ptrs;
for (int i = 0; i < 1000; i++) {
    heap_ptrs.push_back(new int(i));  // 每个 int 可能在不同的缓存行
}

// 访问时缓存未命中率高
for (auto* ptr : heap_ptrs) {
    process(*ptr);  // 每次访问可能导致缓存未命中
}

// 栈分配:对象连续存储
int stack_arr[1000];
for (int i = 0; i < 1000; i++) {
    stack_arr[i] = i;  // 连续内存
}

// 访问时缓存友好
for (int val : stack_arr) {
    process(val);  // 顺序访问,预取效率高
}

开销总结

开销类型 影响
系统调用 陷入内核态,开销大
元数据维护 查找空闲块、更新链表
内存碎片 降低内存利用率、触发额外分配
缓存未命中 内存访问延迟增加
对象构造析构 new/delete 调用构造/析构函数

替代方案一:栈分配与固定大小容器

栈分配在函数调用时自动分配和释放,无系统调用开销,内存连续,缓存友好。对于大小确定的数据,使用栈分配或固定大小容器(如 std::array)替代堆分配。

栈分配

栈分配只需调整栈指针,零开销:

cpp 复制代码
void processData() {
    // 栈分配:自动分配和释放
    int arr[1000];  // 编译期确定大小
    // 使用 arr...
}  // 函数返回时自动释放,无需 delete

汇编代码(简化):

asm 复制代码
; 分配栈空间
sub rsp, 4000  ; 移动栈指针,分配 4000 字节

; 函数体...

; 释放栈空间
add rsp, 4000  ; 恢复栈指针

栈分配只需一条指令,相比堆分配(需要查找空闲块、更新链表),性能提升显著。

std::array

std::array(C++11) 是栈上分配的固定大小数组,提供 STL 容器接口:

cpp 复制代码
#include <array>
#include <vector>

void comparePerformance() {
    // std::vector:堆分配
    std::vector<int> vec(1000);  // 调用 new int[1000]
    // 使用 vec...
    // vec 析构时调用 delete[]

    // std::array:栈分配
    std::array<int, 1000> arr;  // 栈上分配,零开销
    // 使用 arr...
}  // arr 自动释放,无需析构操作

小字符串优化(SSO)

std::string 使用 SSO(Small String Optimization),短字符串存储在栈上的内部缓冲区,避免堆分配:

cpp 复制代码
#include <string>
#include <iostream>

int main() {
    // 短字符串:使用 SSO,无堆分配
    std::string short_str = "hello";  // 通常 ≤15 字符使用 SSO
    std::cout << "Capacity: " << short_str.capacity() << std::endl;  // 15

    // 长字符串:堆分配
    std::string long_str = "This is a very long string that exceeds SSO buffer";
    std::cout << "Capacity: " << long_str.capacity() << std::endl;  // > 15
}

SSO 实现(简化):

cpp 复制代码
class string {
    union {
        char sso_buffer_[16];  // 短字符串缓冲区
        char* heap_ptr_;       // 长字符串堆指针
    };
    size_t size_;
    size_t capacity_;

public:
    string(const char* str) {
        size_ = strlen(str);
        if (size_ <= 15) {
            // 使用 SSO
            memcpy(sso_buffer_, str, size_);
        } else {
            // 堆分配
            heap_ptr_ = new char[size_ + 1];
            memcpy(heap_ptr_, str, size_);
        }
    }
};

性能对比

方案 分配开销 释放开销 缓存友好性 适用场景
堆分配 大小不确定、大对象
栈分配 极快 极快 大小确定、小对象
std::array 零开销 零开销 固定大小数组
std::string SSO 零开销 零开销 短字符串(≤15 字符)

替代方案二:内存池

内存池(Memory Pool)预先分配大块内存,使用链表管理空闲块,避免频繁的系统调用和内存碎片,显著提升分配/释放性能。

实现:内存池为特定类型预先分配大块内存,使用链表管理空闲块:

cpp 复制代码
#include <cstddef>
#include <cstdlib>

template<typename T>
class MemoryPool {
    union Block {
        T data;        // 对象数据
        Block* next;   // 空闲链表指针(对象未构造时使用)
    };

    Block* free_list_;  // 空闲块链表
    Block* pool_;       // 内存池起始地址
    size_t capacity_;   // 池容量

public:
    MemoryPool(size_t block_count) : capacity_(block_count) {
        // 预先分配大块内存
        pool_ = (Block*)malloc(sizeof(Block) * block_count);

        // 将内存划分为块,连接到空闲链表
        free_list_ = pool_;
        for (size_t i = 0; i < block_count - 1; ++i) {
            pool_[i].next = &pool_[i + 1];
        }
        pool_[block_count - 1].next = nullptr;
    }

    ~MemoryPool() {
        free(pool_);
    }

    T* allocate() {
        if (!free_list_) {
            throw std::bad_alloc();  // 内存池耗尽,抛出异常
        }

        // 从空闲链表头部取出一个块
        Block* block = free_list_;
        free_list_ = block->next;

        // 在块上构造对象
        return new (&block->data) T();
    }

    void deallocate(T* ptr) {
        // 调用析构函数
        ptr->~T();

        // 将块归还到空闲链表头部
        Block* block = reinterpret_cast<Block*>(ptr);
        block->next = free_list_;
        free_list_ = block;
    }
};

// 使用示例
class Object {
    int data_[10];
public:
    Object() { /* 构造逻辑 */ }
    ~Object() { /* 析构逻辑 */ }
};

int main() {
    MemoryPool<Object> pool(1000);  // 1000 个 Object 块

    Object* p1 = pool.allocate();  // O(1),自动调用构造函数
    Object* p2 = pool.allocate();

    pool.deallocate(p1);  // O(1),自动调用析构函数
    pool.deallocate(p2);
}

线程安全说明 :上述内存池实现是线程不安全的,在多线程环境中使用会导致竞态条件。

性能对比

方案 分配开销 释放开销 内存碎片 适用场景
堆分配 通用场景
内存池(自动构造/析构) 快 + 构造开销 快 + 析构开销 固定大小对象的频繁分配/释放
内存池(手动构造/析构) 非常快 非常快 构造/析构开销大的对象复用

替代方案三:自定义分配器

STL 容器支持自定义分配器(Allocator),允许替换默认的堆分配策略。通过自定义分配器可以使用内存池、栈分配等高性能策略。

自定义分配器接口

分配器需要实现以下接口:

cpp 复制代码
template<typename T>
class CustomAllocator {
public:
    using value_type = T;

    // 分配 n 个 T 对象的内存(不调用构造函数)
    T* allocate(size_t n) {
        return static_cast<T*>(malloc(n * sizeof(T)));
    }

    // 释放内存(不调用析构函数)
    void deallocate(T* ptr, size_t n) {
        free(ptr);
    }

    // 比较运算符(判断分配器是否可互换)
    bool operator==(const CustomAllocator&) const { return true; }
    bool operator!=(const CustomAllocator&) const { return false; }
};

使用内存池的分配器

结合前面的内存池实现,创建基于内存池的分配器:

cpp 复制代码
template<typename T>
class PoolAllocator {
    MemoryPool<T>* pool_;  // 共享内存池

public:
    using value_type = T;

    explicit PoolAllocator(MemoryPool<T>* pool) : pool_(pool) {}

    T* allocate(size_t n) {
        if (n != 1) {
            throw std::bad_alloc();  // 内存池只支持单个对象分配
        }
        return pool_->allocate();
    }

    void deallocate(T* ptr, size_t n) {
        pool_->deallocate(ptr);
    }

    bool operator==(const PoolAllocator& other) const {
        return pool_ == other.pool_;
    }
    bool operator!=(const PoolAllocator& other) const {
        return !(*this == other);
    }
};

// 使用示例
int main() {
    MemoryPool<int> pool(10000);
    PoolAllocator<int> alloc(&pool);

    // std::vector 使用自定义分配器
    std::vector<int, PoolAllocator<int>> vec(alloc);
    vec.push_back(1);  // 使用内存池分配
    vec.push_back(2);
}

std::pmr(多态分配器)

C++17 引入 std::pmr(Polymorphic Memory Resource),提供运行时可切换的内存分配策略:

cpp 复制代码
#include <memory_resource>
#include <vector>

int main() {
    // 栈上的单调缓冲区
    char buffer[10000];
    std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));

    // 使用 pmr::vector,内存从 buffer 分配
    std::pmr::vector<int> vec(&pool);
    vec.push_back(1);
    vec.push_back(2);
    // vec 析构时,内存自动归还到 pool(实际上单调缓冲区不回收)
}

std::pmr 提供多种预定义分配器:

  • monotonic_buffer_resource:单调递增分配,不支持单个对象释放
  • unsynchronized_pool_resource:线程不安全的内存池
  • synchronized_pool_resource:线程安全的内存池

性能对比

分配器类型 分配开销 释放开销 线程安全 适用场景
默认分配器(std::allocator) 中等 中等 通用场景
内存池分配器 固定大小对象频繁分配
monotonic_buffer_resource 非常快 零开销 临时数据、批量释放场景
synchronized_pool_resource 较快 较快 多线程环境

自定义分配器适合对性能有极致要求的场景,通过选择合适的分配策略,可以大幅减少内存分配开销。

相关推荐
Lucis__3 小时前
从基本用法到迭代器实现—list重难点突破
c++·容器·list
努力学习的小全全3 小时前
【CCF-CSP】06-01数位之和
c++·ccf-csp
再卷也是菜3 小时前
C++篇(16)C++11(下)
c++
CS_浮鱼3 小时前
【C++进阶】智能指针
开发语言·c++
怕什么真理无穷3 小时前
C++_面试题_21_字符串操作
java·开发语言·c++
Dream it possible!4 小时前
LeetCode 面试经典 150_二叉树_二叉树展开为链表(74_114_C++_中等)
c++·leetcode·链表·面试·二叉树
cookies_s_s5 小时前
C++20 协程
linux·开发语言·c++
hetao17338375 小时前
2025-11-13~14 hetao1733837的刷题记录
c++·算法
hansang_IR5 小时前
【题解】洛谷 P2476 [SCOI2008] 着色方案 [记搜]
c++·算法·记忆化搜索