拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【C++——内存管理】

【C++——内存管理】

目录

  • 一、C/C++内存分布
  • 二、C++内存管理方式
    • 1.new和delete操作内置类型
    • 2.new和delete操作自定义类型
    • 3.new 内存申请失败会抛出异常
  • 三、operator new与operator delete函数
    • 1.operator new
    • 2.operator delete
  • 四、new和delete的实现原理
    • 1.内置类型
    • 2.自定义类型
  • 五、定位new表达式(placement-new)
  • 六、malloc/free和new/delete的区别

一、C/C++内存分布


一个 C/C++ 程序运行时内存主要分为 5 个区域:

  1. 栈(堆栈)
    存放非静态局部变量、函数参数、函数返回值等;栈内存向下增长
  2. 内存映射段
    高效 I/O 映射方式,主要用来加载共享动态库。用户可以通过系统接口创建共享内存,用于进程间通信
  3. 堆
    用于程序运行过程中动态内存分配,堆内存向上增长,是 new/malloc 主要操作的区域
  4. 数据段
    存储全局数据、静态数据(全局 static、局部 static 变量)
  5. 代码段
    存放可执行机器代码、只读常量

二、C++内存管理方式

C 语言动态内存管理:malloc / calloc / realloc / free。

C 语言这套接口 C++ 可以继续使用,但有些场景处理起来比较麻烦,所以 C++ 提供自己的动态内存管理方式:new和delete操作符

1.new和delete操作内置类型

#include<iostream>usingnamespacestd;intmain(){int*p1=newint;// 申请单个int空间,不初始化int*p2=newint[10];// 申请10个int的数组空间deletep1;delete[]p2;//申请对象+初始化int*p3=newint(0);//单个对象 动态申请一个int类型的空间并初始化为0//多个对象int*p4=newint[10]{0};int*p5=newint[10]{1,5,3,4,6,2};return0;}

Tips:申请释放单个元素用new / delete;申请释放连续数组空间用new[] / delete[],一定要匹配

2.new和delete操作自定义类型

对于自定义类型:new 会自动调用构造函数,delete 会自动调用析构函数;malloc/free 只会开辟 / 释放空间,不会调用构造、析构

#include<iostream>usingnamespacestd;classA{public:A(){cout<<"A()默认构造"<<endl;}A(intx){cout<<"A(int)带参构造"<<endl;}~A(){cout<<"~A()析构"<<endl;}};intmain(){A*p1=newA;// 调用默认构造函数A*p2=newA(1);// 调用单参数带参构造函数deletep1;// 先调用析构,再释放堆空间deletep2;return0;}

申请空间,对于自定义类型自动调用构造和析构函数

无默认构造函数,数组对象显式初始化的几种写法:

#include<iostream>usingnamespacestd;classA{public:A(intx,inty):_x(x),_y(y){}private:int_x;int_y;};intmain(){// 已有有名对象,数组元素调用拷贝构造Aaa1(1,1);Aaa2(2,2);Aaa3(3,3);A*p1=newA[3]{aa1,aa2,aa3};// 调用拷贝构造// 传入匿名临时对象A*p2=newA[3]{A(1,1),A(2,2),A(3,3)};// C++11:构造临时对象,再拷贝构造数组元素;编译器允许拷贝消除优化,直接在数组空间构造// C++17:强制拷贝消除,不会发生拷贝构造// 花括号列表初始化,直接调用多参数构造函数A*p3=newA[3]{{1,1},{2,2},{3,3}};delete[]p1;delete[]p2;delete[]p3;return0;}

在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会

3.new 内存申请失败会抛出异常

new申请内存失败不会返回 NULL,而是抛出bad_alloc异常,需要用try‑catch捕获

#include<iostream>#include<exception>usingnamespacestd;intmain(){try{void*p1=newchar[1024*1024*1024];cout<<p1<<endl;void*p2=newchar[1024*1024*1024];cout<<p2<<endl;}catch(constexception&e){cout<<e.what()<<endl;}return0;}

三、operator new与operator delete函数

  • new / delete:供用户使用完成动态内存申请和释放的操作符
  • operator new / operator delete:系统提供的全局函数

new 底层调用operator new申请堆空间;delete 底层调用operator delete释放堆空间。

1.operator new

底层本质调用malloc:

  1. malloc 成功,直接返回内存地址;
  2. malloc 失败,执行用户注册的内存不足处理函数;
  3. 如果没有处理函数或者处理后依旧分配失败,抛出bad_alloc异常
void*__CRTDECLoperatornew(size_t size)_THROW1(_STD bad_alloc){void*p;while((p=malloc(size))==0){if(_callnewh(size)==0){// 申请内存失败,抛出bad_alloc异常staticconststd::bad_alloc nomem;_RAISE(nomem);}}return(p);}

2.operator delete

底层本质调用free完成内存释放,内部做了调试校验、线程加锁保护

voidoperatordelete(void*pUserData){_CrtMemBlockHeader*pHead;RTCCALLBACK(_RTC_Free_hook,(pUserData,0));if(pUserData==NULL)return;_mlock(_HEAP_LOCK);/* 锁住堆,防止多线程冲突 */__TRY pHead=pHdr(pUserData);_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));_free_dbg(pUserData,pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK);/* 释放锁 */__END_TRY_FINALLYreturn;}// free本质就是 _free_dbg#definefree(p)_free_dbg(p,_NORMAL_BLOCK)

总结:
operator new内部调用malloc,申请空间成功直接返回,失败抛异常;
operator delete内部调用free完成内存回收

四、new和delete的实现原理

1.内置类型

对于内置类型,new/malloc、delete/free行为大体相似
区别:

  1. new[] / delete[]用来申请释放数组;new / delete处理单个对象
  2. new 分配失败抛异常;malloc 失败返回NULL

如果是内置类型,调用不匹配:

intmain(){int*p1=newint[10];deletep1;return0;}

内置类型下,new [] 和 delete 不匹配,程序不一定崩溃、也不一定内存泄漏,但这依然是标准定义的未定义行为,不代表代码合法
因为内置类型没有构造析构,底层最终就是 malloc 和 free

2.自定义类型

new的原理:

  1. 调用operator new向堆申请原始空间;
  2. 在这块内存上调用构造函数,完成对象初始化。

delete的原理:

  1. 调用对象的析构函数,清理对象内部资源;
  2. 调用operator delete释放堆内存。

new T[N]的原理:

  1. 调用operator new[],内部再调用operator new,申请 N 个对象的总空间;
  2. 在这块内存上循环执行N 次构造函数。

delete[]的原理:

  1. 在数组对象上循环执行N 次析构函数;
  2. 调用operator delete[],内部调用operator delete释放整块堆内存

数组 new [] 会额外保存对象个数:
当使用new[]创建自定义类型数组,编译器会多开辟一小块空间存储对象的数量,delete[]依靠这个数字知道要调用多少次析构。
如果类没有显式析构,编译器会做优化,不再额外保存计数

自定义类型不匹配:

#include<iostream>usingnamespacestd;// 类B:没有显式写析构classB{private:int_b1=1;int_b2=2;};// 类A:显式提供析构classA{public:~A(){cout<<"~A()调用"<<endl;}private:int_a1=1;int_a2=2;};intmain(){// B:new[]搭配delete,VS下不会崩溃B*p2=newB[10];deletep2;// A:new[]搭配delete,运行直接崩溃A*p3=newA[10];deletep3;return0;}

当开多个对象的数组时,会在数组头上多开4个字节存储对象个数
严格来说这里A和B都应该在头上开4个字节,但是这里B没有开,因为编译器看到B没有写析构函数,所以这里自动优化了
这里多开的四个字节是给[]用的,用于了解有几个对象要调用几次析构函数
这里B没有崩,A崩了,就是因为A是从四个字节之后开始析构的

Tips:一定要保证new <--> delete,new[] <--> delete[]严格匹配

五、定位new表达式(placement-new)

定位new表达式:在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)

place_address:必须是一个指针,表明要对哪个指针指向的空间调用构造
initializer-list:是类型的初始化列表

使用场景:
多用于内存池。内存池只分配原始裸内存,不会自动调用构造;拿到裸内存后,使用定位 new 显式调用构造完成自定义对象初始化

注意:构造函数不能直接手动调用;析构函数可以显式手动调用

#include<iostream>usingnamespacestd;classA{public:A(inta=0):_a(a){cout<<"A():"<<this<<endl;}~A(){cout<<"~A():"<<this<<endl;}private:int_a;};intmain(){//构造A*p1=newA(1);//1.A*p2=(A*)operatornew(sizeof(A);//2.//区别是1.初始化了,2.没有初始化,只开了空间new(p2)A(1);//用定位new对已开好的空间显式初始化//析构deletep1;p2->~A();//析构函数可以直接显式调用,构造函数不行operatordelete(p2);return0;]

六、malloc/free和new/delete的区别

共同点:
都是在堆上申请内存,都需要手动释放内存

不同点:

  1. malloc / free是库函数;new / delete是 C++操作符。
  2. malloc申请出来的内存不会初始化;new支持申请同时完成对象初始化。
  3. malloc需要手动传入字节大小;new直接写类型,编译器自动计算大小,数组只需填写对象个数。
  4. malloc返回void*,使用必须强制类型转换;new返回对应类型指针,不需要强转。
  5. malloc分配失败返回NULL,需要手动判空;new分配失败抛出bad_alloc异常,需要try‑catch捕获。
  6. 自定义类型:malloc/free只负责开辟释放空间,不会调用构造、析构函数;new申请内存后调用构造函数,delete释放内存前调用析构函数清理资源
返回列表