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> |
- 自定义分配器提升性能,减少堆内存使用。
MetaProgramming
什么是模板元编程?
模板元编程(Template Metaprogramming)是在编译期用模板机制编写代码,用于自动生成最终可编译的 C++ 代码。
- 正常的代码在 运行时 执行;
- 元编程是在 编译时 发生,生成新的代码。
用处:
- 实现通用函数和类(支持多种类型)
- 消除重复代码(如浮点版和整型版的函数)
- 做到编译期检查、优化甚至计算
1 | template <typename T> |
编译器行为: 每当你使用不同的类型
T
(比如 float
,
int
),编译器会为你自动生成一个版本:
1 | auto x = pow_n<float>(2.0f, 3); // 生成 float 版本 |
编译器会为每种模板实例化 生成真正的函数/类
模板也可以做限制和校验
1 | template <int N, typename T> |
Type Traits(类型萃取)
type_traits
是 C++ 标准库中提供的一组
编译期工具类模板,用于检查或转换类型信息,全部在
<type_traits>
头文件里。
两种类型萃取
返回布尔/整型值的类型萃取
这些用来判断类型的某些特性(如是否是浮点数、是否是指针等):
C++17 引入了简洁写法:
xxx_v
1
2std::is_floating_point_v<float> // true
std::is_same_v<int, int> // true
使用 type traits 提升函数智能性
1 | template <typename T> |
编译器在编译时就知道 T
是不是
unsigned
,可以直接“裁剪”掉分支:这就是类型萃取结合模板的强大之处:根据类型做出“静态分支优化”,无运行时开销!
decltype
:获取变量或表达式的类型
1 | auto sign_func = [](const auto& v) -> int { |
例子2
1 | template <typename Range> |
std::enable_if_t
条件启用函数(SFINAE)
1 | template <typename T> |
检测类成员函数 ——
std::is_detected
(实验性)
1 | template <typename T> |
enable_if
+
is_detected
组合用法
1 | template <typename T> |
constexpr
编译期函数
当所有参数是编译期常量时,它会在编译时执行。
1 | constexpr auto sum(int x, int y, int z) { |
if constexpr
编译期条件分支
1 | template <typename Animal> |
编译期 vs 运行时 多态性对比
编译期多态(模板 + if constexpr):
- 类型信息在编译时已知,无需虚表,开销小,内联优化能力强。
- 使用场景:追求性能,类型已知。
运行时多态(虚函数 + 多态继承):
- 类型信息运行时才知道,有虚表开销。
- 使用场景:类型不确定或接口抽象统一。
std::tuple
——
固定大小的异构容器
1 | auto tpl = std::make_tuple(42, std::string{"hi"}, true); |
为什么 std::tuple 不能用 range-based for 循环?
因为 std::tuple
是 异构容器,不同于
std::vector
等同构容器,它的每个元素可能是不同类型。而
for (const auto& v : tuple)
中 v
的类型只能是一个确定的类型,无法适配多个类型,编译器在编译期就会报错。另外,std::tuple
也没有 begin()
/end()
,因此也不能用算法如
std::for_each
。
如何“遍历”一个
std::tuple
?
通过 模板元编程+递归调用+索引展开(index sequence) 实现编译期“展开”。
1 | template <size_t Index, typename Tuple, typename Functor> |
如何实现类似 any_of
的操作?
1 | template <typename Tuple, typename Functor, size_t Index = 0> |
结构化绑定(C++17)简化 tuple 解包
1 | auto [name, id, license] = std::make_tuple("James", 7, true); |
用结构体代替 tuple 返回值(更可读)
1 | auto make_bond() { |
变参模板(Variadic Templates)
使函数可以接收任意数量参数
1 | template <typename ...Ts> |
将变参模板转为字符串(结合 tuple_for_each)
1 | template <typename ...Ts> |
动态大小 异构类型
方法一:std::any
(C++17
起)
1 | std::vector<std::any> container{42, "hi", true}; |
方法二:std::variant
(C++17
起)
1 | using VariantType = std::variant<int, std::string, bool>; |
它是一个“受限类型的类型联合体”。
和 std::tuple
不同,它一次只存一个值,但类型由一个
固定列表指定。
编译器知道它可能的类型组合,无需手动判断类型,使用
std::visit
自动分发。
1 | std::visit([](const auto& v) { |
编译器会自动为每个可能的类型生成对应的 operator()
重载,如:
1 | struct FunctorImpl { |
实际上,std::visit
做的就是一个 type-switch:
构建动态大小、异构类型容器
1 | using VariantType = std::variant<int, std::string, bool>; |
遍历
1 | for (const auto& val : container) { |
反射
什么是反射(Reflection)?
“反射”是指程序在运行时可以“查看”或“操作”自己的结构,比如成员变量、类型信息等。
C++ 不支持原生反射,所以我们要“伪造”一个。
问题:如何让类暴露出它的成员变量?
方法:实现一个 reflect()
成员函数,返回
std::tie(...)
构成的 std::tuple
引用。
1 | class Town { |
std::tie
会构造一个 std::tuple
的引用。
这样我们可以操作、比较、访问成员变量,就像处理 tuple
一样。
如何使用
reflect()
?
🎯 目标:自动实现如下功能:
- 比较
(==, !=, <)
- 输出(重载
operator<<
)
1 | auto operator==(const Town& t) const { |
提升:用元编程自动化支持这些功能
🛠 判断类是否 reflect()
可用:
利用 std::experimental::is_detected
判断类是否有
reflect()
方法:
1 | template <typename T> |
基于此,自动启用操作符重载:
1 | template <typename T, bool B = is_reflectable_v<T>> |
完整
1 | class Town { |
这套“伪反射”机制虽然比不上Java/C#
的内建反射强大,但在
C++
中非常实用。它是一种利用元编程提升代码复用性和可维护性的典范,广泛用于:
- JSON/XML 序列化
- 数据库 ORM 映射
- UI 数据绑定
- 自动日志/诊断系统
- 泛型算法适配器
安全泛型类型转换
为什么需要 safe_cast
?
在 C++ 中使用
static_cast
、reinterpret_cast
、dynamic_cast
、const_cast
时,可能会发生以下问题:
- 精度丢失:比如
double
转float
,或者int64_t
转int32_t
; - 符号混淆:比如负数转无符号类型;
- 指针类型转换错误:不同类型的指针强转可能导致 UB(未定义行为);
- 指针转整数:只有
uintptr_t
/intptr_t
是合法的; - float -> float 的截断溢出(如
1e39
转 float 得到 inf);
因此,我们希望有一种方法:
- 在 调试模式 下执行运行时检查;
- 在 发布模式 下快速转换;
- 类型不合法时 编译报错(而不是运行时爆炸)。
1 | template <typename T> constexpr auto make_false() { return false; } |
编译期字符串哈希
目标
避免运行时重复对字符串计算哈希,提高性能。
在 std::unordered_map
中,key 是
std::string
时,每次查找都要重新计算哈希。而如果我们能在编译期就计算好哈希值,可以显著优化:
1 | constexpr auto hash_function(const char* str) -> size_t { |
实现 PrehashedString
类
1 | class PrehashedString { |
并在 std
命名空间里自定义 std::hash
:
1 | namespace std { |
编译期验证示例
1 | auto test() { |