C++学习笔记——内存管理
1、C/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在哪里?____
问题分析:
选项说明:
A. 栈:存储非静态局部变量、函数参数、返回值等
B. 堆:动态内存分配(malloc/calloc/realloc)
C. 数据段(静态区):存储全局数据和静态数据(包括全局变量和static修饰的变量)
D. 代码段(常量区):存储可执行代码和只读常量(如字符串常量)
具体分析:
1.globalVar:全局变量 → 数据段(静态区)→ C
2.staticGlobalVar:静态全局变量 → 数据段(静态区)→ C
3.staticVar:静态局部变量(虽在函数内,但由static修饰)→ 数据段(静态区)→ C
4.localVar:非静态局部变量 → 栈 → A
5.num1:非静态局部数组(在函数内)→ 栈 → A
6.char2:非静态局部字符数组(在函数内)→ 栈 → A
(注意:char2[] = "abcd" 在栈上分配数组并初始化,字符串"abcd"本身在常量区,但数组副本在栈上)
7.*char2:解引用char2(即char2数组的第一个元素)→ 存储在栈上(因为char2本身在栈)→ A
8.pChar3:非静态局部指针变量(在函数内)→ 栈 → A
9.*pChar3:解引用pChar3(指向字符串常量"abcd"的首元素,里面存首元素的地址)→ 字符串常量存储在代码段(常量区)→ D
10.ptr1:非静态局部指针变量(在函数内)→ 栈 → A
11.*ptr1:解引用ptr1(指向malloc动态分配的内存)→ 堆 → B


2、C语言中动态内存管理方式:malloc/calloc/realloc/free
void Test()
{ int* p2 = (int*)calloc(4, sizeof(int));int* p3 = (int*)realloc(p2, sizeof(int) * 10);// 这里需要free(p2)吗? free(p3);
}
这里需要free(p2)吗?
答:不需要,而且绝对不能这样做。
原因分析:realloc 函数的功能是重新分配之前由 malloc, calloc, 或 realloc 所分配的内存块(p2所指向的内存块)的大小。
当调用** int* p3 = (int*)realloc(p2, sizeof(int)*10); **时,会发生以下两种情况之一:
情况一(原地扩容):如果 p2 指向的内存块后面有足够的空闲空间,realloc 会尝试在原地扩大这块内存。函数返回的地址值 p3 等于传入的地址值 p2。此时,p2 和 p3 指向同一个内存块。情况二(异地迁移):如果原地没有足够空间,realloc 会在堆的另一块区域分配一个足够大的新内存块(sizeof(int)*10),将旧内存块(p2指向的)中的数据原样拷贝过来,然后自动释放旧内存块。函数返回的地址值 p3 是一个新地址,与 p2 不同。
关键在于,无论哪种情况,realloc 调用后,之前由 p2 指向的旧内存块都已经被系统接管,程序员不再拥有它的所有权,也不应该再尝试去释放它。
在情况一中,这块内存被扩大了,需要通过 p3 来管理。
在情况二中,这块内存已经被 realloc 函数内部自动 free 掉了。因此,代码中只需要 free(p3) 即可,它释放的就是当前有效的那一块内存。如果在 realloc 后再 free(p2),会导致双重释放的错误,这是一种未定义行为,通常会导致程序崩溃。
牢记:永远只 free 由 malloc, calloc, realloc 返回的最新指针。在一次成功的 realloc 调用后,旧的指针就应该被立即视为无效,不应再使用或释放。
面试题总结
1. malloc/calloc/realloc的区别?
特性 | malloc | calloc | realloc |
功能 | 分配指定大小的内存块 | 分配并初始化指定数量、大小的内存块 | 调整已分配内存块的大小 |
函数原型 | void* malloc(size_t size); | void* calloc(size_t num, size_t size); | void* realloc(void* ptr, size_t new_size); |
初始化 | 不初始化内存内容,内容是随机值 | 初始化内存内容为全零 | 根据情况,新增加的内存区域不会被初始化 |
参数 | size:要分配的字节数 | num:元素个数 size:每个元素的字节数 | ptr:要调整的旧内存指针 new_size:新的总字节数 |
使用场景 | 需要分配内存且不关心初始值,或准备自行初始化时 | 需要分配数组或结构体并希望初始值全为零时 | 需要扩大或缩小之前动态分配的内存时 |
2. malloc的实现原理?
3、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 new/delete操作内置类型
void Test()
{// 动态申请一个int类型的空间 int* ptr4 = new int; // 分配空间,值未初始化(是随机值)delete ptr4; // 释放空间// 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 分配空间并初始化为10delete ptr5; // 释放空间// 动态申请3个int类型的空间 int* ptr6 = new int[3]; // 这里3表示对象个数,值都未初始化delete[] ptr6; // 释放连续的数组空间// 动态申请3个int类型的空间并初始化 int* ptr7 = new int[3] {1,2,3}; // 值初始化为1,2,3delete[] ptr7; // 释放连续的数组空间
}
1.初始化:new 可以通过 new int(10) 的方式对单个元素进行初始化,而 malloc 无法方便地做到这一点。
2.数组操作:使用 new[] 来分配数组,使用 delete[] 来释放数组。必须匹配使用,否则行为未定义(尤其是对自定义类型,会导致析构函数调用次数错误)。
3.对于内置类型(如 int),new/delete 和 malloc/free 在功能上几乎没有区别,都是分配和释放内存。但 new 的语法更简单,且支持初始化。
3.2 new和delete操作自定义类型
class A
{
public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; // 构造函数}~A() { cout << "~A():" << this << endl; // 析构函数}
private: int _a;
};int main()
{ // 对比一:自定义类型A* p1 = (A*)malloc(sizeof(A)); // 只分配内存,不调用构造函数A* p2 = new A(1); // 1. 分配内存 2. 调用构造函数 A(1)free(p1); // 只释放内存,不调用析构函数delete p2; // 1. 调用析构函数 2. 释放内存// 对比二:内置类型(行为几乎相同)int* p3 = (int*)malloc(sizeof(int)); int* p4 = new int; free(p3); delete p4; // 对比三:对象数组A* p5 = (A*)malloc(sizeof(A)*10); // 分配10个A对象的内存,全部未构造A* p6 = new A[10]; // 分配内存,并调用10次默认构造函数free(p5); // 直接释放内存,10个对象都未析构delete[] p6; // 调用10次析构函数,然后释放内存return 0;
}
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与
free不会。
4. operator new与operator delete函数(重点)
new/delete 操作符与operator new/operator delete全局函数之间的关系和区别。
1.new 和 delete 是 C++ 语言中的操作符(关键字),供程序员直接使用来进行动态内存分配和释放。
2.operator new 和 operator delete 是系统提供的全局函数,是 new 和 delete 操作符在底层实现时所调用的工具。
3.调用关系:
new 操作符的底层行为可以分解为两步:
a. 调用 operator new 函数来分配内存。
b. 在分配好的内存上调用对象的构造函数。
delete 操作符的底层行为也可以分解为两步:
a. 调用对象的析构函数。
b. 调用 operator delete 函数来释放内存。
4.功能定位:
operator new/operator delete 的核心职责只是管理内存的分配和释放(类似于更高级的 malloc 和 free),它们不负责调用构造函数和析构函数。调用构造和析构是 new/delete 操作符自己的逻辑。总结:new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new 函数库代码
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid *p;while ((p = malloc(size)) == 0) // 底层调用malloc尝试分配size字节的内存{if (_callnewh(size) == 0) // 如果malloc失败,检查用户是否设置了“new-handler”函数{// report no memory// 如果用户没有设置new-handler,或者new-handler也无法解决问题,则抛出异常static const std::bad_alloc nomem;_RAISE(nomem); // 抛出std::bad_alloc类型异常}// 如果用户设置了new-handler(_callnewh返回非0),循环会继续,再次尝试malloc}return (p); // 分配成功,返回指向内存的指针
}
operator new底层调用 malloc
operator new 的本质是 malloc 的一个封装。它的主要工作就是调用 malloc(size) 来分配指定大小的内存。
失败处理机制(与malloc的关键区别):
1.malloc 失败时直接返回 NULL 空指针。
2.operator new 失败时,会进入一个循环。它首先检查用户是否通过 std::set_new_handler 设置了一个new-handler 函数。如果没有设置new-handler或它也无法解决问题,抛出 std::bad_alloc 异常。
operator delete 函数库代码
void operator delete(void *pUserData)
{... // 一些调试和线程安全代码if (pUserData == NULL) // 遵循C++标准:delete空指针是安全的,直接返回return;..._free_dbg( pUserData, _NORMAL_BLOCK ); // 底层调用free的调试版本...return;
}
// free的本质
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator delete底层调用 free
1.operator delete 的本质是 free 的一个封装。它的主要工作就是调用 free(pUserData) 来释放内存。
2.operator delete 检查传入的指针是否为空 (NULL),如果是则直接返回。这保证了 delete nullptr; 是安全的操作。
上述两个全局函数的总结:
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果
malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
5、new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
5.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来释放空间。
6、定位new表达式(placement-new) (了解)
概念:
定位 new 是一种特殊形式的 new,它不在堆上分配新的内存,而是在已经分配好的原始内存空间上调用构造函数来初始化(创建)一个对象。
使用格式:
1.new (place_address) type
2.new (place_address) type(initializer-list) (带初始化列表,用于有参构造函数)
3.place_address 必须是一个指针,指向预先分配好的内存块。
使用场景:
最主要的用途是配合内存池。内存池为了提高效率,会预先分配大块内存,然后从中切分给小对象使用。这些内存块是“原始”的,没有经过构造函数的初始化。定位 new 就被用来在这些原始内存上构造对象。
注意:由于对象是手动构造的,其析构函数也必须手动调用。内存的释放也同样需要根据其分配方式(如 malloc 或 operator 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对象相同大小的一段空间,还不能算是一个对象,// 因为构造函数没有执行//使用 malloc 分配原始内存A* p1 = (A*)malloc(sizeof(A));new(p1)A; // 使用定位new在p1指向的内存上构造A对象。// 注意:如果A类的构造函数有参数时,此处需要传参 p1->~A(); // 手动调用析构函数,销毁对象。对象所占的内存还在,但对象的生命周期结束free(p1); // 释放p1指向的内存块//使用 operator new 分配原始内存A* p2 = (A*)operator new(sizeof(A)); // 使用operator new分配内存new(p2)A(10); // 使用定位new在p2指向的内存上构造A对象p2->~A(); // 手动调用析构函数operator delete(p2); // 使用operator delete释放内存return 0;
}
注意:使用 malloc/free 或 operator new/operator delete。必须确保分配和释放的方式匹配(malloc 配 free, operator new 配 operator delete)。在对象周期结束时,需要手动调用析构函数,销毁对象。此时,对象所占的内存还在,但对象的生命周期结束。使用匹配释放的方式才会释放之前对象所占的内存空间。
7、malloc/free和new/delete的区别(重点)
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在释放空间前会调用析构函数完成。(最主要不同)