【C++要笑着学】C++动态内存管理 | new/delete底层探索 | new/delete实现原理 | 定位new(二)

简介: 是这样的,C语言里的 "动态内存管理" 放到 C++ 里面,用起来不是那么爽,所以C++就对这一块进行了升级,本章我们就探索探索 C++的内存管理,顺便复习一下C语言里讲过的动态内存管理的知识。学完本章,单身的同学不用怕了,以后没有对象我们可以 new 一个

Ⅲ.  new 和 delete 的底层探索


0x00 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 来申请空间的。


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


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


如果用户提供该措施就继续申请,否则就抛异常。


面向过程的语言处理错误的方式:


返回值 + 错误码解决(这个我们之前学过)。


#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char* p1 = (char*)malloc(1024u * 1024u * 1024u *2u);
    if (p1 == nullptr) {
        printf("%d\n", errno);
        perror("malloc fail");
        exit(-1);
    } else {
        printf("%p\n", p1);
    }
    return 0;
}

cd0775eaf10359e958efc5d36de4e78f_77c7e780ed15471792b35cc6bffc7fbe.png


而面向对象语言处理错误的方式:


一般是抛异常,C++中也要求出错抛异常 —— try catch(后期会细说)。


#include <iostream>
using namespace std;
int main(void)
{
    char* p2 = nullptr;
    try {
        char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
    } catch (const exception& e) {
        cout << e.what() << endl;
    }
    printf("%p\n", p2);
    return 0;
}

0d0d49ceae723c97a56f451dd6275d5c_edef6305320c4f0c9a91ae14967260db.png

🔺 C++ 提出 new 和 delete,主要是解决两个问题:


① 自定义类型对象自动申请的时候,初始化合清理的问题。new / delete 会调用构造函数和析构函数。


② new 失败了以后要求抛异常,这样才符合面向语言的出错处理机制。


(delete 和 free 一般不会失败,如果失败了,都是释放空间上存在越界或者释放指针位置不对)


0x02  operator new 与 operator delete 的类专属重载

下面代码演示了,针对链表的节点 ListNode 通过重载类专属 operator new / operator delete,


实现链表节点使用内存池申请和释放内存,提高效率。


💬 我们先看看按照C的方式写:


struct ListNode {
  ListNode* _next;
  ListNode* _prev;
  int _val;
};
int main(void)
{
  struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
  if (n1 == NULL) {
  printf("malloc failed!\n");
  exit(-1);
  }
  n1->_next = NULL;
  n1->_prev = NULL;
  n1->_val = 0;
  return 0;
}


我们创建节点还需要用 malloc 申请空间,还需要强制类型转换,之后还要自己写上初始化,


因为 malloc 失败返回 NULL,会存在野指针隐患,所以出于安全还要检查一下。


💬 我们再来看看 C++ 的方式:


struct ListNode {
  ListNode* _next;
  ListNode* _prev;
  int _val;
  /* 构造函数*/
  ListNode(int val)
  : _next(nullptr)
  , _prev(nullptr)
  , _val(val) {}
};
int main(void)
{
  ListNode* n2 = new ListNode(0);
  return 0;
}

而在C++里,因为 new 会自动调用构造函数去完成初始化,就很舒服。


而且还不需要去检查是否开辟失败,因为 new 失败不会返回空,而是抛异常。


💬 我们再举个能用得上析构函数的例子 —— Stack:


#include <iostream>
using namespace std;
class Stack {
public:
  Stack(int capacity = 4)
  : _top(0)
  , _capacity(capacity) {
  _arr = new int[capacity];
  }
  ~Stack() {
  delete[] _arr;
  _arr = nullptr;
  _capacity = _top = 0;
  }
  // ...
private:
  int* _arr;
  int  _top;
  int  _capacity;
};
int main(void)
{
  Stack st;  // 完事了
  Stack* pst2 = new Stack;  // 开空间 + 构造函数初始化
  delete pst2;  // 析构函数(清理对象中资源)+ 释放空间
  return 0;
}

Ⅳ.  new 和 delete 的实现原理


0x00  对于内置类型

如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本相似。


不同的地方是,new / delete 申请和释放的是单个元素的空间,


new[] 和 delete[] 申请的是连续空间。而且 new 再申请空间失败时会抛异常。


A* p3 = new A;      // 开辟单个空间
A* p4 = new A[5];   // 开辟的是连续地5个空间

operator new 和 operator delete 就是对 malloc 和 free 的封装。


operator new 中调用 malloc 后申请内存,失败以后,改为抛异常处理错误,


这样符合C++面向对象语言处理错误的方式。


new Stack
call malloc + call Stack 构造函数   ❌ 如果失败返回0,这不符合C++处理错误的方式
new Stack
call operator new + call Stack 构造函数  ✅  失败抛异常,这就非常滴合适


0x01 对于自定义类型

new 的原理:


① 调用 operator new 函数申请空间。


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


delete 的原理:


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


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


new T[N] 的原理:


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


② 在申请的空间上调用 N 次构造函数,对它们分别初始化。


Stack* p1 = new[10];
Stack* pst1 = (Stack*)operator new[](sizeof(Stack) * 10);

2e3f8555dc289b65052030b0da83ae24_74b4644ec3b248e6a0f1e73b81bde51e.png


delete[] 的原理:


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


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


Ⅴ.  定位new


0x00 引入 - 我想手动初始化

如果不用 new,我想手动调用构造函数初始化,


假设我们这有一块空间,是从内存池取来的,或者是 malloc 出来的、operator new 出来的……


我就不想用 new,但是我想对他进行初始化,行不行?


A* p = (A*)malloc(sizeof(A));  // 我能不能调用构造函数初始化?

当然可以!定位new表达式帮你!


0x01 定位new表达式

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


简单来说就是,定位new表达式可以在已有的空间进行初始化。


📚 写法:


new(目标地址指针)类型                         // 不带参
new(目标地址指针)类型(该类型的初始化列表)       // 带参

📌 注意:目标地址必须是一个指针


0x02 定位new的使用场景

定位 new 是很有用的!


比如开的空间是从内存池来的,如果想初始化,我们就可以使用它。


因为内存池分配出的内存初始化,所以如果是自定义类型的对象,


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


0x03 定位new用法演示

不带参定位new:


class A {
public:
  A(int a = 0)
  : _a(a) {
  cout << "A(): " << this << endl;
  }
  ~A() {
  cout << "~A(): " << this << endl;
  }
private:
  int _a;
};
int main(void)
{
  A* p = (A*)malloc(sizeof(A));
  new(p)A;   // 定位new
  return 0;
}

3eafc3526e7b607b412705a238a9e156_952978a881c3491fb20a306edfcd2b31.png



带参定位new:


class A {
public:
  A(int a)
  : _a(a) {
  cout << "A(): " << this << endl;
  }
  ~A() {
  cout << "~A(): " << this << endl;
  }
private:
  int _a;
};
int main(void)
{
  A* p1 = (A*)malloc(sizeof(A));
  new(p1)A(10);
  return 0;
}

5b7278d16328cd5e573dd7dd75a50c6c_1da88d604bcd493cadaa6da9959fb0dc.png

💬 模拟一下 new 的行为:


int main(void)
{
  A* p1 = (A*)malloc(sizeof(A));
  new(p1)A(10);
    // 模拟一下new的行为
  A* p2 = new A(2); 
  // 等价于:
  A* p3 = (A*)operator new(sizeof(A));
  new(p3)A(3);
  return 0;
}

92640b82077d76a620b6961fc2e816cb_4823a3116eff4907b2674a62a4acad17.png


 没事这么写,其实就是脱裤子放屁,


但是有时候,内存不一定是从堆来的,比如从内存池来的,定位 new 就可以大显神功。


高并发内存池,实现定长内存池的时候就需要使用 定位 new。


析构函数释放


析构函数是可以显式调用的(构造函数不行)


p->~A;
int main(void)
{
  A* p1 = (A*)malloc(sizeof(A));
  new(p1)A(1);
  A* p2 = new A(2);
  delete p2;
  // 等价于:
  A* p3 = (A*)operator new(sizeof(A));
  new(p3)A(3);
  p3->~A;
  operator delete(p3);
  return 0;
}


相关文章
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
26天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
47 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
程序员 C语言 C++
C++入门5——C/C++动态内存管理(new与delete)
C++入门5——C/C++动态内存管理(new与delete)
57 1
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
331 0
|
14天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
27 1
|
18天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
22天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。