C++内存模型 详解

一.C/C++内存模型基本概念

1.两大分区:代码区和数据区
2.四大分区:代码区,全局区(全局/静态存储区),栈区,堆区
3.C语言分区:堆,栈,静态全局变量区,常量区
4.C++语言分区:堆、栈、自由存储区、全局/静态存储区、常量存储区(标准答案)
5.C/C++内存模型根据生命周期的不同有三大区:即自由存储区(C++),动态区、静态区
6.代码虽然占内存,但不属于c/c++内存模型的一部分

二.各分区实际内存结构

在这里插入图片描述text段: 用于存放程序代码的区域, 编译时确定, 只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text段合在一起,但不是简单的将它们“堆”在一起就完事,还需要处理各个段之间的函数引用问题。
data段 :用于存放在编译阶段(而非运行时)就能确定的数据,可读可写。也是通常所说的静态存储区,赋了初值的全局变量、常量和静态变量都存放在这个域。 (已经初始化的数据)
bss段:不在可执行文件中,由系统初始化。(未初始化的数据)
heap:由程序员分配和释放;若程序员不释放,程序结束时操作系统会进行回收。(malloc,free,new,delete)
stack:存放函数的参数值和局部变量,由编译器自动分配和释放
env:环境变量

三.各大分区的基本介绍

堆 heap

由程序员释放:指的是程序员使用malloc/free,或者new/delete(自由存储区,自由存储区可以理解是堆区的一个子集,不完全等价)申请或者销毁的内存所在的区域。
如果程序员没有释放,在程序运行过程中,可能出现内存泄漏的状况(堆上无用的内存没有及时的销毁free/malloc,程序无法在堆上找到空闲的内存,目前的操作系统通常有虚拟内存技术,每个进程都有可能将整个内存空间完全沾满,这样内存泄漏是非常危险的,会导致其他进程的内存页被换下,会导致大量的缺页中断,最后死机蓝屏)

代码测试:

#include<iostream>
void test1()
{
    int *p=new int(1);
	printf("test1 p:%p\n",p);
	delete p;
}
int main()
{
	int *p=new int(1);
	printf("main p:%p\n",p);
	int *p2=new int(1);
	printf("main p2:%p\n",p2);
	test1();
	delete p;
	delete p2;
	return 0;
}

在这里插入图片描述
分析:我们先在main new了一个p,然后我们new 了一个p2,最后我们调用test函数,new了一个p;
内存分布流程如下
main(*p);
main(*p)->main(p2);
main(*p)->main(p2)->test1(*p);

2.我们改变顺序再次测试:

#include<iostream>
void test1()
{
    int *p=new int(1);
	printf("test1 p:%p\n",p);
	delete p;
}
int main()
{
	int *p=new int(1);
	printf("main p :%p\n",p);
	test1();
	int *p2=new int(1);
	printf("main p2:%p\n",p2);
	delete p;
	delete p2;
	return 0;
} 

运行结果:
在这里插入图片描述
我们观察到test(*p)和main(*p2)在同一个地方;
分析:我们先new出了main(*p)然后调用了test1(*p) 和 delete test1(*p),最后是main(*p2);
内存分配流程如下 :
main(*p);
main(*p)->test1(*p);
main(*p);
main(*p)->main(*p2);

3.测试没有delete的情况

#include<iostream>
void test1()
{
    int *p=new int(1);
	printf("test1 p:%p\n",p);
	//delete p;
}
int main()
{
	int *p=new int(1);
	printf("main p :%p\n",p);
	test1();
	int *p2=new int(1);
	printf("main p2:%p\n",p2);
	delete p;
	delete p2;
	return 0;
} 

运行情况:
在这里插入图片描述
这里我们发现main(*p),test1(*p),main(*p2)成了一条线;
内存分配流程如下:
main(*p);
main(*p)->test1(*p);
main(*p)->test1(*p)->main(*p2);
看样子很完美是吧,但是,我们在调用完test1函数之后,test1§所占用的内存并没有归还(delete),导致这块内存无法使用,这里就发生了内存泄漏.

栈 stack

由编译器自动分配释放,存放函数的参数值,局部变量,(形参)等

代码测试:
1.

#include<iostream>
void test(int x,int y,int z)
{
	int a=1;
	int b=2;
	int c=3;
	printf("test(x):%p\n",&x);
	printf("test(y):%p\n",&y);
	printf("test(z):%p\n",&z);
	printf("test(a):%p\n",&a);
	printf("test(b):%p\n",&b);
	printf("test(c):%p\n",&c);
}
int main()
{
	int a=1;
	int b=2;
	int c=3;
	int d=4;
	int e=5;
	printf("main(a):%p\n",&a);
	printf("main(b):%p\n",&b);
	printf("main(c):%p\n",&c);
	test(a,b,c);
	printf("main(d):%p\n",&d);
	printf("main(e):%p\n",&e);
	return 0;
} 

运行结果:
在这里插入图片描述
分析:
我们先观察:main(&a),main(&b),main(&c);我们初始化abc的顺序是abc,但是在内存中编址确实从大到小。这就说明了栈的内存分布是从高位向低位进行分配的。
观察main(&c)和test(&x):我们发现main(&c)和test(&z)之间相隔比较大。
观察main(&c)和test(&z);我们发现相隔比main(&c)和test(&z)小一些。根据栈的内存分布是从高位向低位进行分配我们推断函数栈的入参方式是从右到左,并且在入参之前还有其他的参数(这里之后在分析).
观察main(&d)和main (&c):正好又相隔四个字节,说明调用test时入栈的参数全部被自动清除

函数入栈方式

这里我们需要了解几个概念:栈帧,寄存器esp, 寄存器ebp
栈帧:每个函数有者自己的栈帧,栈帧中维持着所需要的各种信息(参数,返回地址,参数个数,其他)
寄存器esp:保存当前栈帧的栈顶
寄存器ebp:报错当前栈帧的栈底
寄存器不在C/C++内存模型的讨论范围.

函数栈入参结构:
函数局部变量->(ebp)最右参数->中间参数->最左参数->返回地址->运行时参数(esp)
举例:

#include<iostream>
void test(int x,int y)
{
	int a=1;
	int b=3;
	printf("test(x):%p\n",&x);
	printf("test(y):%p\n",&y);
	printf("test(a):%p\n",&a);
	printf("test(b):%p\n",&b);
}
int main()
{
	int a=1;
	int b=2;
	test(a,b);
	return 0;
} 

运行结果:
在这里插入图片描述

这里C++中的调用test函数的过程我们可以用汇编来改写一下,加强理解:
main函数中

push a
push b;
call test

test函数中


push ebp
mov esp ebp;
//保存现场,储存其他寄存器的值

这是我们的栈帧结构是这样的:
y->x->返回地址->ebp,esp;
我们要取参数需要进行 ebp+4(返回地址),ebp+8等操作;

//之后哦我们申请了a,b;
push a
mov a 1
push b
mov b 1

这是我们的栈帧结构是这样的:
y->x->返回地址->(ebp)a->b(esp);
最后退出函数

ret 8

ret8 的意思时我们两个参数需要将寄存器偏移8个单位;
我们需要将esp偏移,将ebp偏移清空内存,这样我们的函数就算调用完了。
这里我们可以看到ebp和esp的作用:
ebp 可以定位函数形参,定位返回地址,esp可以清空运行时内存.
这里有个问题,我们为什么要从右到左入参呢?
根据栈的特点,第一个形参将在固定的ebp+4的位置,很方便获取方便使用
为什么方便获取第一个参数就是好呢?
这就要说到C中的可变参数函数
在使用可变参数函数的时候必须有第一个参数,根据第一个参数可以定位可变参数的长度,设想一下如果不是从右向左入参,那么地日光参数无法获取,可变参数的长度将无法获取(读取文件可以解决),所以说向右入参可以兼容C语言中的一些设定

全局/静态存储区

全局变量和静态变量被分配到同一块内存中。在C语言中分为两个大类三个小类两个大类(.bss .data),三个小类(全局未初始化(.bss的高地址),静态未初始化(.bss的低地址),已初始化(.data))。在 C++ 中的.bss段,他们共同占用同一块内存区。

代码测试:
1.C语言

#include<stdio.h>
int a;
int b=1;
static int c;
static int d=2;

int main()
{
	printf("a=%p\n",&a);
	printf("b=%p\n",&b);
	printf("c=%p\n",&c);
	printf("d=%p\n",&d);
	return 0;
} 

运行结果:
在这里插入图片描述
分析:
a和b同为全局变量,但是a没有初始化,b初始化,它分布在不同的两个地方(.bss,.data)
a和c同为未初始化变量,但是a不是静态,c是静态, 他们分布在.bss的不同地方
b和d同为初始化变量,d是静态,它们分布在.data的同一个地方
c和d同为静态变量,c没有初始化,d初始化,它们分布在两个不同地方(.bss .data)
由此可知
全局静态存储器可以分为两个大类(.bss .data),三个小类(全局未初始化(.bss的高地址),静态未初始化(.bss的低地址),已初始化(.data))

2.C++测试

#include<iostream>
int a;
int b=1;
static int c;
static int d=2;

int main()
{
	printf("a=%p\n",&a);
	printf("b=%p\n",&b);
	printf("c=%p\n",&c);
	printf("d=%p\n",&d);
	return 0;
} 

运行结果
在这里插入图片描述
分析:
与C语言有区别的是a和c,说明.bss段不在区分全局和静态只有两个大类了。

常量区

存放常量的区间,如字符串常量等,注意在常量区存放的数据一旦经初始化后就不能被修改。 程序结束后由系统释放。

代码测试:

#include<iostream>
const char* p1 ="abcd";

int main()
{
	const char* p2 ="abcd";
	char * p3 = "abcd";
	char * p4 = p3;
	char s[]="abcd";
	char s2[]="abcd";
	s[1]='1';
	//*p1='1';,这一步会报错 
	*s='1';
	printf("%p\n",p1);
	printf("%p\n",p2);
	printf("%p\n",p3);
	printf("%p\n",p4);
	printf("%p\n",s);
	printf("%p\n",s2);
	return 0;
} 

运行结果:
在这里插入图片描述
很明显 p1,p2,p3,p4都是常量,s,s2都是储存在栈区的变量(s到s2的地址在减小)

四.区别

堆和栈的区别
1.管理方式不同
栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。

2.空间大小不同
栈的空间有限;堆内存可以达到4G,。

3.能否产生碎片不同
栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete(delete顺序没有考究,没有用的数据没有马上delete或者太长时间没有用)
会造成内存的不连续,从而造成大量的碎片。

4.生长方向不同
堆的生长方式是向上的,栈是向下的。(堆保存在低地址,栈保存在高地址)

5.分配方式不同
堆是动态分配的(malloc,new)。栈可以是静态分配和动态分配两种(栈的动态分配是由编译器进行释放alloca函数),但是栈的动态分配由编译器释放。

6.分配效率不同
栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 (主要影响原因,内存碎片,底层支持程度)

堆和自由存储区的区别
我们常说,由malloc/free 申请或者销毁的内存分布在堆区,由new/delete申请的内存在自由存储区。
但是new/delete在默认情况下是使用了malloc/free 那我们能不能说自由存储区是堆区的子集呢?不能
在C++中我们可以对new进行重载(实际上new 和 delete 有一种形式不能被重载,之后会谈到),我们可以让对象的内存空间不在堆区而在全局区或者其他。这个时候自由存储区就不是堆区的子集。
自由存储区:由new/delete管理的内存区域(new和delete能管理到的内存区域都可以叫自由存储区,是一个逻辑概念)
堆区:实际内存区的一个固定部分(是一个物理概念)

malloc/free和new/delete的区别:
1.new、delete是C++中的操作符,而malloc和free是标准库函数。操作符可以在类的内部重载,malloc/free不行,唯一的关联只是在默认情况下new/delete调用malloc/free

2.malloc/free(只是分配内存,不能执行构造函数,不能执行析构函数),new/free。

3.new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而 malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。

C++ new/delete 的工作过程

申请的是普通的内置类型的空间:
1.调用 C++标准库中 operator new函数,传入大小。(如果申请的是0byte则强制转化成1byte)
2.申请相对应的空间,如果没有足够的空间或其他问题且没有定义_new_hanlder,那么会抛出bad_alloc的异常并结束程序,如果定义了_new_hanlder回调函数,那么会一直不停的调用这个函数直到问题被解决为止
3.返回申请到的内存的首地址.

申请的是类空间:
1.如果是申请的是0byte,强制转换为1byte
2.申请相对应的空间,如果没有足够的空间或其他问题且没有定义_new_hanlder,那么会抛出bad_alloc的异常并结束程序
3.如果定义了_new_hanlder回调函数,那么会一直不停的调用这个函数直到问题被解决为止。
4.如果这个类没有定义任何构造函数,析构函数,且编译器没有合成,那么下面的步骤跟申请普通的内置类型是一样的。
5.如果有构造函数或者析构函数,那么会调用一个库函数,具体什么库函数依编译器不同而不同,这个库函数会回调类的构造函数。
6.如果在构造函数中发生异常,那么会释放刚刚申请的空间并返回异常
7.返回申请到的内存的首地址
delete 与new相反,会先调用析构函数再去释放内存(delete 实际调用 operator delete)

operator new[]的形参是 sizeof(T)*N+4就是总大小加上一个4(用来保存个数);空间中前四个字节保存填充长度。然后执行N次operator new
operator delete[] 类似;

注意:void* operator new(size_t, void*); // 不允许重新定义这个版本

  • 12
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值