【C++】内存管理 —— new和delete底层实现原理

简介: 【C++】内存管理 —— new和delete底层实现原理

一、C/C++内存分布


C和C++内存分布如下:


0a2653c851af460fa595bd959398a8f1.png


【说明】


栈又叫堆栈,函数调用会创建栈桢,储存非静态局部变量/函数参数/返回值等,栈是向下增长的。

内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux详细谈)

堆用于程序运行时动态内存分配,堆是可以向上增长的

数据段–存储全局数据和静态数据

代码段–可执行的代码/只读常量

举例:


2d65d23f6d4748949b924e4057485923.png


我们先来看下面的一段代码和相关问题


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在哪里?__C__ static GlobalVar在哪里?__C__

static Var在哪里?__C__ localVar在哪里?__A__

num1 在哪里?__A__

char2在哪里?__A__ *char2在哪里?__ A(易错)__

pChar3在哪里?__A__ *pChar3在哪里?__D__

ptr1在哪里?__A__ *ptr1在哪里?__B__

 

2. 填空题:

sizeof(num1) = __40byte__;

sizeof(char2) = __5byte__; strlen(char2) = __4__;

sizeof(pChar3) = __4byte__; strlen(pChar3) = __4__;

sizeof(ptr1) = __4byte__;


🌈解析——


0a2653c851af460fa595bd959398a8f1.png


ps:只有堆上的空间是我们管的,其他的都不属于我们管


二、C/C++动态内存管理方式对比


C语言中动态内存管理方式:malloc/calloc/realloc/free


void Test ()
{
  int* p1 = (int*) malloc(sizeof(int));
  free(p1);
  // 1.malloc/calloc/realloc的区别是什么?
  int* p2 = (int*)calloc(4, sizeof (int));
  int* p3 = (int*)realloc(p2, sizeof(int)*10);
  // 这里需要free(p2)吗?
  free(p3 );
}


【面试题】


malloc/calloc/realloc的区别?(这篇博客还没写,这就去补!)


malloc的实现原理? glibc中malloc实现原理


C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理


🌈内置类型


💚对于内置类型,malloc/free 和 new/delete没有本质区别,只有用法上的区别

C++更简洁


void Test()
{
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  //申请5个int的数组
  int* p3 = new int[5];
  //申请一个int对象,初始化为5
  int* p4 = new int(5);
  //C++11支持new[] 用{}初始化   C++98不支持
  int* p5 = new int[5]{ 1,2,3 };
  free(p1);
  delete p2;
  delete[] p3;
  delete p4;
  delete[] p5;
}
0a2653c851af460fa595bd959398a8f1.png


注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用


🌈自定义类型


❤️ 在动态申请自定义类型的空间时,new会调用构造函数初始化对象,delete会先调用析构函数清理,而malloc和free不会。


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对于【自定义类型】除了开空间还会调用构造、析构初始化和清理
  A* p1 = (A*)malloc(sizeof(A));
  A* p2 = new A(10);
  free(p1);
  delete p2;
  A* p5 = (A*)malloc(sizeof(A) * 10);
  A* p6 = new A[10];
  free(p5);
  delete[] p6;
  return 0;
}


调试观察到,确实是调用了构造和析构


0a2653c851af460fa595bd959398a8f1.png


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


还要一个重大的区别:


开辟失败,返回值的问题


malloc失败返回NULL

new失败不需要检查返回值,直接崩溃报错



🌍小总结:


对于内置类型的空间申请和释放,仅仅是用法上的差别

但是对于自定义类型,new动态申请的对象,申请空间 + 调用构造函数初始化;delete释放对象时**,**调用析构函数清理对象中资源 +释放空间

new申请失败,直接崩溃报错,malloc则是返回NULL

🎨从此以后,在C++中建议使用 new + delete.


三、new和delete的底层实现


🎨operator new 和 operator delete函数(重点)


new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间


转到反汇编可以看到,new实际上调用了两个函数,operator new和构造函数


0a2653c851af460fa595bd959398a8f1.png


new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间


2d65d23f6d4748949b924e4057485923.png


💦new:其中operator new就是对malloc的封装,使其能在返回0时抛出异常,这才符合C++的机制


/*
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);
}


此处异常,后面会详细谈


💦delete:operator delete可以看做是为了与operator new的对应,最终调用了free,并增加一些检查机制


/*
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和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL


自定义类型


➰new的原理


调用operator new函数申请空间

在申请的空间上执行构造函数,完成对象的构造


➰delete的原理


调用operator delete函数释放对象的空间

在空间上执行析构函数,完成对象中资源的清理工作


➰new T[N]的原理


调用operator new[]函数,在operator new[ ]中实际调用operator new函数完成N个对象空间的申请(调用套娃)

在申请的空间上执行N次构造函数


➰delete[]的原理


在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释

放空间


🎨operator new 和 operator delete的类专属重载(了解)

以带头双向循环链表为例,插入删除节点时,不断地向堆申请/释放内存效率太低。我们可以向内存池申请内存,从此向内存池要空间。(关于STL后序详谈)


于是我们可以在ListNode这个类中,重载专属的operator new函数,这样就不会再去调用全局的了 ———— 内存池


//重载一个类专属的operator new
struct ListNode
{
  int _val;
  ListNode* _next;
  //内存池
  static allocator<ListNode> alloc;
  void* operator new(size_t n)
  {
  cout << "void* operator new(size_t n)" << endl;
  //调用库的内存池 allocator
  void* obj = alloc.allocate(1);
  return obj;
  }
  void operator delete(void* ptr)
  {
  alloc.deallocate((ListNode*)ptr, 1);
  }
  struct ListNode(int val)
  :_val(val)
  ,_next(nullptr)
  {}
};
//指定类域
allocator<ListNode> ListNode::alloc;
int main()
{
  //频繁的申请LIstNode
  ListNode* node1 = new ListNode(1);
  ListNode* node2 = new ListNode(2);
  ListNode* node3 = new ListNode(3);
  delete node1;
  delete node2;
  delete node3;
  return 0;
}//重载一个类专属的operator new
struct ListNode
{
  int _val;
  ListNode* _next;
  //内存池
  static allocator<ListNode> alloc;
  void* operator new(size_t n)
  {
  cout << "void* operator new(size_t n)" << endl;
  //调用库的内存池 allocator
  void* obj = alloc.allocate(1);
  return obj;
  }
  void operator delete(void* ptr)
  {
  alloc.deallocate((ListNode*)ptr, 1);
  }
  struct ListNode(int val)
  :_val(val)
  ,_next(nullptr)
  {}
};
//指定类域
allocator<ListNode> ListNode::alloc;
int main()
{
  //频繁的申请LIstNode
  ListNode* node1 = new ListNode(1);
  ListNode* node2 = new ListNode(2);
  ListNode* node3 = new ListNode(3);
  delete node1;
  delete node2;
  delete node3;
  return 0;
}


若匹配的上则没有内存泄漏,反之则有


0a2653c851af460fa595bd959398a8f1.png


大家可能会有些懵,画图解释一下:


2d65d23f6d4748949b924e4057485923.png


四、定位new表达式(了解)


💫对已有空间调用构造函数初始化一个对象


定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化


💛 使用格式


new(place_adress) type 或者
new(place_adress) type(initializer-list)
 place_adress: 必须是一个指针
 initializer-list: 是类型的初始化列表
#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()
{
  A* p1 = (A*)malloc(sizeof(A));
  new(p1)A(10);
  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在释放空间前会调用析构函数完成空间中资源的清理

🍅 一般从语法使用、本质功能两点区别出发


🌍内存泄漏


动态申请的内存,不使用了,又不主动释放,就存在内存泄漏


🌍内存泄漏的危害


出现内存泄漏的进程正常结束,进程结束时这些内存会还给系统,不会有什么大伤害事前预防型:如智能指针等。(后续详谈)

出现内存泄漏的进程非正常结束,比如僵尸进程(Linux详谈)。危害很大,系统会越来越慢,甚至卡死宕机

需要长期运行的程序出现内存泄漏。危害很大,系统会越来越慢,甚至卡死宕机


🌍如何避免内存泄漏


养成良好的编码规范,申请的内存空间记着匹配的去释放

事前预防型:如智能指针等

事后查错型:如泄漏检测工具

内存泄漏问题要到了智能指针这一块,才能更好的讲明白,所以后面我会回来的


六. ✅课后小练习


易错题:

(1)下面有关c++内存分配堆栈说法错误的是( D )


A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制


B. 对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长


C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题


D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的


解释:


A.栈区主要存在局部变量和函数参数,其空间的管理由编译器自动完成,无需手动控制,堆区是自己申请的空间,在不需 要时需要手动释放

B.栈区先定义的变量放到栈底,地址高,后定义的变量放到栈顶,地址低,因此是向下生长的,堆区则相反

C.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

D.32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用,所以D错误


(2)C++中关于堆和栈的说法,哪个是错误的:( C )

A.堆的大小仅受操作系统的限制,栈的大小一般较小


B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题


C.堆和栈都可以静态分配


D.堆和栈都可以动态分配


答案解析


A.堆大小受限于操作系统,而栈空间一般有系统直接分配


B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题


C.堆无法静态分配,只能动态分配

静态分配:比如int a,也就是a的空间开辟和回收都不需要我们干预


D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放


(3)c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( D )


ClassA *pclassa=new ClassA[5];


delete pclassa;


A.5,1

B.1,1

C.5,5

D.程序可能崩溃


答案解析


A.申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃


B.构造函数会调用5次


C.析构函数此时只会调用1次,要想完整释放数组空间,需要使用[]


D.正确


(4) 设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为? ( B )


C c;

void main()
{
  A*pa=new A();
  B b;
  static D d;
  delete pa;
}


A.A B C D

B.A B D C

C.A C D B

D.A C B D


答案解析


分析:首先手动释放pa, 所以会先调用A的析构函数,其次C B D的构造顺序为 C D B,因为先构造全局对象,在构造局部静态对象,最后才构造普通对象,然而析构对象的顺序是完全按照构造的相反顺序进行的,所以答案为 B


**(5)*使用 char p = new char[100]申请一段内存,然后使用delete p释放,有什么问题?( B )


A.会有内存泄露

B.不会有内存泄露,但不建议用

C.编译就会报错,必须使用delete [ ] p

D.编译没问题,运行会直接崩溃


答案解析


A.对于内置类型,内置类型没有析构函数,所以使用delete与delete []相同,两者都会释放申请的内存空间,此时delete就相当于free,不会调用析构函数,因此不会造成内存泄漏,如果是自定义类型,必须要用delete []来释放,因为要delete []时会逐一调用对象数组的析构函数,然后释放空间,否则有内存泄漏


B.正确


C.编译不会报错,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误


D.对于内置类型,程序不会崩溃,但不建议这样使用


📢写在最后

tes虽然输了,但仍然是正确的五个人


相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
4月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
175 26
|
5月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
96 1
|
8月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
4月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
1415 0
|
4月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
414 1
|
4月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
396 0
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
888 0
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。

热门文章

最新文章