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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 【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虽然输了,但仍然是正确的五个人


相关文章
|
16天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
43 4
|
1月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
41 0
【C++打怪之路Lv6】-- 内存管理
|
1月前
|
C++
C/C++内存管理(下)
C/C++内存管理(下)
50 0
|
1月前
|
存储 Linux C语言
C/C++内存管理(上)
C/C++内存管理(上)
39 0
|
2天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
15 2
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4