在内存和性能敏感的 C++ 系统开发中,语言特性的选择直接决定程序的内存占用和运行效率。本文针对 RTTI、异常处理、虚函数、泛型编程、值语义、动态内存分配等关键语言特性,从底层实现机制出发,深入分析其内存开销和性能代价,并提供多种高性能替代方案的对比。通过理解这些特性的权衡取舍,帮助开发者在内存效率、运行性能和代码质量之间做出明智的技术选择。
禁用 RTTI(运行时类型识别)
RTTI 提供运行时类型识别能力,但依赖 type_info 表和虚函数表,增加内存占用和运行时开销。使用编译选项 -fno-rtti 禁用 RTTI 后,typeid、dynamic_cast、std::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; // 异常处理函数指针
};
当异常抛出时,运行时系统执行以下步骤:
- 查找栈展开表:根据当前指令地址查找对应的 UnwindEntry
- 调用析构函数:按 LSDA 记录的顺序调用局部对象的析构函数
- 恢复栈帧:恢复寄存器状态,跳转到上一层调用者
- 重复步骤 1-3:直到找到匹配的 catch 块
RTTI 类型匹配
catch 块需要在运行时判断异常对象是否匹配:
cpp
try {
throw DerivedError();
} catch (const BaseError& e) { // 匹配(派生类 → 基类)
// 处理异常
} catch (const OtherError& e) { // 不匹配
// 不执行
}
类型匹配依赖 RTTI,运行时系统需要:
- 获取异常对象的 type_info(通过 vptr)
- 逐个比较 catch 块的目标类型
- 检查继承关系(如果目标是基类)
异常对象分配
抛出异常时,运行时系统需要在堆上分配异常对象的副本:
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/delete、malloc/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 | 较快 | 较快 | 是 | 多线程环境 |
自定义分配器适合对性能有极致要求的场景,通过选择合适的分配策略,可以大幅减少内存分配开销。