文章目录
- 一、为什么要学习 C++ 内存管理?
- 二、C/C++ 程序的内存分布
- 2.1 程序运行时,内存是如何划分的?
- 2.2 栈区:存放普通局部变量等数据
- 2.3 堆区:存放动态申请的内存
- 2.4 数据段:存放全局变量和静态变量
- 2.5 代码段与只读常量
- 三、通过代码练习判断变量存储位置
- 四、C 语言的动态内存管理:malloc、calloc、realloc 和 free
- 4.1 malloc:申请动态内存
- 4.2 calloc:申请内存并清零
- 4.3 realloc:调整动态内存大小
- 4.4 free:释放动态内存
- 4.5 四个函数的区别
- 五、C++ 的动态内存管理:new 和 delete
- 5.1 new:动态申请并初始化对象
- 5.2 new[]:申请连续的多个对象
- 5.3 new 和 delete 如何处理自定义类型?
- 5.4 malloc/free 与自定义类型
- 六、operator new 与 operator delete:理解底层原理
- 6.1 operator new 是什么?
- 6.2 operator new 与 malloc 的关系
- 6.3 operator delete 与 free 的关系
- 七、new 和 delete 的实现原理
- 7.1 内置类型的实现原理
- 7.2 自定义类型的 new 原理
- 7.3 自定义类型的 delete 原理
- 7.4 new[] 和 delete[] 的原理
- 八、定位 new 表达式:在已有内存中构造对象
- 8.1 定位 new 的基本语法
- 8.2 定位 new 示例
- 8.3 定位 new 有什么应用场景?
- 九、malloc/free 与 new/delete 的区别总结
- 初学者应该优先掌握什么?
- 十、常见错误与避坑指南
- 错误一:申请后忘记释放
- 错误二:重复释放同一块内存
- 错误三:释放后继续访问
- 错误四:混用申请和释放方式
- 错误五:认为所有指针都在堆上
- 十一、总结:建立完整的 C++ 内存管理知识体系
- 十二、课后练习
- 结尾
一、为什么要学习 C++ 内存管理?
学习 C++ 时,我们经常会看到这样的代码:
int a = 10; int* p = new int(20); delete p;这段代码看起来很简单,但它涉及几个重要的问题:
- 变量
a和指针p存储在哪里? new申请的内存来自哪里?new和malloc有什么区别?- 为什么使用
new申请的内存要使用delete释放? - 如果申请内存后忘记释放,会发生什么?
这些问题都属于 C++ 内存管理 的范畴。
简单来说,内存管理就是:了解程序运行时内存的组织方式,并正确地申请、使用和释放内存。
掌握这部分知识,不仅有助于理解 C++ 的运行机制,也是学习类与对象、指针、构造函数、析构函数以及面试题的重要基础。
二、C/C++ 程序的内存分布
2.1 程序运行时,内存是如何划分的?
程序运行时,并不是所有变量都存放在同一个地方,可以把相关内存区域概括为以下几部分:
栈区(Stack)
局部变量、函数参数等
堆区(Heap)
程序运行时动态申请的内存
数据段(静态区)
全局变量、静态变量
代码段(常量区)
可执行代码、只读常量等
内存映射区域
可用于共享库映射、共享内存等场景
实际进程的内存布局由操作系统、编译器和平台决定。
下面分别介绍这些区域。
2.2 栈区:存放普通局部变量等数据
例如:
void Test() { int a = 10; int b = 20; }这里的a和b是普通局部变量。
在常见的 C++ 实现中,它们的自动存储期对象通常由函数调用所使用的栈来承载。函数执行结束后,这些局部变量的生命周期也随之结束。
栈的特点:
- 主要用于函数调用相关的数据和自动存储期对象。
- 一般不需要程序员手动释放局部变量占用的存储空间。
- 栈空间通常有限,递归过深或局部对象过大可能导致栈溢出。
初学者可以先记住:普通局部变量通常具有自动存储期,不需要使用delete释放。
2.3 堆区:存放动态申请的内存
如果程序需要在运行过程中申请内存,就可以使用动态内存分配。
例如:
int* p = new int(10); delete p; p = nullptr;这里需要区分两个概念:
p是一个指针变量,保存动态分配对象的地址。new int(10)创建了一个动态分配的int对象,并将其初始化为10。
在常见实现中,指针变量p如果是普通局部变量,通常存放在栈上;它所指向的动态分配对象则通常位于堆上。
栈区:指针变量p
保存对象的地址
p 指向
动态分配对象:int(10)
保存整数值 10
图 2:指针变量与动态分配对象是两个不同的东西。
注意:动态分配的对象不会因为指针变量离开作用域就自动释放。使用普通裸指针管理动态内存时,需要正确释放资源,否则可能造成内存泄漏。
2.4 数据段:存放全局变量和静态变量
例如:
int globalVar = 1; static int staticGlobalVar = 2; void Test() { static int staticVar = 3; }这里的三个变量都具有静态存储期。
globalVar:全局变量。staticGlobalVar:文件作用域的静态变量。staticVar:函数内部的静态局部变量。
它们的生命周期贯穿程序的整个运行期间,但作用域和可见性并不完全相同。
需要注意:static不一定代表变量只能在某个函数内使用。变量定义的位置不同,作用域也会不同。
2.5 代码段与只读常量
例如:
const char* p = "abcd";这段代码中:
p是一个指针变量。"abcd"是字符串字面量。p指向字符串字面量的首字符。
在常见实现中,字符串字面量通常位于只读存储区域;但局部指针变量p本身并不会因为指向字符串字面量,就自动进入只读区域。
此外,const char*表示不能通过该指针修改所指字符,并不意味着指针变量自身不能改变指向。
例如:
const char* p = "abcd"; p = "hello"; // 可以:改变指针指向 // p[0] = 'A'; // 不可以:不能通过 p 修改字符三、通过代码练习判断变量存储位置
综合示例:
int globalVar = 1; static int staticGlobalVar = 1; void Test() { static int staticVar = 1; int localVar = 1; int num1[10] = {1, 2, 3, 4}; char char2[] = "abcd"; const char* pChar3 = "abcd"; int* ptr1 = (int*)malloc(sizeof(int) * 4); int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); }对于这段代码,可以这样理解:
| 变量或对象 | 常见存储位置 | 说明 |
|---|---|---|
globalVar | 数据段 | 全局变量 |
staticGlobalVar | 数据段 | 静态全局变量 |
staticVar | 数据段 | 静态局部变量 |
localVar | 栈区 | 普通局部变量 |
num1 | 栈区 | 局部数组本身 |
char2 | 栈区 | 局部字符数组本身 |
*char2 | 栈区中的数组元素 | 第一个字符'a' |
pChar3 | 栈区 | 普通局部指针变量 |
*pChar3 | 通常在只读存储区域 | 指向字符串字面量的首字符 |
ptr1 | 栈区 | 普通局部指针变量 |
*ptr1 | 堆区 | 动态分配的整数空间中的第一个元素 |
表格中的“栈区”“堆区”是常见实现下的便于理解的描述,并非 C++ 标准对所有对象物理地址的统一规定。
这一部分最重要的结论是:判断内存位置时,要区分指针变量本身与它所指向的对象。
四、C 语言的动态内存管理:malloc、calloc、realloc 和 free
C 语言提供了四个常用的动态内存管理函数。C++ 也可以继续使用它们。
4.1 malloc:申请动态内存
malloc用于申请指定字节数的内存空间。
语法:
void* malloc(size_t size);示例:
#include <cstdlib> #include <iostream> using namespace std; int main() { int* p = (int*)malloc(sizeof(int)); if (p == nullptr) { cout << "内存申请失败" << endl; return 1; } *p = 10; cout << *p << endl; free(p); p = nullptr; return 0; }运行逻辑如下:
sizeof(int)计算一个int所占的字节数。malloc申请对应大小的内存。- 检查返回的指针是否为空。
- 通过指针写入并读取数据。
- 使用
free释放内存。
需要注意,malloc申请的空间不会自动将内容初始化为0,因此不能直接假设其中已经存放了有效的整数值。
4.2 calloc:申请内存并清零
calloc的作用是申请指定数量、指定大小的内存,并将申请到的内存字节初始化为零。
语法:
void* calloc(size_t num, size_t size);示例:
#include <cstdlib> #include <iostream> using namespace std; int main() { int* p = (int*)calloc(4, sizeof(int)); if (p == nullptr) { return 1; } for (int i = 0; i < 4; i++) { cout << p[i] << " "; } free(p); p = nullptr; return 0; }在常见平台上,这个程序会输出:
0 0 0 0calloc(4, sizeof(int))表示申请能够容纳四个int的空间,并将这些空间的字节清零。
对于常见的整数类型,这样得到的值为0。但要注意,将内存字节清零并不等同于适用于所有 C++ 对象的通用初始化方式,尤其不能用它替代对象构造函数。
4.3 realloc:调整动态内存大小
当原先申请的空间不够用时,可以使用realloc调整空间大小。
void* realloc(void* ptr, size_t new_size);例如:
#include <cstdlib> #include <iostream> using namespace std; int main() { int* p = (int*)malloc(4 * sizeof(int)); if (p == nullptr) { return 1; } for (int i = 0; i < 4; i++) { p[i] = i + 1; } int* tmp = (int*)realloc(p, 10 * sizeof(int)); if (tmp == nullptr) { // realloc 失败时,原来的 p 仍然有效 free(p); return 1; } p = tmp; for (int i = 0; i < 4; i++) { cout << p[i] << " "; } free(p); p = nullptr; return 0; }这里有一个特别重要的细节:不要直接把realloc的返回值覆盖到原指针上。
不推荐这样写:
p = (int*)realloc(p, 10 * sizeof(int));如果realloc失败并返回空指针,原来的地址就可能丢失,导致原内存无法正常释放。
使用临时指针tmp,能够在调整失败时保留原指针。
此外,realloc可能在原地址调整空间,也可能分配新空间并搬移原有数据。成功后,应当使用它返回的新指针。
4.4 free:释放动态内存
free用于释放由 C 动态内存分配函数申请的空间。
int* p = (int*)malloc(sizeof(int)); if (p != nullptr) { *p = 100; free(p); p = nullptr; }使用free后,原来指向该空间的指针不会自动变为空指针。因此,将其设置为nullptr是一个有用的习惯。
但是要记住:free不会自动修改其他指向同一内存的指针。 如果还有别的指针指向已经释放的空间,就需要避免继续通过它们访问该空间。
4.5 四个函数的区别
| 函数 | 主要作用 | 初始化特点 |
|---|---|---|
malloc | 申请指定字节数的内存 | 不初始化 |
calloc | 申请指定数量的内存单元 | 将申请到的字节清零 |
realloc | 调整已有动态内存的大小 | 保留原有数据中能够保留的部分 |
free | 释放动态内存 | 不负责对象的析构 |
这四个函数的使用规则可以概括为:申请、使用、释放。 申请成功后,必须确保程序在适当的时候释放内存,并正确处理失败的情况。
五、C++ 的动态内存管理:new 和 delete
C++ 保留了 C 的动态内存管理函数,同时提供了更符合面向对象编程特点的new和delete。
5.1 new:动态申请并初始化对象
先看一个简单的例子:
#include <iostream> using namespace std; int main() { int* p1 = new int; int* p2 = new int(10); cout << *p2 << endl; delete p1; delete p2; return 0; }其中:
int* p1 = new int;表示动态分配一个int对象。对于这种没有显式初始化的写法,不能假设它的值为0。
而:
int* p2 = new int(10);则表示动态分配一个int对象,并将其初始化为10。
这里可以看到,new不仅涉及内存分配,还可以完成对象初始化。
5.2 new[]:申请连续的多个对象
如果希望申请一组连续的整数,可以使用new[]:
int* p = new int[5]; for (int i = 0; i < 5; i++) { p[i] = i + 1; } for (int i = 0; i < 5; i++) { cout << p[i] << " "; } delete[] p; p = nullptr;输出:
1 2 3 4 5注意下面两个写法的区别:
int* p1 = new int; // 一个 int 对象 int* p2 = new int[5]; // 五个 int 对象组成的数组对应的释放方式也不同:
delete p1; delete[] p2;申请与释放必须匹配:
new对应delete。new[]对应delete[]。
不能把new[]申请的数组交给普通的delete释放,否则会产生未定义行为。
5.3 new 和 delete 如何处理自定义类型?
这是 C++ 内存管理中非常重要的知识点。
假设我们定义一个类:
#include <iostream> using namespace std; class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; };这个类包含:
- 构造函数
A():创建对象时初始化对象。 - 析构函数
~A():对象生命周期结束时执行清理工作。
使用new和delete:
int main() { A* p = new A(10); delete p; p = nullptr; return 0; }程序执行时,主要经历两个过程:
第一步:new A(10)
申请足够的存储空间,并在该空间中构造A对象,将成员变量_a初始化为10。
第二步:delete p
调用对象的析构函数,执行对象的清理工作,然后释放该对象所占用的动态存储空间。
图 3:new/delete 对自定义类型的基本处理过程。
需要特别注意:析构函数负责清理对象管理的资源,但不意味着它会自动释放所有外部资源。比如类中持有的裸指针,如果其指向的资源需要释放,仍然需要编写相应的清理逻辑。
5.4 malloc/free 与自定义类型
对比下面两段代码:
A* p1 = (A*)malloc(sizeof(A)); free(p1);以及:
A* p2 = new A(10); delete p2;第一段代码仅仅申请和释放原始存储空间,不会自动调用A的构造函数和析构函数。
第二段代码则会在对象创建时调用构造函数,在对象销毁时调用析构函数。
因此,对于需要执行构造和析构逻辑的 C++ 自定义类型,通常应该使用new和delete,或者更推荐使用标准库的智能指针及容器管理资源。
还要注意,malloc得到的内存并不会因为强制转换成A*就自动变成一个已经构造好的A对象。
六、operator new 与 operator delete:理解底层原理
前面我们知道,new和delete可以创建和销毁动态对象。那么,它们底层是如何完成内存分配的呢?
接下来介绍了两个重要的全局函数:
operator newoperator delete
6.1 operator new 是什么?
先区分两个概念:
| 名称 | 作用 |
|---|---|
new表达式 | 申请存储空间,并初始化对象 |
operator new函数 | 负责提供所需的原始存储空间 |
例如:
int* p = new int(10);可以从概念上理解为:
- 调用适当的
operator new分配存储空间。 - 在分配的空间中初始化
int对象。 - 返回指向该对象的指针。
需要注意,这是一种帮助理解的流程描述;实际调用会受到重载、分配形式等因素影响。
6.2 operator new 与 malloc 的关系
常见实现中,operator new会通过malloc申请内存。
如果申请成功,就返回相应的地址;如果申请失败,则可能按照相应的处理机制继续尝试,或者抛出std::bad_alloc异常。
示意代码如下:
void* operator new(size_t size) { void* p = malloc(size); if (p == nullptr) { throw std::bad_alloc(); } return p; }这段代码只是用于理解原理的简化示例,并不是完整的标准库实现。实际的全局operator new还涉及分配失败处理机制等细节。
6.3 operator delete 与 free 的关系
同样,在常见实现中,operator delete最终会通过free释放相应的存储空间。
可以用下面的示意代码理解:
void operator delete(void* p) noexcept { free(p); }这同样是简化的原理示例,不应直接当作完整的标准库实现。
这里最重要的结论是:
new表达式不等于operator new函数,delete表达式也不等于operator delete函数。
new和delete还承担着对象初始化、对象销毁等工作,而底层分配函数主要负责原始存储空间的获取与释放。
七、new 和 delete 的实现原理
掌握前面的内容后,我们可以进一步理解普通类型、数组以及自定义类型的内存管理流程。
7.1 内置类型的实现原理
对于int等内置类型:
int* p = new int(10); delete p;可以理解为:
new int(10)
申请存储空间
将对象初始化为 10
delete p
释放动态存储空间
对于内置类型,new和malloc、delete和free在基本用途上存在相似之处,但它们并不能随意混用。
另外,分配失败时的处理方式也不同:普通的new通常抛出std::bad_alloc,而malloc返回空指针。
7.2 自定义类型的 new 原理
对于前面定义的A类:
A* p = new A(10);其基本过程为:
- 调用适当的分配函数申请存储空间。
- 在该空间上执行构造过程。
- 完成
A对象的初始化。
其中,构造函数会执行成员变量初始化以及编写在构造函数中的其他操作。
7.3 自定义类型的 delete 原理
当执行:
delete p;基本过程为:
- 调用对象的析构函数,完成资源清理。
- 调用相应的释放函数,释放对象占用的存储空间。
这也是为什么new/delete比简单的内存申请和释放更适合管理具有复杂生命周期的 C++ 对象。
7.4 new[] 和 delete[] 的原理
如果要创建多个对象:
A* p = new A[3]; delete[] p;创建数组时,需要为数组中的对象申请存储空间,并完成各个元素的构造。
释放数组时,需要销毁相应的元素,再释放数组所占用的存储空间。
一般情况下,数组元素的析构顺序与构造顺序相反。
可以将整个过程总结为:
new A[3]
申请数组所需的存储空间
构造 A[0]、A[1]、A[2]
delete[] p
销毁数组元素,再释放存储空间
图 4:自定义类型数组的创建与销毁流程。
注意:new[]和delete[]是一组匹配的操作。对于需要执行析构函数的对象数组,使用正确的释放方式尤其重要。
八、定位 new 表达式:在已有内存中构造对象
它主要用于:在一块已经分配好的原始存储空间中,构造一个对象。
普通的new通常负责申请空间并创建对象,而定位 new 使用调用者提供的地址,不负责重新申请一块独立的存储空间。
8.1 定位 new 的基本语法
new (place_address) type;也可以带初始化参数:
new (place_address) type(initializer);其中,place_address是提供给定位 new 使用的地址。
8.2 定位 new 示例
继续使用前面的A类:
#include <cstdlib> #include <iostream> #include <new> using namespace std; class A { public: A(int a = 0) : _a(a) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { // 先申请原始存储空间 void* buffer = malloc(sizeof(A)); if (buffer == nullptr) { return 1; } // 在已有空间中构造 A 对象 A* p = new (buffer) A(10); // 显式调用析构函数 p->~A(); // 释放原始存储空间 free(buffer); return 0; }理解这段代码时,要注意三个步骤:
malloc只负责申请原始存储空间。new (buffer) A(10)在这块空间中构造对象。p->~A()显式调用析构函数,随后使用free释放原始存储空间。
特别注意:定位 new 创建的对象,不能简单地使用普通的delete p来释放。 因为这块存储空间不是通过普通new表达式获得的。这里应该先结束对象的生命周期,再通过与原始内存分配方式相匹配的方式释放空间。
8.3 定位 new 有什么应用场景?
定位 new 通常与内存池配合使用。
内存池可以预先申请一大块存储空间,在需要创建对象时,再从中划分出合适的区域。
由于这些区域最初可能只是原始存储空间,因此需要通过定位 new 构造相应的 C++ 对象。
这有助于理解 C++ 中存储空间的管理和对象生命周期管理是两个相关但不同的概念。
九、malloc/free 与 new/delete 的区别总结
这是本章最值得掌握的一组对比,也是常见的 C++ 面试知识点。
| 对比维度 | malloc/free | new/delete |
|---|---|---|
| 本质 | 函数 | 运算符及表达式 |
| 申请大小 | 需要指定字节数 | 根据类型确定 |
| 初始化 | malloc不初始化 | 可以在创建时初始化 |
| 返回值 | void* | 返回相应类型的指针 |
| 申请失败 | malloc返回空指针 | 普通new通常抛出异常 |
| 构造函数 | 不会自动调用 | 创建类对象时会调用 |
| 析构函数 | 不会自动调用 | 销毁类对象时会调用 |
| 数组操作 | 申请合适的总字节数 | 使用new[]和delete[] |
| 释放匹配 | malloc对应free | new对应delete |
补充两点:
- 如果使用
new (std::nothrow),分配失败时可以返回空指针,而不是抛出分配失败异常。 new和delete的调用规则还会受到自定义分配函数、重载以及对象类型等因素影响。上表概括的是初学阶段最常见的情况。
初学者应该优先掌握什么?
建议记住以下四条:
malloc申请原始存储空间,free释放这类空间。new创建对象,delete销毁对象并释放相应存储空间。- 自定义类型通常需要通过构造函数初始化,通过析构函数清理资源。
new[]必须与delete[]匹配,不能随意混用。
在现代 C++ 项目中,还应优先考虑std::vector、std::string、std::unique_ptr等资源管理工具,尽量避免手动管理裸指针带来的错误。
十、常见错误与避坑指南
错误一:申请后忘记释放
int* p = new int(10); // 忘记 delete如果程序反复执行类似操作,就可能发生内存泄漏。
建议: 让动态资源具有明确的所有权和生命周期,优先使用智能指针或标准容器。
错误二:重复释放同一块内存
int* p = new int(10); delete p; // delete p; // 错误:重复释放重复释放同一块已经释放的内存会产生未定义行为。
将p置为nullptr有助于避免某些误用,但不能替代对所有权的正确管理。
错误三:释放后继续访问
int* p = new int(10); delete p; // cout << *p; // 错误:访问已释放的对象释放对象后,原指针可能变成悬空指针。继续解引用会产生未定义行为。
错误四:混用申请和释放方式
int* p = new int[10]; // delete p; // 错误 delete[] p;数组应该使用delete[]释放。
类似地,使用malloc申请的空间应当使用相匹配的释放方式,而不是直接交给普通delete。
错误五:认为所有指针都在堆上
void Test() { int a = 10; int* p = new int(20); }这里,a是普通局部变量;p是局部指针变量;new int(20)创建的是动态分配对象。
在常见实现中,a和p通常由栈承载,而动态分配对象通常位于堆上。Test()返回后,动态对象不会因为p消失就自动释放,因此这段代码存在内存泄漏风险。
十一、总结:建立完整的 C++ 内存管理知识体系
通过本章学习,我们可以把 C++ 内存管理串联成一条完整的知识链。
第一步:理解内存分布
栈区、堆区、数据段、代码段
第二步:掌握 C 动态内存函数
malloc、calloc、realloc、free
第三步:掌握 C++ 动态内存操作
new、delete、new[]、delete[]
第四步:理解底层机制
operator new、operator delete
第五步:理解对象生命周期
构造、析构、定位 new、资源释放
图 5:C++ 内存管理知识体系。
对于刚开始学习 C++ 的同学来说,不需要一开始就研究复杂的内存分配器实现。先理解不同内存区域的用途,熟练掌握malloc/free和new/delete的区别,再逐步理解构造函数、析构函数和底层分配机制,就能建立扎实的知识基础。
最后送给初学者一句话:内存管理不仅是申请和释放空间,更重要的是理解对象的生命周期,并确保资源能够被正确管理。
把这句话真正理解透彻,后续学习 C++ 类与对象、智能指针、容器以及资源管理时,就会轻松很多。
十二、课后练习
练习 1:下面哪种方式适合释放new int[10]申请的数组?
A.free(p)
B.delete p
C.delete[] p
练习 2:使用malloc申请A类对象的空间,会自动调用A的构造函数吗?
A. 会
B. 不会
练习 3:int* p = new int(10);中,指针p与它指向的整数对象有什么区别?
A. 指针和整数对象是同一个东西
B. 指针保存地址,整数对象保存值 10
C. 指针一定在堆上,整数对象一定在栈上
结尾
uu们,本文的内容到这里就全部结束了,道尔在这里再次感谢您的阅读!
👀【关注】跟随我一起深耕技术领域,见证每一次成长。 ❤️【点赞】让优质内容被更多人看见,让知识传递更有力量。 ⭐【收藏】把核心知识点存好,在需要时随时查、随时用。 💬【评论】分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主“一键四连”哦! |
结语:希望对学习C++相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!