C++——内存管理

文章详细介绍了C++中的内存分布,包括栈、堆、数据段等区域的用途。讨论了C和C++中的动态内存管理函数如malloc、calloc、realloc和new、delete的用法与区别。特别强调了new和delete在处理自定义类型时调用构造和析构函数的重要性,以及new[]和delete[]的配对使用。此外,提到了内存泄漏的概念及其危害,并给出了相关练习题以巩固理解。
摘要由CSDN通过智能技术生成

目录

1.c++内存分布

2.c语言中动态内存管理

3.c++动态内存管理

4.operator new 和 operator delete 函数

5. new和delete的实现原理

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

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

8.什么是内存泄漏,内存泄漏的危害


1.c++内存分布

内核空间用户代码不能读写
向下增长
内存映射段文件映射、动态库、匿名映射
向上增长
数据段 或静态区全局数据、静态数据
代码段 或常量区可执行代码/只读常量

代码是存在文件里的,文件存在磁盘里(不是在栈或者代码段上)

上面的内存分布是针对虚拟进程地址空间划分的。

每个进程编译好是一个可执行程序,可执行程序会运行起来,运行起来以后就把全局变量、静态变量、常量加载到相应区域,栈和堆里的数据不需要从文件中加载进来提前开好空间。局部变量只有在程序运行起来以后main函数执行,才开始建立栈帧,main函数调用下一个函数,继续建立栈帧。堆也是,不用提前给它开空间,要用的时候申请就行。

虚拟进程地址空间不止有这几个区域,在Linux下,32位程序有2^32个地址,每个地址占一个字节,每个地址都有一个编码。平时我们说的指针其实就是地址(虚拟进程地址空间里面一个一个空间的编号)。

空指针指向一个实际的空间,编号为32个0的空间。

指针是用来存地址编码的,一个编码是32位数字,就需要4字节来存,指针大小就是4字节。

2^322^64
32位机器(指针4字节)64位机器(指针8字节)
4G

2^32*4G

练习:

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. 选择题:
 选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
 globalVar在哪里?____  staticGlobalVar在哪里?____
 staticVar在哪里?____  localVar在哪里?____
 num1 在哪里?____
 char2在哪里?____  *char2在哪里?___
 pChar3在哪里?____    *pChar3在哪里?____
 ptr1在哪里?____     *ptr1在哪里?____
2. 填空题:
 sizeof(num1) = ____; 
 sizeof(char2) = ____;    strlen(char2) = ____;
 sizeof(pChar3) = ____;   strlen(pChar3) = ____;
 sizeof(ptr1) = ____;
3. sizeof 和 strlen 区别?

2.c语言中动态内存管理

malloc: void* malloc (size_t size);//size是申请空间大小,单位字节

calloc:void* calloc (size_t num , size_t size);//num是申请元素个数,size是每个元素大小,单位字节。calloc还会自动把开辟的空间,每一个字节都初始化为0。

realloc:void* realloc (void* ptr , size_t size);//ptr是起始地址,size是扩容后的总大小,单位字节

3.c++动态内存管理

以开辟整形数组为例:使用new 和 delete,在申请的时候就可以初始化

int main()
{
	//内置类型
	//相比malloc和free,除了用法的区别没有其他区别
	int *p1 = new int;//用new申请空间,不初始化
	delete p1;//释放

	int* p2 = new int(0);//初始化成0
	delete p2;

	int* p3 = new int[10];//开辟10个int
	delete[] p3;

	int* p4 =new int [10] {1,2,3,4 };//前四个是1234,后面的都是0
	delete[]p4;

	return 0;
}

new和delete对自定义类型会调构造和析构函数,而自malloc不调。

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 = new A;//自定义类调构造函数
	A* p2 = (A*)malloc(sizeof(A));//malloc不调构造

	delete p1;//调析构函数
	free(p2);//不调析构

	return 0;
}

用类实现链表:再也不用写BuyNode了(给新节点用malloc开辟空间,还要初始化新节点),new一下自动调构造函数。

class ListNode
{
public:

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

//private:
	ListNode * _next;
	int _val;

};

int main()
{
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	n1->_next = n2;
	return 0;
}

new和delete要匹配,new[ ]和delete[ ]配对。new和free也不要混合用。

不匹配可能报错也可能不报错。反正就是要匹配用,就不会出问题。

例:

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

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

int main()
{
	
	//以下所有情况都不一定,编译器不同,报不报错也不一定。这里说的报错指vs环境下会报错


	A* p3 = new A[10];
	delete p3;//会报错,且只调了一次析构函数。不写10的话,这一行不知道有多少个对象,就不知道调多少次析构函数。底层只知道总空间有多少个字节,但不知道对象大小,也就不知道对象个数。

	//在堆上开辟空间的时候不是开辟40字节(一个A的大小是4字节),实际申请空间是44,头四个字节(一个整形的大小)用来存对象个数,返回的时候把指针加4个字节,返回对我们来说有用的部分。释放的时候写了[]个数的话,指针又减4字节,取到对象个数来确定调多少次析构函数


	//屏蔽掉析构函数之后,就不报错了。原因:编译器自动生成析构函数,发现A只有一个内置类型,也不用释放其他的堆上的空间,所以调不调析构都无所谓,直接不多开四个字节来记录对象个数,这个个数就是用来让编译器知道调多少次析构函数的。优化后也就不报错了,释放位置不会发生偏移
	
	

	//————————--再看一个例子

	A* p4 = new A;
	delete[]p4;
	//vs报错
	//new了一个对象,delete的时候用的[],编译器以为前面存了对象个数,是new[]出来的。会让指针往前偏移,释放就出问题了



	return 0;
}

检查是否开辟成功

malloc失败,返回空指针。

new失败,抛异常。不需要检查返回值。 

int main()
{
	try
	{
		int* p1 = new int[20];
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

底层机制:

new

1.operator new (它的底层又是malloc)

2.调用构造函数

operator new是库里面实现的全局函数,不是new的运算符重载(运算符重载参数必须有自定义类型)。void * operator new(size_t size)

delete的底层是operate delete(它底层是free)。

4.operator new 和 operator delete 函数

也可以手动调用operator new函数来动态开辟,但没必要,用new就行。

	char* p2 = (char*)operator new(1);//括号里传想要空间的字节数,返回值是void*,要强转。
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 = new A[10];
	delete p1;//会报错,之前讲过了
	
	//屏蔽掉析构函数之后就不报错(没有多开四个字节),有没有内存泄漏
	
	delete[] p1;
	free(p1);
	//delete和free的区别就是会不会调析构函数
	//但是这里析构函数调不调都无所谓的情况下(屏蔽掉析构函数,编译器就自己优化不调析构函数了),这俩底层是一样的,没有内存泄漏


	return 0;
}

5. new和delete的实现原理

对内置类型:
new/delete申请或释放单个元素空间,new []/delete []申请或释放连续的空间。

申请失败new抛异常,malloc返回NULL。

对自定义类型:

new先调operator new申请空间,然后调构造函数

delete先调析构函数释放对象中资源,然后调operator delete释放对象本身

new [],先调operator new[]开空间(operator new[]实际上是在调operator new),然后调n次构造。

delete [],先调n次析构函数,再调delete []

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

对自定义类型,new是开空间加调用构造函数初始化。

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


定位new,和new的不同就是不想用失败抛异常,就可以用定位new

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

place_address是指针, initializer-list是类型的初始化列表

new ( place_address ) type:

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

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

 int main()
 {
	 A* p3 = (A*)malloc(sizeof(A));//malloc出来的不会调构造函数
	 if (p3 == nullptr)
	 {
		 perror("malloc fail\n");
		 exit(-1);
	 }


	 //定位new,和new的不同就是不想用失败抛异常,就可以用定位new

	 new(p3)A(1);//对p3指向空间,调用构造函数初始化

	 p3->~A();//析构函数可以直接显示调用

	 free(p3);

	 直接用delete行不行,也可以,底层是一样的
	 //delete(p3);

	 return 0;
 }

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

相同点:

都是在堆上动态申请空间,且需要手动释放。

不同点:

1.malloc和free是函数,new和delete是操作符

2.malloc申请空间不会初始化,new自定义类型会初始化

3.malloc申请空间需要手动计算空间大小并传参,new 只需要写上类型名,开辟多个对象,在[ ]里指定个数即可

4.malloc的返回值类型为void* ,接收时必须强制类型转换,new不需要,new后跟的就是类型名

5. malloc失败返回NULL,使用时必须判断是否开辟成功。new不需要判空,但是要捕获异常

6.为自定义类型申请空间时,malloc和free不会调构造和析构函数。new在申请空间后会调析构函数初始化对象,delete在释放空间前会调用析构函数完成资源清理。

8.什么是内存泄漏,内存泄漏的危害

内存泄漏并不是指内存消失了,而是指针丢失,失去对改该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:程序正常结束,内存泄漏了最终还是会还给操作系统。但长期运行的程序如果出现内存泄漏,影响很大,会导致响应越来越慢,最终卡死。         

练习:

1.C++中关于堆和栈的说法,哪个是错误的( )

A.堆的大小仅受操作系统的限制,栈的大小一般较小

B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题

C.堆和栈都可以静态分配

D.堆和栈都可以动态分配

A.堆大小受限于操作系统,而栈空间一般由系统直接分配

B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

C.堆无法静态分配,只能动态分配

D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放

2.ClassA *pclassa=new ClassA[5];

delete pclassa;

c++语言中,类ClassA的构造函数和析构函数的执行次数(D)

A.5,1

B.1,1

C.5,5

D.程序可能崩溃

申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃。

3.使用 char* p = new char[100]申请一段内存,然后使用delete p释放,有什么问题?(B)
A.会有内存泄露
B.不会有内存泄露,但不建议用
C.编译就会报错,必须使用delete []p
D.编译没问题,运行会直接崩溃


答案解析:
A.对于内置类型,此时delete就相当于free,因此不会造成内存泄漏

B.正确

C.编译不会报错,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误

D.对于内置类型,程序不会崩溃,但不建议这样使用

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值