C++内存管理:从基础到高级应用
立即解锁
发布时间: 2025-08-20 01:47:49 阅读量: 1 订阅数: 3 


C++高性能编程:从入门到精通
### C++ 内存管理:从基础到高级应用
#### 1. 新和删除运算符
在 C++ 中,`operator new` 函数负责在使用 `new` 表达式时分配内存。它既可以是全局定义的函数,也可以是类的静态成员函数。全局的 `new` 和 `delete` 运算符是可以重载的,这在分析内存使用情况时非常有用。
以下是重载全局 `new` 和 `delete` 运算符的示例代码:
```cpp
auto operator new(size_t size) -> void* {
void* p = std::malloc(size);
std::cout << "allocated " << size << " byte(s)" << '\n';
return p;
}
auto operator delete(void* p) noexcept -> void {
std::cout << "deleted memory\n";
return std::free(p);
}
```
我们可以通过创建和删除一个 `char` 对象来验证重载的运算符是否被实际使用:
```cpp
auto* p = new char{'a'}; // 输出 "allocated 1 byte(s)"
delete p; // 输出 "deleted memory"
```
当使用 `new[]` 和 `delete[]` 表达式创建和删除对象数组时,会使用另一对运算符,即 `operator new[]` 和 `operator delete[]`。我们可以用同样的方式重载它们:
```cpp
auto operator new[](size_t size) -> void* {
void* p = std::malloc(size);
std::cout << "allocated " << size << " byte(s) with new[]" << '\n';
return p;
}
auto operator delete[](void* p) noexcept -> void {
std::cout << "deleted memory with delete[]\n";
return std::free(p);
}
```
需要注意的是,如果重载了 `operator new`,也应该重载 `operator delete`。内存分配和释放函数是成对出现的,必须由分配内存的分配器来释放内存。例如,使用 `std::malloc()` 分配的内存应该始终使用 `std::free()` 释放,使用 `operator new[]` 分配的内存应该使用 `operator delete[]` 释放。
我们还可以重载特定类的 `operator new` 和 `operator delete`。这可能比重载全局运算符更有用,因为我们更有可能需要为特定类定制动态内存分配器。以下是为 `Document` 类重载 `operator new` 和 `operator delete` 的示例:
```cpp
class Document {
// ...
public:
auto operator new(size_t size) -> void* {
return ::operator new(size);
}
auto operator delete(void* p) -> void {
::operator delete(p);
}
};
```
当我们创建动态分配的 `Document` 对象时,将使用类特定版本的 `new`:
```cpp
auto* p = new Document{}; // 使用类特定的 operator new
delete p;
```
如果我们想使用全局的 `new` 和 `delete`,可以使用全局作用域 `::`:
```cpp
auto* p = ::new Document{}; // 使用全局的 operator new
::delete p;
```
#### 2. 内存对齐
CPU 每次将一个字的内存读入其寄存器。在 64 位架构中,字大小为 64 位;在 32 位架构中,字大小为 32 位,依此类推。为了让 CPU 在处理不同数据类型时高效工作,它对不同类型对象的存储地址有一定限制。C++ 中的每种类型都有对齐要求,它定义了某种类型的对象在内存中应该存储的地址。
如果一个类型的对齐值为 1,意味着该类型的对象可以存储在任何字节地址;如果对齐值为 2,则该类型的对象只能存储在 2 的倍数的地址上,依此类推。我们可以使用 `alignof` 来查看一个类型的对齐值:
```cpp
// 可能的输出是 4
std::cout << alignof(int) << '\n';
```
运行这段代码时,输出为 4,这意味着在我的平台上,`int` 类型的对齐要求是 4 字节,即 `int` 类型的对象必须存储在 4 的倍数的地址上。
使用 `new` 或 `std::malloc()` 分配内存时,返回的内存应该为我们指定的类型正确对齐。以下代码显示了为 `int` 分配的内存至少是 4 字节对齐的:
```cpp
auto p = new int();
auto address = reinterpret_cast<std::uintptr_t>(p);
std::cout << (address % 4ul) << '\n'; // 输出 0
```
实际上,`new` 和 `malloc()` 保证始终返回适合任何标量类型对齐的内存(如果它们能够返回内存的话)。`<cstddef>` 头文件提供了一个名为 `std::max_align_t` 的类型,其对齐要求至少与所有标量类型一样严格。以下代码显示了 `new` 返回的内存对于 `std::max_align_t` 和任何标量类型都是正确对齐的:
```cpp
auto* p = new char{};
auto address = reinterpret_cast<std::uintptr_t>(p);
auto max_alignment = alignof(std::max_align_t);
std::cout << (address % max_alignment) << '\n'; // 输出 0
```
让我们连续两次使用 `new` 分配 `char` 类型的内存:
```cpp
auto* p1 = new char{'a'};
auto* p2 = new char{'b'};
```
`p1` 和 `p2` 之间的空间取决于 `std::max_align_t` 的对齐要求。在我的系统上,这个空间是 16 字节,因此即使 `char` 的对齐值仅为 1,每个 `char` 实例之间也有 15 字节的间隔。
#### 3. 填充
编译器有时需要在我们自定义的类型中添加额外的字节,即填充。当我们在类或结构体中定义数据成员时,编译器必须按照我们定义的顺序放置这些成员。然而,编译器还必须确保类内部的数据成员具有正确的对齐,因此在必要时需要在数据成员之间添加填充。
例如,假设我们定义了一个 `Document` 类:
```cpp
class Document {
bool is_cached_{};
double rank_{};
int id_{};
};
std::cout << sizeof(Document) << '\n'; // 可能的输出是 24
```
可能输出为 24 的原因是,编译器在 `bool` 和 `int` 之后插入了填充,以满足各个数据成员和整个类的对齐要求。编译器将 `Document` 类转换为类似以下的形式:
```cpp
class Document {
bool is_cached_{};
char padding1[7]; // 编译器插入的不可见填充
double rank_{};
int id_{};
char padding2[4]; // 编译器插入的不可见填充
};
```
`bool` 和 `double` 之间的第一个填充是 7 字节,因为 `double` 类型的
0
0
复制全文
相关推荐










