【C++】(四)内存状态

13 篇文章 1 订阅
7 篇文章 0 订阅

内存空间分配

首先,在C++里面大体可以把内存分为五个区,分别是:堆区、栈区、全局存储区、常量存储区和程序代码区。

  • :堆是我们通过new关键字来动态申请的内存空间,堆上分配的内存需要我们手动管理,如果我们没有正确释放堆上的内存可能会出现内存泄漏。
  • :存放局部变量和函数参数的地方,它在需要的时候会由编译器自动分配。当函数调用结束时栈上对应的空间会被自动释放。栈里执行的效率很高,但是分配的内存容量有限。
  • 全局存储区:通常用来存储全局变量和静态变量。在C语言里面,全局变量又分为初始化的和未初始化的,但是C++里面没有这个区分。
  • 常量存储区:这里通常用于存储常量数据,这部分内存是只读的。
  • 程序代码区:它的主要功能就是存放程序的二进制代码,这部分内存一般也是只读的。通常代码区是可以共享的,因为对于频繁被执行的程序,只需要在内存中有一份代码就行了。

new和delete原理

new / delete是C++里的关键字,它们底层是执行的是C语言中的malloc和free,new / delete用于内存的动态申请和释放。
new的过程
先调用标准库里的operator new函数在堆上分配一块连续的内存空间;调用相应类的构造函数,对内存空间中的对象进行构造和初始化;最后返回指向这片空间的指针。
delete的过程
先调用对象的析构函数,完成对象资源的清理工作;然后调用标准库里的operator delete函数来释放这个对象所占用的空间。

C++有几种new

new是C++里面用来动态申请内存空间的关键字,它返回的是指定类型的指针。

广义上来说有三种常用的new,分别是new operatoroperator newplacement new

new operator也就是我们平时用的new操作符,我们用了之后它会有两个操作:

1.调用operator new 申请内存,operator new 是一个全局的函数。当使用 new 关键字的时候,编译器会自动找到这个函数,并且调用这个函数来分配空间。
2.调用类的构造函数进行初始化动作。

placement new:placement new又称为定位new运算符,它能够让我们指定需要使用的位置,定位new运算符直接使用传递给它的地址。

malloc / free 与 new / delete区别

  • 它们都用于内存的动态申请和释放。malloc/free是C和C++语言里的标准库函数。new / delete是C++里的关键字,它底层是执行的malloc/free。。
  • new/delete比malloc/free更加智能,因为malloc仅仅分配内存空间,free仅仅回收空间,它们并不具备调用构造函数和析构函数的功能。
    new和delete在对象创建的时候能自动执行构造函数,对象消亡之前会自动执行析构函数,所以和malloc/free比起来更智能一些。
  • new返回的直接就是我们指定类型的指针,而malloc返回void类型指针,我们必须要强行转换成实际需要的类型指针。

既然new/delete的功能完全覆盖了malloc和free,为什么C++中不把malloc/free淘汰出局呢
因为c++程序经常要调用c函数,而c程序里只用malloc/free管理动态内存。

malloc和free原理?

  • malloc:操作系统会维护一个记录空闲内存地址的链表,当调用malloc函数时,它就会去链表里找一个能够满足用户申请大小的内存块。然后把这个内存块分给用户,如果有多余的内存就继续返回到链表上。
  • 当调用free函数时,它会把用户释放的内存块连接到空闲链表上。时间一长,空闲链表会被切成很多的小内存片段。再有用户申请一个大内存片段的话,那空闲链上可能没有可以满足用户要求大小的片段了。所以,malloc函数会请求延时,并开始在空闲链上整理内存片段,把相邻的小空闲块合并成大的内存块。如果实在不能找到符合要求的内存块,malloc函数会返回NULL指针。

delete和delete[]区别?

delete只会调用一次析构函数。delete[]会调用数组中每个元素的析构函数。

当new[]时,需要用delete[]来释放。new[]会创建一个对象数组,假设一个对象需要N字节大小,K个对象的数组就需要KN个空间。new[]会在KN个空间的基础上在头部多申请4个字节用于存储数组长度,delete[]时候会释放KN+4大小的内存。

C++内存泄漏

一般我们常说的内存泄漏指的是,我们在堆内存里动态申请的内存在使用完毕后没有释放掉,进而造成内存的浪费。总结一句话:就是new出来的内存没有通过delete合理的释放掉
内存泄漏的情况:
1.new申请的内存没有释放
2.释放数组内存时用的是delete
3.父类的析构函数不是虚函数
可以使用智能指针解决
可以用计数法来解决,使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露。

malloc申请的存储空间能用delete释放吗?

malloc和delete是不兼容的,malloc申请的空间应该使用free函数来释放。

malloc、calloc函数、realloc函数

malloc

void* malloc (size_t size);
// malloc函数用来动态开辟内存,返回void类型指针,我们必须要强行转换成实际需要的类型指针。
// 不会自动初始化,值为随机值

calloc函数

void* calloc (size_t num, size_t size);
// 为num个size大小的元素开辟一块空间,并且把空间的每个字节初始化为0。

realloc函数
realloc函数用来动态调整内存空间

void* realloc(void* ptr, size_t szie);
// ptr是需要调整的空间地址,size是调整之后新大小,返回参数为调整之后的内存起始位置。

存在两种情况
1.原地扩容
在需要扩容的空间后有足够的空间进行扩容,要扩展内存就直接原有内存之后直接追加空间。
2.异地扩容
原有空间之后没有足够多的空间时,会在堆空间上另找一个合适大小的连续空间,并且把原内存空间的数据拷贝回来,释放旧的内存空间还给操作系统,最后返回一个新的内存地址。

C++中浅拷贝与深拷贝

浅拷贝
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,新旧指针还是指向同一块内存地址。所以如果这个时候任意一个指针修改这块内存地址上的值,另一个指针指向的值也会跟着被修改。

深拷贝
深拷贝与浅拷贝的区别在于深拷贝不仅拷贝值,还会开辟出一块新的内存空间用来存放新的值,即使原先的对象被析构掉,也不会影响到深拷贝得到的值。

栈和队列的区别


栈实际上是一种线性表,它只允许在固定的一端进行插入和删除元素。允许插入和删除的一端叫做栈顶,而不进行操作的一端叫做栈底。栈所遵循的原则是先进后出,主要的操作有push、top还有pop。其实我对它的理解它就是一个线性表,只不过这个线性表比较特殊,有个底儿,只能在另外一端进行插入和删除,就像个瓶子一样,最早放进去的东西,能拿出来是最晚的。
队列
队列和栈不一样,栈只能在一头进行插入和删除,而队列是两头都能用,具体是一头儿用来加新元素,另外一头用来删除元素。它所遵循的原则是先进先出,我对它的理解就是像火车进隧道,火车头就是队列头,火车尾就是队列尾。火车既然进隧道了,那肯定火车头先进那也先出,火车尾后进那肯定也就后出。

C++里堆和栈的区别

  1. 申请方式不同:堆是由我们自己用new和delete来申请和释放的,而频繁的new/delete会造成一定的内存碎片,所以分配的速度慢且会有碎片产生。而栈是由系统自动分配的,所以分配的速度快且不会有碎片。
  2. 内存连续不同:堆是不连续的内存区域,因为系统里是用链表来实现链接空闲地址的。而栈是一块儿连续的内存区域,大小也是操作系统预定好的。
  3. 管理机制不同:对于堆来说,系统有一个记录空闲内存地址的链表,当系统收到程序申请时就遍历这个链表,如果能够找到一个空间大于申请空间的堆节点,就会把这个节点空间分配给程序,然后删除链表里本节点记录。对于栈来说,只要栈的空余空间大于申请空间,系统就能分配成功,否则就要报栈溢出。
    我自己的理解:栈就像是吃饭时候去饭店里吃,我们只管点菜付钱,方便便捷,但是饭的质量就不知道了,也就是没那么自由。而堆就是你亲自下厨,麻烦一点,但是自由度高,自己能够控制。

堆快还是栈快

栈快
这个问题可以从两方面来考虑:

  1. 内存分配释放角度,栈是程序运行前就已经分配好的空间,所以运行时几乎不需要再花费额外的时间去分配。但是堆的话是运行时动态申请的。
  2. 访问内存时间的角度,要访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次根据指针保存的地址来访问实际数据的内存。而栈只需访问一次。

既然栈更快,为什么不多用栈
原因其实是栈的地址空间必须连续,如果让它任意成长的话,会给内存的管理带来困难。

为什么C++没有垃圾回收机制?这点跟Java不太一样。

1.首先,如果要实现一个垃圾回收器就会带来额外的空间和时间开销垃圾,这违反了C++的设计原则,也就是"不为不必要的功能付出代价",这不符合C++高效的特性。
2.C++有析构函数、智能指针、引用计数去管理资源的释放,对GC的需求并没有那么迫切.
3.消耗内存:C++产生的年代内存很少,垃圾回收机制需要占用更多的内存.

结构体内存的对齐?

要讲结构体内存对齐首先需要讲一下内存对齐是什么

我们都知道在内存中放数据之后CPU会来读取,但是CPU从内存中读取数据的时候并不是一个字节一个字节来读,而是以块儿的形式来读的,这个块儿的大小是内存的读取粒度。那么现在有几种情况:

第一种情况就是假如现在CPU从内存的0位置开始每次读8个字节,而我们的int数据刚好在内存的7~10位置处,这种情况CPU读这个int数据就要读两次(先0-7,再8-15),那这读一个int都要两次这效率太低了。
第二种情况:比如说现在有个结构体里面分别有一个char、一个int、一个char,那正常我们觉得这个结构体所占的内存就是1+4+1=6个字节,但我们实际sizeof的时候会显示它是占12个字节。那这就很奇怪对吧。
第三种情况:还是刚才这个结构体,里面三个元素按顺序是char、int、char,我们用sizeof的时候是12。但如果说,我们把它里面的元素顺序换一下,从char、int、char换成char、char、int,再用sizeof的时候就发现它占用的内存大小变成了8。那这几种情况其实都是内存是否对齐所带来的影响。

对于结构体来说,内存对齐是针对基本类型的。我们都知道结构体它里面的元素都是按照顺序定义的,依次放到内存里。实际上每个元素在放的时候都是有一个偏移量的,第一个元素的偏移量是0,其他元素的偏移量是对齐数的整数倍(那么所谓这个对齐数,指的是当前元素大小和当前编译器默认对齐数,它们两个之间的最小值),偏移了之后就开始放元素,当元素都放完之后,还需要看一下当前总的存储单元是不是这些元素中最大对齐数的整数倍,如果不是的话需要补成整数倍,也就是后面会空几个位置。
举个例子比如现在有个结构体,里面按顺序是一个char、一个int、一个double。那最开始的时候这个char变量存入第0个字节处,再放入int时,因为这个int的对齐数是它自己大小和当前编译器默认对齐数的最小值(也就是4和8比较),所以这个int的对齐数是4.由于内存里第一个四个字节的位置已经有了数据,所以它会存到第二个四字节里,也就是第4~7处。再放入double,同理第一个8字节的位置已经被占了,所以它存到第8-15位置上。元素都放完之后,再来看看已经占用的内存字节数是不是元素里最大对齐数的整数倍,是就结束,不是就在后面补上空的位置。
在C++11以后引入了两个关键字 alignas 和 alignof。alignas可以指定结构体的对齐量,alignof可以计算出类型的对齐量。如果alignas指定的值小于自然对齐的最小单位,那它就会失效。

总体来说
结构体的内存对齐就是拿空间换时间。用了内存对齐之后,CPU访问内存的效率会更高,而且有一些硬件平台可能必须要求内存对齐,否则抛出异常。那所以这就是我对结构体的内存对齐的一些理解。

获得结构成员相对于结构开头的字节偏移量

使用<stddef.h>头文件中的,offsetof宏。

struct  S
{
	int x;
	char y;
	int z;
	double a;
};
int main()
{
	cout << offsetof(S, x) << endl; // 0
	cout << offsetof(S, y) << endl; // 4
	cout << offsetof(S, z) << endl; // 8
	cout << offsetof(S, a) << endl; // 12
	return 0;
}

一个类对象占用的空间

  1. 非静态成员变量所占大小总和。
  2. 加上编译器为了CPU计算,作出的数据对齐处理。
  3. 加上为了支持虚函数,产生的额外内存。
    因为当有虚成员函数的类实例化的时候,会有一个指针vptr指向虚函数表vtbl,而虚函数表里就存储着类中定义的所有虚函数。所以虚函数引起的额外内存占用就是指针vptr占用内存的大小。

空类(无非静态数据成员)的实例大小为1, 因为不同的对象不能具有相同的地址,所以编译器会给空类加上1个字节,保证用这个类定义的对象都有一个独一无二的地址。
类的静态成员变量不占用类的存储空间,因为类里的静态成员存储在全局数据区中,全局只有一份。
类的成员函数(除虚函数外)不占用类的存储空间,因为成员函数(包括构造和析构函数)编译后存放在程序代码区。

实现内存池

首先讲一下为什么需要内存池
通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。而内存池则是在真正使用内存之前,预先申请分配一定数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
经典内存池设计:
(1)先申请一块连续的内存空间,把这块内存空间划分为若干个子内存块,每个子内存块连同一个指向下一个内存块的指针一起构成一个内存节点,然后用指针把这些内存节点连接成一个链表,链表的每一个内存节点都是一块可供分配的内存空间;;
(2)如果有内存节点分配出去,就从空闲节点链表中删除这个节点;如果有内存节点被释放,就把这个节点重新加入空闲内存节点链表里;
(3)当一个内存块的所有内存节点都已经分配出去了,还要申请新的对象空间,就会再去申请一个内存块来容纳新的对象,再新申请的内存块加入到内存块链表中。

堆和自由存储区

自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行的内存申请,这块内存就在自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存。
简而言之:堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,堆与自由存储区并不等价。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值