【C++初阶:内存管理】C/C++内存分布及管理方式 | new/delete实现原理及operator new和operator delete函数 下

简介: 【C++初阶:内存管理】C/C++内存分布及管理方式 | new/delete实现原理及operator new和operator delete函数

四、operator new与operator delete函数 —— 重点

💦 operator new与operator delete函数

new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 严格来说不是 new 和 delete 的重载 (名字确实容易误导),而是系统提供的全局库函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。

new A:

  1. 申请内存 —— 调用 operator new
  2. 构造函数

delete A:

  1. 析构函数
  2. 释放内存 —— 调用 operator delete

new 的反汇编 ❗

❓ 为什么要去调用 operator new 而不是调用其它的呢 ❔

int main()
{
  //malloc失败返回空
  char* p1 = (char*)malloc(0xffffffff);
  if (p1 == NULL)
  {
    printf("malloc fail\n");
  }
  else
  {
    printf("malloc success:%p\n",p1);
  }
  //new失败抛异常
  char* p2 = new char[0x7fffffff];
  if (p1 == NULL)
  {
    printf("malloc fail\n");
  }
  else
  {
    printf("malloc success:%p\n", p1);
  } 
  return 0;
}

📝说明

因为 new 和 malloc 它们失败时,处理的方式不一样

malloc 失败了,这里的检查起作用了

new 失败了,这里的检查没起作用,还引发了一个崩溃 —— 抛异常后没有解决

抛异常 ❓

int main()
{
  try
  {
    char* p2 = new char[0x7fffffff];//出错,抛异常,它会跳到捕获异常的位置
  }
  catch(const exception& e)
  {
    cout << e.what() << endl; 
  }
  return 0;
}

📝说明

这里就可以看到 new 和 malloc 处理的方式不一样,毕竟一个是面向过程,一个是面向对象

关于什么是异常后面我们会具体学习

所以再看 new 的底层的实现,new 的底层申请内存时是不能让 malloc 去完成的,因为 malloc 失败就直接返回空了,就无法达到让它失败后抛异常的机制,所以其中就产生了 operator new

对于 delete 就不存在失败了抛异常,我们说 malloc 会失败,但没有说 free 会失败 (free 的失败是对越界的空间 free 等),free 失败就不是说抛异常或返回空这样的概念了,这种是属于比较严重的错误,它是直接中止掉程序

operator new 和 operator delete 源码 ❗

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

📝说明

通过上述两个全局函数的实现知道,operator new 实际也是通过 malloc 来申请空间,如果 malloc 申请空间

成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异

常。operator delete 最终是通过 free 来释放空间的。

operator new 就是对 malloc 的封装,目的就是如果申请内存失败了抛异常:

  • new = 封装 malloc + 失败抛异常 + 调用构造函数

operator delete 就是对 free 的封装,目的主要还是和 operator new 对应起来

  • delete = 调用析构函数 + operator delete

这里具体后面也会学习

直接使用 operator new 和 operator delete ❓

class A
{
public:
  A(int a = 0)
    : _a(a)
  {
    cout << "A()" << endl;
  }
  ~A()
  {
    cout << "~A()" << endl;
  }
private:
  int _a;
};
int main()
{
  //调用构造和析构
  A* p1 = new A;
  delete p1;
  //不会调用构造和析构
  A*p2 = (A*)operator new(sizeof(A));
  operator delete(p2);
  return 0;
}

📝说明

我们直接使用 operator new 和 operator delete 本质上和 malloc 和 free 没有区别

所以平时我们也几乎不会用 operator new 和 operator delete

💦 operator new与operator delete的类专属重载

检测程序有没有 ListNode 的节点有没有释放,如果存在没有释放的节点,请说明是哪里没有释放 ❓

🔑移除链表元素

//重载一个类ListNode,专属的operator new: 
struct ListNode
{
  int val;
  struct ListNode* next;
  static int _count;//统计
  ListNode(int x)
    : val(x)
    , next(nullptr)
  {}
  //new _count就++,delete _count就--
  void* operator new(size_t n)
  {
    ++_count;
    return ::operator new(n);//::代表是全局的
  }
  void operator delete(void* p)
  {
    --_count;
    return ::operator delete(p);//::代表是全局的  
  }
};
int ListNode::_count = 0;
struct ListNode* removeElements(struct ListNode* head, int val)
{
  struct ListNode* prev = NULL, *cur = head;
  while(cur)
  {
    if(cur->val == val)
    {
      prev->next = cur->next;
      delete cur;
      cur = prev->next;
    } 
    else
    {
      prev = cur;
      cur = prev->next; 
    }
  }
  return head;
}
int main()
{
  ListNode* n1 = new ListNode(1);
  ListNode* n2 = new ListNode(2);
  ListNode* n3 = new ListNode(2);
  ListNode* n4 = new ListNode(3);
  ListNode* n5 = new ListNode(4);
  ListNode* n6 = new ListNode(2);
  n1->next = n2;
  n2->next = n3;
  n3->next = n4;
  n4->next = n5;
  n5->next = n6;
  n6->next = nullptr;
  ListNode* list = removeElements(n1, 2);
  cout << "没有释放的节点数量:" << ListNode::_count << endl;
  return 0;
}

📝说明

当 new ListNode 时,那么申请空间就会调用专属的 operator new 和 operator delete

了解一下即可,这个语法实际中价值不大

这里想看具体哪里没有释放那就比较复杂了,后面学了 map 后就可以行号给记录下来

五、new和delete的实现原理

💦 内置类型

如果申请的是内置类型的空间,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 来释放空间

场景使用 ❓

class Stack
{
public:
  Stack(int capacity = 4)
    : _a(new int[capacity])
    , _size(0)
    , _capacity(capacity)
  {
    cout << "Stack(int capacity = 4)" << endl;
  }
  ~Stack()
  {
    delete[] _a;
    _size = _capacity = 0;  
    cout << "~Stack()" << endl;  
  }
private:
  int* _a;
  int _size;
  int _capacity;
};
int main()
{
  //1
  Stack st;
  //2
  Stack* ps = new Stack;
  delete ps;
  return 0;
}

📝说明:

六、定位new表达式(placement-new) —— 了解

定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

  • new(place_address) type 或者 new(place_address) type(initializer-list)
  • place_address 必须是一个指针,initializer-list 是类型的初始化列表

如果想对 malloc 开辟的已有的一块空间去调用构造函数 ❓

struct ListNode
{
  int val;
  struct ListNode* next;
  ListNode(int x)
    : val(x)
    , next(nullptr)
  {
    cout << "ListNode(int x)" << endl;
  }
  ~ListNode()
  {
    cout << "~ListNode()" << endl;  
  }
};
int main()
{
  //实例化一个对象构造函数、析构函数自动调用
  ListNode node(1);
  //new调用构造函数、delete调用析构函数
  ListNode* p = new ListNode(2);
  delete p;
  //显示调用构造函数、析构函数
  ListNode* n1 = (ListNode*)malloc(sizeof(ListNode));
  new(n1)ListNode(3);
  n1->~ListNode();
  free(n1);
  return 0;
}

📝说明

使用场景:

定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义

类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。

这里举一个好理解的 —— 复制一份 a数组到另一块空间 pb ❗

class A
{
public:
  A(int a = 0)
    : _a(a)
  {
    cout << "A()" << this << endl;  
  }
  A(const A& a)
  {
    cout << "A(const A& a)" << this << endl;  
  }
  A& operator=(const A& a)
  {
    cout << "A& operator=(const A& a)" << this << endl;
    return *this;
  }
  ~A()
  {
    cout << "~A()" << this << endl;
  }
private:
  int _a;
};
int main()
{
  //构造+赋值
  A a[5];
  A* pb = new A[5];
  for(int i = 0; i < 5; i++)
  {
    pb[i] = a[i];
  }
  delete []pb;
  //拷贝构造
  A* pd = (A*)malloc(sizeof(A) * 5);
  for(int i = 0; i < 5; i++)
  {
    new(pd + i)A(a[i]); 
  }
  for(int i = 0; i < 5; i++)
  {
    (pd+i)->~A(); 
  }
  return 0;
}

📝说明

由此可见,使用定位 new 表达式的效率更高。

七、面试题

💦 malloc/free和new/delete的区别

malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

这里主要从特点和用法、底层原理 (本质区别)、处理错误的方式三个方面进行说明

  1. malloc 和 free是函数,new 和 delete 是操作符
  2. malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可
  3. malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
  4. malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常
  5. 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理

💦 内存泄漏

1、什么是内存泄漏,以及内存泄漏的危害

什么是内存泄漏:

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

具体的说,从我们的几个区域角度看,除了堆之外其它区域的内存是不需要我们管的 —— 栈、数据段、代码段;所以更具体的说内存泄漏就是在堆上申请了空间,我们不用这块空间了,且它没有释放,就存在内存泄漏,因为你不用了,也没有还给系统,别人也用不了。通俗点说:内存泄漏的本质就是站着茅坑不拉屎。

内存泄漏的危害:

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

int main()
{
  char* p = new char[1024 * 1024 * 1024];//1个G
  return 0;
}

📝说明

当我们调试起来就可以打开任务管理器看到我们的程序:

上面的程序存在内存泄漏,一次泄漏 1G,但是多次泄漏,对我们的系统也没有什么影响 ❓

一个程序正常结束后,会把映射的内存都会释放掉, 所以上面的程序,我们虽然没有主动释放,但是进程结束也会释放掉,对于进程和线程相关的知识,会在 Linux 内系统的学习。

那内存泄漏好像也没啥事,因为进程正常结束都会释放,其实不然,如下场景:

  1. 少数情况:进程没有正常结束 ——僵尸进程,就可能存在资源没释放
  2. 多数情况:长期运行的服务器程序,比如王者荣耀的后台服务 (只有升级时才会停,且都是半夜),服务器每次一运行就是两三个月,如果每天内存泄漏一点,可能上线才一个月,服务器就越来越慢了
  3. 其它情况:物联网设备,如扫地机器人、冰箱等,它们的内存很小,它们就更经不起内存泄漏的折腾了,所以它们在设计的时候是绝不允许内存泄漏的

正常停机升级这是正常的流程,意外挂掉在公司叫事故,比如你去了一家公司,你把公司的服务器搞崩了,那么可能你们组的年终奖都无了,甚至严重的还会被开了。

对 C++ 而言,我们需要主动释放内存;对 JAVA 而言,我们不需要主动释放内存,因为 JAVA 后台有垃圾回收器,接管了内存释放,当然接管也会付出一些代价,而 C++ 是一个极度关注性能的语言。

2、内存泄漏的分类(了解)

C/C++ 程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏 (Heap leak)
    堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。
  • 系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3、如何检测内存泄漏(了解)

如何检测内存泄漏:

4、如何避免内存泄漏

如何避免内存泄漏:

  1. 智能指针 (将会在 C++ 进阶进行学习)
  2. 内存泄漏检测工具 (如上)

内存泄漏非常常见,解决方案分为两种:

  1. 事前预防型。如智能指针等
  2. 事后查错型。如泄漏检测工具

💦 如何一次在堆上申请4G的内存

32 位程序在虚拟进程地址空间只有 4G,还有 1G 是供内核使用的,况且还有其它的,怎么能申请 4G;但如果是 64 位程序,可以认为这个虚拟进程空间我们是用不完的,因为它是 264 个字节。

因为我的机器是 64 位的,当然可以支持 32 位、64 位,所以我们这里切换成 64 位即可:

int main()
{
  try
  {
    //char* p = new char[0x7fffffff];//2G
    char* p = new char[0xffffffff];//4G
    printf("%p\n", p);
  }
  catch (const exception& e)
  {
    cout << "内存申请失败" << endl;
  }
  return 0;
}
相关文章
|
1月前
|
存储 缓存 C语言
【c++】动态内存管理
本文介绍了C++中动态内存管理的新方式——`new`和`delete`操作符,详细探讨了它们的使用方法及与C语言中`malloc`/`free`的区别。文章首先回顾了C语言中的动态内存管理,接着通过代码实例展示了`new`和`delete`的基本用法,包括对内置类型和自定义类型的动态内存分配与释放。此外,文章还深入解析了`operator new`和`operator delete`的底层实现,以及定位new表达式的应用,最后总结了`malloc`/`free`与`new`/`delete`的主要差异。
51 3
|
2月前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
47 0
【C++打怪之路Lv6】-- 内存管理
|
2月前
|
C++
C/C++内存管理(下)
C/C++内存管理(下)
52 0
|
2月前
|
存储 Linux C语言
C/C++内存管理(上)
C/C++内存管理(上)
42 0
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4