您现在的位置是:首页 >技术教程 >【C++初阶】C/C++内存管理(没有对象的都进来看看吧~)网站首页技术教程

【C++初阶】C/C++内存管理(没有对象的都进来看看吧~)

Weraphael 2024-08-09 00:01:02
简介【C++初阶】C/C++内存管理(没有对象的都进来看看吧~)

在这里插入图片描述

?个人主页:@Weraphael
✍?作者简介:目前学习C++和算法
✈️专栏:C++航路
? 希望大家多多支持,咱一起进步!?
如果文章对你有帮助的话
欢迎 评论? 点赞?? 收藏 ? 加关注✨


一、C/C++内存分布

首先回顾,数据是如何在内存中存储的:

在这里插入图片描述

接着可以来看看以下的题目:

在这里插入图片描述

【选择题】

选项: 
A.(局部变量)  
B.堆  
C.静态区(全局变量和静态变量)  
D.代码段(常量区)

1. a在哪里? 
2. static_b在哪里? 
3. static_c在哪里?
4. d在哪里?
5. arr1在哪里?
6. arr2 在哪里?
7. *arr2在哪里?
8. arr3在哪里?
9. *arr3在哪里?
10. ptr1在哪里?
11. *ptr1在哪里?

【答案 + 解析】

在这里插入图片描述

【填空题】

1. sizeof(arr1) = ____;
2. sizeof(arr2) = ____; 
3. sizeof(arr3) = ____; 
4. sizeof(ptr1) = ____;
5. strlen(arr2) = ____;
6. strlen(arr3) = ____;

【答案+ 解析】

在这里插入图片描述

【问答题】

sizeofstrlen区别?

  • strlen函数用于计算字符串的长度,只统计字符串中字符的数量,不包括结尾的空字符。
  • sizeof操作符用于计算变量或类型的大小,一般单位为字节,通常用于计算内存大小。

若还想做以上类似题目 —> [点击跳转]

二、C语言中动态内存管理

往期回顾:[C语言中动态内存管理]

#include <iostream>
#include <stdlib.h>
using namespace std;

int main()
{
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);

	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	free(p3);

	return 0;
}

问:这里需要free(p2)吗?
答案当然是不需要的。realloc功能是在已有的内存空间扩容,然而扩容会存在原地扩容和异地扩容(当原有空间之后没有足够大的空间)。但是扩容后,返回的是调整之后的内存的起始地址,也就p2指向的地址,因此只要释放了p3p2自然而然就被释放掉了。

2.1 面试题

  1. malloc/calloc/realloc的区别?
  • malloccalloc都是向内存申请空间,不同的是malloc分配的内存区域中的初始值是随机的,而calloc会将分配的内存区域中的初始值全部初始化为0
  • realloc可以扩大原有的内存空间,而扩容的方式有2种,一种是原地扩容(在已有的内存空间后扩容);还有一种是异地扩容(当原有空间之后没有足够大的空间),它的扩容方式是:它会在内存空间重新开辟一块空间,然后 把原有的内存空间中的数据复制到新的空间,最后再释放原有的内存空间
  1. malloc的实现原理
  1. 维护一个空闲内存块链表,其中每个内存块都记录了其大小和是否已被分配。
  2. 当程序调用malloc函数请求分配一块内存时,遍历空闲内存块链表,找到第一个大小足够的内存块并将其从链表中移除。
  3. 如果找到的内存块比请求的内存大,则将其分裂成两个部分,一个用于满足请求,另一个用于将来的分配请求。
  4. 如果没有找到足够大小的内存块,则向操作系统请求更多的内存。
  5. 将已分配的内存块的大小和状态记录下来,并将其返回给程序。

需要注意的是,malloc的实现可能因操作系统和编译器而异,但其基本原理都是以上述步骤为基础的。同时,为了避免内存泄漏和内存碎片化等问题,malloc通常还会实现一些额外的功能,如内存池、缓存、对齐等。

三、C++内存管理方式

由于C++是兼容C语言的,因此C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

3.1 new/delete操作内置类型

在这里插入图片描述

#include <iostream>
using namespace std;

int main()
{
	// C语言版
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);

	// C++版 new + delete
	// 用法: new + 类型
	int* p2 = new int;
	delete p2; // 释放

	// 开辟40个字节,也就是10个整型
	// C语言版
	int* p3 = (int*)malloc(sizeof(int) * 10);
	free(p3);

	// C++版
	int* p4 = new int[10];
	delete[] p4;

	return 0;
}

当然,C++中的new还能对空间进行初始化

#include <iostream>
using namespace	std;

int main()
{

	int* p4 = new int[10]{ 1, 2 ,3 };
	for (int i = 0; i < 10; i++)
	{
		cout << p4[i] << ' ';
	}
	cout << endl;
	delete[] p4;

	int* p5 = new int(10);

	cout << *p5 << endl;
	delete p5;

	return 0;
}

【程序结果】

在这里插入图片描述

注意:

  • p4是申请10int的数组,对于数组要用{}初始化;p5是申请1个数组,初始化为10,用()初始化
  • 若不进行初始化,newmalloc一样,开辟的空间都是 随机值
    在这里插入图片描述
  • newdelete操作符需要 匹配 起来使用,不能与mallocfree混用

3.2 new和delete操作自定义类型

new/deletemalloc/free最大区别是:new/delete对于自定义类型除了开空间,还会调用构造函数(new)和析构函数delete)。

#include <iostream>
#include <stdlib.h>
using namespace std;

struct ListNode
{
public:
    // 构造函数
    ListNode(int x)
        :_val(x)
        , _next(nullptr)
    {
        cout << "调用了构造函数" << endl;
    }
    // 析构函数
    ~ListNode()
    {
        cout << "调用析构函数" << endl;
    }

    int _val;
    struct ListNode* _next;
};

struct ListNode* BuyListNode(int x)
{
    // 单纯开空间
    // C语言版太太太长了
    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    if (newnode == nullptr)
        return nullptr;
    newnode->_next = nullptr;
    newnode->_val = x;

    return newnode;
}

int main()
{
    // C语言版
    struct ListNode* n1 = BuyListNode(1);
    struct ListNode* n2 = BuyListNode(2);
    struct ListNode* n3 = BuyListNode(3);

    // 开空间+调用构造函数初始化
    ListNode* nn1 = new ListNode(1);
    ListNode* nn2 = new ListNode(2);
    ListNode* nn3 = new ListNode(3);

    delete nn1;
    delete nn2;
    delete nn3;

    return 0;
}

【调试结果 + 程序运行结果】

在这里插入图片描述

在这里插入图片描述

四、operator new与operator delete函数(重点)

4.1 介绍operator new与operator delete函数

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

以下是operator newoperator 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);
}
/*
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)
// 是个宏

通过以上两个全局函数,我们发现:

  1. operator new函数实际是通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
  2. operator new函数本质是封装了mallocoperator delete本质是封装了free

以下是operator newoperator delete的使用方法:

// operator new和operator new是库函数提供的
// 因此可以直接使用
// 用法和malloc、free是一样的
#include <iostream>
using namespace std;

int main()
{
	int* p1 = (int*)operator new(sizeof(int));
	operator delete(p1);

	return 0;
}

通过以上代码我们发现:operator newoperator delete的用法和功能都分别与mallocfree一样。它们都不会去调用构造函数和析构函数,不过还是有区别的:

  1. operator new不需要检查开辟空间的合法性(不需要特别判断p1的返回值)。
  2. operator new开辟空间失败就抛异常(对于面向对象语言处理失败,不喜欢用返回值,建议用抛异常)。
    在这里插入图片描述

4.2 new和delete的底层原理

  • new的底层原理就是转换成调用operator new + 构造函数,我们可以通过查看反汇编来验证:
#include <iostream>
using namespace	std;

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

int main()
{
	A* a1 = new A;
	delete a1;

	return 0;
}

【反汇编】

在这里插入图片描述

  • 同样的,delete的底层原理:转换成调用operator delete + 析构函数

【反汇编】

在这里插入图片描述

因此,我们就可以得到new、operator new、mallocdelete、operator delete、free之间的关系:

在这里插入图片描述

五、new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

  • new的原理
  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间
  • new 类型[数组元素个数]的原理
  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  2. 在申请的空间上执行N次构造函数
  • delete[]的原理
  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

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

  • 用途

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

  • 使用格式
  1. new(指针) + 类型
  2. new (指针) type(类型的初始化列表)
  • 使用场景

定位·new·表达式在实际中一般是 配合内存池使用内存池知识点)。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

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

// 定位new/replacement new
int main()
{
	// p1指向一块内存空间,但空间未被初始化
	A* p1 = (A*)malloc(sizeof(A));

	// 定位new表达式:new(指针) + 类型
	// 注意:如果A类的构造函数有参数时,此处需要传参
	new(p1)A; 
	
	// 调用析构函数
	p1->~A();
	// p1的空间是malloc出来的
	// 因此要用free来释放
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();
	operator delete(p2);

	return 0;
}

七、常见面试题

7.1 malloc/freenew/delete的区别

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

7.2 内存泄漏

7.2.1 什么是内存泄漏

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

7.2.2 内存泄漏的危害

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

7.2.3 内存泄漏分类(了解)

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

  • 堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc/calloc/realloc/new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏

  • 系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

7.2.4 如何避免内存泄漏

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

1. linux下内存泄漏检测工具
3. 在windows下使用第三方工具:VLD工具说明
3. 其他工具:内存泄漏工具比较

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。