希望本文有助于学习C++的同学们理解C++的内存结构
路漫漫,道阻且长。
文章目录
一、C++的内存结构是什么?
C/C++不同于其他的语言的其中一个地方就是,其可以直接操纵内存来提成效率。以下是C/C++的内存结构。
如上图我们可以了解到,C/C++的内存结构分为5个区,接下来让我们来详细的来了解一下这些区域的具体信息以及作用。
二、代码区
作用:存储程序的机器码指令,包括执行代码和只读数据。
使用:程序在启动时被加载到内存中,指令和制度数据在代码区执行。
在C++程序中,代码区是存储程序执行代码的一部分内存区域;它通常被划分为两个主要部分:代码段和只读数据段。
代码区通常指的是程序在内存中的一部分,而不是存储在硬盘上的代码。在计算机程序执行时,代码从硬盘加载到内存中,其中的一部分就被分配到代码区,代码区包括代码段和只读数据段,用于存储程序的可执行指令和只读的常量数据。
- 代码段(Text Segment)
- 结构:代码段存储程序的可执行指令,即机器码。这是程序中实际执行的代码部分。
- 使用场景:包括程序的函数、方法、控制流等。这部分内存是只读的,程序在运行时不能修改代码段中的内容。
如下代码示例:
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello World!"<<endl;
return 0;
}
以上例子中的主函数的机器码将会存储在代码段中。
只读数据段(Read-Only Data Segment)
-
结构:只读数据段存储常量数据,例如字符串常量,以及全局变量、静态变量的初始化值。
-
使用场景:用于存储不可修改的数据,字符串字面量是一个常见的只读数据段的例子。
-
示例:在上述例子中,字符串常量"Hello World!"将存储在只读数据段中。
下图为简化的结构示意图:
-
注意事项:
-
代码段和只读数据段通常在程序加载时由操作系统加载到内存中,一旦加载就不能被修改。
-
在函数调用时,函数的机器码也存储在代码段中,每个函数都有其独特的代码段地址。
-
字符串常量等只读数据段中的数据段是不可修改的,任何试图修改这些数据的尝试都将会导致运行时错误。
一般情况下,‘只读’和‘共享’(可复用)是代码区的两个重要特点。
-
只读
代码区的代码段和只读数据段通常是只读的。这就意味着在程序运行时,这些部分的内容是不能被修改的,这有助于确保程序执行期间的数据的一致性和安全性。
例如,程序的机器码、字符串常量等数据是放在只读部分的,防止程序在运行时意外的修改这些数据。 -
共享(可复用)
代码区的内容通常是可共享的,尤其是对于相同的程序的多个实例或同时运行的多个程序来说。多个程序实例可以共享相同的机器码,这有助于节省内存,
共享的代码段通常位于所有程序的相同虚拟地址,但实际的物理内存可能被多个进程共享。
这些特点使得代码区能够更有效的支持多个程序的并发执行,并在运行时提供一定程度的保护,确保代码和制度数据的完整性。在共享库(shared libraries)的概念中,这种可复用性得到了更进一步的利用,允许多个程序共享同一个库的代码段,减少了内存占用。
二、常量存储区
常量存储区通常是指存储程序中的常量数据的一块内存区域。在C++中,常量数据包括字符串常量、全局常量、const修饰的全局/局部变量等,它们的值在程序执行期间不可被修改。这些常量数据通常存储在只读数据段(Read-Only Segment)中。
- 作用:存储不可修改的常量数据,如字符串常量、全局常量、const修饰的变量等。
- 使用:常量数据在程序加载时被分配到内存中,通常以只读的方式存储。存储不可变的常量数据,如字符串常量、常量变量等。
- 结构:常量存储区是程序内存布局的一部分,主要包含只读数据,这些数据在程序执行期间不可被修改。常量数据的存储方式取决于其他类型和声明位置,可能包括字符串常量、全局变量、以及使用const修饰的全局/局部变量。
-代码示例
#include <iostream>
using namespace std;
// 全局变量
const int global=10;
int main()
{
// 字符串常量
const char *p="hello";
//局部常量
const double PI=3.14;
//使用常量
cout<<global<<endl;
cout<<PI<<endl;
cout<<p<<endl;
return 0;
}
在这个例子中,global是一个全局变量,p是一个指向字符串常量的指针,PI是一个double类型的局部常量,这些常量数据在程序执行期间被存储在常量存储区。
注意: 常量存储区的数据是只读的,试图修改这些数据会导致运行时错误。字符串常量通常以null结尾(null-terminated),而全局常量和const修饰的变量的值在编译时就被确定。
三、全局/静态存储区
全局/静态存储区是程序中用于存储全局变量和静态变量的内存区域。这些变量在程序的整个生命周期内都存在,并且内存分配发生在程序启动前,直到程序结束。全局/静态存储区报错两个主要部分:全局变量区和静态变量区。
- 作用:存储全局变量和静态变量,其生命周期贯穿整个程序执行过程。
- 使用:全局变量在程序启动时被分配到内存,静态变量在声明时分配内存,它们的数据在整个程序 执行期间可读可写。
- 全局变量区代码示例
#include <iostream>
using namespace std;
// 全局变量,存储在全局变量区
int global=10;
void func1()
{
//可以访问全局变量global
global++;
}
void func2()
{
//也可以访问全局变量global
global--;
}
int main()
{
//程序入口
func1();
func2();
cout<<global<<endl;
return 0;
}
- 静态变量区
静态变量区用于存储静态变量,即在函数内部声明但是用static关键字修饰的变量,与全局变量一样,静态变量的内存分配发生在程序启动时。适用于函数调用之间保持持久性数据的需求。 - 静态变量区代码示例
void func3()
{
//静态局部变量,存储在静态变量区
static int local=20;
//对静态变量的操作将在函数调用之间保持
local--;
}
int main()
{
func3();//静态变量local被初始化为20;
func3();//静态变量local被减1;
return 0;
}
注意:
全局/静态存储区的数据在程序启动时分配,在程序结束时释放。
全局变量区的数据可以被整个程序访问,而静态变量区的数据尽在声明它的函数内可见。
在多线程环境中,全局变量和静态变量可能需要额外的同步机制,以确保多个线程对它们的安全访问。
四、栈(Stack)区
栈(Stack)是一种内存分配和管理的数据结构,它遵循后进先出(Last In First Out,即LIFO)的原则。栈内存主要用于存储局部变量,函数调用信息以及一些临时数据,是程序执行时自动管理的一块内存区域与。
-
作用:用于存储函数调用信息、局部变量、临时数据等。遵循后进先出原则(LIFO)。
-
使用:每个函数调用都会创建一个栈帧,包含局部变量和函数的调用信息,栈帧在函数返回时被销毁。
-
结构:栈是一种线性数据结构,可以想象成一个具有两个主要操作的容器:压栈(Push)和出栈(Pop)。数据项按照后进后出的次序进出栈。
-
使用场景:存储函数的局部变量,每次函数调用时,其局部变量被分配到栈上,函数返回时这些变量就会被自动释放。
存储函数的调用信息,每次函数调用时,函数的返回地址和一些其他信息被压入栈中,函数返回时再从栈中弹出这些信息。
临时数据的存储,栈也用于一些函数调用过程中的临时数据存储。 -
栈帧
在函数调用时,一个栈帧(Stack Frame)被压入栈中。栈帧包含了函数的局部变量、返回地址和其他调用相关的信息。
当函数返回时,栈帧被弹出,函数的局部变量被销毁、控制流回到调用函数的位置。 -
代码示例
#include <iostream>
using namespace std;
int add(int a,int b)
{
//栈帧开始,局部变量a和b被分配到栈上
int result=a+b;
//栈帧结束,局部变量result、a和b被释放销毁
return result;
}
int main()
{
//栈帧开始,局部变量x和y被分配到栈上
int x=10,y=20;
int sum= add(x,y);//函数调用,新的栈帧被压入栈中
//栈帧结束,局部变量x、y和sum被释放销毁
cout<<sum<<endl;
return 0;
}
在这个例子中,main函数和add函数的栈帧依次被创建和销毁,add函数的局部变量和临时变量都存储在栈上,随着函数的返回,这些变量也被销毁,函数调用过程中的栈帧操作清晰地展示了栈的特性。
注意:
栈的管理通常由编译器负责。编译器根据程序的结构和函数调用关系来分配和管理栈空间。在编译阶段,编译器会生成一些代码来处理栈的操作,包括栈帧的创建和销毁,局部变量的分配和释放,以及函数调用时相关的操作。
编译器在编译源代码时会:
1.分析函数调用关系:编译器需要了解程序中函数的调用关系,以便正确生成栈帧和处理函数调用时的参数传递和返回值。
2.分配栈空间:对于每个函数,编译器需要决定分配多少空间用于栈帧,以容纳局部变量、函数参数、返回地址等。
3.生成栈操作指令:编译器会生成相应的汇编或机器码指令,用于执行栈的压栈和出栈操作,以及处理函数调用时的栈操作。
虽然编译器负责生成大部分与栈相关的代码,但在一些特殊情况下,程序员也可以通过手动操作栈指针来进行底层的栈管理。在汇编语言等低级语言中,程序员有更多的控制权,可以直接操作栈。在高级语言中,这种底层的栈操作通常由编译器自动处理。
栈 Stack:时那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。存放在栈中的数据只在档期那函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。
五、堆区
堆时程序运行时用于动态分配内存的一种内存区域,也称为自由存储区。堆上的内存可以在运行时动态地分配和释放,由程序员负责管理。与栈不同,堆上的内存分配和释放不受程序的执行顺序限制。
- 作用:用于动态分布内存,存储在堆上的数据的生命周期由程序员自己管理。
- 使用:通过C++的new操作符或C语言的malloc等操作在堆上分配内存,程序员负责在合适的时间使用delete或free操作符释放内存,堆上的数据可以在程序的不同部分共享。
- 结构:堆时由操作系统分配的一块较大的内存区域,程序运行时可以在堆上动态地分配内存。堆上的内存分配和释放是由程序员手动控制的。
- 代码示例:
分配内存
//在堆上分配整数数组
int *arr=new int[10];
释放内存
//释放堆上数组
delete [] arr;
完整示例:
int main()
{
//在堆上分配整数数组
int *arr=new int;
*arr=42;
//在堆上分配一个字符串
char *str=new char[10];
strcpy(str,"Hello");
//使用分配的内存
cout<<*arr<<endl;
cout<<str<<endl;
//释放堆上的内存
delete arr;
delete [] str;
return 0;
}
在这个例子中,通过new操作符在堆上分配了一个整数和一个字符串,并使用delete操作符释放了这些内存,这样的动态内存管理允许程序在运行时动态地创建和销毁对象,增加了灵活性,但需要注意,堆上的内存分配和释放需要程序员小心管理,以避免内存泄漏或悬挂指针等问题。在现代C++中,推荐使用智能指针等资源管理工具来减少手动内存管理的复杂性。
堆Heap:由new分配的内存块,其释放编译器不去管,由程序员自己控制,如果程序员没有释放掉,在程序结束时,系统会自动回收。涉及的问题:“缓冲区溢出”、“内存泄漏”等问题。
六、各个内存区域之间的联系
- List item代码区与其他区域
代码区的指令和只读数据在程序加载时分配,是程序的初始状态,其他区域的数据可以被代码区读取。 - 常量存储区和代码区
常量存储区中存储的数据可以在程序启动时被代码区读取。 - 全局/静态存储区与其他区域
全局变量和静态变量的数据可以在程序的任何地方访问,包括代码区、堆和栈。 - 堆区和栈区
栈主要存储局部变量和函数的调用信息,而堆用于动态分配内存。栈上的指针变量可以存储堆上数据的地址,实现在栈上引用堆上的数据。
这些内存区域协同工作,形成程序的内存布局。栈和堆的管理由程序员自己管理,而代码区、常量存储区、全局/静态存储区由编译器和操作系统管理。在程序执行期间,这些区域协同工作以支持程序的运行、数据存储和动态分配内存。
总结
当你在代码编辑器(Visual Studio 、Clion等)中写下一段代码,按道理讲,你并不需要太在意,你的代码变量被分配到哪个内存区域,也就是说这对程序员来说,基本就是透明的,你可以在一,也可以不在意。但是,作为一个专业的C++使用者来说,你应该对自己所写的代码由比较清晰的把握,清楚自己所写的代码中的变量存在什么区域中,者会有非常大的好处,这不但能够让你写出高性能的代码,还有助于你减少一些比较深层次的BUG。
- 使用C++时的一些注意事项:
1.内存泄漏:确保在动态分配内存后及时释放,避免出现内存泄漏问题。
2.野指针:注意在指针使用时将其置为nullptr,避免野指针的产生,避免访问已经释放的内存。
3.栈溢出:谨慎使用递归或者分配大量的局部变量,以避免栈溢出。
4.悬挂指针:避免悬挂指针问题,即指向已经释放了的内存区域。
5.智能指针:考虑使用C++的智能指针,如std::unique_ptr和std::shared_ptr,以提高内存管理的安全性和便利性。
6.局部变量生命周期:理解局部变量的生命周期,确保在离开其作用域前不再访问。