C++高性能编程
CMake
g++14
支持C++23需要g++14版本,可以安装
1 | sudo apt install gcc-14 g++-14 |
Cmake
新建 CMakeLists.txt
里面写
1 | cmake_minimum_required(VERSION 3.28) |
构建
优点: - 明确指定源代码和构建目录 - 避免在源代码目录中生成构建文件 - 支持out-of-source构建(推荐做法)
1 | cmake -S /mnt/d/Fyind/Master_Semester7/cpp -B /mnt/d/Fyind/Master_Semester7/cpp/build |
C++23
1 |
|
noexcept
noexcept 是 C++11 引入的异常规范关键字,用于声明函数不会抛出异常。
作用: 1. 编译器优化 - 编译器可以进行更激进的优化 2. 移动语义 - 标准库容器会优先使用noexcept的移动构造函数 3. 文档化 - 明确告知调用者函数不会抛异常
1 | //基本语法: |
内存管理
内存管理的重要性
计算机内存
虚拟地址空间,内存页

当前主流的虚拟内存实现方式是:将虚拟地址空间划分为固定大小的块,称为“内存页(page)”。
当一个进程访问某个虚拟地址时,操作系统会检查该虚拟地址所在的页是否已加载到物理内存中(即是否映射到了物理页框 page frame)。如果没有,就会发生 页错误(page fault)。
- 页错误并不是程序错误,而是一个正常的、受控的硬件中断,目的是从磁盘加载数据到内存。
如果物理内存没有空闲页框了,系统就必须从内存中移除一个已有的页(称为“页置换”)
如果被移除的页是“脏页(dirty page)”,即该页自从从磁盘加载进内存之后有过修改,就必须先将其写回磁盘,以防数据丢失。
如果是“干净页(clean page)”,即未被修改,直接丢弃即可,无需写回磁盘。
这整个过程被称为 paging(分页置换)。
- iOS不支持脏页写回,内存不足时直接终止进程。
抖动(Thrashing)
- 系统物理内存不足时,频繁进行页面置换。
- 导致系统性能急剧下降。
- 通过监控page fault频率判断是否发生抖动。
进程内存
栈内存
- 栈是一块连续的内存区域。
- 每个线程有独立的栈。
- 栈的大小固定,超出会栈溢出。
- 栈内存分配和释放非常快,不会产生碎片。
- 栈增长方向通常向下。
- 示例:递归函数可能导致栈溢出。
- 默认栈大小约为8MB(在Mac系统上)。
- 每个线程都有自己独立的栈 → ✅ 线程安全
堆内存
- 堆是全局共享的内存区域。
- 用于动态内存分配(new/malloc)和释放(delete/free)。
- 堆内存分配模式不固定,容易产生内存碎片。
- 示例:频繁分配和释放不同大小内存可能导致碎片。
- 因为堆是共享资源 → ❌ 不是线程安全,需要配合互斥锁等机制。
内存中的对象
而堆内存就很“混乱”:你可以随时在任意位置 new
和
delete
。
这就可能造成 内存碎片(fragmentation)。
创建与删除对象
new 与 delete 的工作原理
- new 操作包含两个步骤:
- 分配内存(调用 operator new)。
- 构造对象(调用构造函数)。
- delete 操作也包含两个步骤:
- 析构对象(调用析构函数)。
- 释放内存(调用 operator delete)。
1 | class User { |
定置 new(Placement new)
允许分离内存分配与对象构造。就是 placement new,意思是“在这块内存上构造对象”。
它不会分配内存,只是调用构造函数。
使用示例:
1
2
3
4
5auto memory = std::malloc(sizeof(User));
auto user = new (memory) User("John");
user->print_name();
user->~User();
std::free(memory);⚠️ 如果你用了 placement new,就必须手动调用析构函数!
C++17 提供了
std::uninitialized_fill_n
再内存构造一个对象std::destroy_at
调用析构函数。
1
2
3
4
5
6auto memory = std::malloc(sizeof(User));
auto user_ptr = reinterpret_cast<User*>(memory);
std::uninitialized_fill_n(user_ptr, 1, User{"John"});
user_ptr->print_name();
std::destroy_at(user_ptr);
std::free(memory);
new 和 delete 操作符
可以全局或类内重载 operator new 和 operator delete。
示例重载:
1
2
3
4
5
6
7
8
9
10auto operator new(size_t size) -> void* {
void* p = std::malloc(size);
std::println("Allocated: {}", size);
return p;
}
auto operator delete(void* p) noexcept -> void {
std::println("Deleting");
return std::free(p);
}数组操作符:
operator new[]
和operator delete[]
。
类内重载 new/delete 可用于特定类的内存管理。
如果要访问类外的
1
2auto* p = ::new Document(); // 调用全局 operator new
::delete p; // 调用全局 operator delete为什么
new[]
和new
实现一样
你只重载了内存分配函数本身(operator new[]
和 operator new
)
构造函数和数组元信息管理是编译器负责的,不是
operator new
负责的
编译器会自动在分配的内存中“隐藏元信息”(如元素数量),让
delete[]
知道需要调用多少次析构函数
内存对齐
内存对齐是指:不同数据类型的变量必须存储在符合其“对齐要求”的地址上,以提高 CPU 访问效率,甚至在某些平台上是必须遵守的规则。
对齐基础
CPU 以字为单位读取内存(如64位架构为8字节)。
每种类型都有对齐要求:
使用
alignof
查看类型对齐要求。1
std::cout << alignof(int) << '\n'; // 输出可能是 4
不对齐的内存访问可能导致性能下降或程序崩溃。

内存分配对齐保证
使用 new 或 malloc 分配的内存满足最大对齐要求。
std::max_align_t
表示最大对齐类型。也就是说,任何基本类型(比如int
、double
、long double
、char
等)的对齐要求都不会超过std::max_align_t
的对齐要求。它的作用是为内存分配和布局提供一个“最大对齐保证”,确保分配的内存地址满足任何类型的对齐需求。1
auto max_alignment = alignof(std::max_align_t);
即使分配单个 char,也按最大对齐方式对齐。
填充(Padding)
- 编译器会在类成员之间插入填充字节以满足对齐要求。
- 示例:
class Document { bool; double; int; }
会因填充导致大小为24字节。
- 优化方法:将对齐要求大的成员放在前面。
- 优化后的示例:
class Document { double; int; bool; }
大小为16字节。
- 对齐与缓存友好性:
- 可以将对象对齐到缓存行边界以提高性能。
- 将频繁使用的成员放在一起以减少缓存行切换。
内存所有权
所有权(ownership)表示某个变量、对象或代码块对资源(如内存、文件、数据库连接等)的控制权。
拥有某个资源就意味着负责它的释放和清理。
处理资源隐式
- 使用自动变量处理动态内存的分配/释放。
- 通过析构函数释放动态内存,避免内存泄漏。
- RAII(资源获取即初始化)技术用于管理资源生命周期。
- 使用RAIIConnection类自动管理连接资源,确保连接在使用后关闭。
1 | class RAIIConnection { |
容器
- 使用标准容器自动管理动态内存。
- 容器负责其存储对象的内存所有权。
- 减少代码中显式使用new和delete的情况。
智能指针
独占指针
std::unique_ptr
表示独占所有权。- 独占所有权不可复制,但可转移。
1 | auto owner = std::make_unique<User>("John"); |
共享指针
std::shared_ptr
表示共享所有权。- 使用引用计数跟踪对象的所有者数量。
- 当最后一个所有者释放时,对象自动删除。
1 | auto i = std::make_shared<double>(42.0); |
弱指针
示例应用场景:
场景 | 解决方案 |
---|---|
GUI 中父子窗口 | 父窗口持有子窗口(shared),子窗口持有父窗口(weak) |
树形结构节点 | 父节点持有子节点(shared),子节点回指父节点(weak) |
观察者模式 | 被观察者持有 weak_ptr 指向观察者 |
std::weak_ptr
表示弱所有权,不延长对象生命周期。是一种不会增加引用计数的指针。- 用于打破共享指针之间的循环引用。
1 |
|
- 使用
lock()
方法将weak指针转换为shared指针。
1 | if (auto shared_i = weak_i.lock()) { |
关键词 | 含义 |
---|---|
weak_i |
一个 std::weak_ptr<int> ,指向某个
std::shared_ptr<int> 管理的对象(可能已释放) |
weak_i.lock() |
尝试从 weak_ptr 获取一个临时的
shared_ptr ,如果对象还存在,返回有效的
shared_ptr ,否则返回空指针 |
if (auto shared_i = ...) |
如果成功获取到了有效的 shared_ptr ,则进入
if 分支;否则说明原对象已经销毁,进入
else |
小型优化
动态内存分配开销大:普通的容器如
std::vector
、std::string
在存储数据时,通常会在堆上分配内存。当存储的数据量很小,比如只有几个字符时,分配和释放堆内存的开销反而会影响性能。
- 对于短字符串或小容器,使用栈内存代替堆内存以提升性能。
- 标准库
std::string
通常使用小字符串优化(SSO)。 - 实际使用
union
实现短模式和长模式的内存布局切换。 - 示例:
std::string
在24字节栈内存中可存储22字符。

自定义内存管理
构建一个内存池(Arena)
- Arena是一个连续内存块,用于高效分配和回收内存。
- 支持固定大小分配、单线程优化、有限生命周期等策略。单线程:无需锁,速度快
- 示例:使用Howard Hinnant的short_alloc实现栈分配器。
1 | template <size_t N> |
- Arena类模板支持对齐内存分配。
- allocate和deallocate方法用于分配和回收内存。
1 | template<size_t N> |
- 示例:为User类重载new和delete操作符,使用Arena分配内存。
1 | auto user_arena = Arena<1024>{}; // 创建一个1024字节的arena |
自定义内存分配器
- 为什么类特定的 operator new 没有被调用?
- 因为
std::make_shared
需要一次性分配足够空间给对象和引用计数控制块。它使用的是一次内存分配 + placement new 构造对象,而不是单纯调用new User()
。 std::vector<User> users; users.reserve(10);
reserve 只分配内存,不构造元素。这内存分配调用的是 vector 默认的分配器(std::allocator
),不会调用 User 的 operator new。
- 因为
- 自定义分配器可用于标准容器和智能指针。
- C++11中自定义分配器的最小接口包括allocate和deallocate方法。
- 示例:实现Mallocator使用malloc/free进行内存管理。
- 实现ShortAlloc分配器,绑定Arena实例进行栈内存分配。
- 示例:使用栈内存。
1 | template <class T, size_t N> |
- 自定义分配器提升性能,减少堆内存使用。