欢迎大家订阅我的《计算机底层原理》、《深度解析C++》、《自顶向下看Java》专栏,能够帮助到大家就是对我最大的鼓励。
目录
系列文章目录
前言
一、栈区
1.函数调用:
1.保存当前执行状态:
2.为新函数创建栈帧:
3.传递参数:
4.跳转到函数代码:
5.执行函数:
6.返回地址:
2.栈的管理
1.函数调用时栈的管理
压栈:
传递参数:
2.函数返回时的栈管理
保存返回值:
弹栈:
跳转到返回地址:
3.主函数栈帧
二、堆内存(可以共享)
1.动态内存分配
1.堆的性质:
2.灵活性和潜在风险:
3.堆内存存储哪些数据
1.动态数组:
2.对象:
3.动态或大型数据结构:
4.字符串:
1.全局变量:
2.静态变量:
3.初始化和默认值:
4.内存分配和释放:
拓展:局部静态变量和全局静态变量的区别
1.作用域:
2.生命周期:
3.访问权限:
四、常量区(可以共享)
1.存储常量数据:
2.只读的性质:
3.字符串常量:
4.常量区的初始化:
5.节省空间:
五、代码区
1.存储机器指令:
2.程序计数器:
3.执行权限:
4.共享性质:
拓展:函数到底是怎么调用的,对象当中存放了些什么?
1.函数的地址绑定:
2.函数的调用:
3.对象的内存布局:
总结
今天这篇文章主要为大家介绍以下C++当中的内存分区,已经带大家深刻地认识一下C++当中成员函数的调用过程,以及C++对象的布局,这将为我们后续的学习提供很大的帮助。
栈用于存储局部变量和函数调用信息的,每次调用函数的时候栈都会分配一块内存用于存储函数的局部变量、参数和一些其他信,栈的内存管理是由编译器自动进行的,变量的生命周期与其所在函数的执行期间相对应,下面我为大家详细地解释C++当中的栈区的具体功能!
1.函数调用:
当程序执行到一个函数调用语句的时候,它需要在内存当中位即将执行的函数创建一个执行环境以便存储该函数执行过程当中的各种信息,这个执行环境通常被称为栈帧,而整个存储这些栈帧的数据结构被称为调用栈,当栈帧当中执行函数调用的时候通常需要以下过程。
1.保存当前执行状态:
当程序执行到一个函数调用语句的时候,当前函数的执行状态包括局部变量、函数参数当前位置信息等都需要被保存下来,这是为了确保在函数执行完之后能够回到调用的地方,继续执行。
2.为新函数创建栈帧:
为即将执行的函数调用栈上分配一个新的内存区域,这个区域即为新函数的栈帧,栈帧包含了函数的局部变量、参数、返回地址等信息。
3.传递参数:
将调用函数时提供的参数传递给即将执行的函数,这些函数通常被存储在新函数的栈帧当中。因为当我们调用一个函数并且为他们传参的时候,这些参数的值需要被传递到即将执行的函数内部,以便函数能够使用这些值进行计算或者处理,这个过程涉及将参数的值传递给被调用函数,并在被调用函数内部存储这些值。
4.跳转到函数代码:
执行程序跳转到即将执行的函数代码的开始处、开始执行函数体内的语句。
5.执行函数:
函数体内的代码开始执行,操作栈帧当中的各种局部变量和参数进行系列的运算等等。
6.返回地址:
当函数执行完成以后需要返回到调用他的地方买这个时候会使用保存在栈帧当中的返回地址,程序跳转到这个地址继续执行。
这整个过程确保了程序的控制流在函数调用和返回之间能够正确地进行切换,同时保留了每个函数执行时的局部状态,这种使用调用栈的方式使得程序能够正确灵活地管理函数的嵌套调用和返回,确保了程序的正确性和可维护性。
2.栈的管理1.函数调用时栈的管理
压栈:
在函数调用之前,编译器会生成指令将当前函数的执行状态包括局部变量、函数参数、返回地址等信息压入调用栈、这通常通过将栈帧的大小分配给栈空间、并且将相信的数据复制到栈空间当中完成。
传递参数:
编译器会生成指令跳转到被调用函数的代码开始处,开始执行函数体
2.函数返回时的栈管理保存返回值:
如果函数有返回值,编译器会生成指令将函数计算的返回值存储在适当的寄存器或者内存的某个区域。
弹栈:
编译器生成指令将当前函数的栈帧从调用栈当中移除,即弹出栈帧,这样就会释放栈空间,同时恢复调用前的执行状态。
跳转到返回地址:
编译器生成指令将程序的控制流跳转到调用函数时保存的返回地址,继续执行调用函数处之后的代码。
3.主函数栈帧C++当中的主函数main函数也会在程序执行的时候创建一个栈帧,栈帧是用来管理函数调用和局部变量的内存空间的一种机制,当程序执行到一个函数的时候,会在栈上分配一块内存空间,用于存储函数的局部变量、参数以及函数调用相关的信息。
主函数作为程序的入口,同样会有一个对应的栈帧,在主函数当中声明的局部变量和函数参数都将被存储在主函数的栈帧当中,当主函数执行完毕的时候,其栈帧会被销毁释放相应的空间。
1.动态内存分配
堆是在程序运行时动态分配内存的地方,与栈上的内存分配不同、堆上的内存分配全部都是手动管理的,程序员可以通过操作符例如new或者malloc来请求堆上面的一块内存,分配之后程序员必须手动地释放,否则就会导致内存泄漏。
1.堆的性质:
堆的大小可以根据需要动态地增长或者减小、在堆上面分配的内存不会随着函数的调用而销毁、而是需要显示地释放,在C++当中使用new操作符来动态地分配内存、而在C语言当中则需要使用malloc函数、这些操作符或者函数或函数返回一个指向分配内存的指针。
2.灵活性和潜在风险:
动态内存分配提供了灵活性、因为程序员可以根据需要在运行的时候动态分配内存,然而这也带来了一些潜在的风险,例如内存泄漏、野指针等问题,因此在使用动态内存分配的时候程序员需要特别注意正确的内存管理和释放。
3.堆内存存储哪些数据
1.动态数组:
当数组的大小在编译的时候无法确定,或者需要在运行的时候动态地调整数组的大小时,程序员通常会使用堆上的动态数组,这样可以使用new或者malloc来分配适当大小的内存。
2.对象:
对象的生命周期需要超出其所在的作用域的时候,可以将对象放置在堆上,这样可以在需要时手动分配和释放对象的内存空间,而不是依赖于栈上的自动变量。
3.动态或大型数据结构:
例如链表、树等动态数据结构,或者某些大型数据结构无法在栈上开辟空间或者内存的时候可以考虑将它们放置在堆上,这样可以避免栈空间的不足问题。
4.字符串:
堆上存储字符串允许它们的长度在运行时发生变化,而不受栈上内存的限制。
三、静态区(全局区(可以共享))
全局区或者静态区是程序内存当中的一部分,用于存储全局变量和静态变量,这个区域的特点就是程序的整个运行周期内它们都存在。
1.全局变量:
全局变量是在程序的任何地方都可以访问的变量,它们在静态区被分配内存,并且在程序启动的时候初始化,全局变量的生命周期从程序启动的时候到结束,它们的值在整个程序执行期间保持不变。
2.静态变量:
静态变量是具有静态存储期,这意味着它们在整个程序的执行期间都存在,静态变量可以是全局的,也可以是局部的,局部静态变量在函数第一次调用的时候初始化,但是只在程序的生命周期内分配一次内存。
3.初始化和默认值:
在静态区,分配的全局变量和全局静态变量在程序启动的时候会被自动初始化,如果没有赋予它们显示的初始值,它们将被设置为默认值,通常是零或者空,具体取决于数据类型。
4.内存分配和释放:
静态区的内存分配是在程序启动的时候由操作系统分配的,在程序结束的时候由操作系统释放,这使得全局变量和全局静态变量的内存管理是由系统自动处理的,静态区(也叫全局区)提供了一个存储整个程序生命周期内都可以访问数据的地方,全局变量和全局静态变量它们的生命周期与程序的运行时间相对应。
拓展:局部静态变量和全局静态变量的区别
1.作用域:
局部静态变量只能在声明他的函数内部或者块内部可见,一旦离开作用域就不可以再被访问。
2.生命周期:
局部静态变量和全局静态变量一样都是再程序的整个生命周期内存在,它们再程序启动的时候初始化,知道程序的结束,与静态变量不同的是全局静态变量的初始化时机与函数调用无关,它们再程序启动的时候进行初始化。
3.访问权限:
局部静态变量只能够在声明它们的函数内部访问,具有局部的访问权限。全局静态变量则可以在整个程序内部都可以访问拥有全局访问权限。
常量区用于存储常量数据、例如字符串常量,这部分内存通常是只读的,程序运行期间不能够被修改
1.存储常量数据:
常量区主要用于存放程序当中的常量数据,包括字符串常量,全局常量等这些数据在程序运行期间保持不变,因此被放置在只读的常量区
2.只读的性质:
常量区的内存通常是只读的,这意味着程序在运行期间不能够修改常量区当中的数据,这种只读性质确保了存储在常量区的常量数据在整个执行期间的稳定性质。
3.字符串常量:
字符串常量是常量区常见的一种数据类型,例如C语言,字符串通常可以通过双引号括起来,例如"Hello World",这些字符串存储在常量区、并且这些常量的值在程序运行期间是不可变的。
4.常量区的初始化:
常量区当中的数据在程序启动的时候由编译器进行初始化,字符串常量和全局常量的值在编译的时候就被确定,并且在程序执行之前就被存储在常量区当中。
5.节省空间:
常量区的只读性质允许多个部分的程序共享相同的常量数据,从而节省内空间。总的来说常量区提供了一个安全且只读的存储位置,用于存储在程序执行期间不会改变的常量数据,例如字符串常量和全局常量,这种设计有助于确保常量数据的稳定性,并在需要时共享相同的常量值。
代码区存储程序的执行代码,这部分是只读的,并且存储程序的机器指令,用于存储程序的执行代码,也被称为文本段或者指令段,代码区存放的通常是函数体的机器指令、其中包含程序当中的各种函数、控制结构、操作等实现的具体实现。以下是代码区的一些重要特点。
1.存储机器指令:
代码区存储了被编译或者汇编后的机器指令,这是计算机能够直接执行二进制形式的指令,这些指令是程序的实际代码用于完成各种操作和任务。
2.程序计数器:
程序计数器是一个寄存器、存储着当前正在执行的指令的地址,它通常指向代码区的下一条指令,随着程序的执行,程序计数器逐步递增使得程序能够按顺序执行指令。
3.执行权限:
代码区的执行权限是由操作系统来控制的,程序加载到内存的时候,操作系统将代码区标记为只读,以防止在运行时堆代码的修改,这种保护措施有助于防止恶意软件的攻击和确保程序的正确执行。
4.共享性质:
通常多个相同程序的实例可以共享相同的代码区,这种共享性质可以节省系统内存,并且允许多个进程或者实例同时运行相同的程序代码。
当对象调用函数的时候从底层或者编译的角度来看的话主要涉及到两个方面,函数的地址绑定和函数的调用。
1.函数的地址绑定:
在编译的阶段,编译器会根据对象的类型来确定调用的函数,对于非虚成员函数的调用,编译器会直接将函数调用绑定到该函数的地址处,这是因为非虚函数的调用在编译期间就能够确定了,编译器会自动生成代码,使得对象的成员函数调用直接映射到相应的函数地址处。
2.函数的调用:
这里其实就涉及到了刚才我给大家讲解的关于代码区当中的内容,所有函数的函数体实现包括相应的机器指令经过编译器翻译之后全部变成机器码存储到代码段当中,当对象调用成员函数的时候实际上执行的是该函数的机器码指令,编译器在编译的时候已经将成员函数调用转化为直接的函数地址,当程序执行到成员函数调用的地方的时候,就会跳转到该函数的地址处执行相应的指令,这里的函数地址说白了就是成员函数的函数实现在代码区当中的内存地址。
这样来看非虚函数调用是一种直接的,静态的调用方式,编译器在编译的时候就能够确定具体调用的函数地址,因此不需要在运行的时候进行函数查找,这提高了调用的效率。
因为C++当中有一个与多态相关的概念就是虚函数,后续讲到多态的时候再为大家讲解,这里就不做赘述了。
3.对象的内存布局:那么到这里我回答第二个问题,C++当中的对象到底存放了什么内容呢?
其实很简单,对象通过new关键字创建存储在堆上,(C++的对象也可以不适用new存储在栈上),对象包含成员函数的函数地址,还有一系列的成员变量,这些变量已经在调用构造函数的时候被初始化了,当我们调用关键字new的时候,new关键字就会为我们自动在堆上开辟空间,并且自动调用构造函数进行初始化成员变量,如果有虚函数的话,对象会包括虚函数指针指向虚函数表。虚函数指针和虚函数表都存储在对象里面,每一个对象都有一个虚函数表。
所以概括下来对象的内存布局包括以下几点:
对象内存布局 成员变量 函数指针(针对非虚成员函数) 虚函数指针(针对虚成员函数) 虚函数表 这就是对象的内存布局,相信我说到这里的时候,大家对对象以及C++的内存布局已经有了一个深刻的认识。
这篇文章的篇幅虽然短,但是内容都非常地重要,C++这门语言他的学习门槛还是比较高的,需要我们熟练地掌握对这门语言的底层理解,C++内存布局将对我们后续理解学习继承和多态的相关概念都非常地重要,希望我的文章能够帮助到大家。