C++内存分布 new和delete介绍

本文详细介绍了C++中内存分布的不同区域,包括栈区、堆区、静态区和常量区的特点,以及new和delete与malloc和free之间的异同。重点讨论了内存泄漏和指针悬挂的概念,并强调了构造函数和析构函数在内存管理中的作用。
摘要由CSDN通过智能技术生成

目录

C/C++内存分布

栈区

堆区

静态区

常量区

C++   new和delete

分配空间形式对比

new  delete与malloc  free的区别

可不可以串着使用new和free呢


C/C++内存分布

C++的内存分布,大体上分为栈区 堆区  静态区 常量区

栈区

栈区是用于存储函数调用时的局部变量 函数参数 返回地址等,当一个函数被调用的时候,栈区会分配一块空间给这个函数,其局部变量会被分配到栈上,函数执行结束后,这些局部变量的内存空间会被释放。栈内存是自动管理的,不需要手动分配或释放。常见的存栈区的有局部变量,函数,函数调用的参数,函数的返回地址以及返回值。请注意编译器产生的临时变量也存在栈区,虽然说它具有常性,但那只是一个权限限制(相当于自带const),它依旧存储在栈区,用完了自己就会销毁。以上的也全适用于main函数。

堆区

堆区是一种动态分配的内存区域,用来存动态分配的对象和数据,直白点讲malloc,calloc,new等动态分配出来的空间都在堆区。堆区最重要的议题就是内存泄漏了,手动申请的堆空间如果不手动free虽然程序结束这段空间会自动,但是资源没回收会发生内存泄漏。

  内存泄漏举个例子,出去旅行在酒店开了房(这个房间就是我malloc申请的空间),但是我行李箱(行李箱就是资源,其实就是这段空间定义的变量之类的东西,这些变量也会使用申请的空间)忘拿走扔在酒店直接退房走了。我退房了我对这个房间就没有使用权了,我的行李箱也许还在原地也许已经被人拿走了。现实生活中肯定会回去找酒店帮忙取出来行李箱,但是对于计算机来说这片空间已经没有使用权了,也就没办法取出行李箱(资源)了,此时行李箱可能会扔在酒店的某个地方(反正没人管了),行李箱也会占用空间的,如果遗留的行李箱(资源)越来越多,终有一天会把整个空间占满,导致下一次手动申请空间空间会不足。

  我只是举个例子帮助理解,现实生活中肯定不会像上面那样。

  总的来说内存泄漏就是指那些已经被分配但未被正确释放(即未处理)的内存资源。这些资源在逻辑上已经不再被程序所使用,但由于没有调用相应的释放函数(如free在C语言中),malloc申请的空间虽然程序结束会自动收回,但是这片空间里的资源(变量什么的)不会清理,它们仍然占据着物理内存空间。由于这些未释放的资源仍然占用空间,如果程序持续运行且不断产生新的内存泄漏,这些闲散资源确实会越积越多。在堆(heap)上分配的内存空间是有限的,因此,如果泄漏的内存量持续增长,最终确实有可能将堆空间占满。当堆空间被占满时,程序将无法再成功分配新的内存块,这可能会导致程序崩溃或表现出不可预测的行为。

      为什么内存泄漏不会报错呢,内存泄漏相当于慢性病,平时可能都看不出来,但不表示没有问题。等变得严重了,身体受不了了才会给你反应,但是到那时候可能就已经晚了

       那么指针悬挂有又是什么呢,指针变量是保存地址的变量,解引用才是访问这片空间。malloc申请空间时一般会使用指针变量来保存这片空间的地址,如果你已经free这块空间,但是指针变量没有手动置为空,那么就产生了指针悬挂。指针悬挂不会直接报错,只是风险特别大,只有使用的时候才会报错。如果你free完空间后指针变量没有置为空,那么接下来不小心又使用了这个指针变量,进行解引用想要访问指针变量保存地址的那块空间时就会程序崩溃。因为空间已经回收了,你还要访问一个你已经没权限访问的空间(这片空间可能已经被系统回收并分配给其他程序或变量使用,或者根本就不再属于程序的可用内存范围),这就意味着已经退了房,但是房卡忘记还了,有一天又偷偷拿房卡回这个房间看看,这样就会发生不可预估的问题。所以已经释放空间的指针变量手动置空比较好,减少风险

静态区

全局变量和静态变量存储在静态存储区中,静态变量在整个程序的整个执行过程都存在,但是静态成员函数不在静态区,静态成员函数和别的成员函数都一样放在公共代码段里,这个代码段是指程序的一部分,包含了程序的所有指令。静态区的东西作用域仅限于定义它们的文件或函数,虽然作用域在函数里,但是这只是说在别的函数里不能使用在这个函数里定义的静态的局部变量,但是它已经存在没有销毁,等程序结束才会销毁。

静态区被称为静态的主要原因是因为它的内存分配和释放是在程序运行期间静态确定的,即在程序启动时就会被分配内存,在程序结束时才会被释放。静态区中的数据在程序整个执行过程中保持不变,不会随着函数的调用或代码块的结束而被销毁,因此被称为静态。

加static的变量基本都是存静态区的,全局变量也在这里,全局的指针变量也在静态区。

常量区

常量数据(如字符串常量)存储在常量存储区中。这些数据在程序运行期间保持不变,通常是只读的。常量存储区的数据在程序启动时被加载,直到程序结束时才被释放。

来看一道题目

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 区别?

globalVar全局变量在静态区,常量才放常量区。 staticGlobalVar用static修饰,全局的静态变量放在静态区。静态区不只放静态变量还放全局变量

staticVar局部静态变量依旧在静态区。localVar局部变量放到栈里面。char2在栈区,虽然它存字符串常量,但是是在栈上为数组 char2 分配了足够的空间来存储字符串 "abcd" 的拷贝。编译器会将字面量字符串 "abcd" 的内容复制到 char2 数组中。这时,char2 数组包含了与字面量字符串相同的内容,但它存储在栈上,而不是常量区。

*char2是数组第一个元素,也就是a,因为此时是相当于把字符串拷贝过来存到数组里,数组在栈上开了空间存这些东西。即使他是常量此时被拷贝过来也是在栈上,受装它的容器限制。但是因为是字符串常性不能修改还是要保证的,所以加const

pChar3在栈区,指针变量本身在栈区,指针变量用来保存地址的,但是它本身也需要空间去存。此时定义在局部函数里(main也算),所以在栈区

*pChar3在常量区,与char2不同的是这是指针直接指向常量区,pChar3保存的是常量区存字符串本体的空间地址,所以*pChar3在常量区

ptr1定义在栈区,所以在栈区,保存的地址是堆区空间的地址,所以*ptr1在堆区

sizeof(num1) 存的是数组,sizeof计算类型或对象在内存中的大小(以字节为单位),这是数组,1个int数占4个字节,这个数组总共10个数(不明显写的自动填充0),所以sizeof(num1)是40

sizeof(char2sizeof计算类型或对象在内存中的大小(以字节为单位),字符串会在末尾存一个\0作为结束标志,所以sizeof(char2)是5。 

strlen(char2)strlen只会算字符串有效字符,\0不会计算进去,所以结果是4

sizeof(pChar3) pChar3是指针,指针在32位下sizeof是4,64位下是8

strlen(pChar3)  strlen只会算字符串有效字符,\0不会计算进去,所以结果是4

sizeof(ptr1)  是个指针,同pChar3

C++   new和delete

new和delete是C++进行动态管理内存的方式,是C语言里malloc等和free的升级版,malloc他们俩能干到的事,new他们都能干到。

分配空间形式对比

单个类型分配空间malloc和free形式

int main()
{
	int* arr = (int*)malloc(sizeof(int));
	int* brr = (int*)malloc(sizeof(int)*10);//分配多个int类型的空间
	if (arr == NULL)
	{
		perror("malloc fail");//内存分配失败打印
		exit(1);//退出
	}
	if (brr == NULL)
	{
		perror("malloc fail");//内存分配失败打印
		exit(1);//退出
	}
	free(arr);//释放空间
	arr = NULL;//手动置空,防止悬挂指针
    brr=NULL;
}

new和delete形式

int main()
{
	int* arr = new int;
	int* brr = new int[10];//分配多个类型的空间
	delete arr;
	delete[] brr;
	arr = NULL;
	brr = NULL;
}

new  delete与malloc  free的区别

1.对于自定义类型new会自动调它的构造函数,delete会自动析构函数,而malloc和free不会

而内置类型基本是一样的

malloc的情况,不会调用构造函数

 new和delete的情况

new A[3]相当于构造了三次对象,相当于对象数组,所以可以通过数组的方式来访问他们,brr[0],brr[1],brr[2]

析构这个对象数组不是用delete brr[],这样会报错的,而是delete [] brr;

2.malloc分配空间返回空所以一定要判空,new会抛异常需要捕获异常

在C语言中,malloc函数用于动态内存分配,它返回一个指向所分配内存的指针,如果内存分配失败,则返回NULL。由于C语言不支持异常处理机制,因此malloc需要程序员显式地检查其返回值是否为NULL,以确定内存是否成功分配。这是C语言处理错误的一种常见方式——通过返回值或特殊错误码来指示函数调用的成功或失败。

而在C++中,new操作符被设计为在内存分配失败时自动抛出std::bad_alloc异常。C++的异常处理机制允许程序在运行时遇到错误情况时,通过抛出和捕获异常来优雅地处理这些错误。因此,当new无法分配所需内存时,它会自动触发异常处理流程,而不需要程序员显式地检查返回值。

区别在于C语言返回的是错误码,而且你用malloc每次开一次空间就要写一次判断错误码是什么,然后要手动进行退出。而C++是自动抛出异常,他们会自动退出。

如果你捕获了std::bad_alloc异常,你可以选择在捕获块中打印任何你想要的信息。通常,你可能会打印一条错误消息,告知用户或开发者内存分配失败的情况。例如:

try {  
    int* largeArray = new int[SOME_LARGE_NUMBER];  
    // ... 使用largeArray ...  
    delete[] largeArray;  
} catch (const std::bad_alloc& e) {  
    std::cerr << "内存分配失败: " << e.what() << std::endl;  
    // 可以在这里添加其他错误处理代码  
}

那么new究竟是怎么来实现开空间的呢,可以通过反汇编来看一下

call是调函数的意思,通过反汇编可以看出来new其实调了一个operator new的函数(这个不是重载),operator new函数内容如下,可以看出operator new实际上底层依旧是malloc,也需要类型大小,也需要强转类型,但是多了抛异常

 其实operator new因为底层是malloc,而operator delete 是free,所以他们其实也可以直接使用

定位new表达式(placement-new)

但是 为什么不会打印构造函数和析构函数字样呢,因为operator new他们也不会走构造和析构,但是new会。总结一下malloc的功能其实就只是开空间而已,而operator new不只可以开空间,而且可以抛异常,而new不只能通过operator new来开空间和抛异常,而且可以完成析构。那么究竟在operator new上面加了什么以用来调构造函数呢。直接通过开辟空间地址的指针arr->A去访问是不行,可以试一下但是基本都是报错。

这就引出了定位new表达式,定位new表达式是在已分配的原始内存空间中调用构造函数的表达式

使用格式:
new (place_address) type 或者 new (place_address) type(initializer-list)
place_address 必须是一个指针, initializer-list 是类型的初始化列表
使用场景:
定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义
类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。

多重对象数组开空间构造定位new表达式是通过偏移量来访问各个对象空间来进行构造和析构的

 为什么构造不能用arr->A(1),而析构可以直接这样呢,虽然可能很多人会说这是规定,然后就不了了之,我还是想说得清楚些

在C++中,构造函数和析构函数的使用方式确实有所不同,这主要源于它们各自的目的和生命周期管理的差异。

构造函数用于初始化对象,它在创建对象时自动调用。当你使用new关键字创建一个对象时,C++会首先分配内存,然后调用对象的构造函数来初始化这块内存。构造函数没有返回值(也不是void),因为它们负责构建对象本身,而不是执行某个操作并返回结果。因此,你不能直接通过指针来调用构造函数,因为构造函数的调用是与对象的创建紧密绑定的。

相比之下,析构函数用于销毁对象并清理资源。当对象的生命周期结束时(例如,当对象离开其作用域或被delete释放时),析构函数会自动调用。你可以通过指针显式地调用析构函数,因为这时你已经有了一个存在的对象,你只是想提前结束它的生命周期。通过指针调用析构函数的形式是ptr->~TypeName(),其中ptr是指向对象的指针,TypeName是对象的类型。这种显式调用通常与定位new(placement new)一起使用,当你手动管理对象的内存时,需要显式地调用析构函数来执行清理操作。

可不可以串着使用new和free呢

对于内置类型来说new和free是可以串着用的,但是不推荐串着用,因为有可能发生奇怪的问题

对于 内置类型是不行的,一方面malloc和free调不了构造函数和析构函数,一方面在于free可能会极大可能造成内存泄漏

为什么呢,对于delete来说分为两个步骤,operator delete这个底层是free,另一方面它还会调析构

 如果类里面的成员变量是别的类类型并且涉及到要另外malloc开空间的话,调这个类的析构函数函数的同时会自动调这个类类型成员的析构函数把它的空间资源先析构了,然后是析构类。如果是free会直接把类的空间释放了,但是这个类也可能有类成员开了空间啊,free不会考虑这个,这就造成了内存泄漏。

比如说类与对象里面写的函数两个别的类类型stack的类queue

总结一下

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

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值