首页 > 系统相关 >初识C/C++内存结构

初识C/C++内存结构

时间:2024-12-20 16:59:00浏览次数:9  
标签:存储 常量 代码 程序 C++ 初识 内存 数据

希望本文有助于学习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分配的内存块,其释放编译器不去管,由程序员自己控制,如果程序员没有释放掉,在程序结束时,系统会自动回收。涉及的问题:“缓冲区溢出”、“内存泄漏”等问题。

六、各个内存区域之间的联系

  1. List item代码区与其他区域
    代码区的指令和只读数据在程序加载时分配,是程序的初始状态,其他区域的数据可以被代码区读取。
  2. 常量存储区和代码区
    常量存储区中存储的数据可以在程序启动时被代码区读取。
  3. 全局/静态存储区与其他区域
    全局变量和静态变量的数据可以在程序的任何地方访问,包括代码区、堆和栈。
  4. 堆区和栈区
    栈主要存储局部变量和函数的调用信息,而堆用于动态分配内存。栈上的指针变量可以存储堆上数据的地址,实现在栈上引用堆上的数据。

这些内存区域协同工作,形成程序的内存布局。栈和堆的管理由程序员自己管理,而代码区、常量存储区、全局/静态存储区由编译器和操作系统管理。在程序执行期间,这些区域协同工作以支持程序的运行、数据存储和动态分配内存。

总结

当你在代码编辑器(Visual Studio 、Clion等)中写下一段代码,按道理讲,你并不需要太在意,你的代码变量被分配到哪个内存区域,也就是说这对程序员来说,基本就是透明的,你可以在一,也可以不在意。但是,作为一个专业的C++使用者来说,你应该对自己所写的代码由比较清晰的把握,清楚自己所写的代码中的变量存在什么区域中,者会有非常大的好处,这不但能够让你写出高性能的代码,还有助于你减少一些比较深层次的BUG。

  • 使用C++时的一些注意事项:
    1.内存泄漏:确保在动态分配内存后及时释放,避免出现内存泄漏问题。
    2.野指针:注意在指针使用时将其置为nullptr,避免野指针的产生,避免访问已经释放的内存。
    3.栈溢出:谨慎使用递归或者分配大量的局部变量,以避免栈溢出。
    4.悬挂指针:避免悬挂指针问题,即指向已经释放了的内存区域。
    5.智能指针:考虑使用C++的智能指针,如std::unique_ptr和std::shared_ptr,以提高内存管理的安全性和便利性。
    6.局部变量生命周期:理解局部变量的生命周期,确保在离开其作用域前不再访问。

标签:存储,常量,代码,程序,C++,初识,内存,数据
From: https://blog.csdn.net/hujiahangdewa/article/details/144609232

相关文章

  • 7-206 6翻了 C++
    “666”是一种网络用语,大概是表示某人很厉害、我们很佩服的意思。最近又衍生出另一个数字“9”,意思是“6翻了”,实在太厉害的意思。如果你以为这就是厉害的最高境界,那就错啦——目前的最高境界是数字“27”,因为这是3个“9”!本题就请你编写程序,将那些过时的、只会用一连......
  • C++实现windows自动化按键
    1.选择目标窗口获取窗口句柄voidKeyPresser::selectWindow(){SetWinEventHook(EVENT_SYSTEM_FOREGROUND,EVENT_SYSTEM_FOREGROUND,NULL,WinEventProc,0,0,WINEVENT_OUTOFCONTEXT);selectedWindowLabel->setText("请点击目标窗口...");}voidCALLBACKKeyPr......
  • 07 C++
    C++1.C++中类成员的访问权限访问权限总结表访问权限当前类内部派生类内部类外部public✔️✔️✔️protected✔️✔️❌private✔️❌❌继承与访问权限基类成员权限公有继承保护继承私有继承public仍为public变为protected变为private......
  • x264 亚像素插值及其内存结构
    参考:https://blog.csdn.net/leixiaohua1020/article/details/459362671.亚像素插值原理先简单介绍一下亚像素插值是如何进行的,基本来自这篇博客https://blog.csdn.net/leixiaohua1020/article/details/45936267h264中像素可以分为整像素、半像素、1/4像素,其中半像素和1/4像......
  • C++学习笔记
    C++学习一、基础语法:1.整型:short:2字节,1字节占8位,可表示的数据范围是-2^15~2^15-1,第一位表示正负,所以数据范围只能是15次方int:4字节long:4字节longlong:8字节2.sizeof统计数据类型(变量)所占内存的字节大小3.浮点型:float:4字节floata=3.14fdouble:......
  • c++自带的比较大小的函数和stl直接取最大最小值的函数
    在比较个数较少的数时,c++函数库自带有max()和min()函数,可以判断其中的最大最小值;代码如下:2个代码时: 多个代码时:你也可以将其赋值给一个变量还有一个STL容器的函数可以返回最大最小值的迭代器,它需要包含头文件<algorithm>执行如下:......
  • C++的类和对象(中)
    类的默认成员函数默认成员函数:用户没有显式实现,编译器会自动生成的成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个(最后两个取地址重载不重要,稍微了解⼀下即可)。其次就是C++11以后还会增加两个默认成员函数,移动构造......
  • C++ ——— 完善日期类
    目录日期类的整体结构获得当月的天数构造函数打印函数拷贝构造函数重载赋值运算符=重载小于运算符<重载全等于运算符==重载小于等于运算符<=重载大于运算符>重载大于等于运算符>=重载不等于运算符!=重载加等运算符+=重载加法运算符+重载减等运算符-=重载减法运......
  • C++ vector动态扩容及动态数组C语言实现
    std::vector是stl中的动态数组,支持动态扩容,stl是如何进行动态扩容的呢?了解其动态扩容过程有什么用?一、探究std::vector动态扩容过程我们通过下面这段代码来了解一下std::vector的动态扩容过程。#include<iostream>#include<vector>intmain(){std::vector<int......
  • Golang内存模型与源码解析
    0、引言本篇笔记用于记录作者在学习Golang的GC模型之前,对Golang内存模型的学习。目前使用的Go版本为1.22.41、Golang内存管理宏观结构假设我们每次向内存池申请空间时,都需要频繁地向操作系统发出请求,这不仅会增加内存分配的时间,还可能引入竞争和锁的开销,从而导致性能瓶颈。尤其......