%%
# 纲要
> 主干纲要、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