zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【C++初阶】C++内存管理

C++内存 管理 初阶
2023-06-13 09:16:43 时间

文章目录

一.C/C++内存分布图

作为C/C++方向的从业者,必须关注的四块空间:

  1. 栈(局部数据)
  2. 堆(动态申请数据)
  3. 数据段(全局数据和静态数据)
  4. 代码段(可执行代码和可读常量)
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定义在函数体外[全局],且被static修饰[静态],所以是静态(全局)变量,位于数据区
  3. staticVar定义在函数体外[局部],且被static修饰[静态],所以是静态(局部)变量,位于数据区
  4. localVar定义在函数体内[局部],所以是局部变量,位于栈区
  5. num1是整型数组名,定义在函数体内[局部],所以是局部变量,位于栈区
  6. char2是字符数组名,定义在函数体内[局部],所以是局部变量,位于栈区
  7. *char2是字符数组存放的内容,位于栈区
  8. pChar3是一个指针,指向代码段中常量字符串“abcd”,定义在函数体内[局部],位于栈区
  9. *pChar3是常量字符串“abcd”,位于代码段
  10. ptr1指向动态申请的空间,定义在函数体内[局部],位于栈区
  11. *ptr1是动态申请的空间里的内容,位于堆区

关于第7题和第9题区别:

二.new和delete内存管理

C 语言中的malloc是函数,C++中的new是关键字,操作符,都是在堆上动态申请的空间 下面我针对内置类型和自定义类型比较new,delete和malloc,free

1.对于内置类型

C 语言和C++默认都没有对各自动态申请的内存进行初始化

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


	//C++,默认不初始化
	int* ptr1 = new int;
	delete ptr1;
	//指定初始化
	int* ptr2 = new int(100);
	//ptr2 = nullptr;如果后面不使用了,可以置空
	delete ptr2;

	//动态申请数组
	//不初始化
	int* ptr3 = new int[10];
	delete[] ptr3;
	//完全初始化
	int* ptr4 = new int[10]{ 1,2,3,4,5,6,7,8,9,10 };
	delete[] ptr4;
	//不完全初始化
	int* ptr5 = new int[10]{ 1,2,3,4,5 };
	delete[] ptr5;

	return 0;

}

对于内置类型: new/delete相比与malloc/free,只是用法上的区别

2.对于自定义类型(重点)

new/delete主要是针对自定义类型设计的,对于自定义类型, new除了在堆上开辟空间,还会自动调用构造函数,完成对象的初始化 delete除了在堆上释放空间,还会自动调用析构函数,完成对象的资源清理

class A
{
public:
	A(int a = 10)
		:_a(a)
	{
		cout << "构造函数" << endl;
	}
	~A()
	{
		cout << "析构函数" << endl;
	}
private:
	int _a;
};

int main()
{
	A* ptr1 = new A;
	delete ptr1;
	cout << "____________________________________" << endl << endl;
	A* ptr2 = new A[4];
	delete[] ptr2;
	return 0;
}

案例:对于我们之前学过的单链表那块

C语言:

ListNode* BuyListNode(int val)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_val = val;
	newnode->_next = nullptr;
}
int main()
{
    ListNode* n1 = BuyListNode(1);
	ListNode* n2 = BuyListNode(2);
	ListNode* n3 = BuyListNode(3);
	n1->_next = n2;
	n2->_next = n3;
    return 0;
}

C++:

struct ListNode
{
	int _val;
	ListNode* _next;
	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
	{}
};


int main()
{
	//创建链表
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	ListNode* n4 = new ListNode(4);
	ListNode* n5 = new ListNode(5);
	n1->_next = n2;
	n2->_next = n3;
	n3->_next = n4;
	n4->_next = n5;
	return 0;
}

3.new和delete不匹配问题(了解)

案例:不匹配现象

	//1.
	int* ptr1 = new int;
	delete[] ptr1;

	//2.
	int* ptr2 = new int[10];
	delete ptr2;

	//3.
	int* ptr3 = new int;
	free(ptr3);

不匹配后果:未定义,由于环境(linux还是windows或者不同编译器)不同,结果不同,不要尝试不匹配 ps:这个问题不是内存泄漏问题(内存泄漏是不会报错的,类似一种慢性病,报错类似一种急性病)

4.new的底层机制(了解)

new的底层机制其实是调用operator new函数申请空间 + 调用构造函数初始化 而operator new申请空间的底层实现也是调用malloc, 所以new的效率并没有比malloc高

封装malloc,申请内存失败,抛异常 封装malloc只是为了符合面向对象处理出现错误的处理方式—抛异常

  • 我们其实可以手动调用operator new函数

ps:operator new函数的使用方式和malloc一样,唯一不同的是operator new开空间失败不会返回nullptr,而是抛异常.

给大家看一下调用new的时候的反汇编:

  • 内置类型
int main()
{
	int* a = new int;

	return 0;
}

这个call调用的是operator new函数

  • 自定义类型
struct ListNode
{
	int _val;
	ListNode* _next;
	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
	{}
};


int main()
{
	//创建链表
	ListNode* n1 = new ListNode(1);

	return 0;
}

第一个call是调用operator new函数 第二个call是调用构造函数

同理就有operator new[]函数,调用多次operator new 还有operator delete和operator delete[]函数

ps:我们知道new的底层机制,但是我们没有必要使用operator new去实际编程.

5.定位new表达式(了解)

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

构造函数有点不一样,在我们之前学的都不能显式调用,但是定位new表达式就可以完成显式调用 ps:析构函数可以显式调用(下图证明)

class A
{
public:
	A(int a = 10)
		:_a(a)
	{
		cout << "构造函数" << endl;
	}
	~A()
	{
		cout << "析构函数" << endl;
	}
private:
	int _a;
};

int main()
{
	A* ptr1 = (A*)malloc(sizeof(A));
	if (ptr1 == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}
	//定位New--- 对ptr1指向的这块空间,显示调用构造函数初始化
	new(ptr1)A(1);

	//ps:析构函数可以显式调用
	ptr1->~A();
	free(ptr1);
	//上面两行相当于delete ptr1;
	//上节课讲过delete等同于 调用析构函数+operator delete(失败抛异常)
	return 0;
}

定位new案例: 我们听说过内存池还有池化技术,那我百度了一下,我就给大家讲一下我的理解: 举一个例子: 山上有好多和尚,他们每天需要来山脚下挑已经过滤好的自来水喝, 但每一次都要排老长老长的队,于是各个和尚都在自己家里建水池蓄水,可以避免每天排队,提高效率 于此同时也产生一个问题:蓄水池的水需要一个过滤装置定时过滤杂质后才能饮用

上述的山脚下的自来水就类似new/malloc,挑山脚下的别人已经过滤好的纯净水就是调用new/malloc开辟空间并且开好的空间是已经初始化好的, 于是和尚建蓄水池蓄水就是建内存池,提高效率 内存池的水需要定时过滤就类似定位new,对内存池的空间进行初始化

三.面试题

1.new/delete和malloc/free的区别(理解)

malloc/free

new/delete

函数

操作符

对内置类型和自定义类型都不初始化

对内置类型不初始化,对自定义类型初始化

申请空间时有时类型的大小需要计算

直接跟类型和个数

返回值为void*,使用前要强转

new直接返回对应的指针类型

开辟空间失败返回null

开辟空间失败抛异常

最大的区别是new/delete对于自定义类型能够自动调用构造函数和析构函数

2.内存泄漏

ps:内存泄漏是指针丢了,而不是内存丢了(内存一直都在)—–-指针丢了就是找不到这块空间了 (想想永不关闭的程序,比如后台服务器就知道危害了)

内存泄漏指由于疏忽或者错误造成程序未能释放已经不再使用的内存的情况 并不是指物理上的消失,而是失去了对这段内存的控制,从而造成了内存的浪费.