【c++ 学习笔记】内存管理

🙊 C 语言内存管理方式🙊

💖 示例代码

先看以下代码,需不需要 free(p2)

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

答案是不需要,因为这里已经对 p2 进行扩容了,扩容分为原地扩容和异地扩容,如果源地址有足够的空间,就原地扩容,此时 p2p3 的地址相同,如果 p2 没有足够的空间,就进行异地扩容,将内容拷贝过去再 freep2

🙊 C ++ 内存管理方式🙊

💖 new / delete 操作内置类型

C 语言的内存管理机制是返回值为 void* 的函数调用,需要自己去计算大小并强制类型转换,而 c++ 在 c 语言扩容机制基础上添加了自己的扩容机制,使用 new 操作符创建一个对象并用一个指针进行接收。
在这里插入图片描述

代码如下:

void Test()
{
  // 动态申请一个int类型的空间
  int* ptr4 = new int;
  
  // 动态申请一个int类型的空间并初始化为10
  int* ptr5 = new int(10);
  
  // 动态申请10个int类型的空间
  int* ptr6 = new int[3];
  int* ptr7 = new int[10] {1,2,3,4};
  delete ptr4;
  delete ptr5;
  delete[] ptr6;
}

ptr5 是申请了一个 int 空间并初始化为 10,而 ptr6 是申请了 40 个字节的空间用于初始化数组。如果想把数组初始化可以写成 ptr7 这种方式。

从功能上来说,C 语言和 c++ 的功能都是一样的。对于内置类型,使用哪种方式都可以,但是对于自定义类型,就需要用 new 来进行空间的申请。看下面一段代码:

int main()
{
	int* pp1 = (int*)malloc(sizeof(int));
	int* p1 = new int;

	free(pp1);

	delete p1;

	int* pp2 = (int*)malloc(sizeof(int)*10);
	int* p2 = new int[10];

	free(pp2);
	delete[] p2;

	A* pp3 = (A*)malloc(sizeof(A));
	free(pp3);

	A* p3 = new A(1);
	delete p3;
	return 0;
}

对自定义类型使用 malloc 只会开空间而不能进行初始化,而使用 new 除了开空间,还调用了构造函数进行初始化。同理 free 只会释放空间,而 delete 除了释放空间,还会调用析构函数。看下面一段代码体会 new 的方便之处:

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}

private:
	int _a;
};

struct ListNode
{
	int _val;
	ListNode* _next;

	ListNode(int val)
		:_val(val)
		, _next(nullptr)
	{}
};

//ListNode BuyListNode(int x)
//{
//	//...
//}

int main()
{
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);


	return 0;
}

如果是 C 语言,对于链表创建一个新节点需要单独写一个创建新节点的函数,而在 c++ 中的自定义类型一般使用 new 来开空间同时调用构造函数进行初始化。所以对于默认类型,用哪种方式开辟空间都行,对于自定义类型,就要使用 new 开辟空间。

🙊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、globalVar 是一个全局变量,全局变量静态变量都放在静态区,从系统的角度静态区也叫数据段

2、staticGlobalVar 是一个静态全局变量静态全局变量全局变量都放在静态区

3、staticVar 是一个局部静态变量,也是放在静态区的,它们三个作用域不相同,但是其生命周期是整个程序期间都存在

4、localVar 是一个局部变量,存放在

5、num1是数组名,代表整个数组,存放在

6、char2 是一个数组char2num1 的区别是,num1 是自己确定大小,而 char2 是通过初始化来确定大小,初始化多大,char2 的大小就是多少。所以 char2 存在于栈上

7、char2 由于 char2 是一个数组名,存放数组首元素的地址,而 char2 代表数组的第一个元素,首先在栈上开辟五个字节的数组,将 char2 中常量字符串的内容包含 \0 拷贝到数组中

在使用 sizeof 时,char2 代表整个数组,但是实际上 char2 的值是首元素的地址,所以 * char2 存在于 上。

因为 “ a b c d ” 确实存在于常量区,但是 char char2[ ] = “abcd”; 写法的意思是将常量区的内容拷贝到栈上的数组中,而数组开多大是初始化决定的。

8、pchar3 存在于栈上,因为 pchar3 也是一个局部变量存在于栈上,但是注意 pchar3 指向的空间在常量区。

9、pchar3 指向的空间属于
代码段
* / 常量区

10、ptr1 是一个指针,且指针是一个局部变量,而局部变量都是在上,所以 ptr1 存放在

11、ptr1ptr 指向的空间,存放在堆上所以 ptr1 在堆区

12、sizeof(num1) 大小为 40 个字节,因为 sizeof(数组名) 代表的是
整个数组**,虽然只给了 4 个值,但是数组大小是固定的 10 个字节,前 4 个位置为 1234,后 6 个位置被初始化成 0

13、sizeof(char2) 大小为 5,因为 sizeof(数组名) 代表数组的大小,这里多开了一个字节的空间给 \0

14、strlen(char2)4,因为 strlen 遇到 \0 就截止。

15、sizeof(pchar3)4 / 8,因为 pcahr3 是一个指针,指针的大小为 4 个字 (32位)、或者 8 个字节 (64 位)。

16、strlen(pcahr3)4,由于 strlen 遇到 \0 就截止,计算 pchar3 有效字符的个数,所以这里为 4

17、sizeof(ptr1)4 / 8。因为 ptr1 也是一个指针。

在这里插入图片描述
注意:

常量区存储的数组是不可以更改的,而 char2 指向的数组存在于栈上,是可以更改的。

🙊operator new 与 operator delete 函数🙊

💖 介绍

newdelete 是用户进行动态内存申请和释放的操作符,operator newoperator delete 是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。注意这两个函数并不是重载而是库里面实现的全局函数。

💖 operator new 源代码分析

来看一下源代码:

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

operate new 源代码 _RAISE 表明如果 malloc 失败了就会抛出异常。

💖 operator delete 源代码分析

代码如下:

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

operate delete 进行了很多内存越界的检查,并最终 free

💖 本质

operate newoperate delete 的本质是封装,operate new 的用法和 malloc 类似。

int main()
{
	//失败以后抛异常
	int* p1 = (int*)operate new(sizeof(int*));
	//失败以后返回NULL
	int* p2 = (int*)malloc(sizeof(int*));
	if(p2 == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}
	return 0;
}

operate newmalloc 的区别是,operate new 失败以后抛异常,而 malloc 失败以后返回 nullptroperate deletefree 区别不大,只是多了一些检查。

为什么会有 operate new 呢?

int main()
{
		A* p1 = new A;
		delete p1;
		A* p2 = new A[10];
		delete[] p6;
		return 0;
}

1、由于 new 创建一个变量,需要先申请空间,再调用构造函数,而申请空间需要在上申请,这里 new 申请空间调用的不是 malloc 而是 operate new,因为 c++ 是面向对象的语言,面向对象使用抛异常来处理错误。所以使用 operate new 封装 malloc,封装失败就抛异常。

2、在这里 delete p1,是先调用析构函数再释放 p5 指向的空间。

3、而 new A[10] 申请空间调用的是 operate[ ] newoperate[ ] new 再调用 operate newoperate new 再调用 malloc 函数。因为有 10A 对象,这里调用了 10 次构造函数。

4、 delete[ ] p6 先调用 10析构函数,再 operate delete[ ] 整体释放 p6 指向的空间。

注意:

局部变量 p1p2 定义在上,函数结束出了作用域栈帧销毁,栈上的变量 p1p2 就销毁。但是 p1p2 指向的空间在堆上,堆上的内容是需要我们去手动释放的。

再看下面一段代码:

class Stack
{
public:
	Stack()
	{
		cout << "Stack()" << endl;
		_a = new int[4];
		_top = 0;
		_capacity = 4;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
	return 0;
}

此段代码创建的局部对象 st 不需要手动释放,因为局部对象定义的时候会调用默认构造函数,出了作用域会调用析构函数。再看第二种写法:

class Stack
{
public:
	Stack()
	{
		cout << "Stack()" << endl;
		_a = new int[4];
		_top = 0;
		_capacity = 4;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack* pst = new Stack;
	delete pst;
	return 0;
}

此段代码由于 pst 是内置类型,因为无论什么类型的指针都是内置类型,而与上面一种写法相比,由于 pst 是内置类型,在创建完成后不会调用析构函数,因此需要用 delete 进行释放。

在这里插入图片描述
如果将 delete 改成 free,相当于没有调用析构函数就将 pst 指向的空间释放了,_a 指向的空间没有被释放,这种情况就是内存泄漏。由于申请的内存都是由程序员自己管理,C / C++ 不会检查内存泄漏

🙊 malloc / free 和 new / delete 的区别 🙊

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

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

🙊 定位new表达式 🙊

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

使用格式:
new ( place_address ) type 或者 new ( place_address ) type ( initializer-list )
place_address 必须是一个指针initializer-list 是类型的初始化列表

使用场景:
malloc 或者 new 一块空间创建堆想的时候,想要对其初始化,由于成员私有,没办法直接进行初始化。那么如果想进行初始化就需要定位 new。构造函数不可以显示调用,而析构函数可以显式调用。

int main()
{
	A aa;
	A* p1 = (A*)malloc(sizeof(A));
	if (p1 == nullptr)
	{
		perror("malloc fail");
	}

	// 对一块已有的空间初始化 -- 定位new
	//new(p1)A;
	new(p1)A(1);
	p1->~A();
	free(p1);
	return 0;
}

当然,这种方法不如直接去 new 一个空间简洁。

int main()
{
	A* p2 = new A;
	delete p2;
	return 0;
}

但是有一个场景使用定位 new 更合理,直接 new 是向操作系统堆区申请内存,为了提高行能,使用定位 new 去内存池申请。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值