小谈C++内存管理

小谈内存管理

本文涉及内存分区、堆栈区别、new关键字、字节对齐以及字节存储大小端相关问题。

一、内存分区

我们知道不同类型的变量生命周期是不同的,为了方便管理,将一个进程的用户空间依次进行了划分。根据内存地址从高到底,依次分为:

1、栈段

主要是存放局部变量、函数参数、临时变量和返回值等。
1)什么是栈溢出?
程序中使用的栈内存超过了编译器为栈空间分配的最大值。
2)容易造成栈内存的几种场景?
①递归。函数调用会伴随着堆栈开销, 随着递归调用的层数增加开销会越来越大,栈内存不断被消耗。
②开辟的数组过大
3)如何避免栈溢出?
1)对于递归,使用内联
2)使用堆内存,即使用vector(使用的堆内存)代替 数组

2、内存映射段(自由存储区)

内存映射区的作用比较多。
1)可当作共享内存使用,用于进程间通信。
2)使用动态链接时,可用于加载动态链接库。
3)可用于malloc函数中(mmap())申请大于128字节的内存块。

3、堆段

用户程序员动态管理内存,实际上是malloc函数中的 brk()函数负责管理,通过移动堆顶指针向上增长。

4、全局/静态存储区

该区主要是存放全局变量和静态变量(包括全局和局部),然后根据全局/静态变量是否初始化又分为bss段和数据段
1)bss段,存放未初始化的全局/静态变量。
2)数据段,存放以初始化的全局/静态变量。

5、常量区

用于存放全局常量。注意常量区是属于数据段的

6、代码区

存放程序编译后形成的机器指令。

二、堆栈的区别

1、管理方式不同

1)栈是由编译期管理的,自动回收
2)堆是由程序员自行管理的,需要手动回收(可能造成内存泄漏,智能指针)

2、内存碎片不同

1)对于堆栈,堆栈类似于数据结构中的栈,具有先进后出的特性,中间的变量不可能随意出栈,因此不会造成堆栈溢出
2)堆区的内存如果频繁进行new/malloc,势必会造成空间的不连续,产生外部碎片

3、生长方式不同

1)对栈,自顶向下生长,即想着地址减小的方向进行生长
2)对堆,自底向上生长,一般是通过brk()函数移动堆顶指针,向着地址增大的方向生长。

4、分配方式不同

1)栈内存支持静态分配和动态分配。所谓静态分配就是由编译器完成局部变量的内存分配和回收;而动态分配是指编译器提供了allocate() 函数用于程序员动态分配,并由编译器自动释放。
2)堆内存只支持动态分配。需要程序员自己申请和释放内存。

5、分配效率不同

1)栈内存因为有底层的编译器、寄存器等支持,分配效率高;
2)堆内存是由malloc()函数提供支持的,通过一系列的复杂函数机制才完成内存的分配。

三、new 关键字

new关键字,是C++中提供的一种动态内存分配方式。使用new 关键字申请内存的过程一共有三步:
1)先由operator new 运算符调用 malloc函数(默认) 申请对象数据类型需要的内存空间(非静态成员变量 + 虚函数表指针大小num),并赋值给一个空指针(void
2)强制类型转换,即将void* 类型转换成 目标数据类型
3)调用构造函数,对该指针指向的对象空间进行赋值操作

1、关于operator new运算符

由以上我们可以知道,new 操作符 实际是使用 operator new 运算符进行内存分配的,然后在进行强制类型转换和调用构造函数的。而operator new 运算符实际就是对 malloc 函数的封装,但是在malloc 申请失败时添加了内存不足处理机制,如果人为的指定了内存不足应对措施,就会按照措施继续申请,否则抛出异常。也就是 所谓的new handle 机制
另外虽然operator new 运算符默认是调用的 malloc 函数,但是该运算符支持重载。也就是说而我们可以不使用malloc 函数进行分配内存空间。因为频繁的使用malloc 函数申请小的内存会造成内存碎片,因此我们使用 空间配置器(内存池)来进行内存的管理和分配,也就所谓的专属重载
关于专属重载的详情请参考 efective c++.

2、new 出的内存是否可以使用free?

基本数据类型无论使用哪种方式都不会执行构造函数的,同时new需要注意new 可能会发生专属重载。
1)基本数据类型且 operaor new 运算符不发生专属重载, 可以
2)基本数据类型且 operaor new 运算符发生专属重载,不可以
3)自定义数据类型,不可以

3、new 申请的内存是连续的吗?物理内存上是连续的吗?

是连续的虚拟内存,但是映射到物理空间上有可能不是连续的。

4、new失败后如何禁止抛出异常?

1)采用 new-handle 机制,即在 内存申请失败后添加 OOM处理函数
2)使用nothrow关键字

int* p = new(std::nothrow)int(5);

四 、new关键字、malloc函数的区别

1、相同点

都是用于动态分配内存的一种方式,在对上开辟内存,需要手动释放。并且本质上都是通过malloc函数进行的内存申请。(因为operator new 运算符默认是对的malloc函数的封装)

2、不同点

1)本质区别
new是关键字,需要编译器进一步解释;而malloc 是函数,可直接调用
2)返回值类型不同
new 返回指定数据类型的指针;而malloc返回void* 类型
3)申请失败的返回值不同
new 抛出异常;malloc 返回空指针
4)是否指定申请空间的大小
new 不需要指定大小;而malloc 需要指定申请空间的大小
5)是否调用构造函数
new 调用构造函数;而malloc 不会调用
6)内存是否可以扩展
new 申请的内存不可以扩展;而malloc 函数申请的内存可以使用realloc函数进行扩展。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

四、字节对齐

字节对齐实际是针对结构体成员或类成员的内存对齐问题;因为计算机是以2、4、8字节的倍数进行读取字节块的,对于这种复合型的数据结构,其存取速度与字节对齐有关。其核心思想就是 用空间换时间
结构体/类成员根据其顺序以及数据类型按照一定的规则在空间上排列,从而使得CPU快速定位成员地址并存取数据,这就是字节对齐。
对齐规则如下:
1)每个成员相对于结构体首地址的偏移是自身字节大小的整数倍
2)结构体的大小是最大成员变量的整数倍
3)结构体变量的首地址能够被其对齐字节数大小所整除(对齐字节数是由操作系统的位数决定的,比如64位系统是8字节对齐)

五、大小端

字节存储方式分为大端法和小端法。其中大端法是按照高有效字节存储在低地址规则,一般用于网络通信;而小端法是按照低有效字节对应地址的规则,一般用于操作系统。
一般处理数据的话无论大端小端,都是从低地址开始处理,那么对于大端法来说,低地址对应最高字节那么判断符号位比较简单。而对于小端法,低地址对应低有效字节。那么在做强制类型转换的时候比较方便处理。

1、如何判断大小端?

1)定义一个整形并进行赋值操作,取地址并打印其第一个字节的内容。如果是高位有效字节 就是大端法,否则就是小端法(输入输出必须为 16进制的数字)

	int a = 0x12345678;
	char * c = (char*)(&a);

	int i = 0;
	while (c[i] != '\0') {
		printf("%x\n", c[i]);
		i++;
	}
	/*for (int i = 0; i < 4; i++) {
		printf("%x\n", c[i]);
	}*/

	return 0;

2)也可以通过union 联合体进行判断,联合体中包含两个值 (int char), 对int 进行赋1操作,然后打印 char 的值,如果是1 则说明是小端

	union t {
		int a;
		char b;
	};

	t t;
	t.a = 1;
	cout << (int)t.b << endl;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值