%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # C 与 C++ 动态内存 API 总结 - **C++**: - **new/delete 表达式**: `new T`、`new T[]`、`delete ptr`、`delete [] ptr`; - **定位 new 表达式**:`new (*ptr) T`; - **分配器**:`std::allocator<T>` - 分配器接口类:`std::allocator_traits<std::allocator<T>>` - 智能指针:`std::unique_ptr`、`std:shared_ptr`、`std::weak_ptr` - 其他:`aligned_alloc(alignment, size)` (since C++17) - **C**: - `malloc` 、`calloc`、`realloc` - `free` > [!info] malloc/free 以及 new/delete 是 "==线程安全==" 的 > > - C 标准库实现例如 glibc 保证 `malloc/free` 是线程安全的; > - C++中的 `new/delete` 背后仍是调用 `malloc/free`,故也是线程安全的。 > [!note] new 表达式包含 **"内存分配" 与 "对象构造"** 操作;delete 表达式包含 "**对象析构" 与 "内存释放"** 操作。 > > 如果希望分离这些步骤,有两种方式: > > - **使用 `std::allocator<T>` 分配器**: > - 其 `.allocate()`,`.construct()`,`.destroy()`,`.deallocate()` 4 个成员函数将上述 4 个步骤进行了分离。 > > - **使用定位 new 表达式**: > 1. 先通过 `void *mem = ::operator new(sizeof(T));` **申请未初始化的内存空间**; > 2. 使用定位 new 表达式:`T* ptr = new (mem) T; ` ,**调用 `T` 类型的构造函数**。 > [!caution] 上述各类 "动态内存" API 不能混用! > > 在 "**内存分配**" 与 "**对象构造**" 方面: > > - 对于 `std::allocator<T>`: > - 传递给 `.construct()` 的指针 **必须指向 "由==同一个 allocator 对象==通过 `.allocate()` 分配的内存空间"**,而不能是 `operator new()` 或 `malloc()` 分配的内存; > - 对于**定位 new 表达式**: > - 传给其的指针可以指向 "**==任何方式分配的内存==**"(甚至不必是堆内存) > - 例如,可以是 `operator new()`,`malloc()`,`allocator.allocate()` 分配的动态内存,或者是 **栈上内存**。 > > 在 "**内存分配**" 与 "**内存释放**" 方面: > > - "`new` 表达式" 分配和构造动态对象,**只能由 `delete` 析构并释放**; > - "`operator new()` " 申请的动态内存,**只能由 `operator delete()` 释放**,而不能用 `free()` 或 `allocator.deallocate()`; > - "`allocator.allocate()`" 申请的动态内存,只能由 **==同一个 allocator 对象==通过 ==`.deallocate()` 释放==**; > - "`malloc`" 等 C API 申请的动态内存,**只能由 `free` 进行释放**,而不能用 `operator delete()`; > <br><br><br> # 动态内存管理机制 > 参见 [^1] [^2] [^3] C/C++ 中 `malloc` 与 `new` 背后有一套 **空闲内存管理机制**,用以管理 **当前堆区上既有的空闲内存**。 每次通过这俩接口**请求动态内存**时,是从 "**==当前堆区的既有空闲内存==**" 中进行分配,不一定涉及 `brk` 或 `sbrk` 系统调用。 仅在 "**当前堆区内存耗尽**" 时,才会**发起==系统调用== `brk` 或 `sbrk` 请求 OS 扩展堆的大小**(修改 **==堆顶位置==**)。 单次扩展时通常会**请求相对较大的堆空间**,从而减少发起系统调用的次数。 进程运行时,**堆的初始大小为 0**,故进程首次调用 `malloc`/`new` 时,就会**发起系统调用 `brk` 或 `sbrk`** 请求 OS 扩展堆的大小。 ## malloc/new 申请失败 在 Linux 的 "**延迟映射**" 机制下,`brk`, `sbrk`, `mmap` 等系统调用只是 "**申请==虚拟地址空间==**"。 内核**并没有立即实际分配物理页**,而是直至 "**某个虚拟页被==首次访问==而触发==缺页异常==**" 时,缺页异常处理程序中才**实际分配物理页并更新页表映射**。 所以,即使 **物理内存耗尽**,通常来说 `malloc` / `new` 请求内存时也不会失败,因为其只是申请 "**虚拟地址空间**"。 **导致 malloc/new 申请内存失败**的直接原因通常是: 1. **==虚拟地址空间耗尽==——堆区无法再被 `brk`/`sbrk`扩展,`mmap` 也无法申请到所需空间** - 32 位系统上,进程虚拟地址空间中,内核地址空间占 1G,**用户地址空间上限为 3G**,后者很容易耗尽; - 64 位系统上,进程虚拟地址空间极大,故通常是**受系统参数限制**而申请失败, 2. **==系统参数限制==** 1. `ulimit -v` 中的 "**virtual memory**" 软限制,进一步约束了 **单进程的虚拟空间大小**; 2. 内核参数`/proc/sys/vm/overcommit_memory` 控制的 **==虚拟内存分配策略==**: - 0:内核根据**启发式策略**决定是否允许分配; - 1:总是允许(即使物理内存不足) - 2:严格限制,总虚拟内存使用不能超过 `swap + overcommit_ratio * RAM` > [!NOTE] Linux 内核参数 `/proc/sys/vm/overcommit_memory` 控制 "虚拟内存分配策略" > > 默认值为 0,会结合**当前空闲物理内存**进行限制,允许一定程度的超量分配(另一参数 `/proc/sys/vm/overcommit_ratio` 控制比例,默认 50%) > > 在 "延迟映射机制" 下, `malloc` 可以 **不受物理内存限制地申请==虚拟内存==**(只要虚拟地址空间足够便成功分配)。 > > 为实现任意分配,需将 `overcommit_memory` 参数修改为 `1`: > > ```cpp > int main() { > constexpr unsigned long long GB = 1024 * 1024 * 1024; > unsigned long long size = 1024 * GB; // 申请1TB内存 > // 修改内核参数`sudo sysctl -w vm.overcommit_memory=1`后, malloc可返回分配地址, 否则申请失败返回空指针. > auto ptr = malloc(size); > cout << ptr << endl; > } > ``` > <br> ## malloc 与 free 的实现原理 - 调用 `malloc(n)` 时,实际上会**请求大于 `n` 的空闲内存**,**整块内存的起始部分**用以存储 "**==头块==**"——其中包含了 "**实际请求的原始内存大小**"、"**幻数**"(用于完整性检查)等信息,而**实际返回的是 "==头块之后的内存地址=="**。 - 调用 `free(ptr)` 时,会**取得 `ptr` 之前的头块地址 `header_t* hptr = (char*)ptr - sizeof(header_t)`**,获取头块中存储的 "**实际请求的原始内存大小 `size`**",再**由 malloc 背后的空闲内存管理机制==释放头块地址 `hptr` 起始的 `size` 个字节==**。 > [!info] malloc 背后实现中,对于小块内存从堆区分配,大块内存可能直接通过 `mmap` 映射到独立区域 > [!example] 单次调用 `malloc(n)` 时,实际请求内存的示意图[^2] > > ![[_attachment/02-开发笔记/02-c/c-内存管理.assets/IMG-c-内存管理-715F03CC262DCA750C79917F1CBA89B7.png|620]] > <br> ## brk 与 sbrk 系统调用 > 参见[^3] ![[_attachment/02-开发笔记/02-c/c-内存管理.assets/IMG-c-内存管理-FD60FAC1BFCC6CFEE9ED9BD6A713E1E6.png|574]] 进程虚拟内存空间中,**==堆顶位置==(高地址)的内存地址** 称之为 "**program break**",也称 "**==brk 指针==**"。 两个系统调用均用于 "**==扩展堆区大小==**",即修改 **brk 指针** 位置: - `bkr(addr)`:直接设置 **堆顶地址为 `addr`**(内存以页为单位分配,故会向上取整对齐 **==内存页边界==**) - `sbrk(size)`:在 **原有堆顶地址** 上,增加 `size` 大小(可为负,则减小),成功时返回 "**前一堆顶地址**"(也即新扩展的堆内存的起始地址) - 调用 `sbrk(0)` 将可获取 **当前堆顶位置**; <br><br><br> # malloc 与 free 函数 > 参见 [^4] [^5] > ![[_attachment/02-开发笔记/02-c/c-内存管理.assets/IMG-c-内存管理-75C2F3865F7E3A1EBFCE4A16799DC6CC.png|778]] > | 接口 | 说明 | | ----------------------------- | --------------------------------------------------------------------------------------------------- | | `void* malloc(size)` | 分配 **==指定字节大==小的内存块**,返回指向该内存的指针 | | `void* calloc(nojb, size)` | **为 `nobj` 个大小为 `size` 字节的对象分配内存块**,并且将内存 **==全置 0==**,返回指向该内存的指针 | | `void* realloc(ptr, newsize)` | **增加或减少** `ptr` 指向的**已分配内存块**的大小为 `newsize`,返回**指向新内存位置的指针**<br>(增加大小时,可能重新申请一块内存,将**旧内存内容拷贝到新内存**) | | `void free(ptr)` | 释放指针所指的**动态内存**区域,传入指针可以是 `NULL`,此时不执行任何操作。 | > [!note] 标准库的实现保证 mallloc 等分配函数的返回地址能 **"满足任意数据类型的对齐要求"** [^5][^3] > > - 32 位系统下,`malloc` 返回地址按 8 字节对齐; > - 64 位系统下,`malloc` 返回地址按 16 字节对齐[^6] 。 <br><br><br> # new 与 delete 表达式 | 表达式 | 说明 | | -------------- | ----------------------------------------- | | `new T` | **动态分配一个对象并初始化**,返回指向该对象的指针 | | `new T[N]` | **动态分配一个大小为`N`的数组并初始化**,返回指向该数组中首元素的指针 | | `delete ptr` | 释放指针所指**对象**的动态内存(必须是由 `new` 分配的内存空间) | | `delete[] ptr` | 释放指针所指 "**数组**" 的动态内存(必须是由 `new` 分配的内存空间) | > [!caution] 传递给 delete 的指针必须:1)指向动态内存;或者 2)是个空指针(则不执行任何操作) ```cpp // 动态分配对象 string *ps = new string; // 默认初始化, 调用默认构造函数, 得到空字符串. string *ps = new string(10, '9'); // 10个'9'构成的字符串. int* pi = new int; // 默认初始化, 随机值 int* pi = new int(); // 值初始化, 0值; // 动态分配常量对象 const int* pci = new const int(1024); // 动态分配一个const对象, 对于内置类型必须显式初始化, 对于类类型可以默认初始化(调用默认构造函数). const string* pcs = new const string; // 调用默认构造函数 => 空字符串; // 动态分配数组 int* array = new int[10]; // 默认初始化, 随机值 int* array = new int[10]{}; // 值初始化, 0值. int* array = new int[10]{4, 3, 2}; // 直接初始化, 剩余元素则进行值初始化; // 释放内存 delete pi; // 释放动态分配的单个对象. delete[] array; // 释放动态分配的数组 ``` <br><br> ## 区分 "**`new` / `delete` 表达式**" 与 "**`operator new/delete` 操作符**" > 参见 [^7] - **==new 表达式==**(`new T` 或 `new T[N]`)背后,编译器实际执行三个步骤: - (1)调用标准库函数 `operator new(sizeof(T))` 或 `operator new[](N * sizeof(T))`,**分配指定字节的、==未初始化的原始内存空间==** - (2)调用**类型 `T` 的==构造函数==**,**在所申请的内存地址上完成对象 or 数组构造**。 - (3)返回**指向该内存地址的指针**(`T*` 类型) - **==delete 表达式==**(`delete ptr` 或 `delete [] ptr`)背后,编译器实际执行两个步骤: - (1)对指针所指的对象或数组中元素调用其 "**==析构函数==**",**销毁对象**。 - (2)调用标准库函数 `operator delete()` 或 `operator delete[]()`,**释放 new 分配的动态内存空间**。 因此,**操作符函数**本身只具有 "**申请内存**" 与 "**释放内存**" 的功能: - `operator new()` 操作符:**==仅分配内存==**,其**函数原型为 `void* operator new(std::size_t)`,支持重载**。 - `operator delete()` 操作符:**==仅释放内存==**,其**函数原型为 `void operator delete(void*)`,支持重载**。 - `operator new[]()` / `operator delete[]()` 与上述二者类同; > [!caution] 能够被 "重定义" 的是 `operator new()/delete()` 操作符函数,而不是 new/delete 表达式的行为,后者无法被改变! ```cpp class MyClass {}; int main() { // 手动模拟 new 表达式的执行过程 void* mem = ::operator new(sizeof(MyClass)); // 1.分配未初始化的内存 MyClass* ptr = new (mem) MyClass; // 2.调用构造函数(placement new), 分配的内存上构造对象 // 手动模拟 delete 表达式的执行过程 ptr->~MyClass(); // 1. 显式调用析构函数 ::operator delete(mem); // 2. 释放内存 } ``` <br><br> ## new 与 delete 操作符 运算符 `new` , `new[]`, `delete`, `delete[]` 的函数原型[^8]: > ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-动态内存管理.assets/IMG-cpp-动态内存管理-22CB06AB6274792490DFCC352FC012E4.png|696]] 上述运算符可以作为 "**==全局作用域函数==**" 或 "**==类成员函数==**" 被**重定义**。 - 对于 "**类类型**",编译器首先在其**类作用域**及**基类作用域**中查找。 - 当定义为 "**类成员函数**" 时,上述函数默认为 "**==隐式的静态成员函数==**"(无需声明`static`)。 - 因为 `operator new()` 用于 "**构造对象之前**",而 `operator delete()` 用于 "**析构对象之后**",因此必须得是静态的。 <br> %% ## 自定义 new/delete 操作符 一个自定义实现版本[^8]: ```cpp #include <cstdlib> #include <iostream> using std::cout; using std::endl; /* 自定义new, new[], delete, delete[] */ void* operator new(std::size_t size) { cout << "[Custom new] Allocating " << size << " bytes\n"; void* ptr = std::malloc(size); // 使用 malloc分配内存 if (!ptr) { throw std::bad_alloc(); // 分配失败时, 抛出异常. } return ptr; } void operator delete(void* ptr) noexcept { cout << "[Custom delete] Freeing memory\n"; std::free(ptr); // 编译器会在释放内存前, 自动调用对象的析构函数. } void* operator new[](std::size_t size) { cout << "[Custom new[]] Allocating array of " << size << " bytes\n"; // 额外分配空间, 存储该地址上"数组"大小. void *ptr = std::malloc(size + sizeof(std::size_t)); if (!ptr) { throw std::bad_alloc(); } // 在分配的起始位置, 存储数组大小. *(static_cast<std::size_t*>(ptr)) = size; // 返回实际的"数据起始位置": static_cast<char*>(ptr) + sizeof(std::size_t), 再转成void*返回. return static_cast<void*>(static_cast<char*>(ptr) + sizeof(std::size_t)); } void operator delete[](void* ptr) noexcept { if (!ptr) return; // 得到这块动态内存的真正起始地址. void* real_ptr = static_cast<char*>(ptr) - sizeof(std::size_t); // 获取存储的数组大小 std::size_t size = *(static_cast<std::size_t*>(real_ptr)); cout << "[Custom delete[]] Freeing array of " << size << " bytes\n"; // 释放整块内存 std::free(real_ptr); } struct Test { Test() { cout << "Test constructor" << endl; } ~Test() { cout << "Test destructor" << endl; } }; int main() { Test* t = new Test; delete t; Test* arr = new Test[5]; delete[] arr; } ``` %% <br><br><br> # 定位 new 表达式 > "**placement new**" (定位 new) "**定位 new 表达式**" 接受一个 "**`void*`指针**" 参数,用以**在==指定的、预先分配的内存地址==上**构造对象。 其通常用于**自定义内存管理场景**,例如内存池或嵌入式编程(例如,需要**通过特定地址进行访问的硬件**,或者在**预分配的内存池中创建对象**) ## 定位 new API > 由头文件 `<new>` 提供。说明参见[^8] > > "**定位 new 表达式**"的使用形式: > > ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-动态内存管理.assets/IMG-cpp-动态内存管理-6F4DA9E09F1C8D6B8C5D16B199F7424D.png|711]] 定位 new 表达式背后调用的是下列重载版本的 **new 函数**,其 **直接使用传递给它的地址**,而**不会申请任何内存**。 ```cpp // 函数原型. 其中`ptr` 指定用以构建目标对象的内存地址. void* operator new(size_t size, void* ptr) noexcept; // 实现示意: 直接返回传入的指针, 不做任何其余操作. inline void* operator new(std::size_t, void* ptr) noexcept { return ptr; } ``` **==定位 new 表达式==** 背后,编译器实际执行下述步骤: - (1)**调用上述重载版本的标准库 ==new 函数==**——该函数==**不分配任何动态内存**==,**仅简单地直接返回指针实参`ptr`**。 - (2)在指定地址 `ptr` 上**调用 `Type` 的构造函数**,**完成对象或数组元素的构造**; - (3)返回 **`Type*` 类型的指针** > [!error] 定位 new 表达式调用的标准库函数 `void* operator new(size_t size, void* ptr);` 不允许被重定义! > [!NOTE] 传递给定位 new 表达式的指针 "**==可指向任意内存==**",不一定是堆内存,也可以是栈区内存。 ```cpp // 使用方式: T* ptr = new (ptr) T; // placement new exam void placement_new_exam() { // 以字节数组形式获取内存块. 此处为栈上内存. char buffer[sizeof(int)]; cout << static_cast<void*>(buffer) << endl; int *p1 = new (buffer) int(11); cout << static_cast<void*>(p1) << ": " << *p1 << endl; // 使用malloc分配原始内存块. 堆上动态内存. void* memo = std::malloc(sizeof(int)); cout << static_cast<void*>(memo) << endl; int* p2 = new (memo) int(42); // 在指定地址memo上构造对象. cout << static_cast<void*>(p2) << ": " << *p2 << endl; free(memo); // 释放动态内存. } ``` <br><br> ## 🚨 定位 `new` 使用注意 使用定位 new时,是在 **指定地址** 上构造对象,通常是 **预分配内存** 后取这块内存空间的首地址。 但是,这块内存可能并不是 **通过常规`new` 操作符从堆上分配的** 。 例如,声明了一个字符数组 `char buffer[N];` 来作为这块预分配内存。 该如果直接使用 `delete` 释放,将会导致未定义的行为,因为 **`delete` 会试图释放并归还这块内存到堆**,但**这块内存并不是从堆上分配**的。 所以,对于“定位 new”构造的对象,如果这块内存是在**栈区或静态区**,则只需 **显式地调用对象的析构函数** 来销毁对象,而**不需要对内存空间做任何释放**,不应使用 `delete`。 <br> <br> # 内存操作 API > 位于头文件 `<string.h>` 中 ![[_attachment/02-开发笔记/02-c/c-内存管理.assets/IMG-c-内存管理-E46D57A7C7CB4E2DA64DD17DA5EB303E.png|674]] - `memset(*s, value, n)`: 将 `*s` 所指内存处的 **n 个字节==设置==为指定值** - `memcpy(*dest, *src, n)`: 将 `*src` 所指内存处的 **n 个字节==复制==到 `*dest`** ,要求**两内存区域不能有重叠**,否则为 ub。 - `memmove(*dest, *src, n)`:将 `*src` 所指内存处的 **n 个字节==复制==到 `*dest`**,支持在 "**==内存重叠==**" 的情况下安全进行复制。 - `memchr` - `memcmp` > [!caution] `memcpy` 与 `memmove` 的区别 > > 两个函数均用于**处理原始内存(字节级数据)的复制**,差异在于: > - `memcpy` 的实现是从 "**指针所指处首字节**" 开始逐一拷贝,因此**若两区域重叠,可能导致 `src` 尾部数据被覆盖**。 > - `memmove` 的实现是从 "**尾部字节**"(`src + n`)开始逐一拷贝,因此即使两内存区域重叠,也不会出现问题。 > > [!NOTE] C++标准库头文件 `<algorithm>` 提供了 `std::copy()` 与 `std::copy_backward()` 函数 > > C++ 提供的两个函数支持 "**复制对象**",可处理任意类型(**类型安全的** ),会根据对象类型调用**拷贝构造函数**,其接受参数为 "**迭代器**",返回值为 "**指向目标范围最后一个拷贝元素的后一位置的迭代器**" 适用于 STL 容器、类类型和任何对象。 > > - `std::copy()`:要求**源和目标范围不重叠**。如果重叠,为未定义的行为。 > - `std::copy_backward()`:用于处理 "**源和目标范围重叠的情况**",给定 `dst` 作为末尾位置,复制方向 "**==从后往前==**",同 `memmove`。 ```cpp constexpr int m = 48, n = 48; bool visited[m][n]; memset(visited, false, sizeof(visited)); ``` <br><br><br> # 常见的动态内存相关错误 - **未初始化分配的内存**:未对申请的动态内存进行**初始化**,其中**值为任意随机数据**(野指针) - **内存泄露**:未对动态内存手动进行释放(通过`free` 或 `delete`) - 常见于:1)指针在多个对象间传递时;2)函数抛出异常时,无异常处理或异常处理代码中没有 delete; - **悬空指针**:指针指向**已被释放的动态内存**,使用该指针时将导致未定义行为。 - **重复释放**:多次调用 `free` 或 `delete` 释放同一块动态内存,导致**未定义行为**。 - **错误释放**:传给 `free` 或 `delete` 的**并非指向动态内存空间的指针**。 %% # 内存检测工具 - `purify` - `valgrind` %% <br><br><br> # ❓FAQ #### ❓<font color="#c00000">malloc 与 new 的区别</font> - `malloc` 是 C 的函数,其会申请**指定字节大小的、未初始化的内存**,当申请失败时**返回空指针**(值为 0 或者 NULL 宏 )。 - `new` 是 C++中的关键字,`new` 表达式本身包括 "**申请内存**" 以及 "**构造对象**" 两个步骤; 1. **调用 `operator new()` 操作符==申请未初始化的内存==**,申请内存失败时**将抛出一个 `std::bad_alloc` 异常**。 2. 内存申请成功时,在所得地址上**调用对象的==构造函数==**,**返回一个指向该对象的指针**。 <br><br> # 参考资料 # Footnotes [^1]: 《操作系统:原理与实现》(P109) [^2]: 《操作系统导论 OSTEP》(P123,第 17 章,空闲空间管理) [^3]: 《Linux-UNIX 系统编程手册》(P113,第 7 章,内存分配) [^4]: 《C 语言程序设计:现代方法》 (P325) [^5]: 《Unix 环境高级编程》(P165) [^6]: 《深入理解计算机系统》(P588) [^7]: 《C++ Primer》P726 [^8]: 《C++ Primer》P727-P729