在进入这篇的学习之前,我们做一做以下这些题目( 涉及C语言的内存管理知识):
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);
}选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
1.globalVar 在哪里? ____ 2. staticGlobalVar在哪里?____ 3.staticVar在哪里?____
4.localVar 在哪里? ____ 5.num1 在哪里?____ 6.char2在哪里?____
7.* char2 在哪里? ___ 8.pChar3在哪里?____ 9. * pChar3 在哪里? ____
10.ptr1在哪里?____ 11. * ptr1 在哪里? ___
下面我开始解答:
1.
globalVar 是 全局变量,所以是存储在 全局存储区/静态存储区/数据段,选C
2.
static GlobalVar是全局变量,所有存储在全局存储区/静态存储区/数据段,选C
3.
statiVar 是静态的局部变量,所以存储在静态存储区/全局存储区/数据段,选C
4.
localVar是局部变量,所以存储在栈区,选A
5.
num1是整型数组,是局部变量,存储在区。选A
6.
char2是字符串数组,其 所在地址 接收了字符串内容的拷贝 ,是局部变量,存储在栈区,选A
7.
*char2是字符串数组的解引用, 获取的是数组内容,而内容是字符串内容的拷贝,而非字符串本
身,所以数据存储在 栈区,选A
8.
pChar3是字符指针, 字符指针本身是局部变量存储在栈区,但他指向字符串常量,并且const修饰导致指针指向不可修改,所有等效于指向的字符串本身 ,即存储在常量区/代码段,选D
9.
*pChar3是对字符指针的解引用,获取的是指针指向的首地址,也就是字符串常量的首地址,
存储在 常量区/代段,选D
10.
ptr1是整型指针,指向了malloc在堆区开辟的一片空间,但ptr本身属于局部变量,所以存储栈区,选A
11.*ptr1是整型指针的解引用,获取的是malloc开辟在堆区的那片空间,所有存储在堆区,选B
看完这些题,你是否找回了对C语言内存分配的记忆?那我们来认识一下C/C++中程序在内存区的划分:

如图,这是上面一题在执行程序时各自在内存区的划分,顺便说明一下:
1. 栈 又叫堆栈 -- 非静态局部变量/函数参数/ 返回值等等, 栈是向下增长的 。
2. 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。 ( 现在只需要了解一下,后面Linex会讲解)
3. 堆 用于程序运行时动态内存分配, 堆是可以上增长的 。
4. 数据段/静态区/全局区 -- 存储全局数据和静态数据。
5. 代码段/常量区 -- 可执行的代码/ 只读常量。
上面的说明中提及了 栈是向下增长的,我用代码举例:

如图,地址向下递减,这就是栈向下增长的逻辑,向低地址的地方开辟空间给后来的变量或者函数
堆也同理,大方向是向高地址的地方开辟空间给后来的变量或者函数,但是不总是这样,堆区比较复杂,这点我们以后再理解。
在堆区动态开辟内存和释放内存,我们之前(C语言)使用这几个函数:malloc,calloc,realloc,free
那这几个函数又有什么区别呢?
malloc(size_t size):在堆区动态开辟一块大小为size字节的连续空间,并返回指向该内存的void*类型指针,不初始化(可能残留之前的数据,需要手动初始化,如使用memset初始化)
calloc(size_t nmemb, size_t size):在堆区动态开辟nmemb个大小为size的空间,并返回指向该内存的void*类型指针,会将其初始化为0 (空间总大小为 nmemb*size)
realloc(void* ptr, size_t new_size):对一块已分配的内存进行扩容。然后返回扩容后的首地址。这又分为异地扩容和原地扩容:
异地扩容:若原内存块没有足够大的连续空间,会在堆区重新分配一块空间足够的地方,将ptr的原数据复制过去,然后释放ptr的原内存块,返回新地址(可能导致指针失效,需注意)
原地扩容:原内存块后有足够大的连续空间,则会直接在该内存块后面直接扩展,然后返回ptr
将动态分配的内存(堆区)归还给操作系统,但记住其本质:只负责归还,不负责清除数据。归还的内存,系统可再次分配使用。
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过 new和delete操作符 进行动态内存管理。
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的连续空间
int* ptr6 = new int[10]; //实际上是10个int元素的连续数组
delete ptr4;
delete ptr5;
delete[] ptr6; //申请用了 [],那销毁也要用[],不用会报错
}还可以给申请的数组初始化:int* ptr6 = new int[10]{1,2,3}; 剩下未初始化的默认为0
销毁就还是一样:delete[ ] ptr6;
注意:申请和释放单个元素的空间,使用new和delete操作符, 申请和释放连续的空间,使用
new[]和delete[],匹配起来使用。

以上是对new delete对内置类型的代码解析。
new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数

如图,使用new自动调用了默认构造函数,使用delete自动调用了析构函数。

对于内置类型而言,因为不需要调用析构和构造,所以这两种方式显得没区别。但明显用new更方便,代码可读性更高。

这图与上面结论一样,new更便捷,可读性更高。
既然malloc和new都是动态开辟空间的,那总会开辟空间失败,虽然概率很小。那我们看看
malloc和new是怎么对错误进行处理的?
malloc:malloc开辟空间后,会返回该内存的首地址给一个指针接收,若内存开辟失败,则malloc会返回NULL给指针,此时只需要用if判断语句判断该指针是否为NULL即可。
new:new分配空间失败后,会抛异常(std::bad_alloc),不会返回NULL。此时需要用
try-catch 块 捕获异常 ( try-catch 和 抛异常 涉及的内容较为复杂,以后再讲)。抛异常必须被捕获,如果没被捕获则程序会崩溃(运行到错误的地方,导致程序终止),除非你明确不可能抛异常,不然必须捕获,捕获的代码演示:
try
{
// 可能抛 std::bad_alloc 的代码,例如:
int* p = new int[1000000000];
delete[] p;
}
catch (const exception& e)
// 精准捕获 new 分配失败的异常类型
{
cout << e.what() << endl;
}其中try括号中的那块内容可以是函数,由函数间接捕获(执行进入函数发现抛异常,然后跳转到main函数中的捕获),也可以直接塞可运行代码(如图中代码),直接对代码捕获。
函数内部抛异常但未在函数内捕获,异常会自动上传到外层作用域(平行作用域也不影响异常传播,比如上面的例子:函数与main函数),直到被某个try-catch块 捕获。一直未被捕获则导致程序会终止。
没学过的参数涉及更多知识,目前只需了解
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。
operator new 和 operator delete 不算是 对new和delete 的重载,算是两个 特殊的,系统提供的全局函数。 用作new和delete的底层调用
以下是这两个函数在库中的部分源码:
operator new:
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);
}
return (p);
}实际上operator就是封装了malloc函数。图中若malloc申请空间失败,则会尝试释放内存,然后抛异常。 为什么不直接使用malloc? 因为malloc申请失败返回NULL,而C++申请失败的特性是要返回抛异常,所以进行封装。
operator delete:
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)这里的代码就比较晦涩难懂,但你会发现其中调用了_free_dbg,再看最底下:_free_dbg其实是free的宏替换。
这说明,operator delete实际上是free的封装。 (因为operator new,搞个配套operator delete)
总结,operator new 和operator delete 其实是对malloc 和 free 的封装,并且我们可以调用。用法基本相同,唯一的区别就是operator new 失败会返回抛异常。
这两个函数我们可以调用,但一般都不这么用,因为上面说了,这两个函数是new和delete的底层调用:
new的反汇编:

如图,这是调用new的语句的反汇编代码,大部分我们都看不懂,但只需要看懂红框两句call是调用,mov是移动。
所以图中调用(call)了operator new函数 和 A类的构造函数。 这说明new的底层确实是调用了operator new,new的异常也是从operator new中抛出。
delete的反汇编:

图中第一句红框的代码是A类产生的辅助函数,现在不需要了解那么细,只需要知道它会展开两个核心步骤,也就是下面两个红框:
1.调用析构函数 2.调用operator delete函数。
是先使用析构,然后使用operator delete 原因: 因为析构要析构的是指向的资源,若先delete删除了指针,那就找不到指向的资源了(指向丢失,会非法访问、删除资源,导致野指针),所以需要先用析构清理掉指向的资源,然后再删除指针。
上面学习了operator delete 和 operator new ,现在我们更全面地总结一下:
内置类型
如果申请的是内置类型的空间, new和malloc,delete和free 基本类似,不同的地方是:
new/delete 申请和释放的是单个元素的空间, new[] 和 delete[] 申请的是连续空间,而且 new 在申
请空间失败时会抛异常, malloc 会返回 NULL 。
2 自定义类型
new 的原理
1. 调用 operator new 函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造
delete 的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用 operator delete 函数释放对象的空间
new T[n] 的原理
1. 调用 operator new[ ] 函数,在 operator new[ ]中实际调用operator new函数完成n个对象空间的申请
2. 在申请的空间上执行n 次构造函数
delete[] 的原理
1. 在释放的对象空间上执行n 次析构函数,完成n 个对象中资源的清理
2. 调用 operator delete[ ] 释放空间,实际在 operator delete[ ]中调用operator delete来释放空间
//A类的私有成员仅有int _a
A* p1 = new A[10];
delete[] p1; 如图,根据注释可以直到,一个 A对象占4个字节,new动态开辟了10个A类的连续空间,然后返回首地址给p1,所以可以知道: p1是40个字节。 但是,系统会在数组头部额外分配4字节,存放数组元素数量,(开辟的单位大小空间个数,也就是10):

如图,原本预计是40字节,但是系统多加了4个字节的空间,用来存放数字,这个数字就是表示开多少空间的,实际上是分配了44字节。
别小看这个数字,系统区分 delete[ ] p1 和delete p1 看的就是这个:delete[ ] p1,加了方括号,系统会自动读取头部的元素数量(10),并对每个对象调用析构函数(10次)。 若是delete p1,则系统会默认只开了一个单位大小的空间,系统会默认只调用一次析构函数,导致剩余 9 个对象的资源未被清理,引发内存泄漏和野指针风险。
所以new [ ] 要配对 delete [ ]
定位 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);如图,p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行。malloc只是负责分配空间,不负责构造对象,没构造就没对象。(同理换成operator new也一样,因为只是malloc的封装,不会进行构造)
这时候就需要显式调用构造函数,然后普通方法都不能调用,这时候定位new的作用就显现出来:new(p1)A
因为A有默认构造函数,所以不需要给参数,若A无默认构造,则需根据函数参数给参数,比如:A(int a),此时 new(p1) A(1)
free不能清理资源,所以得显式调用析构函数去清理资源,析构和构造不同,析构可直接调用:p1->~A();
共同点:
malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同点:
1. malloc 和 free 是函数, new 和 delete 是操作符
2. malloc 申请的空间不会初始化, new 可以初始化
3. malloc 申请空间时,需要手动计算空间大小并传递, new 只需在其后跟上空间的类型即可,
如果是多个对象, [] 中指定对象个数即可
4. malloc 的返回值为 void*, 在使用时必须强转, new 不需要,因为 new 后跟的是空间的类型
5. malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空, new 不需要,但是 new 需
要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数 , 而new
在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
空间中资源的清理释放
区别,总结于3个方面: 用法(1),核心特性(2-5),原理(6)。 死记硬背背不会的,理解知识更好。
C++内存管理就先到这了,这一篇干货满满,请大家喝水观看!有问题和错误欢迎指出,请各位多多点赞支持~