CMake

g++14

支持C++23需要g++14版本,可以安装

1
sudo apt install gcc-14 g++-14

Cmake

新建 CMakeLists.txt 里面写

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.28)


set(CMAKE_CXX_COMPILER g++-14)
set(CMAKE_CXX_STANDARD 23)

project(exampleproject LANGUAGES CXX)

add_executable(exampleproject main.cpp)

构建

优点: - 明确指定源代码和构建目录 - 避免在源代码目录中生成构建文件 - 支持out-of-source构建(推荐做法)

1
cmake -S /mnt/d/Fyind/Master_Semester7/cpp -B /mnt/d/Fyind/Master_Semester7/cpp/build

C++23

print

1
2
3
4
5
6
7
8
#include <print>

int main() {
std::println("Hello world");
std::string name = "Fyind";
int age = 25;
std::println("Name: {}, Age: {}", name, age);
}

noexcept

noexcept 是 C++11 引入的异常规范关键字,用于声明函数不会抛出异常。

作用: 1. 编译器优化 - 编译器可以进行更激进的优化 2. 移动语义 - 标准库容器会优先使用noexcept的移动构造函数 3. 文档化 - 明确告知调用者函数不会抛异常

1
2
3
4
5
6
//基本语法:
// 声明函数不抛出异常
void func() noexcept;

// 条件性noexcept
void func() noexcept(condition);

内存管理

内存管理的重要性

计算机内存

虚拟地址空间,内存页

image-20250717203926581
  • 当前主流的虚拟内存实现方式是:将虚拟地址空间划分为固定大小的块,称为“内存页(page)”

  • 当一个进程访问某个虚拟地址时,操作系统会检查该虚拟地址所在的页是否已加载到物理内存中(即是否映射到了物理页框 page frame)。如果没有,就会发生 页错误(page fault)

    • 页错误并不是程序错误,而是一个正常的、受控的硬件中断,目的是从磁盘加载数据到内存
  • 如果物理内存没有空闲页框了,系统就必须从内存中移除一个已有的页(称为“页置换”)

  • 如果被移除的页是“脏页(dirty page)”,即该页自从从磁盘加载进内存之后有过修改,就必须先将其写回磁盘,以防数据丢失。

  • 如果是“干净页(clean page)”,即未被修改,直接丢弃即可,无需写回磁盘。

这整个过程被称为 paging(分页置换)

  • iOS不支持脏页写回,内存不足时直接终止进程。

抖动(Thrashing)

  • 系统物理内存不足时,频繁进行页面置换。
  • 导致系统性能急剧下降。
  • 通过监控page fault频率判断是否发生抖动。

进程内存

栈内存

  • 栈是一块连续的内存区域。
  • 每个线程有独立的栈。
  • 栈的大小固定,超出会栈溢出。
  • 栈内存分配和释放非常快,不会产生碎片。
  • 栈增长方向通常向下。
  • 示例:递归函数可能导致栈溢出。
  • 默认栈大小约为8MB(在Mac系统上)。
  • 每个线程都有自己独立的栈 → ✅ 线程安全

堆内存

  • 堆是全局共享的内存区域。
  • 用于动态内存分配(new/malloc)和释放(delete/free)。
  • 堆内存分配模式不固定,容易产生内存碎片。
  • 示例:频繁分配和释放不同大小内存可能导致碎片。
  • 因为堆是共享资源 → ❌ 不是线程安全,需要配合互斥锁等机制。

内存中的对象

而堆内存就很“混乱”:你可以随时在任意位置 newdelete

这就可能造成 内存碎片(fragmentation)

创建与删除对象

new 与 delete 的工作原理

  • new 操作包含两个步骤:
    • 分配内存(调用 operator new)。
    • 构造对象(调用构造函数)。
  • delete 操作也包含两个步骤:
    • 析构对象(调用析构函数)。
    • 释放内存(调用 operator delete)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User {
std::string name;
public:
User(std::string name) : name(name) {}

void print_name();
};

void User::print_name() {
std::println("Name: {}", this->name);
}
// main
auto user = new User{"John"};
user->print_name();
delete user;

定置 new(Placement new)

  • 允许分离内存分配与对象构造。就是 placement new,意思是“在这块内存上构造对象”。

    它不会分配内存,只是调用构造函数。

  • 使用示例:

    1
    2
    3
    4
    5
    auto 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
    6
    auto 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
    10
    auto 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
    2
    auto* 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
  • 不对齐的内存访问可能导致性能下降或程序崩溃。

image-20250717212604845

内存分配对齐保证

  • 使用 new 或 malloc 分配的内存满足最大对齐要求。

  • std::max_align_t 表示最大对齐类型。也就是说,任何基本类型(比如 intdoublelong doublechar 等)的对齐要求都不会超过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RAIIConnection {
public:
RAIIConnection(const std::string& url)
: connection_{open_connection(url)} {}

~RAIIConnection() {
try {
close(connection_);
} catch (const std::exception&) {
// 不应从析构函数中抛出异常
}
}

auto& get() { return connection_; }

private:
Connection connection_;
};

容器

  • 使用标准容器自动管理动态内存。
  • 容器负责其存储对象的内存所有权。
  • 减少代码中显式使用new和delete的情况。

智能指针

独占指针
  • std::unique_ptr 表示独占所有权。
  • 独占所有权不可复制,但可转移。
1
2
auto owner = std::make_unique<User>("John");
auto new_owner = std::move(owner); // Transfer ownership
共享指针
  • std::shared_ptr表示共享所有权。
  • 使用引用计数跟踪对象的所有者数量。
  • 当最后一个所有者释放时,对象自动删除。
1
auto i = std::make_shared<double>(42.0);
弱指针

示例应用场景:

场景 解决方案
GUI 中父子窗口 父窗口持有子窗口(shared),子窗口持有父窗口(weak)
树形结构节点 父节点持有子节点(shared),子节点回指父节点(weak)
观察者模式 被观察者持有 weak_ptr 指向观察者
  • std::weak_ptr 表示弱所有权,不延长对象生命周期。是一种不会增加引用计数的指针。
  • 用于打破共享指针之间的循环引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>
#include <iostream>

struct B;

struct A {
std::shared_ptr<B> b;
~A() { std::cout << "A destroyed\n"; }
};

struct B {
std::weak_ptr<A> a; // 改为 weak_ptr,打破循环
~B() { std::cout << "B destroyed\n"; }
};

int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b = b; // A 持有 B(shared)
b->a = a; // B 持有 A(weak)

return 0; // 程序结束后 A 和 B 都被正确销毁
}
  • 使用lock()方法将weak指针转换为shared指针。
1
2
3
4
5
if (auto shared_i = weak_i.lock()) {
std::cout << *shared_i << '\n'; // 安全访问
} else {
std::cout << "对象已被销毁,无法访问\n";
}
关键词 含义
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::vectorstd::string 在存储数据时,通常会在堆上分配内存。当存储的数据量很小,比如只有几个字符时,分配和释放堆内存的开销反而会影响性能。

  • 对于短字符串或小容器,使用栈内存代替堆内存以提升性能。
  • 标准库 std::string 通常使用小字符串优化(SSO)。
  • 实际使用union实现短模式和长模式的内存布局切换。
  • 示例:std::string在24字节栈内存中可存储22字符。
image-20250717221907713

自定义内存管理

构建一个内存池(Arena)

  • Arena是一个连续内存块,用于高效分配和回收内存。
  • 支持固定大小分配、单线程优化、有限生命周期等策略。单线程:无需锁,速度快
  • 示例:使用Howard Hinnant的short_alloc实现栈分配器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <size_t N>
class Arena {
static constexpr size_t alignment = alignof(std::max_align_t); // 最大对齐需求
public:
Arena() noexcept : ptr_(buffer_) {} // 指针初始指向buffer头部
Arena(const Arena&) = delete; // 禁止拷贝构造和赋值
Arena& operator=(const Arena&) = delete;

auto reset() noexcept { ptr_ = buffer_; } // 重置指针,回收所有内存
static constexpr auto size() noexcept { return N; }
auto used() const noexcept { return static_cast<size_t>(ptr_ - buffer_); } // 已用内存大小

auto allocate(size_t n) -> char*; // 分配函数声明
auto deallocate(char* p, size_t n) noexcept -> void; // 释放函数声明

private:
// 对齐函数:将n向上取整到alignment的倍数
static auto align_up(size_t n) noexcept -> size_t {
return (n + (alignment-1)) & ~(alignment-1);
}

// 检查指针是否在buffer范围内
auto pointer_in_buffer(const char* p) const noexcept -> bool {
return buffer_ <= p && p <= buffer_ + N;
}

alignas(alignment) char buffer_[N]; // 内存缓冲区,大小N
char* ptr_{}; // 当前分配指针
};

  • Arena类模板支持对齐内存分配。
  • allocate和deallocate方法用于分配和回收内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<size_t N>
auto Arena<N>::allocate(size_t n) -> char* {
const auto aligned_n = align_up(n); // 调整大小到对齐边界
const auto available_bytes = static_cast<decltype(aligned_n)>(buffer_ + N - ptr_); // 剩余空间大小
if (available_bytes >= aligned_n) {
char* r = ptr_; // 返回当前指针
ptr_ += aligned_n; // 移动指针,表示已使用
return r;
}
return static_cast<char*>(::operator new(n)); // 缓冲区不够大时,调用全局new分配
}
template<size_t N>
auto Arena<N>::deallocate(char* p, size_t n) noexcept -> void {
if (pointer_in_buffer(p)) { // 如果指针属于 buffer_
n = align_up(n);
if (p + n == ptr_) { // 只有释放的是最后分配的内存块,才回收指针
ptr_ = p;
}
// 否则忽略释放请求(不能释放中间内存)
} else {
::operator delete(p); // 非buffer内存交给全局delete处理
}
}

  • 示例:为User类重载new和delete操作符,使用Arena分配内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto user_arena = Arena<1024>{};  // 创建一个1024字节的arena

class User {
public:
auto operator new(size_t size) -> void* {
return user_arena.allocate(size);
}
auto operator delete(void* p) -> void {
user_arena.deallocate(static_cast<char*>(p), sizeof(User));
}
// 支持数组new/delete
auto operator new[](size_t size) -> void* {
return user_arena.allocate(size);
}
auto operator delete[](void* p, size_t size) -> void {
user_arena.deallocate(static_cast<char*>(p), size);
}

private:
int id_{};
};

自定义内存分配器

  • 为什么类特定的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template <class T, size_t N>
struct ShortAlloc {
using value_type = T;
using arena_type = Arena<N>;

ShortAlloc(arena_type& arena) noexcept : arena_(arena) {}

template <class U>
ShortAlloc(const ShortAlloc<U, N>& other) noexcept : arena_(other.arena_) {}

T* allocate(size_t n) {
return reinterpret_cast<T*>(arena_.allocate(n * sizeof(T)));
}

void deallocate(T* p, size_t n) noexcept {
arena_.deallocate(reinterpret_cast<char*>(p), n * sizeof(T));
}

template <class U, size_t M>
bool operator==(const ShortAlloc<U, M>& other) const noexcept {
return N == M && std::addressof(arena_) == std::addressof(other.arena_);
}

template <class U, size_t M>
bool operator!=(const ShortAlloc<U, M>& other) const noexcept {
return !(*this == other);
}

private:
arena_type& arena_;
};

  • 自定义分配器提升性能,减少堆内存使用。