您现在的位置是:首页 >技术交流 >learn C++ NO.7——C/C++内存管理网站首页技术交流

learn C++ NO.7——C/C++内存管理

玩铁的sinZz 2024-07-04 11:18:06
简介learn C++ NO.7——C/C++内存管理

引言

现在是5月30日的正午,图书馆里空空的,也许是大家都在午休,也许是现在37摄氏度的气温。穿着球衣的我已经汗流浃背,今天热火战胜了凯尔特人,闯入决赛。以下克上的勇气也激励着我,在省内垫底的大学中,我不觉得气馁,我要更加努力学习,让自己能够越来越好,以后肯定也会”晋级决赛”。

1.C/C++程序的内存分布

  1. 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
    创建共享共享内存,做进程间通信。
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段–存储全局数据和静态数据。
  5. 代码段–可执行的代码/只读常量
    在这里插入图片描述

2.变量在内存中存储的位置

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

在这里插入图片描述
下面我按照从左往右从上到下的顺序依次分析上面的题目。globalVar和staticGlobalVar都是定义在全局域中,所以是存储在静态区中的。staticVar虽然是在局部域内定义的,但是它是static修饰的变量所以依旧是存储在静态区中的。localVar和num1都是在局部域内定义的局部变量,都是存储在栈区空间的。上面第一部分比较简单,下面我以画图加分析的来看下面部分。
为什么char2和*char2都在局部域呢?
在这里插入图片描述
pChar3是一个定义在栈区的const char类型指针,它保存的是常量字符串"abcd"的首元素地址。*pChar3是对常量字符串的首元素地址进行解引用操作,访问的是常量区的空间。ptr1是一个在栈区上创建的指针变量,存放的是动态开辟空间首字节地址。解引用访问ptr1访问的是堆区空间。

数组名单独放在sizeof内部表示整个数组,所以是4*10字节。"abcd"其实是隐含了’’字符,所以是4+1=5字节。无论什么指针都是4 or 8字节,指针的大小取决于平台,64位平台8字节,32位平台4字节。

3.C语言的动态内存管理

常见的C语言动态内存管理如下:malloc/calloc/realloc/free。具体细节可以移步c语言动态内存管理,查看这里不做赘述。

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

1.malloc就是单纯地开辟堆区空间。calloc是可以指定初始化内容开辟堆区空间。realloc就是动态调整堆区申请的空间。当调整后的空间无法在原空间后扩容,则会将原空间的内容拷贝到新的空间上再申请到连续空间。
在这里插入图片描述

4.C++的动态内存管理

因为C++是兼容C语言的,所以C语言的动态内存管理依旧是可以在C++中使用的。由于C语言的动态管理方式存在缺陷,所以C++也提供了两个操作符来进行动态内存管理。分别是new 和 delete。

4.1.new和delete管理内置类型

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

	//cpp
	//new后面跟的是类型
	int* p2 = new int;
	delete p2;
	
	//malloc单纯开辟空间
	//new支持初始化
	int* p3 = new int(10);
	delete p3;

	int* p4 = (int*)malloc(sizeof(int) * 10);
	free(p4);

	int* p5 = new int[10];
	delete[] p5;//一定要带[]

	//开辟连续空间也是支持初始化的
	int* p6 = new int[10]{1,2,3,4};
	delete[] p6;

}

需要注意的是使用malloc申请的内存就用free来释放,使用new申请的内存,就用delete来清理。不可以free来释放new的空间,或者用delete来释放malloc的空间。虽然在内置类型中,可能不会出问题,但是,这样使用是不规范也不正确的。这就好比吃面要用筷子,吃炸鸡要用手套,吃炒饭用勺子。你用free去释放new的内存,就好比用勺子去吃炸鸡,这样是不合适的。在某些场景下,你用free去释放new的内存就会好比用手套去吃火锅,那肯定是不行的。

4.1.new和delete操作自定义类型

struct ListNode
{
	//c++写法
	ListNode(int x)
		:_next(nullptr)
		, _val(x)
	{}

	int _val;
	struct ListNode* _next;
};

//c语言写法
struct ListNode* BuyNode(int x)
{
	struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

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

	return newnode;
}

int main()
{
	struct ListNode* n1 = BuyNode(1);
	struct ListNode* n2 = BuyNode(2);
	struct ListNode* n3 = BuyNode(3);
	free(n1);
	free(n2);
	free(n3);

	//struct升级成了类
	ListNode* nn1 = new ListNode(1);
	ListNode* nn2 = new ListNode(2);
	ListNode* nn3 = new ListNode(3);
	delete nn1;
	delete nn2;
	delete nn3;

	return 0;
}

new可以去调用自定义类型的构造函数来初始化对象,这样写比c语言的写法香多了。

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

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{
	A* p1 = (A*)malloc(sizeof(A) * 4);
	A* p2 = new A[4]{1,2,3,4};
	free(p1);
	delete[] p2;
	return 0;
}

在这里插入图片描述
从上面样例可以看到,new自定义类型对象的时候,会去调用它的构造函数。当然这里是以整型值初始化自定义类型,会产生隐式类型转换,因为是在同一行编译器自动进行了优化,所以没有调用拷贝构造函数。而malloc只是单纯地开空间。delete自定义类型对象时,会去调用他的析构函数,而free只是单纯释放空间。

4.1.c++和c语言在开辟动态内存时失败处理细节

在C语言中,当使用malloc()函数开辟动态内存时,如果内存不足或者没有足够的连续空间,函数将返回NULL指针,表示内存分配失败。而在C++中,使用new操作符开辟动态内存时,如果内存不足或者没有足够的连续空间,将抛出一个std::bad_alloc异常,表示内存分配失败。

//C语言
int main()
{
	int* p1 = (int*)malloc(sizeof(int) * 1024);
	if (p1 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	return 0;
}

//C++
int main()
{
	int* p1 = nullptr;
	try
	{
		do
		{
			p1 = new int[1024];
			cout << p1 << endl;
		} while (p1);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

在面向对象的编程语言的编程中,try-catch是一种异常处理机制。在try块中,我们编写可能引发异常的代码。如果在try块中的代码引发了异常,程序会立即跳转到与其对应的catch块。catch块定义了异常处理程序,它会处理try块中引发的异常。这里稍微了解即可,具体细节得等到后面学习后再和大家介绍了。

5.new和delete的原理

5.1.operator new与operator delete函数的介绍

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

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 new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。

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 delete:通过调用free来释放空间,而且operator delete和宏函数free的底层都是调用_free_dbg来进行释放空间的

5.2.operator new、new、operator delete和delete函数的底层实现

首先,通过下面代码的汇编代码来看看operator new、new、operator delete和delete函数究竟是怎么样的吧。

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

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

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

int main()
{
	A* p1 = (A*)operator new(sizeof(A));
	A* p2 = new A(1,1);
	operator delete(p1);
	delete p2;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
通过查看汇编代码我们可以看到,new的会先调用operator new开辟空间,然后在调用构造函数初始化。delete会先调用析构函数进行清理,然后调用operator delete来释放动态内存。当然用new 自定义类型[N] 个对象,会先调用operator new开辟空间,然后在调用N次构造函数初始化。delete[] 会先调用N次析构函数,然后释放动态内存。

在这里插入图片描述

下面通过一个比较复杂的场景来看一下new和delete对于自定义类的的处理

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	// 其他方法...

	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int       _capacity;
	int       _size;
};

int main()
{
	Stack* p1 = new Stack;
	delete p1;

	return 0;
}

在这里插入图片描述

6.定位new

定位new是new关键字的另一种用法,用于给已经分配好的堆区空间进行调用构造函数来初始化对象。

6.1.代码样例演示

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

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

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

int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
	p1->~A();//显示调用析构函数
	free(p1);
	return 0;
}

在这里插入图片描述

6.2.定位new的应用场景

在一些需要频繁申请堆区内存的程序中,通常需要提前开辟一个内存池,用于提高获取堆区内存的效率。而定位new就能将申请的内存通过调用构造函数来进行初始化。在后面学习的STL的链表中就能遇到这一场景。
在这里插入图片描述

7. malloc/free和new/delete的区别

1、malloc/free是库函数。new和delete是操作符。
2、对于内置类型来说malloc/free和new/delete的区别不是很大,对于自定义类型new/delete分别会去调用构造函数/析构函数。来进行初始化和清理工作。
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
4. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。

8.内存泄漏的概念

8.1.什么是内存泄漏

内存泄漏指的是在程序运行过程中,程序分配了一段内存空间,但在使用完这段内存空间后,没有及时释放掉这段内存,导致这段内存不能被再次使用,从而造成了内存空间的浪费。如果程序中存在内存泄漏问题,并且这种泄漏的情况不断累积,最终可能会导致程序所能使用的内存空间越来越小,甚至导致程序崩溃。内存泄漏是一种常见的编程错误,需要开发人员注意及时释放不再使用的内存空间,避免内存泄漏问题的出现。下面我通过一个样例来看看内存。

int main()
{
	char* p1 = (char*)malloc(1024 * 1024 * 1024);
	cout << p1 << endl;
	return 0;
}

在这里插入图片描述

8.2.内存泄漏的危害

对于客户端,内存泄漏的危害比较的小。对于服务端,内存泄漏的危害极大。因为想游戏服务、电商服务等服务端中,内存泄漏会导致服务程序宕机,进而导致软件业务的事故。所以我们程序员在操作动态内存时,一定要注意在哪里申请了就在对应的位置释放。内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

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