内存管理简介
详细技术视频请看我的主页
C++ 提供了多种内存管理方式,包括传统的 C 风格的 malloc
和 free
,以及 C++ 专用的 new
和 delete
。
理解这些内存管理方法对于编写高效、安全的 C++ 程序至关重要。
本文将详细介绍这些内存管理方法,包含基本用法、复杂操作(如 realloc
),并配以实际案例和代码示例。
内存管理基础
在 C++ 程序中,内存主要分为以下几个区域:
- 栈(Stack):自动管理内存,存储局部变量和函数调用信息。内存分配和释放速度快,但空间有限。
- 堆(Heap):手动管理内存,用于动态分配内存。内存分配和释放由程序员控制,灵活但易出错(如内存泄漏、悬挂指针)。
- 全局/静态区(
Data/BSS Segment
):存储全局变量和静态变量。
了解栈和堆的区别,以及如何有效地在堆上分配和管理内存,是编写高效且安全的 C++ 程序的基础。
C 风格内存管理
malloc
函数
malloc
(memory allocation)用于在堆上分配指定字节数的内存。其原型如下:
1 |
|
- 参数:
size
- 要分配的内存字节数。 - 返回值:指向分配内存的指针,如果分配失败则返回
nullptr
。
free
函数
free
用于释放之前由 malloc
、calloc
或 realloc
分配的内存。其原型如下:
1 | void free(void* ptr); |
- 参数:
ptr
- 要释放的内存指针。
示例代码
1 |
|
注意事项
- 类型转换:
malloc
返回void*
,需要显式转换为所需类型的指针。 - 初始化:
malloc
分配的内存未初始化,内容不确定。 - 释放对应性:由
malloc
分配的内存必须使用free
释放,避免使用delete
。
C++ 内存管理
C++ 提供了更高层次的内存管理操作符:new
和 delete
,它们不仅分配和释放内存,还调用构造函数和析构函数,提供类型安全。
new
操作符
用于在堆上分配对象,并调用其构造函数。
单个对象
1 | Type* ptr = new Type(parameters); |
- 例子:
1 |
|
输出:
1 | Constructor called with value: 10 |
数组
1 | Type* array = new Type[size]; |
- 例子:
1 |
|
输出:
1 | arr[0] = 0 |
delete
操作符
用于释放由 new
分配的内存,并调用析构函数。
释放单个对象
1 | delete ptr; |
释放数组
1 | delete[] ptr; |
区别于 malloc
和 free
- 类型安全:
new
返回正确类型的指针,免去了强制类型转换。 - 构造/析构:
new
和delete
自动调用构造函数和析构函数。 - 异常处理:在分配失败时,
new
默认抛出std::bad_alloc
异常,而malloc
返回nullptr
。
异常安全的 new
可以通过 nothrow
参数防止 new
抛出异常,改为返回 nullptr
。
1 |
|
总结和对比
了解 malloc/free
与 new/delete
的区别,有助于在编写 C++ 程序时正确选择内存管理方法。
特性 | malloc/free | new/delete |
---|---|---|
类型安全 | 需要显式类型转换 | 自动类型转换,无需显式转换 |
构造/析构函数 | 不调用对象的构造/析构函数 | 调用对象的构造/析构函数 |
返回值 | void* ,需要转换为目标类型 |
返回目标类型指针,类型安全 |
错误处理 | 分配失败返回 nullptr |
分配失败抛出 std::bad_alloc 异常 |
多态行为 | 无 | 支持多态,通过虚函数正确调用析构函数 |
内存分配与释放对应性 | 必须使用 free 释放由 malloc 分配的内存 |
必须使用 delete 释放由 new 分配的内存 |
示例对比
使用 malloc
和 free
1 |
|
注意:使用 malloc
分配 C++ 对象时,需要手动调用构造函数和析构函数,这非常不便且易出错。因此,推荐使用 new
和 delete
。
使用 new
和 delete
1 |
|
输出:
1 | Constructor called |
兼容性
- C++ 类型特性:
new
和delete
支持 C++ 的类型特性,包括构造函数、析构函数、多态等。 - C 兼容性:在需要兼容 C 代码或通过 C 接口分配内存时,仍可能需要使用
malloc
和free
。
高级内存管理
使用 realloc 进行内存重分配
realloc
用于调整之前分配的内存块大小。这在动态数组等数据结构中非常有用。
原型
1 |
|
参数
:
ptr
:指向之前分配的内存块。new_size
:新的内存大小(以字节为单位)。
返回值:指向重新分配后的内存块的新指针。如果重新分配失败,返回
nullptr
,原内存块保持不变。
示例代码
1 |
|
输出:
1 | Initial array: 1 2 3 |
动态数组管理
使用 malloc
和 realloc
来手动管理动态数组可以实现可变大小的数组,但需要处理内存分配、释放和数据复制。
封装动态数组
1 |
|
输出:
1 | Initial array: 1 2 3 |
注意:这种方式需要手动管理内存和数组大小,且缺乏类型安全性和自动化。推荐使用 C++ 标准容器如 std::vector
来代替。
实际案例
案例一:动态数组实现
实现一个简单的动态数组类,支持添加元素、访问元素和自动扩展。
代码示例
1 |
|
输出:
1 | Dynamic Array Contents: |
案例二:自定义内存管理器
实现一个简单的内存池,用于高效分配和释放固定大小的对象。
代码示例
1 |
|
输出:
1 | MyClass constructor: 100 |
说明:
- MemoryPool 管理固定大小的内存块,避免频繁调用
malloc
和free
。 - 使用“定位 new”在预分配的内存上构造对象。
- 需要手动调用析构函数和将内存返回给内存池。
注意:这种方法适用于大量小对象的高效管理,但需要确保正确使用构造和析构函数。
避免内存泄漏
内存泄漏是指程序分配的内存未被释放,导致内存被浪费,甚至耗尽。避免内存泄漏的策略包括:
- **确保每个
new
有对应的delete
**。 - 使用
RAII
和智能指针:自动管理资源,避免手动管理内存。 - 工具辅助:使用工具如
Valgrind
检测内存泄漏。
示例:内存泄漏
1 |
|
解决方法:
1 |
|
RAII
(资源获取即初始化)
RAII
是 C++ 中的一种编程惯用法,通过对象的生命周期管理资源,确保资源在对象构造时获取,析构时释放,避免泄漏。
示例:RAII
实现类似于shared_ptr
智能指针
std::shared_ptr
是 C++ 标准库中功能强大的智能指针之一,提供了共享所有权的能力,使得多个指针可以共同管理同一个动态分配的对象。通过引用计数机制,shared_ptr
确保了对象在最后一个指针被销毁时自动释放,极大地简化了内存管理,防止了内存泄漏和悬挂指针问题。
SimpleSharedPtr
的基本概念
SimpleSharedPtr
是一个简化版的 shared_ptr
实现,旨在帮助理解其核心机制。其基本功能包括:
- 共享所有权:多个
SimpleSharedPtr
实例可以指向同一个对象,共享对该对象的所有权。 - 自动管理生命周期:当最后一个
SimpleSharedPtr
被销毁或指向其他对象时,管理的对象被自动释放。 - 引用计数:内部维护一个引用计数,记录有多少个
SimpleSharedPtr
实例指向同一个对象。
引用计数控制块的设计
为了实现引用计数机制,SimpleSharedPtr
需要一个控制块(Control Block),它包含:
- 引用计数(
ref_count
):记录有多少个SimpleSharedPtr
指向同一个对象。 - 指向对象的指针(
ptr
):指向实际管理的对象。
控制块通常与被管理对象一起被分配,但为了简化实现,本示例将它们独立管理。
1 | struct ControlBlock { |
SimpleSharedPtr
的实现
类结构
SimpleSharedPtr
是一个模板类,模板参数 T
表示它所管理的对象类型。
1 | template <typename T> |
构造函数与析构函数
- 默认构造函数:初始化指针和控制块为空。
- 参数化构造函数:接受一个裸指针,初始化控制块,并引用计数为1。
- 析构函数:减少引用计数,若引用计数为0,则释放对象和控制块。
1 | // 默认构造函数 |
**辅助函数 release
**:
1 | private: |
拷贝构造与拷贝赋值
拷贝构造函数和拷贝赋值操作符允许多个 SimpleSharedPtr
实例共享同一个对象,共享相同的控制块。
1 | // 拷贝构造函数 |
移动构造与移动赋值
移动语义允许资源所有权从一个 SimpleSharedPtr
转移到另一个,而不增加引用计数。
1 | // 移动构造函数 |
操作符重载
重载 *
和 ->
操作符,以便像使用原生指针一样使用 SimpleSharedPtr
。
1 | // 解引用操作符 |
其他成员函数
- **
use_count
**:返回当前引用计数。 - **
get
**:返回裸指针。 - **
reset
**:重置指针,指向新对象或nullptr
。
1 | // 获取引用计数 |
完整代码示例
以下是 SimpleSharedPtr
的完整实现及其使用示例。
1 |
|
SimpleUniquePtr
的实现
std::unique_ptr
是一种独占所有权的智能指针,确保在任意时刻,只有一个 unique_ptr
实例指向特定资源。它不支持拷贝操作,只支持移动操作。
基本结构
首先,定义一个模板类 SimpleUniquePtr
,它持有一个指向资源的裸指针:
1 | template <typename T> |
构造函数与析构函数
- 默认构造函数:初始化指针为空。
- 参数化构造函数:接受一个指向资源的裸指针。
- 析构函数:当
SimpleUniquePtr
被销毁时,释放所管理的资源。
1 | // 默认构造函数 |
删除拷贝构造与拷贝赋值
为了确保唯一性,禁止拷贝构造和拷贝赋值:
1 | // 删除拷贝构造 |
移动语义
支持移动构造和移动赋值,以转移所有权:
1 | // 移动构造 |
操作符重载
重载 *
和 ->
操作符,以模拟指针的行为:
1 | // 解引用操作符 |
示例代码
以下示例展示了如何使用 SimpleUniquePtr
:
1 |
|
输出:
1 | Test Constructor: 1 |
解释:
- 创建
ptr1
并指向一个Test
对象。 - 使用
std::move
将所有权转移到ptr2
,ptr1
变为nullptr
。 - 使用
release()
释放ptr2
的所有权,获取裸指针后需要手动delete
。 - 使用
reset()
重新指向一个新的Test
对象,自动释放之前的资源。
总结
本文详细介绍了 C++ 中的内存管理方法,包括基础的 malloc
和 free
,以及更现代的 C++ 风格的 new
和 delete
。通过对比两者的特点,强调了 new
和 delete
在 C++ 中的优势,如类型安全、自动调用构造和析构函数等。
高级内存管理部分探讨了如何使用 realloc
进行内存重分配,并通过实际案例展示了如何实现动态数组和自定义内存管理器。最后,介绍了最佳实践,强调避免内存泄漏的重要性,以及 RAII
和智能指针对内存管理的帮助。