C/C++内存管理

2023-12-13 15:37:17

目录

C/C++内存区域的划分

C语言中的内存管理

free函数:

malloc函数:

calloc函数:

realloc函数:

C++内存管理方式

new/delete操作内置类型:

new和delete操作自定义类型:

operator new与operator delete函数:

new和delete的实现原理:

内置类型:

自定义类型:

定位new表达式(placement-new)

C/C++内存区域的划分

?要学习程序的内存管理,首先要了解程序中内存区域的划分,知道每个区域是干什么的。

说明:

1. 栈区(又叫堆栈)---非静态局部变量、函数参数、返回值等等,栈是向下增长的。

2. 内存映射段---是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。

3. 堆区---用于程序运行时动态内存分配,堆是向上增长的。

4. 数据段(静态区)--存储全局数据静态数据

5. 代码段(常量区)--可执行的代码、只读常量

注意:栈区的向下增长和堆区的向上增长说的是创建变量时,变量地址的变化

有了上边对内存区域的了解,接下来看一下下边这段代码:

int globalVar              = 1;
static int staticGlobalVar = 1;
int main()
{
	static int staticVar = 1;
	int localVar    = 1;
	int a1[10]      = { 1, 2, 3, 4 };

	char  a2[]      = "abcd";
	const char* ptr = "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);
	return 0;
}

这段代码中各数据所对应的存储区域为:

分析:

1.globalVar是全局变量,staticGlobalVar是全局静态变量,staticVar是局部静态变量,所以它们是在静态区存储。

2.localVar、a1数组、a2数组、ptr指针、ptr1指针、ptr2指针、ptr3指针都是局部的非静态变量,所以它们在栈区存储。

3.ptr1、ptr2、ptr3三个指针指向的空间都是动态开辟的,并且动态开辟的空间都是在堆区上开辟的,所以ptr1、ptr2、ptr3指针指向的空间是在堆区的。

4.两个“abcd”字符串都属于只读常量,所以是存储在常量区的。

可能会有人对第四条有疑问,因为两个“abcd”字符串都是只读常量,那就意味着“abcd”字符串中的内容不可被修改,ptr指针是一个被const修饰的指针,它指向的内容不能通过指针的解引用操作修改,所以ptr指针可以指向一个只读常量字符串,但是a2数组没有const修饰,是可以修改a2数组中的内容的,这不是和“abcd”字符串的只读特性冲突了吗?这不是把权限放大了吗?

答案是并没有出现冲突,也没有权限放大的问题。因为a2数组虽然是通过一个只读常量字符串“abcd”进行初始化的,但是a2数组中存储的并不是这个常量区的只读常量字符串“abcd”,它会把常量区的只读常量字符串“abcd”拷贝一份到栈区的a2数组中,因此a2数组中存储的“abcd”是常量区“abcd”的拷贝是存储在栈区的,没有只读特性,所以不存在特性冲突和权限放大的问题。可以调试代码验证:

通过监视a2、&a2、ptr、&ptr可以发现指针是直接指向的常量区的只读字符串,而数组是把常量区的只读字符串拷贝一份到栈区,两者是不一样的,所以使用常量初始化指针必须使用const修饰,但是使用常量初始化数组就不用使用const修饰

C语言中的内存管理

在C语言中是使用malloc/calloc/realloc/free实现对内存的动态管理的。

int main()
{
	int* p1 = (int*)malloc(4*sizeof(int));//开辟16个字节空间
	free(p1);
	
	int* p2 = (int*)calloc(4, sizeof(int));//开辟4个整型空间
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);//把p2执行的空间扩大到10个整型
	
	free(p3);
	return 0;
}

其中malloc、calloc、realloc用于内存开辟,free用于内存释放。?

free函数:

void free (void* ptr);

1、free函数用来释放动态开辟的内存

2、如果参数 ptr 指向的空间不是动态开辟的,那free的行为是未定义的

3、如果参数 ptr 是NULL指针则free函数什么事都不做

malloc函数:

void* malloc (size_t size);

1、向内存(堆区)申请一块大小为size(字节)的连续可用的空间,并返回指向这块空间的指针。

????????如果开辟成功,则返回一个指向开辟好空间的指针。

????????如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

2、返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候要自己决定。

3、如果参数 size 为0malloc的行为是标准是未定义的,取决于编译器。

int main()
{
	int* p1 = (int*)malloc(100);
	char* p2 = (char*)malloc(100);
	float* p3 = (float*)malloc(100);

	free(p1);
	free(p2);
	free(p3);
	return 0;
}

calloc函数:

void* calloc (size_t num, size_t size);

1、开辟 num 个大小为 size(字节)?的空间,并且把空间的每个字节初始化为0

2、与 malloc 相同,calloc 返回值的类型也是 void* ,所以calloc函数并不知道开辟空间的类型,具体在使用的时候要自己决定。

3、与 malloc 的主要区别在于 calloc 会把申请的空间的每个字节初始化为全0

int main()
{
	int* p1 = (int*)malloc(4*sizeof(int));
	char* p2 = (char*)malloc(4 * sizeof(char));
	float* p3 = (float*)malloc(4 * sizeof(float));

	int* p4 = (int*)calloc(4,sizeof(int));
	char* p5 = (char*)calloc(4, sizeof(char));
	float* p6 = (float*)calloc(4, sizeof(float));


	free(p1);
	free(p2);
	free(p3);
	free(p4);
	free(p5);
	free(p6);
	
	return 0;
}

可以发现使用malloc开辟的空间都没有初始化,而使用calloc开辟的空间都被初始化为0了。

realloc函数:

void* realloc (void* ptr, size_t size);

1、realloc可以调整动态开辟的空间的大小。?

2、ptr 是要调整的内存地址 ,size 是调整之后新大小 ,返回值为调整之后的内存起始位置。 3、realloc也可以缩小容量。

4、realloc在扩大内存空间的时候存在两种情况:

????????情况1:原有空间之后有足够大的空间:直接原有内存之后直接追加空间,原来空间的数据不发生变化,新开辟的空间不进行初始化。

? ? ? ? 情况2:原有空间之后没有足够大的空间:在堆空间上另找一个合适大小的连续空间来 使用,并把原空间的内容拷贝进新空间中,多出的空间不进行初始化,再释放原空间,最后返回的是一个新的内存地址。

int main()
{
	int* p1 = (int*)malloc(4*sizeof(int));
	char* p2 = (char*)malloc(4 * sizeof(char));
	float* p3 = (float*)malloc(4 * sizeof(float));

	int* p4 = (int*)calloc(4,sizeof(int));
	char* p5 = (char*)calloc(4, sizeof(char));
	float* p6 = (float*)calloc(4, sizeof(float));


	int* p7 = (int*)realloc(p4, 2 * sizeof(int));
	char* p8 = (char*)realloc(p5, 6 * sizeof(char));
	float* p9 = (float*)realloc(p6, 100000 * sizeof(float));

	free(p1);
	free(p2);
	free(p3);
	/*free(p4);
	free(p5);
	free(p6);*/
	free(p7);
	free(p8);
	free(p9);

	return 0;
}

C++内存管理方式

C++在内存管理方面兼容C语言中malloc、calloc、realloc、free的操作方式,同时为了解决C语言的一些不足,提出了自己的一套内存管理的方式:通过 newdelete 操作符进行动态内存管理。

new/delete操作内置类型:

int main()
{
	// 动态申请一个int类型的空间,没有初始化
	int* p1 = new int;

	// 动态申请一个int类型的空间并初始化为10
	int* p2 = new int(10);

	// 动态申请3个连续的int类型的空间,没有初始化
	int* p3 = new int[3];

	// 动态申请3个连续的int型空间并初始化(与数组初始化类似)
	int* p4 = new int[3]{ 1,2,3 };

	// 动态申请3个连续的int型空间并初始化为0(与数组初始化类似)
	int* p5 = new int[3]{ 0 };

	delete p1;
	delete p2;
	delete[] p3;
	delete[] p4;
	delete[] p5;

	return 0;
}

注意:申请和释放单个元素的空间使用new和delete操作符,申请和释放连续的空间使用 new[]和delete[],一定要匹配起来使用!

new和delete操作自定义类型:

C和C++的内存管理方式的主要区别在于在申请自定义类型的空间时,new/new[]?会调用构造函数delete/delete[]?会调用析构函数malloc 与 free 不会

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{	
	A* p1 = (A*)malloc(sizeof(A));//C语言
	A* p2 = new A(1);//C++

	A* p3 = (A*)malloc(sizeof(A) * 10);//C语言
	A* p4 = new A[10];//C++
	
	free(p1);
	delete p2;

	free(p3);
	delete[] p4;
	return 0;
}

把C++的注释掉,运行代码:

把C语言的注释掉,运行代码:

可以发现c++的new会调用自定义类型的构造函数,delete会调用析构函数,并且在连续定义多个对象时,定义几个对象就调用几次构造函数,delete销毁几个对象就会调用几次析构函数,而且对连续空间的销毁会从后往前销毁

operator new与operator delete函数:

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

operator new和operator delete的本质是对malloc和free的封装operator new底层调用malloc来申请空间,若malloc申请空间成功则直接返回,若malloc申请空间失败则尝试执行用户提供的空间不足时的应对操作,若果设置了应对措施则继续申请,否则抛异常;operator delete 最终是通过free来释放空间的

new和delete的实现原理:

内置类型:

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

自定义类型:

new:

????????1. 调用operator new函数申请空间

????????2. 在申请的空间上执行构造函数,完成对象的构造

delete:

????????1. 在空间上执行析构函数,完成对象中资源的清理工作

????????2. 调用operator delete函数释放对象的空间

new T[N]:

????????1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请

????????2. 在申请的空间上执行N次构造函数

????????3.使用 new[] 开辟自定义类型的空间时会在这段空间的首部多开辟四个字节的空间,用以存储开辟的连续空间个数以供delete[]使用。

delete[]:

????????1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

????????2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间

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

class Date
{
public:
	Date()
		:_year(1)
		, _month(1)
		, _day(1)
	{ }
	~Date()
	{ }

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	A* p1 = new A[5];

	Date* d = new Date[5];

	int* p2 = new int[5]{ 0 };

	char* p3 = new char[5]{ 0 };

	float* p4 = new float[5]{ 0 };

	delete[] p1;
	delete[] p2;
	delete[] p3;
	delete[] p4;
	delete[] d;

	return 0;
}

注意:new[]和delete[]一定要配合使用,否则容易导致程序崩溃。

定位new表达式(placement-new)

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

使用格式:

????????new (place_address) type?或者 new (place_address) type(initializer-list)

????????place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

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

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

int main()
{

	A* p1 = (A*)malloc(sizeof(A));// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	new(p1)A;  //显示调构造函数   注意:如果A类的构造函数有参数时,此处需要传参
	p1->~A();  //显示调析构函数
	free(p1);


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

	return 0;
}

文章来源:https://blog.csdn.net/qq_74190188/article/details/134831646
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。