文章目录
- 前言
- 一、内存分区
- 1. 内存划分情况
- 2. 最大内存计算
- 二、malloc/calloc/realloc 与 free
- 1. malloc
- 2. calloc
- 3. realloc
- 4. free
- 5. 差异对比
- 6. 失败处理
- 三、内存分配题目
- 1. 题目
- 2. 内存区域划分
- 四、C++内存管理方式
- 1. new 与 delete
- 2. new/delete操作内置类型
- 3. new和delete操作自定义类型
- 五、operator new与operator delete函数
- 1. 原理
- 2. 异常捕获方法
- 六、new和delete的实现原理
- 1. 内置类型
- 2. 自定义类型
- 七 、定位new表达式(placement-new)
- 八、对比malloc/free和new/delete的区别
- 总结
前言
今天我们来看C/C++中对于内存的管理
一、内存分区
1. 内存划分情况
在C/C++中,数据的存储位置取决于数据类型和作用域。通常分为以下几类:
-
栈(Stack)
- 自动变量:局部变量、函数参数等会存储在栈上。当函数调用时,栈帧分配用于存储这些变量,函数结束后栈帧被销毁,变量随之消失。
- 特点:栈内存分配快,自动管理,但空间有限(通常为几MB)。
-
堆(Heap)
- 动态分配的内存:使用
malloc
、calloc
(C语言)或new
(C++)分配的内存位于堆上。程序员需手动管理堆内存,使用free
(C语言)或delete
(C++)释放。 -
- 特点:堆空间大,但分配速度较慢,且需要手动释放,易出现内存泄漏。
- 动态分配的内存:使用
-
全局/静态区(Global/Static)
- 全局变量:定义在函数外部的变量,作用域为整个程序,存储在全局/静态区。
- 静态变量:使用
static
修饰的变量,即使定义在函数内,生命周期也是整个程序运行期间,存储在全局/静态区。 - 特点:内存一直保留,直到程序结束。
-
常量区(Text Segment/ROData)
- 字符串字面值:如
"Hello, World!"
这样的字符串存储在常量区,只读不可修改。 const
修饰的变量:也可以存储在常量区,具体取决于编译器实现。
- 字符串字面值:如
-
代码区(Code Segment)
- 函数代码:编译后的程序代码(如函数体)存储在代码区,程序执行时从这里读取指令。
2. 最大内存计算
在32位(x86)和64位(x64)架构中,CPU的寻址能力决定了能够访问的最大内存空间。
-
32位(x86)架构
- 寻址能力:在32位系统中,CPU使用32位地址(即4字节)来表示内存地址。
- 最大寻址空间:32位地址可以表示的最大数值为 ( 2^{32} ) ,即 4,294,967,296 个地址(字节),也就是4GB内存空间。
- 具体解释:CPU可以通过一个32位地址生成从0到(2^{32}-1)的地址,这意味着它能够访问最多4GB的内存。
-
64位(x64)架构
- 寻址能力:在64位系统中,CPU使用64位地址(即8字节)来表示内存地址。
- 理论最大寻址空间:64位地址理论上可以表示的最大数值为 ( 2^{64} ),即 18,446,744,073,709,551,616 个地址(字节),也就是 16 exabytes(EB) 的内存空间。
- 实际情况:目前的操作系统和硬件并不会使用所有的64位地址位。现代操作系统和硬件会限制实际的可用内存寻址范围。例如,Windows 64位系统支持的最大内存一般为几TB到几十TB,具体取决于版本和硬件的限制。
二、malloc/calloc/realloc 与 free
在C语言中,malloc
、calloc
、realloc
和 free
是动态内存管理的四个重要函数。它们在程序运行时负责分配、重新分配和释放内存。下面详细讲解它们的工作机制,并总结它们的区别。
1. malloc
malloc
(Memory Allocation)- 功能:分配指定大小的内存块。
- 语法:
其中,void* malloc(size_t size);
size
是要分配的内存大小(以字节为单位),返回值是指向已分配内存块的指针。如果分配失败,malloc
返回NULL
。 - 特点:
malloc
分配的内存块中的数据不初始化,内容可能是随机的(内存中的残留数据)。- 适用于一次性分配特定大小的内存。
- 示例:
int* ptr = (int*) malloc(10 * sizeof(int)); // 分配存储10个整数的内存
2. calloc
calloc
(Contiguous Allocation)- 功能:分配内存并初始化为零。
- 语法:
其中,void* calloc(size_t num, size_t size);
num
是要分配的元素个数,size
是每个元素的大小(以字节为单位)。返回指向内存块的指针,如果失败,返回NULL
。 - 特点:
calloc
分配的内存会自动初始化为全零。- 适用于需要多个连续内存块的情况,且希望这些内存块初始化为零。
- 示例:
int* ptr = (int*) calloc(10, sizeof(int)); // 分配并初始化存储10个整数的内存
calloc 就相当于 malloc + memset
3. realloc
realloc
(Reallocation)- 功能:重新分配已分配内存的大小。
- 语法:
其中,void* realloc(void* ptr, size_t new_size);
ptr
是指向之前分配的内存块的指针,new_size
是新内存块的大小(以字节为单位)。realloc
返回指向新内存块的指针。 - 特点:
- 用于扩展或缩小已经分配的内存块。
- 如果需要扩展且原有内存块之后的空间不够,
realloc
会在新位置分配内存,并复制旧数据,然后释放原有内存块;如果足够,则会在原位置上扩展。 - 如果缩小内存块,多余的内存将被释放,但原始数据仍保留。
- 示例:
int* ptr = (int*) realloc(ptr, 20 * sizeof(int)); // 将原来10个整数的内存扩展到20个
realloc扩容空间的做法:
4. free
free
(Memory Deallocation)- 功能:释放之前用
malloc
、calloc
或realloc
分配的内存。 - 语法:
其中,void free(void* ptr);
ptr
是指向需要释放的内存块的指针。 - 特点:
- 必须为动态分配的内存显式调用
free
,否则会导致内存泄漏(内存不会被释放,导致系统内存资源逐渐减少)。 - 释放后,指针指向的内存不再有效,访问它会导致未定义行为(例如:访问已释放的内存可能引发段错误)。
- 必须为动态分配的内存显式调用
- 示例:
free(ptr); // 释放之前分配的内存
- 功能:释放之前用
5. 差异对比
总结:malloc
、calloc
、realloc
与 free
的区别
函数名 | 主要功能 | 内存初始化 | 参数 | 用途 |
---|---|---|---|---|
malloc | 分配指定大小的内存块 | 无(内容未初始化) | size :分配的字节数 | 动态分配内存 |
calloc | 分配并初始化内存 | 全部初始化为0 | num :元素个数size :每个元素的大小 | 分配多个内存块并初始化为零 |
realloc | 重新分配已分配的内存 | 原数据保留 | ptr :原内存指针new_size :新大小 | 扩展或缩小现有内存 |
free | 释放已分配的内存 | 无(释放操作) | ptr :待释放的内存指针 | 释放不再需要的内存 |
区别和应用场景:
- 内存初始化:
malloc
分配的内存不初始化,calloc
分配的内存会被初始化为 0。 - 用法不同:
- 使用
malloc
适合一次性分配已知大小的内存。 - 使用
calloc
适合分配多个元素的内存块,并且需要初始化为零。 - 使用
realloc
适合在内存不足或需要调整内存时动态调整内存大小。
- 使用
- 释放内存:所有通过
malloc
、calloc
、realloc
分配的内存,最后都必须通过free
释放,否则会造成内存泄漏。
6. 失败处理
对于如果开空间失败了怎么办
//realloc, calloc同理
int* ptr = (int*) malloc(10 * sizeof(int));
if (ptr == NULL) {
// 内存分配失败的处理逻辑
perror("malloc fail!");
exit(-1);
}
三、内存分配题目
1. 题目
我们先来看下面的一段代码和相关问题:
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";
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);
}
- 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?C staticGlobalVar在哪里?C
staticVar在哪里?C localVar在哪里?A
num1 在哪里?A
分析:
globalVar全局变量在数据段 staticGlobalVar静态全局变量在静态区
staticVar静态局部变量在静态区 localVar局部变量在栈区
num1局部变量在栈区
char2在哪里?A *char2在哪里?A
pChar3在哪里?A *pChar3在哪里?D
ptr1在哪里?A *ptr1在哪里?B
分析:
char2局部变量在栈区
char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上
pChar3局部变量在栈区 *pChar3得到的是字符串常量字符在代码段
ptr1局部变量在栈区 *ptr1得到的是动态申请空间的数据在堆区
- 填空题:
sizeof(num1) = 40;//数组大小,10个整形数据一共40字节
sizeof(char2) = 5;//包括\0的空间
strlen(char2) = 4;//不包括\0的长度
sizeof(pChar3) = 4;//pChar3为指针
strlen(pChar3) = 4;//字符串“abcd”的长度,不包括\0的长度
sizeof(ptr1) = 4;//ptr1是指针
2. 内存区域划分
-
栈
- 又称堆栈,用于存储非静态局部变量、函数参数、返回值等,栈是向下增长的。
-
内存映射段
- 高效的I/O映射方式,用于加载共享的动态内存库。通过系统接口创建共享内存,进行进程间通信。(如果还没学到这部分内容,现在只需了解即可。)
-
堆
- 用于程序运行时的动态内存分配,堆是向上增长的。
-
数据段
- 存储全局变量和静态变量。
-
代码段
- 包含可执行代码和只读常量。
四、C++内存管理方式
1. new 与 delete
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦(主要是在对类)
,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2. new/delete操作内置类型
这段代码展示了在C++中如何使用new
和delete
进行动态内存分配和释放。下面逐行讲解代码的含义:
int main()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
- 动态申请单个
int
类型的内存:- 使用
new int
分配一个int
类型的内存空间,并返回其地址,赋值给指针ptr4
。 - 此时,内存中的值未初始化,可能是一个随机值。
- 使用
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
- 动态申请单个
int
类型的内存并初始化:- 使用
new int(10)
分配内存并将其初始化为10。指针ptr5
指向这个内存位置。
- 使用
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
- 动态申请数组:
- 使用
new int[3]
分配一个能够存储3个int
的数组。内存中的值未初始化,因此每个元素的值都是随机的。
- 使用
int* ptr7 = new int[3]{ 1,2,3 };
- 动态申请数组并初始化:
- 使用
new int[3]{ 1,2,3 }
分配一个能够存储3个int
的数组,并将第一个元素初始化为1,第二个元素初始化为2,第三个元素初始化为3。未指定的元素会被初始化为0。
- 使用
int* ptr8 = new int[5]{ 1,2,3 };
- 动态申请更大数组并部分初始化:
- 使用
new int[5]{ 1,2,3 }
分配一个能够存储5个int
的数组,前3个元素分别被初始化为1、2、3,后2个元素自动初始化为0。
- 使用
delete ptr4;
delete ptr5;
- 释放单个变量的内存:
- 使用
delete
释放由new
分配的单个int
类型的内存。ptr4
和ptr5
所指向的内存空间被释放。
- 使用
delete[] ptr6;
delete[] ptr7;
delete[] ptr8;
- 释放数组的内存:
- 使用
delete[]
释放动态分配的数组内存。ptr6
、ptr7
和ptr8
分别指向的数组内存被正确释放。 注意:内存泄漏
使用delete
释放一个数组分配的内存可能会导致内存泄漏(如:释放这个数组不加[]),delete ptr6;
。delete 只会释放第一个元素的内存,后续元素的内存没有被正确释放,这会使得程序中的内存使用逐渐增加。
- 使用
3. new和delete操作自定义类型
现在我们有一个A类:
class A
{
public:
A(int a = 1)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main() {
// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
// 还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A)); // 使用malloc申请内存
A* p2 = new A(1); // 使用new申请内存,并调用构造函数
- 内存分配:
malloc
:分配内存给一个A
类型的对象(p1
),但不会调用构造函数。new A(1)
:分配内存并调用构造函数,初始化_a
为 1,并将指针赋值给p2
。
free(p1); // 使用free释放内存
delete p2; // 使用delete释放内存
- 内存释放:
free(p1)
:释放通过malloc
分配的内存,不会调用析构函数。delete p2
:释放通过new
分配的内存,同时会调用析构函数,打印析构信息。
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int; // C++
free(p3); // 释放p3
delete p4; // 释放p4
- 内置类型的内存管理:
malloc
和free
的使用与new
和delete
处理内置类型(如int
)几乎一样。malloc
不会初始化内存,而new
会为内置类型分配内存。
A* p5 = (A*)malloc(sizeof(A)*10); // 使用malloc申请10个A对象的内存
A* p6 = new A[10]; // 使用new申请10个A对象的内存
- 数组的内存分配:
malloc
:分配内存给10个A
类型的对象,但不会调用构造函数。new A[10]
:分配内存并调用构造函数,为每个对象初始化。
free(p5); // 释放通过malloc分配的内存
delete[] p6; // 释放通过new[]分配的内存,调用每个对象的析构函数
- 数组内存释放:
free(p5)
:释放10个A
类型对象的内存。delete[] p6
:释放通过new[]
分配的内存,确保调用每个对象的析构函数。
总结
new
和delete
vsmalloc
和free
:new
和delete
用于分配和释放自定义类型的内存时,会自动调用构造函数和析构函数,而malloc
和free
只进行内存分配和释放,不调用构造和析构函数。
五、operator new与operator delete函数
1. 原理
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
2. 异常捕获方法
#include <iostream>
#include <exception>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A constructed: " << _a << endl;
}
~A() {
cout << "A destructed." << endl;
}
private:
int _a;
};
void Func() {
int* p1 = new int[1024 * 1024 * 100]; // 试图分配内存
cout << p1 << endl;
int* p2 = new int[1024 * 1024 * 100]; // 试图分配内存
cout << p2 << endl;
int* p3 = new int[1024 * 1024 * 100]; // 试图分配内存
cout << p3 << endl;
int* p4 = new int[1024 * 1024 * 100]; // 试图分配内存
cout << p4 << endl;
int* p5 = new int[1024 * 1024 * 100]; // 试图分配内存
cout << p5 << endl;
}
int main() {
try {
Func(); // 调用函数以分配内存
} catch (const exception& e) {
cout << "Memory allocation failed: " << e.what() << endl; // 捕获异常并打印信息
}
return 0;
}
六、new和delete的实现原理
1. 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,
不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
2. 自定义类型
new
和 delete
原理
操作 | 原理说明 |
---|---|
new | |
1. 调用 operator new | 通过调用 operator new 函数申请足够的内存空间。 |
2. 执行构造函数 | 在申请的内存空间上执行构造函数,完成对象的构造并返回指向对象的指针。 |
delete | |
1. 执行析构函数 | 在对象的内存空间上执行析构函数,清理对象中占用的资源。 |
2. 调用 operator delete | 通过调用 operator delete 函数释放对象所占用的内存空间。 |
new T[N] | |
1. 调用 operator new[] | 调用 operator new[] 函数,通过实际调用 operator new 函数申请 N 个对象所需的内存空间。 |
2. 执行 N 次构造函数 | 在申请的内存空间上执行 N 次构造函数,构造 N 个对象。 |
delete[] | |
1. 执行 N 次析构函数 | 在释放的对象空间上执行 N 次析构函数,清理 N 个对象中占用的资源。 |
2. 调用 operator delete[] | 调用 operator delete[] 函数释放内存空间,实际在 operator delete[] 中调用 operator delete 来释放内存。 |
七 、定位new表达式(placement-new)
使用这个的理由是,我们对一个已经存在的对象,不能在外面显示调用它的构造函数,但是析构函数可以。
为了解决这个问题,引入定位new表达式。
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
//有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
八、对比malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放
总结
到这里,内存管理的东西就结束了,谢谢大家~