C/C++ 内存管理详解:从内存分布到 new/delete 底层原理
文章目录
- [C/C++ 内存管理详解:从内存分布到 new/delete 底层原理](#C/C++ 内存管理详解:从内存分布到 new/delete 底层原理)
-
- [一、C/C++ 程序的内存分布](#一、C/C++ 程序的内存分布)
-
- [1 栈区](#1 栈区)
- [2 堆区](#2 堆区)
- [3 数据段](#3 数据段)
- [4 代码段](#4 代码段)
- 二、通过代码判断变量到底在哪里
-
- [1 globalVar](#1 globalVar)
- [2 staticGlobalVar](#2 staticGlobalVar)
- [3 staticVar](#3 staticVar)
- [4 localVar](#4 localVar)
- [5 num1](#5 num1)
- [6 char2](#6 char2)
- [7 pChar3](#7 pChar3)
- [8 ptr1](#8 ptr1)
- 三、C语言动态内存管理
- 四、malloc
-
- [1 基本形式](#1 基本形式)
- [2 malloc不会初始化](#2 malloc不会初始化)
- [3 malloc申请失败](#3 malloc申请失败)
- 五、calloc
-
- [1 基本形式](#1 基本形式)
- [2 calloc与malloc的核心区别](#2 calloc与malloc的核心区别)
- 六、realloc
-
- [1 基本形式](#1 基本形式)
- [2 realloc可能发生什么](#2 realloc可能发生什么)
- [3 realloc之后还需要free原指针吗](#3 realloc之后还需要free原指针吗)
- 七、free
- 八、C++中的动态内存管理
- 九、new和delete
-
- [1 new申请单个对象](#1 new申请单个对象)
- [2 new申请并初始化](#2 new申请并初始化)
- [3 new申请连续空间](#3 new申请连续空间)
- [4 delete释放单个对象](#4 delete释放单个对象)
- [5 delete\[\]释放数组](#5 delete[]释放数组)
- 十、自定义类型的new和delete
- 十一、malloc/free与new/delete最大的区别
- [十二、operator new与operator delete](#十二、operator new与operator delete)
-
- [1 new是操作符](#1 new是操作符)
- [2 operator new是函数](#2 operator new是函数)
- [3 delete与operator delete](#3 delete与operator delete)
- [十三、operator new的底层实现](#十三、operator new的底层实现)
- [十四、operator delete的底层实现](#十四、operator delete的底层实现)
- 十五、new的底层原理
- 十六、delete的底层原理
- 十七、new\[\]的底层原理
- 十八、delete\[\]的底层原理
- 十九、为什么new\[\]和delete\[\]必须匹配
- [二十、placement new](#二十、placement new)
-
- [1 为什么需要placement new](#1 为什么需要placement new)
- [2 完整过程](#2 完整过程)
- [二十一、placement new的典型使用场景](#二十一、placement new的典型使用场景)
- 二十二、malloc/free与new/delete的完整区别
-
- [1 本质不同](#1 本质不同)
- [2 初始化不同](#2 初始化不同)
- [3 申请空间时的写法不同](#3 申请空间时的写法不同)
- [4 返回值不同](#4 返回值不同)
- [5 申请失败时的处理不同](#5 申请失败时的处理不同)
- [6 对自定义类型的处理不同](#6 对自定义类型的处理不同)
- 二十三、几组最容易混淆的概念
-
- [1 指针和指针指向的空间不是一回事](#1 指针和指针指向的空间不是一回事)
- [2 数组和字符串常量不是一回事](#2 数组和字符串常量不是一回事)
- [3 static局部变量和普通局部变量不同](#3 static局部变量和普通局部变量不同)
- [4 new和operator new不同](#4 new和operator new不同)
- 二十四、底层调用关系
- 二十五、几组代码的完整分析
一、C/C++ 程序的内存分布
学习 C/C++ 内存管理,首先要搞清楚一个问题
一个变量到底存在哪里
在一个典型的 C/C++ 程序中,可以把主要内存区域理解成
text
高地址
┌─────────────────────┐
│ 栈区 │
│ 局部变量、函数参数 │
│ 函数返回值等 │
│ ↓ │
├─────────────────────┤
│ │
│ 内存映射段 │
│ │
├─────────────────────┤
│ │
│ 堆区 │
│ malloc/new 动态申请 │
│ ↑ │
├─────────────────────┤
│ 数据段/静态区 │
│ 全局变量、静态变量 │
├─────────────────────┤
│ 代码段/常量区 │
│ 程序代码、只读常量 │
└─────────────────────┘
低地址
其中最重要的是理解下面几个区域
1 栈区
栈主要存放
- 非静态局部变量
- 函数参数
- 函数返回值等
栈的一个重要特点是
栈是向下增长的
例如
cpp
void Test()
{
int localVar = 1;
}
这里的 localVar 是普通局部变量,因此存储在栈区
2 堆区
堆主要用于程序运行过程中进行动态内存分配
例如
cpp
int* p = (int*)malloc(sizeof(int));
这里 p 本身是一个局部变量
所以
text
p
存储在栈区
而
text
*p
所指向的动态申请出来的空间位于堆区
可以简单理解成
text
栈
┌─────────────┐
│ p │ ──────────┐
└─────────────┘ │
↓
堆区
┌─────────┐
│ int空间 │
└─────────┘
堆的另一个特点是
堆是向上增长的
3 数据段
数据段也叫静态区
主要存放
- 全局变量
- 静态变量
例如
cpp
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
}
这三个变量都属于静态存储的数据
因此都位于数据段
这里尤其容易混淆的是
cpp
static int staticVar = 1;
虽然 staticVar 写在函数内部
但它不是普通局部变量
因为它具有静态存储期
所以它存储在数据段,而不是栈区
4 代码段
代码段主要存放
- 可执行代码
- 只读常量
例如字符串常量
cpp
"abcd"
通常位于只读常量区域
二、通过代码判断变量到底在哪里
看下面这段代码
cpp
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);
}
判断这种题时,不要只看变量的类型
最重要的是看
变量本身存在哪里,以及变量指向的内容存在哪里
1 globalVar
cpp
int globalVar = 1;
这是全局变量
所以
text
globalVar → 数据段
2 staticGlobalVar
cpp
static int staticGlobalVar = 1;
这是静态全局变量
所以
text
staticGlobalVar → 数据段
3 staticVar
cpp
static int staticVar = 1;
虽然它定义在函数内部
但是它是静态变量
所以
text
staticVar → 数据段
4 localVar
cpp
int localVar = 1;
这是普通局部变量
所以
text
localVar → 栈区
5 num1
cpp
int num1[10] = {1, 2, 3, 4};
num1 是函数内部定义的普通局部数组
因此数组本身位于栈区
text
num1 → 栈区
数组中的元素也是这个数组空间的一部分
因此
text
*num1
访问到的内容也在栈区
6 char2
cpp
char char2[] = "abcd";
这里特别容易和字符串常量混淆
char2 是一个数组
它会直接创建一块数组空间,并把字符串内容复制进去
实际上相当于
text
char2
┌───┬───┬───┬───┬────┐
│ a │ b │ c │ d │ \0 │
└───┴───┴───┴───┴────┘
因为 char2 是函数内部的普通局部数组
所以整个数组位于栈区
text
char2 → 栈区
*char2 → 栈区中的 'a'
7 pChar3
cpp
const char* pChar3 = "abcd";
这里必须把
text
pChar3
和
text
*pChar3
分开来看
pChar3 本身是一个局部变量
所以
text
pChar3 → 栈区
但是它指向的是字符串常量
cpp
"abcd"
字符串常量存放在代码段/常量区
所以
text
*pChar3 → 常量区
可以画成
text
栈区
┌──────────────┐
│ pChar3 │
└──────┬───────┘
│
↓
常量区
┌──────────────┐
│ a b c d \0 │
└──────────────┘
这也是内存分布题中非常经典的考点
看指针变量在哪里不能直接推出指针指向的内容在哪里
8 ptr1
cpp
int* ptr1 = (int*)malloc(sizeof(int) * 4);
这里存在两个东西
text
ptr1
以及
text
malloc申请出来的空间
ptr1 是函数内部的局部变量
所以
text
ptr1 → 栈区
而 malloc 申请出来的空间位于堆区
所以
text
*ptr1 → 堆区
可以理解成
text
栈区
┌──────────┐
│ ptr1 │
└────┬─────┘
│
↓
堆区
┌────────────────┐
│ int int int int │
└────────────────┘
三、C语言动态内存管理
C语言提供了四个非常重要的动态内存管理函数
cpp
malloc
calloc
realloc
free
它们操作的主要对象都是堆上的动态内存
四、malloc
1 基本形式
cpp
void* malloc(size_t size);
作用是
申请一块指定大小的连续内存空间
例如
cpp
int* p = (int*)malloc(sizeof(int) * 10);
表示申请能够存放 10 个 int 的空间
2 malloc不会初始化
例如
cpp
int* p = (int*)malloc(sizeof(int) * 10);
malloc 只是申请空间
不会主动把这块空间初始化为 0
因此其中的内容是未确定的
3 malloc申请失败
如果申请失败
cpp
malloc
返回
cpp
NULL
因此使用 malloc 时通常需要考虑空指针问题
cpp
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
{
// 申请失败
}
五、calloc
1 基本形式
cpp
void* calloc(size_t num, size_t size);
例如
cpp
int* p = (int*)calloc(10, sizeof(int));
表示申请 10 个 int 大小的空间
2 calloc与malloc的核心区别
例如
cpp
int* p1 = (int*)malloc(sizeof(int) * 10);
int* p2 = (int*)calloc(10, sizeof(int));
二者都申请了能够存放 10 个 int 的空间
但是 calloc 会对申请的空间进行初始化
这里初始化为 0
所以最核心的区别可以记成
text
malloc
申请空间
不负责初始化
calloc
申请空间
并进行初始化
六、realloc
1 基本形式
cpp
void* realloc(void* ptr, size_t size);
主要用于
调整一块已经申请的动态内存空间大小
例如
cpp
int* p = (int*)malloc(sizeof(int) * 4);
p = (int*)realloc(p, sizeof(int) * 10);
原来空间可以存放 4 个 int
调整之后希望能够存放 10 个 int
2 realloc可能发生什么
当扩大空间时,如果原来的位置后面有足够的连续空间
那么可能直接在原来的位置扩展
如果后面没有足够空间
那么 realloc 可能重新找一块更大的空间
然后把原来的数据复制过去
最后释放原来的空间
所以
realloc之后原来的指针可能失效
3 realloc之后还需要free原指针吗
例如
cpp
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
free(p3);
如果 realloc 成功并返回了新的空间
那么原来的空间已经由 realloc 处理
因此不能再对原来的 p2 进行 free
正确的处理是最终释放 realloc 返回的指针
cpp
free(p3);
如果直接写成
cpp
free(p2);
free(p3);
就可能导致重复释放
七、free
free 用于释放动态申请的堆空间
例如
cpp
int* p = (int*)malloc(sizeof(int) * 10);
free(p);
释放之后,这块动态内存就不能继续当成有效空间使用
通常还可以把指针置空
cpp
free(p);
p = NULL;
这样可以降低后续误用悬空指针的风险
八、C++中的动态内存管理
C语言中的
cpp
malloc
calloc
realloc
free
在 C++ 中依然可以使用
但是对于 C++ 的自定义类型来说,仅仅申请和释放内存是不够的
因此 C++ 提供了
cpp
new
delete
new[]
delete[]
用于动态内存管理
九、new和delete
1 new申请单个对象
例如
cpp
int* p = new int;
表示动态申请一个 int 对象
2 new申请并初始化
可以直接进行初始化
cpp
int* p = new int(10);
表示
text
申请一个int对象
并初始化为10
3 new申请连续空间
例如
cpp
int* p = new int[3];
表示申请 3 个连续的 int 空间
4 delete释放单个对象
如果使用
cpp
new
申请单个对象
就使用
cpp
delete
释放
例如
cpp
int* p = new int;
delete p;
5 delete\[\]释放数组
如果使用
cpp
new[]
申请数组
就必须使用
cpp
delete[]
释放
例如
cpp
int* p = new int[3];
delete[] p;
最重要的匹配关系
text
new → delete
new[] → delete[]
不能混用
十、自定义类型的new和delete
这是 C++ 动态内存管理中非常重要的一部分
例如
cpp
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
创建对象
cpp
A* p = new A(10);
这里并不只是简单地申请一块内存
还会调用构造函数
text
申请空间
↓
调用A的构造函数
↓
对象构造完成
释放对象
cpp
delete p;
则会先调用析构函数
然后释放空间
text
调用A的析构函数
↓
释放对象空间
十一、malloc/free与new/delete最大的区别
对于内置类型
cpp
int
char
double
malloc/free 和 new/delete 的行为比较接近
但对于自定义类型,区别非常明显
例如
cpp
A* p1 = (A*)malloc(sizeof(A));
malloc 只负责
text
申请空间
不会调用
text
A的构造函数
释放时
cpp
free(p1);
也不会调用
text
A的析构函数
而
cpp
A* p2 = new A(1);
会
text
申请空间
↓
调用A的构造函数
释放
cpp
delete p2;
会
text
调用A的析构函数
↓
释放空间
所以最核心的一句话是
malloc/free只负责内存空间,new/delete除了管理空间,还负责对象的构造和析构
十二、operator new与operator delete
这里很容易把
cpp
new
和
cpp
operator new
混为一谈
实际上它们不是一回事
1 new是操作符
我们平时写
cpp
A* p = new A(10);
这里的 new 是 C++ 的操作符
它负责完成整个对象创建过程
2 operator new是函数
cpp
operator new
是系统提供的全局函数
它主要负责
申请原始内存空间
new 的底层会调用 operator new
可以简单理解成
text
new
│
├── operator new
│ │
│ └── 申请内存空间
│
└── 调用构造函数
3 delete与operator delete
同样
cpp
delete
是操作符
cpp
operator delete
是函数
delete 的底层会通过 operator delete 来释放空间
可以理解成
text
delete
│
├── 调用析构函数
│
└── operator delete
│
└── 释放内存空间
十三、operator new的底层实现
text
operator new
↓
malloc申请空间
↓
申请成功
↓
直接返回
如果 malloc 申请失败
text
malloc申请失败
↓
尝试执行空间不足的应对措施
↓
能够继续申请
↓
重新申请
如果仍然无法解决
text
抛出std::bad_alloc异常
所以一个非常重要的区别是
text
malloc申请失败
→ 返回NULL
new申请失败
→ 抛出异常
十四、operator delete的底层实现
operator delete 最终会通过类似 free 的机制释放空间
可以简单理解为
text
delete
↓
operator delete
↓
释放空间
所以可以把两组关系记住
text
operator new → malloc
operator delete → free
这里说的是底层实现上的关系
不能因此认为
text
new == malloc
delete == free
因为 new/delete 还承担对象构造和析构等工作
十五、new的底层原理
对于自定义类型
cpp
A* p = new A(10);
可以拆成两个核心步骤
第一步
调用
cpp
operator new
申请原始内存空间
第二步
在申请出来的空间上调用构造函数
完成对象构造
所以
text
new A(10)
↓
operator new
↓
申请原始内存
↓
调用A(10)构造函数
↓
得到A对象
十六、delete的底层原理
对于
cpp
delete p;
可以拆成两个核心步骤
第一步
调用析构函数
完成对象内部资源清理
第二步
调用
cpp
operator delete
释放对象占用的空间
所以
text
delete p
↓
调用析构函数
↓
operator delete
↓
释放内存
十七、new\[\]的底层原理
如果写
cpp
A* p = new A[10];
表示申请 10 个 A 对象的空间
底层过程可以理解成
text
new A[10]
↓
operator new[]
↓
申请10个对象所需要的空间
↓
依次调用10次构造函数
↓
10个对象构造完成
也就是说
new\[\]不仅申请连续空间,还需要完成多个对象的构造
十八、delete\[\]的底层原理
对应
cpp
delete[] p;
需要完成
text
调用10次析构函数
↓
清理10个对象中的资源
↓
operator delete[]
↓
释放整块空间
因此
cpp
new A[10]
对应
cpp
delete[] p;
而不是
cpp
delete p;
十九、为什么new\[\]和delete\[\]必须匹配
例如
cpp
A* p = new A[10];
delete[] p;
创建的是 10 个对象
所以释放时需要知道
text
这里存在多个对象
然后逐个调用析构函数
因此必须使用
cpp
delete[]
而不是普通的
cpp
delete
对于自定义类型尤其需要注意这一点
二十、placement new
placement new 又叫
定位 new
它和普通 new 最大的区别是
它不是负责申请内存,而是在已经分配好的原始内存空间中调用构造函数初始化对象
基本形式
cpp
new (place_address) type;
或者
cpp
new (place_address) type(initializer-list);
其中
text
place_address
必须是一个指针
1 为什么需要placement new
假设
cpp
A* p1 = (A*)malloc(sizeof(A));
此时虽然已经获得了一块足够存放 A 的空间
但它还不能简单理解成一个完整构造好的 A 对象
因为
text
A的构造函数还没有执行
这时可以使用
cpp
new(p1) A;
在这块已经存在的内存上调用 A 的构造函数
2 完整过程
例如
cpp
A* p1 = (A*)malloc(sizeof(A));
new(p1) A;
p1->~A();
free(p1);
整个过程是
text
malloc
↓
获得原始内存
↓
placement new
↓
调用构造函数
↓
对象可以正常使用
↓
调用析构函数
↓
free释放原始内存
注意这里最后使用的是
cpp
free
因为最开始的内存是通过
cpp
malloc
申请的
二十一、placement new的典型使用场景
placement new 一般和
内存池
配合使用
内存池提前申请一大块原始内存
然后在这块原始内存中构造一个个对象
因为内存池提供的只是原始内存
并没有自动调用对象的构造函数
所以需要使用 placement new
可以理解为
text
内存池
│
├── 原始内存
│
├── placement new
│ ↓
│ 构造对象
│
└── placement new
↓
构造对象
二十二、malloc/free与new/delete的完整区别
1 本质不同
text
malloc/free → 函数
new/delete → 操作符
2 初始化不同
malloc
cpp
int* p = (int*)malloc(sizeof(int));
不会主动进行对象初始化
new
cpp
int* p = new int(10);
可以在申请空间的同时进行初始化
3 申请空间时的写法不同
malloc 需要自己计算字节数
例如
cpp
int* p = (int*)malloc(sizeof(int) * 10);
new 直接指定类型
cpp
int* p = new int[10];
因此 new 的表达方式更加直接
4 返回值不同
malloc 返回
cpp
void*
在 C++ 中通常需要进行类型转换
例如
cpp
int* p = (int*)malloc(sizeof(int));
new 则直接返回对应类型的指针
cpp
int* p = new int;
不需要手动强转
5 申请失败时的处理不同
malloc
text
申请失败
↓
返回NULL
new
text
申请失败
↓
抛出异常
因此使用 new 时需要通过异常机制处理申请失败
6 对自定义类型的处理不同
malloc
cpp
A* p = (A*)malloc(sizeof(A));
只负责申请空间
不会调用
text
构造函数
free 也不会调用
text
析构函数
而
cpp
A* p = new A;
会
text
申请空间
+
调用构造函数
对应
cpp
delete p;
会
text
调用析构函数
+
释放空间
二十三、几组最容易混淆的概念
1 指针和指针指向的空间不是一回事
例如
cpp
int* p = new int;
这里
text
p
是一个指针变量
如果它定义在函数内部,那么它本身位于栈区
而
text
*p
访问的是 new 在堆区申请的空间
所以
text
p → 栈区
*p → 堆区
2 数组和字符串常量不是一回事
cpp
char str1[] = "abcd";
这是数组
数组空间由 str1 自己拥有
如果是函数中的局部数组,那么位于栈区
而
cpp
const char* str2 = "abcd";
这里的 "abcd" 是字符串常量
指针变量 str2 本身可以位于栈区
但是它指向的是常量区
所以
text
str1
→ 栈区
str2
→ 栈区
*str2
→ 常量区
3 static局部变量和普通局部变量不同
cpp
void Test()
{
int a = 10;
static int b = 10;
}
虽然两个变量都写在函数内部
但是
text
a → 栈区
b → 数据段
关键不在于它是不是写在函数内部
而在于它是不是静态变量
4 new和operator new不同
cpp
new A;
中的 new 是操作符
而
cpp
operator new(sizeof(A));
中的 operator new 是函数
new 完成的是完整的对象创建过程
operator new 主要负责原始内存空间的申请
二十四、底层调用关系
把整个体系串起来,可以得到下面这张关系
text
C语言
malloc
↓
申请堆空间
free
↓
释放堆空间
text
C++
new
↓
operator new
↓
申请原始空间
↓
调用构造函数
↓
对象创建完成
text
C++
delete
↓
调用析构函数
↓
operator delete
↓
释放空间
数组则是
text
new[]
↓
operator new[]
↓
申请连续空间
↓
调用N次构造函数
以及
text
delete[]
↓
调用N次析构函数
↓
operator delete[]
↓
释放连续空间
二十五、几组代码的完整分析
代码一
cpp
int* p = (int*)malloc(sizeof(int));
结果
text
p本身
→ 如果是局部变量,则在栈区
malloc申请的int空间
→ 堆区
代码二
cpp
int* p = new int(10);
结果
text
p
→ 栈区
new出来的int对象
→ 堆区
对象值
→ 10
代码三
cpp
A* p = new A(10);
执行过程
text
operator new
↓
申请空间
↓
调用A(10)
↓
构造函数执行
↓
A对象创建完成
代码四
cpp
delete p;
执行过程
text
调用A的析构函数
↓
operator delete
↓
释放空间
代码五
cpp
A* p = new A[10];
delete[] p;
创建过程
text
申请10个A对象的空间
↓
调用10次构造函数
释放过程
text
调用10次析构函数
↓
释放整块空间
代码六
cpp
A* p = (A*)malloc(sizeof(A));
new(p) A;
p->~A();
free(p);
执行过程
text
malloc
↓
申请原始内存
↓
placement new
↓
调用构造函数
↓
对象构造完成
↓
手动调用析构函数
↓
free
↓
释放原始内存
这里可以清楚地看到
申请内存和构造对象是两个不同的概念
text
内存空间存在
≠
对象已经构造完成
这也是 placement new 存在的重要原因