目录
内存管理的划分
C语言中动态内存管理的方式
C++内存管理方式
new/delete操作内置类型
new/delete操作自定义类型
operator new和operato delete函数
new和delete的实现原理
内置类型
自定义类型
定位new表达式
malloc/free和new/delete的区别
内存管理的划分
C/C++ 程序运行时内存主要划分为栈、堆、全局静态区、常量区、代码区,还有内存映射段。
栈由编译器自动分配与释放,存放非静态局部变量、函数参数、函数返回值,栈向下增长,函数调用结束后栈上的数据会自动回收,不需要程序员手动管理。
堆用于程序运行过程中的动态内存分配,由程序员手动申请和释放,malloc、new 都是在堆上开辟空间,free、delete 完成释放,堆向上增长,如果申请的堆内存没有释放,就会发生内存泄漏。
全局静态区存放全局变量和 static 修饰的静态变量,分为已初始化和未初始化两个部分,这块内存在程序启动的时候分配,直到程序结束才释放。常量区存放字符串字面量等只读常量,该区域的内容不允许修改,强行修改会造成程序崩溃
了解这些知识以后,我们来看下面的一段代码和相关问题。
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. 选择题:
选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____staticGlobalVar在哪里?____
staticVar在哪里?____localVar在哪里?____
num1 在哪里?____char2在哪里?____
* char2在哪里?___
pChar3在哪里?____* pChar3在哪里?____
ptr1在哪里?____* ptr1在哪里?____
2. 填空题:
sizeof(num1) = ____;
sizeof(char2) = ____;strlen(char2) = ____;
sizeof(pChar3) = ____;strlen(pChar3) = ____;
sizeof(ptr1) = ____;3. sizeof 和 strlen 区别?
- globalVar 是全局变量,全局变量存放在数据段也就是静态区,选C。
- staticGlobalVar 是静态全局变量,static 修饰的全局变量依旧存放在数据段静态区,只是作用域被限制在当前文件,选 C。
- staticVar 是函数内部的静态局部变量,普通局部变量在栈,但加 static 修饰后,变量不再放在栈,而是放到数据段静态区,程序结束才销毁,选C。
- localVar 是函数内普通局部变量,局部变量创建在栈上,函数调用结束后栈空间自动回收,选A。
- num1 是函数内部定义的局部数组,数组属于局部变量,数组开辟的内存空间在栈上,选A。
- char2 是函数内的局部字符数组,数组本身是局部变量,存储在栈上,字符串 "abcd" 的内容会拷贝到这个栈数组里,所以 char2 在栈,选A。
- *char2 代表 char2 数组里的第一个字符,数组整体在栈,数组里的元素也存放在栈空间,选A。
- pChar3 是一个局部指针变量,指针变量本身是局部变量,变量本体存放在栈,选 A。
- *pChar3 是指针 pChar3 指向的内容,字符串字面量 "abcd" 存放在常量区,指针指向这个常量字符串,所以pChar3 在常量区,选 D。
- ptr1 是局部的指针变量,指针这个变量本身是局部变量,存放在栈,选 A。
- *ptr1 是 ptr1 指针指向的内存,ptr1 由 malloc 在堆上开辟内存,所以ptr1 访问堆空间,选B。
2.
- sizeof (num1),num1 是 int 类型数组,一共有 10 个元素,单个 int 占 4 字节,10 乘 4 等于 40,所以结果是 40。
- sizeof (char2),char2 是字符数组,用 "abcd" 初始化,数组里面会存放 a、b、c、d还有字符串结束符 '\0',一共 5 个 char,一个 char 占 1 字节,所以结果是 5。
- strlen (char2),strlen 统计字符串有效字符数量,不算末尾的 '\0',abcd 一共 4 个有效字符,所以结果是 4。
- sizeof (pChar3),pChar3 是 char 类型的指针变量,在 32 位环境指针占 4 字节,64 位环境指针占 8 字节。
- strlen (*pChar3),*pChar3 指向常量字符串 "abcd",有效字符是 a、b、c、d,不统计 '\0',结果是 4。
- sizeof (ptr1),ptr1 是 int 类型的指针变量,指针变量存储的是地址,64 位环境下地址占 8 字节,结果为 8。
3.
- sizeof 是运算符,在编译阶段计算,用来获取变量或者类型占用内存的总字节数,它会包含字符串末尾的 '\0',可以用于所有类型。
- strlen 是库函数,运行时执行,只针对以 '\0' 结尾的字符串,统计 '\0' 之前有效字符的个数,不包含结束符 '\0'。
C语言中动态内存管理的方式
C 语言动态内存管理依靠四个库函数实现。
malloc 函数用来申请一块未初始化的连续堆内存,参数是需要开辟的总字节数,返回 void * 类型的指针,使用的时候需要强制转换成对应类型的指针。如果内存开辟失败,会返回 NULL 空指针,使用前要判断返回值是否为空。开辟出来的内存里面的值是随机垃圾值,不会自动清零。
calloc 函数用来申请指定数量、指定大小的连续堆内存,有两个参数,第一个是元素个数,第二个是单个元素占用字节。它会把申请到的这块内存全部初始化为 0,返回值同样是 void * 指针,开辟失败返回 NULL。
realloc 函数用来调整已经开辟好的堆内存大小,第一个参数是旧内存的指针,第二个参数是调整之后新的总字节数。它会根据情况选择原地扩容或者重新找一块新空间拷贝原有数据,释放旧空间。如果第二个参数设置为 0,效果近似 free。realloc 失败的时候会返回 NULL,原来的内存不会释放,所以不能直接用原指针接收返回值,防止原地址丢失。
free 函数用来释放堆上申请的内存,参数是动态内存的指针。free 只会把这块内存的使用权交还给操作系统,不会把指针本身置空,所以 free 之后建议手动把指针赋值为 NULL,避免出现野指针。不能释放栈上的内存,也不能重复释放同一块堆内存,否则程序会报错。
这四个函数配合使用,malloc、calloc、realloc 负责申请或者调整堆内存,free 负责释放,堆内存需要程序员手动管理,忘记释放会造成内存泄漏。
C++内存管理方式
new/delete操作内置类型
new 和 delete 负责在堆上分配和回收内存。
申请单个内置类型对象,可以用 new,还可以选择初始化。比如 int* p = new int; 这样只会开辟 4 字节堆内存,里面的值是随机值。如果写成 int* p = new int (10); 就会在开辟内存的同时,把这个 int 空间赋值为 10。释放的时候使用 delete p;。
申请内置类型数组,使用 new [],语法是 int* p = new int [10]; 开辟 10 个连续 int 空间,默认不初始化,里面是随机值。也可以部分初始化,int* p = new int [10]{1,2,3}; 前三个元素赋值为 1、2、3,剩下的元素自动初始化为 0。释放数组内存必须用 delete [] p;,不能只用 delete。
内置类型使用 new 和 malloc 有一点区别,new 申请失败默认抛出异常,malloc 失败返回 NULL。语法上 new 直接写类型,不用手动计算字节大小,返回的指针不用强制转换。
使用的时候要注意配对,new 出来的单个变量用 delete 释放,new [] 开辟的数组,必须用 delete [] 释放。释放之后指针不会自动置空,最好手动把指针赋值成 nullptr,防止野指针。
new/delete操作自定义类型
当 new 和 delete 操作自定义类型的时候,不仅仅是开辟和释放堆内存,还会自动调用构造函数与析构函数。
使用 new 创建单个自定义类型对象时,首先底层会调用operator new在堆上开辟足够大小的内存,开辟成功之后,自动调用这个类的构造函数,完成对象的初始化。比如 Date* p = new Date (2026,9,29);,先分配内存,再执行构造函数给年月日赋值。释放这个对象用 delete p;,delete 会先调用对象的析构函数,清理对象内部的资源,之后调用 operator delete 把堆内存还给操作系统。
#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; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间 //还会调用构造函数和析构函数 A* p1 = (A*)malloc(sizeof(A)); A* p2 = new A(1); free(p1); delete p2; // 内置类型是几乎是一样的 int* p3 = (int*)malloc(sizeof(int)); // C int* p4 = new int; free(p3); delete p4; A* p5 = (A*)malloc(sizeof(A)*10); A* p6 = new A[10]; free(p5); delete[] p6; return 0; }如果使用new []创建自定义类型的数组,会开辟能够存放多个对象的连续堆内存,然后循环调用每一个对象的构造函数。释放的时候必须使用 delete [],它会先循环调用数组里每一个对象的析构函数,再释放整块堆内存。new [] 和 delete [] 必须配对使用,不能混用普通 delete,否则会出现资源没有完全释放或者程序崩溃的问题。
使用的时候要注意几个要点,new 成功会返回对应类型指针,不需要强制转换,内存申请失败默认抛出bad_alloc 异常。delete 执行完成后,指针变量本身不会自动变成空指针,需要手动赋值为 nullptr,避免产生野指针。同时要保证 new 和 delete 配对,new [] 和 delete [] 配对,一旦配对错误,析构函数调用次数出错,会引发未定义行为。
operator new和operato delete函数
operator new 和 operator delete 是 C++ 提供的全局库函数,并不是 new 和 delete 运算符。new 运算符底层会调用 operator new 函数来开辟堆内存,delete 运算符底层会调用 operator delete 函数来释放堆内存,这两个函数内部本质上是对 malloc 和 free 做了一层封装。
operator new 的作用是申请指定大小的堆内存,参数是需要开辟的字节数,返回 void * 类型的指针。如果内存申请成功,就返回内存起始地址;如果申请失败,默认会执行异常处理流程,抛出 bad_alloc 异常。它只负责分配原始内存,不会调用构造函数,构造函数是 new 运算符在拿到这块内存之后才去调用的。
operator delete 的作用是释放 operator new 开辟出来的内存,参数是 void * 类型的内存地址,它只负责归还堆内存,不会调用析构函数,析构函数是 delete 运算符在调用 operator delete之前执行的。它内部调用 free 完成内存回收。
还有配套的 operator new [] 和 operator delete [],专门给 new [] 和 delete [] 使用。new [] 会调用 operator new [] 申请一块连续内存,之后循环调用多次构造函数;delete [] 会先循环调用多次析构函数,再调用 operator delete [] 释放整块内存。
需要分清几个关键点:new 是运算符,不能被重载;operator new 是全局函数,可以重载。new 做的两件事,第一件调用 operator new 开辟内存,第二件调用构造函数。delete 做的两件事,第一件调用析构函数,第二件调用 operator delete 释放内存。对于内置类型,因为没有构造析构,new 就基本等价于调用 operator new。
使用的时候要注意,不要手动混淆配对,operator new 申请的内存,要用 operator delete 释。同时 operator new 只是分配原始内存,不会初始化对象,想要在这块内存上创建对象,需要使用定位 new。
/* 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); } return (p); } /* 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)new和delete的实现原理
内置类型
对于内置类型来说,new 和 delete 本质上就是封装好的内存申请和释放。
当使用 new 申请单个内置类型,比如 int* p = new int (5),new 运算符内部会调用 operator new 函数,operator new 底层调用 malloc 在堆上开辟对应字节大小的内存。内存开辟成功后,会把 5 赋值到这块内存空间。如果不带括号初始化,开辟出来的内存里面是随机值。释放的时候 delete p,delete 内部调用 operator delete,operator delete 内部调用 free 回收这块堆内存,没有析构函数调用。
当使用 new [] 申请内置类型数组,比如 int* p = new int [10]{1,2,3},new [] 会调用 operator new [],底层调用 malloc 开辟一整块连续的内存。可以选择性初始化,给出的元素会赋值进去,剩下的元素自动置 0。释放数组内存要使用 delete [] p,delete [] 调用 operator delete [],底层调用 free 释放整块内存,没有析构函数调用。
自定义类型
自定义类型使用 new 和 delete 的时候,不只是单纯开辟和释放内存,还会自动调用构造函数和析构函数。
对于单个自定义对象,执行 new A (1) 的时候分为两步。第一步调用 operator new 函数,operator new 底层封装 malloc,在堆上开辟对应大小的原始内存。第二步拿到内存地址之后,调用类的构造函数,在这块内存上完成对象初始化,给成员变量赋值。
执行 delete p 的时候同样分为两步。第一步先调用对象的析构函数,清理对象内部管理的资源,比如对象里面动态申请的内存。第二步调用 operator delete 函数,内部调用 free,把堆内存归还给操作系统。
如果使用 new [] 创建自定义类型数组,比如 new A [10]。第一步调用 operator new []开辟一整块连续的堆内存。第二步循环调用 10 次构造函数,依次初始化数组里每一个对象。释放的时候必须使用 delete [],delete [] 会先循环调用 10 次析构函数,逐个清理数组内所有对象,之后调用 operator delete [] 释放整块内存。
operator new 只负责开辟内存,不会创建对象;构造函数才负责初始化对象。同理 operator delete 只负责回收内存,析构函数负责清理资源。
定位new表达式
定位 new 也叫placement new,它是 new 的一种特殊用法,它不会在堆上申请新的内存,只是在一块已经提前分配好的内存空间上调用构造函数,创建对象。
定位 new 的语法是new (地址) 类型 (初始化参数)。使用的前提是这块内存已经提前准备好,可以是 malloc 开辟的堆内存,也可以是提前准备好的缓冲区内存。定位 new 只负责调用构造函数,不会分配内存,所以不会抛出 bad_alloc 异常。
举个例子,先用 malloc 申请一块足够存放 A 对象的内存,A* p = (A*) malloc (sizeof (A));,这时候 p 指向的只是一块原始内存,还不是有效的对象,没有执行构造函数。这时写new (p) A (1),这就是定位 new,它会在 p 指向的这块内存上调用 A 的构造函数,完成对象初始化,这块内存原本就是 malloc 开辟出来的,定位 new 不会额外再开辟内存。
使用定位 new 构造出来的对象,不能直接使用 delete 释放。因为 delete 会先调用析构函数,再去调用 operator delete 释放内存,而这块内存不是 new 申请的,直接 delete 会出错。正确的销毁步骤分为两步,第一步手动调用对象的析构函数,p->~A ();,第二步再用 free 或者 operator delete 释放这块底层的内存。
定位 new 最主要的使用场景是内存池。内存池会一次性预先开辟一大块内存,之后从这块内存里分出小块空间给对象使用,不需要每次都调用 malloc。分出的空间只是原始内存,就依靠定位 new 来调用构造函数创建对象,销毁的时候手动调用析构,再把内存归还内存池,减少频繁调用 malloc 带来的性能损耗。
需要注意几个要点,定位 new 只是调用构造函数,内存的生命周期由我们自己管理。传入的内存地址空间大小必须足够容纳对象,不然会发生越界。定位 new 不改变内存本身,只是在已有内存上构造对象。还有,定位 new 可以作用于堆内存,也可以作用于栈上预先开辟的缓冲区,但是栈内存不能用 free 释放。
举完整的简单例子,class A 有构造和析构函数,void* buf = malloc (sizeof (A));,new (buf) A (10);,这一步执行构造函数。用完之后,((A*) buf)->~A ();,然后 free (buf);。
定位 new 和普通 new 的区别,普通 new 先调用 operator new 开辟堆内存,再调用构造函数。定位 new 不分配内存,只调用构造函数。普通 delete 同时执行析构和释放内存,定位 new 构造的对象需要分开执行析构和内存释放两步操作。
// 定位new/replacement new int main() { // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行 A* p1 = (A*)malloc(sizeof(A)); new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参 p1->~A();//注意,这里不调用析构函数的话,p1是不会自己调用析构函数的,因为p1是指针,是内置类型,不是自定义类型,只有自定义类型才会自动调用构造和析构函数 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 需要包含头文件 stdlib.h 才能使用,new 和 delete 不需要头文件。malloc 申请内存时需要手动计算要开辟的字节大小,返回值是 void * 类型,使用时必须强制转换成对应类型的指针;new 后面直接写类型,编译器自动计算大小,返回对应类型的指针,不需要强制转换。内存申请失败时,malloc 会返回 NULL 空指针,需要手动判断返回值是否为空;new 默认会抛出 bad_alloc 异常,需要用异常捕获机制处理。
对于自定义类型,malloc 只会开辟一块原始堆内存,不会调用构造函数,free 只会释放内存,不会调用析构函数。new 会先开辟内存,再自动调用构造函数完成对象初始化;delete 会先调用析构函数清理对象内部资源,再释放堆内存,这是 C++ 里二者最核心的差别。对于内置类型,new/delete 和 malloc/free 功能大体相近,new 额外支持申请内存时直接初始化。
数组开辟区别:malloc 开辟数组直接计算总字节数;new [] 用来开辟对象数组,会循环调用多次构造函数,释放数组必须使用 delete []。
内存释放规区别:free 只负责归还内存,不会修改指针本身,释放后指针依旧保留原来地址,容易形成野指针;delete 同样不会自动把指针置空,用完后都建议手动将指针置为 nullptr。
还有底层关系的区别:new 底层会调用 operator new,operator new 内部封装 malloc;delete 底层调用 operator delete,内部封装 free。但是不能简单认为 new 等于 malloc 加构造函数,二者语法、异常机制、对自定义类型的处理都存在差异。同时要注意配对使用,malloc 对应 free,new 对应 delete,new [] 对应 delete [],不能交叉混用。
适用场景上,C 语言只能使用 malloc 和 free。C++ 中如果是自定义类型,优先使用 new 和 delete,自动调用构造析构;如果只需要单纯开辟一块原始内存,也可以继续使用 malloc 和 free。