动态内存管理:new和delete的底层探索

时间:2024-03-01 20:29:18

之前我们在C语言上是学过malloc和calloc还要realloc等函数来在堆上获取相应的内存,但是这些函数是存在缺陷的,今天引入对new和delete的学习,来了解new和delete的底层实现。

首先就是在C++中我们为什么要对内存进行区域的分块? 

答案是为了对内存进行更好的管理

那这些区域中对我们程序员来说最重要的区域就是堆,因为堆上的空间需要我们自行的进行申请和释放。

C语言内存题目

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 在哪里? ____   staticGlobalVar 在哪里? ____
  staticVar 在哪里? ____   localVar 在哪里? ____
  num1 在哪里? ____
  char2 在哪里? ____   * char2 在哪里? ___
  pChar3 在哪里? ____       * pChar3 在哪里? ____
  ptr1 在哪里? ____         * ptr1 在哪里? ____
2. 填空题:
  sizeof ( num1 ) = ____ ;  
  sizeof ( char2 ) = ____ ;       strlen ( char2 ) = ____ ;
  sizeof ( pChar3 ) = ____ ;     strlen ( pChar3 ) = ____ ;
  sizeof ( ptr1 ) = ____ ;

栈主要存放的是一些局部变量,函数参数,栈的最大特点就是向下增长,而堆是向上增长的,函数执行之后栈的的空间会自动的进行释放,函数栈帧的销毁其实就是将内存空间返回给我们的操作系统这个过程。栈还有一个特点就是它的效率高且容量空间有限

堆是给程序员进行管理的一块内存,堆是向上进行增长的,程序员进行使用之后需要进行释放,如果不进行释放就会存在很大的问题,内存泄漏是最主要的一个问题。分配的方式可能不是连续的,类似链表这样随机化。

代码段

存放常量和一些可读的代码

静态区
存放的是一些全局常量和静态数据 

那有了上面的基础我们就来完成上面的题目。

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___   staticGlobalVar在哪里?__C__
  staticVar在哪里?__C__   localVar在哪里?_A___
  num1 在哪里?__A__

  char2在哪里?_A___   *char2在哪里?_A__
  pChar3在哪里?____ A     *pChar3在哪里?___D_
  ptr1在哪里?____   A     *ptr1在哪里?_B___
2. 填空题:
  sizeof(num1) = 40____;  
  sizeof(char2) = _5___;      strlen(char2) = ___4_;
  sizeof(pChar3) = __4_/ 8_;     strlen(pChar3) = __4__;
  sizeof(ptr1) = _4_/8__;

sizeof(指针)是指针大小就是4字节或者8字节,然后就要注意的是字符串后面还是有一个\0这个需要注意一下。

C++内存管理方式

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++ 又提出了自己的内存管理方式: 通过 new delete 操作符进行动态内存管理
我们先来看看new和delete是怎么使用的。
#include<iostream>
int main()
{
	int* p1 = new int;
	int* p2 = new int[10];
    delete p1;
    delete[] p2;
	return 0;
}

优势一

竟然学了new和delete,抛弃原来的malloc和realloc还有delete,那总有我们的优势,但是我们现在可以看到的优势好像就是除了简短,没有其他的优势了,它也不会进行初始化,没有将我们的内置类型进行初始化,但是这里我们只要知道它的优势就是代码变的更加简短了。这个是用法上的

 优势二

我们可以进行手动的初始化

 看下面一段代码我们可以看到它的优势更加明显了

场景:

如果我们要实现一个链表的话,C语言是要写一个创造节点的函数,然后再来进行实现的,我们先来看看创造节点的函数是怎么写的,然后再来看看C++中如果要进行创造一个链表的话是怎么实现才是最快的。

struct ListNode
{
	ListNode* _next;
	int _val;
};

ListNode* CreateNode(int x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);

	}
	newnode->_next = NULL;
	newnode->_val = x;
	return newnode;
}

我们可以看到的是需要我们自行的创造节点,然后进行链接,可以写一个for循环,但是每次都要去调用这个函数,很不方便,这个时候就要引出new的第三个优势。

优势三

对于自定义类型的时候new的过程是先开一段空间,然后调用它的构造函数进行初始化。我们来快速的实现一个链表的连接,看看C++里是怎么写的。

#include<iostream>

using namespace std;
struct ListNode
{
	ListNode* _next;
	int _val;
	ListNode(int val)
		:_next(nullptr)
		,_val(val)
	{}
};

ListNode* CreateList(int n)//n表示的是长度
{
	ListNode head(-1);
	int val = 0;
	ListNode* tail = &head;
	for (int i = 0; i < n; i++)
	{
		cin >> val;
		tail->_next = new ListNode(val);
		tail = tail->_next;

	}
	return head._next;
}
ListNode* CreateNode(int x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);

	}
	newnode->_next = NULL;
	newnode->_val = x;
	return newnode;
}
int main()
{
	ListNode* list = CreateList(5);
	return 0;
}

 看到C++里写的链表就会发现实现一个链表其实是很快的。

优势四

new失败之后是直接抛异常的,malloc失败之后是会进行检查的,所以new不需要进行失败检查。

operator newoperator delete函数

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

operator new

它是可以直接调用的,但是我们一般不直接的调用,new失败之后是直接抛异常的,抛异常的整个过程是在operator new上,我们调用new的时候其实是先调用operator new 然后再去调用相应的构造函数,但是operator new的底层其实还是malloc,所以operator new其实就是对malloc的封装,失败之后就抛异常,实现new。

operator是一个对malloc进行封装的函数,但是new是一个操作符,之前说过操作符其实都是在编译的时候就转化为相应的指令了,和函数不同,函数实在我们运行的时候进行的调用,所以两者之前是存在偏差的,new转化为指令是会去调用operator new 函数 然后再去调用构造函数,这两个操作都是new的底层。

operator new[]

其实这里也是一样的道理

我们new [] 之后是回去调用operator new []  然后operator new[] 再去调用operator new 和n次构造函数。

operator delete

这里需要思考的一个问题其实就是delete之后先去调用析构函数还是先去调用operator delete的问题,我们可以来看看下面的这段代码,这是一个stack的析构和构造函数。

class Stack
{
public:
	Stack()
		:_top(0)
		, _capacity(0)
		, _a(new int[4])
	{

	}
	~Stack()
	{
		delete[] _a;
		_capacity = _top = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};

如果先去释放内存的化,这里的一个问题就是内存泄漏,所以我们要做的先去调用析构函数,然后再去调用我们的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)

可以看到其实free也是一个宏。

通过上述两个全局函数的实现知道, operator new 实际也是通过 malloc 来申请空间 ,如果
malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。 operator delete 最终是通过 free 来释放空间的

newdelete的实现原理

这里再和大家分享一个new对于内置类型和自定义类型的处理,和有没有析构函数的处理是不是一样的,首先先写一个类。然后对不同的情况进行处理。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	int* ptr = new int[10];
	delete ptr;
	return 0;
}

先来看看这种情况,我们没有配对的使用,但是最后的运行的情况是合法的,也没有进行报错,是因为这是一个内置类型,来看看汇编代码。

 可以看到的是我们也是new40个字节大小出来,再来看看自定义的的类型是个怎么样子的。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	int* ptr = new int[10];
	delete ptr;

	A* ptr1 = new A[10];
	delete ptr1;
	return 0;
}

再来看看这个编译是没有问题,但是如果我们进行运行的时候就是会报错。

有人会说这里只是调用了一次析构函数,其实不是的,是因为我们释放内存的时候,没有从最开始的指向开始,而是从中间一段地方开始的,这就和我们银行的分期付款是差不多的,我们可以看啊看汇编代码。

 可以看到是44的字节大小,但是我们自定义类型的A其实只要四个字节的存储大小,所以里面的原因就是我们再前面多开一个字节的大小来进行存储。

所以才会这样,但是还有一个奇怪的现象就是如果我们把析构函数去掉的化就会变成不会报错。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	/*~A()
	{
		cout << "~A()" << endl;
	}*/
private:
	int _a;
};
int main()
{
	int* ptr = new int[10];
	delete ptr;

	A* ptr1 = new A[10];
	delete ptr1;
	return 0;
}

 这样写也不对,但是也不会报错,所以得出一个结论。

new和delete要配对使用,要不然结果是不确定的

如果申请的是内置类型的空间, new malloc delete free 基本类似,不同的地方是:
new/delete 申请和释放的是单个元素的空间, new[] delete[] 申请的是连续空间,而且 new 在申
请空间失败时会抛异常, malloc 会返回 NULL

 

 定位new

我不算再这个部分来写,让大家来可以简单的了解一下定位new后面会学一些池化技术,这个时候就是需要用的时候,定位new其实是手动的去调用自定义类型的构造函数,构造函数是不能调用的,因为再定义的时候自动调用,但是定位new可以,所以定位new我们现在就可以理解为它是对一块已有的空间调用构造函数。 

可以来看看它是怎么使用的,这块大家先了解一下就OK了

class A
{
public:
	A(int n )
		:_a(n)
	{
		
	}
	/*~A()
	{
		cout << "~A()" << endl;
	}*/
private:
	int _a;
};

int main()
{
	A* pa = (A*)malloc(sizeof(A));
	new(pa)A(1);
	return 0;
}

malloc/freenew/delete的区别

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

内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

内存泄漏其实就是对一块已经不再使用的空间没有进行释放。

内存泄漏是要程序员进行控制的,内存泄漏是不会报错的。