C++内存分区模型

这篇具有很好参考价值的文章主要介绍了C++内存分区模型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

C++内存分区模型

在执行C++程序的过程中,内存大致分为四个区域:

  1. 栈区(Stack):用于实现函数调用。由编译器自动分配释放,存放函数的参数值和局部变量等

  2. 堆区(Heap):用于存放动态分配的变量。由程序员动态分配和释放,使用new和delete操作符

  3. 全局/静态存储区(Data Segment & BSS Segment):存放全局变量和静态变量,程序结束时释放

    数据段 Data Segment (全局/静态存储区) : 存放初始化了的全局变量和静态变量

    BSS段 BSS Segment : 用于存放未初始化的全局变量和静态变量,节省空间,实际上不占用磁盘空间

  4. 代码区(Text Segment):通常也被称为文本区或只读区。存放程序的二进制代码和常量,代码段是只读的,可以被多个进程共享

也有人认为 常量存储区 在内存中是独立的,C++标准并没有明确将常量存储区单独列为内存分区模型的一部分。因此,内存分区模型的确切细节可以根据不同的观点和上下文而有所不同

注意:不同的操作系统对程序内存的管理和划分会有所不同。上述的C++内存区域划分主要是针对通用的情况,并不限定在某个特定操作系统上

1. 代码区

代码区 (Code Segment) 也被称为文本段 (Text Segment) 或者只读区,主要包括可以执行的文件 ELF (Executable and Linkable Format) 和常量

代码区(Code Segment)也被称为文本段(Text Segment)或只读区,它有以下几个主要特征:

  1. 代码区是程序的静态存储区域,存放程序执行所需的机器指令和常量数据,如字符串字面量和 const 变量的初始化值
  2. 代码区的内容为只读属性,不允许修改程序中的代码,保障程序的安全性和稳定性
  3. 程序运行前,代码区的内存大小已在可执行文件中确定,加载时系统会自动分配适当的内存
  4. 多个进程可以共享相同的代码区,节省内存空间
  5. 为提高效率,一些字符串字面量 (例如 "Hello") 可能会存放在共享的只读数据区而不是代码区
  6. 若代码区中的常量初始化需要运行时计算,会将其放在数据区而不是代码区
  7. 代码区也包含只读数据,如跳转表和常量表等
  8. 程序运行时,代码区通常不会改变大小,若需扩展,依赖操作系统提供的机制

2. 全局/静态存储区

全局/静态存储区主要包括以下两个部分:

  1. 数据段(Initialized Data Segment)

    • 用于存放初始化了的全局变量和静态变量

    • 存储在此段的数据在程序运行前分配,运行结束后释放

    • 有初始化值的全局变量和静态变量存放在此

    • 数据段属于可读可写区域

  2. BSS段(Block Started by Symbol)

    • 用于存放未初始化的全局变量和静态变量

    • 不占用实际的磁盘空间,只在编译时预留内存空间

    • 无初始化值的全局变量和静态变量存放在此

    • 程序启动时会自动初始化为默认值

    • 属于可读写区域

两者的主要区别在于初始化状况。全局变量和静态变量可以显式初始化,如果没有显式初始化,它们会被自动初始化为默认值(0 或 nullptr,取决于变量的类型),该区域的数据在程序整个运行周期中一直存在

3. 栈区

栈区是用于实现函数调用和局部变量存储的一种内存区域。在 C++ 中,每当调用一个函数时,系统会自动在栈区为该函数分配一块内存,称为栈帧(Stack Frame)。栈帧用于存储函数的参数值、局部变量以及函数执行期间的一些控制信息 ^f6a053

以下是栈区的一些关键特点:

  1. LIFO(Last-In-First-Out)原则:栈区采用后进先出的原则,即最后压入栈的数据会最先弹出。这是因为每次调用函数时,会将函数的栈帧压入栈顶,函数执行结束后,栈帧会从栈顶弹出
  2. 自动分配和释放:栈区的内存分配和释放是由编译器自动管理的,当进入函数时,会为该函数分配一块连续的内存区域,并在函数返回时自动释放这块内存。这样的自动分配和释放使得栈区的内存管理相对高效,但也意味着栈上的数据生命周期必须在函数调用内部
  3. 局部变量存储:栈区主要用于存储函数的局部变量,这些变量的生命周期与函数的调用和返回相对应。当函数调用结束,栈帧会被销毁,其中的局部变量也会被销毁,因此在函数外部无法访问这些局部变量
  4. 函数调用:当调用函数时,函数的参数值和返回地址会被压入栈帧中,函数执行过程中的其他局部变量也会存储在栈帧中。函数返回时,栈帧会从栈顶弹出,恢复调用函数的现场
  5. 栈溢出:栈区的大小是有限的,如果递归调用过深或者函数中使用了大量的局部变量,可能导致栈溢出(Stack Overflow)错误,即栈区的内存已被耗尽
  6. 线程私有:每个线程都有自己的栈区,栈区的内存是线程私有的,不同线程之间的栈区不共享

3.1 栈溢出

栈溢出(Stack Overflow)是指程序在运行过程中,不断调用函数,导致栈空间被占满,无法再分配新的栈帧。栈是一种先进后出的数据结构,用于存储局部变量、参数、返回地址等信息。栈的大小是有限的,一般为8~10MB。栈溢出通常发生在递归调用过深或死循环的情况下

以下是一个程序在栈区的示例图。在主函数调用自定义的函数,函数内部形成递归

C++内存分区模型

当程序一直运行时,递归的函数会不断占用栈区的内存空间,从而导致 栈溢出

C++内存分区模型

3.2 缓冲区溢出

在C++中,缓冲区溢出通常发生在栈区。栈区用于存储函数调用时的局部变量和函数调用的返回地址

当函数调用时,函数的栈帧(包含局部变量和其他控制信息)被压入栈中。如果函数中使用了缓冲区(数组)并且没有进行足够的边界检查,很容易导致写入超出缓冲区边界的数据,从而覆盖栈上其他变量和控制信息,这就是缓冲区溢出

例如以下代码就是一个缓冲区溢出的案例:

#include <iostream>

int main()
{
    //创建并初始化数组
    char arr[3] = {'a', 'a', 'a'};

    //向数组写入数据,但超出了数组的边界
    for (int i = 0; i <= 4; i++)
    {
        arr[i] = 'b';
    }
}

我们在合适的位置添加调试断点,并对数组arr添加监视,观察内存变化

C++内存分区模型

找到数组对应的地址,观察内存中的变化

C++内存分区模型

按下F11逐语句调试,发现数组越界后仍然向未被允许访问的内存中写入数据

C++内存分区模型

显示数组所申请的内存附近堆栈被损坏,这就是缓冲区溢出的危害

C++内存分区模型

参考:缓冲区溢出原理——作者:程序员老马

3.3 数据不稳定

栈区中的局部变量生命周期与函数的调用和返回相关。当函数返回后,栈帧会被销毁,其中的局部变量也会被销毁。如果在函数内部使用了指向栈区局部变量的指针,并在函数返回后继续使用这些指针,会导致悬空指针(Dangling Pointers)也就是野指针的问题,访问已被销毁的局部变量的内存区域,可能引发未定义的行为

下面是示例代码,证明栈帧销毁后局部变量也被销毁,造成悬挂指针的问题:

#include <iostream>

int* dangPtr;

void test() {

	int x = 10;
	dangPtr = &x;

	std::cout << "dangPtr in test: " << *dangPtr << std::endl;
}

int main() {

	test();
	std::cout << "dangPtr in main: " << *dangPtr << std::endl;

	return 0;
}

在函数test调用完成之后,此时test函数中的所有局部变量已经被销毁了。这时候指针dangPtr指向的变量x的内存地址被系统释放,指针变为悬空指针 (Dangling Pointers) 也就是野指针,这在内存中是非法的,引发程序中断

C++内存分区模型

此时函数还未结束,局部变量x还存在没有被系统销毁

C++内存分区模型

此时test函数执行结束,局部变量被系统销毁,同样的变量x所存的值也被销毁

这时候指针解引用访问地址后的值就变成了野指针,非法访问未申请的内存

C++内存分区模型

3.4 栈帧重用(Stack Frame Reuse)

栈帧重用(Stack Frame Reuse) 是一种编译器优化技术,旨在减少函数调用时的栈内存分配和释放开销。它是通过在不同函数调用中复用栈空间来实现的,使得多个函数调用可以共享同一块栈空间

除了"栈帧重用",这种优化技术也可能被称为其他类似的术语,例如:

  1. 栈内存优化(Stack Memory Optimization):指代对栈内存的优化措施,其中栈重用是可能的一种优化方式
  2. 栈空间复用(Stack Space Reuse):强调的是复用栈空间,使得多个函数调用可以共享同一块栈空间

栈帧重用是一种现象,出现在自动变量(由编译器自动分配并回收栈上生命周期仅在函数内的变量)位于函数的栈帧上时。在函数执行过程中,每个函数都会为其局部变量在栈上分配内存空间,并在函数结束时释放这些栈内存。当后续函数再次向栈上请求内存时,有可能会获得先前函数释放的栈内存。如果后续函数中出现了和先前函数相同名称和类型的局部变量,那么这些变量的地址可能会重叠,即它们在同一片栈内存区域中

栈区采用自动管理机制,编译器可能出于效率考虑而选择复用先前函数释放的栈空间。这种自动的栈帧重用特性对于那些只关心变量生命周期而不依赖于其地址的代码来说,并不会产生影响

#include <iostream>

void funcA() {
    int a = 10;
    std::cout << "Address of variable 'a' in funcA: " << &a << std::endl;
}

void funcB() {
    int b = 20;
    std::cout << "Address of variable 'b' in funcB: " << &b << std::endl;
}

int main() {
    funcA();
    funcB();
    return 0;
}

输出结果:

Address of variable 'a' in funcA: 0000009F4A6FF884
Address of variable 'b' in funcB: 0000009F4A6FF884

C++内存分区模型

如果确实需要保证变量地址不变,可以采用以下方法:

  1. 使用 static 关键字:在函数内部声明 static 局部变量,这样变量的生命周期将持续到程序的整个执行过程,而不是只在函数执行时存在
  2. 将变量置于堆或全局区中:通过使用动态内存分配(例如 newmalloc 等)在堆上分配内存,或者将变量声明为全局变量,可以保证其地址在函数调用之间不变

需要注意的是,栈帧重用是编译器的一种优化行为,开发者不需要显式地实现它。了解栈帧重用的概念有助于理解编译器的优化机制,但在实际编程中,重要的是编写简洁、易读和正确的代码,而非过度关注微观优化。优化应该基于实际的性能分析和需求。

4. 堆区

当涉及堆区时,需要理解动态内存分配的概念。堆区是程序运行时用于存储动态分配的数据的一部分内存,它的管理与栈区不同

  1. 动态内存分配:在堆区进行动态内存分配意味着程序员可以在运行时请求额外的内存空间来存储数据。与栈区不同,栈区的内存是在编译时自动分配和释放的,而堆区的内存是在运行时手动申请并在不再使用时手动释放

  2. 操作符new和delete:在C++中,使用new操作符可以在堆区动态地分配内存。new返回所分配内存的指针,该指针指向存储分配的数据的堆区内存。而使用delete操作符可以释放堆区内存并将其返回给操作系统,以便其他程序可以使用。注意,堆区内存的释放是程序员的责任,避免内存泄漏

    cppCopy codeint* ptr = new int; // 分配一个int大小的堆区内存
    *ptr = 10; // 将值10存储在堆区内存中
    delete ptr; // 释放堆区内存
    
  3. 生命周期:堆区的生命周期由程序员控制。在使用new分配内存后,内存将一直存在,直到使用delete释放内存或程序结束。如果忘记释放内存,将导致[[^内存泄漏]],堆区的内存将永远无法回收,直到程序结束

  4. 不稳定性:由于堆区的内存是手动管理的,如果程序员使用指针错误地引用已释放的堆区内存(悬空指针),或者释放后继续访问已释放的内存(野指针),会导致不稳定性和未定义行为

  5. 堆区碎片:随着时间的推移,动态内存的频繁分配和释放可能导致堆区出现碎片。堆区碎片是指堆中剩余的不连续、无法利用的小块内存。虽然这不会直接影响程序的正确性,但在某些情况下,可能会降低内存的利用率

  6. 线程共享:堆区内存可以在线程之间共享,多个线程可以访问和使用堆区的相同内存。这使得堆区在多线程编程中非常有用,但也需要注意同步和避免竞争条件

  7. 异常安全性:由于堆区内存的手动管理,需要特别注意异常安全性。如果在使用new分配内存后,出现异常而未能释放内存,可能导致内存泄漏。为了确保异常安全性,可以使用智能指针等资源管理工具来管理动态内存,避免手动释放内存的繁琐工作

4.1 内存溢出

内存溢出(Out of Memory)是指程序在运行过程中,创建了大量的对象或线程,导致堆空间或方法区空间被占满,无法再分配新的内存

堆和方法区是两种动态分配的内存区域,用于存储对象实例、类信息、常量等信息。堆和方法区的大小是可配置的,一般为几百MB到几GB。内存溢出通常发生在对象或线程泄漏、内存分配过大、GC效率低下的情况下

#include <iostream>

int main() {
    // 请求分配一个非常大的整数数组(堆区内存)
    // 这里如果开辟单个数组没办法报错
    // 所以为了模拟更确切的情况,假设就在程序中一次性开辟很大的空间
    int* hugeArray0 = new int[999999999];
    int* hugeArray1 = new int[999999999];
    int* hugeArray2 = new int[999999999];
    int* hugeArray3 = new int[999999999];

    // 使用堆区内存,可能导致内存溢出
    for (int i = 0; i < 999999999; i++) {
        hugeArray0[i] = i;
    }

    // 不要忘记释放堆区内存
    delete[] hugeArray0;

    system("pause");
    return 0;
}

C++内存分区模型

C++内存分区模型

内存分配失败,抛出 std::bad_alloc 异常并终止执行,而且可以看到右侧进程内存已经快达到极限了

C++内存分区模型

4.2 内存泄漏

堆区的内存泄漏是指在程序中动态分配了内存(使用 newmalloc 等操作符),但在不再需要这些内存时未及时释放,导致程序无法再访问这些内存,从而造成了资源浪费

内存泄漏是一种常见的编程错误,特别是在长时间运行的程序中,如果频繁地分配内存而不释放,最终可能会耗尽可用内存,导致程序崩溃或系统变慢

#include <iostream>

int main() {
    while (true) {
        // 在堆区动态分配一个整数数组,但未释放内存
        int* ptr = new int[1000];
        // 未释放内存,重复分配,导致内存泄漏
    }

    return 0;
}

C++内存分区模型

4.3 多重释放与非法指针

多重释放:在堆区释放了内存后,如果再次尝试释放相同的内存,就会导致多重释放问题。这会破坏堆的内存管理结构,可能导致程序崩溃

#include <iostream>

int main()
{
	int* arr = new int[10];

	delete[] arr;
	delete[] arr;

	system("pause");
	return 0;
}

C++内存分区模型

C++内存分区模型

非法指针:在堆区释放了内存后,如果仍然保留指向该内存的指针,就会导致悬空指针。而指向未初始化或已释放的内存的指针称为野指针。使用悬空指针或野指针可能导致程序崩溃或产生不可预测的行为

#include <iostream>

int main()
{
	char* ch = new char[10];
	for (int i = 0; i < 10; i++)
	{
		ch[i] = 'a';
	}
	char* ptr = ch;

	std::cout << ptr << std::endl;
	delete[] ch;
	std::cout << *ptr << std::endl;
	std::cout << *ptr << std::endl;

	return 0;
}

C++内存分区模型

C++内存分区模型

4.4 产生堆内存碎片

内存块碎片化: 多次动态分配和释放堆区内存可能导致内存块碎片化,使得分配大块连续内存变得困难,从而降低堆区内存的效率

#include <iostream>

int main() {
    const int N = 1000;
    const int M = 100;

    // 创建一个数组用于存储指向动态分配的内存块的指针
    int* ptrArray[N];

    // 模拟多次动态分配和释放内存
    for (int i = 0; i < N; i++) {
        // 每次动态分配 M 个 int 大小的内存块
        ptrArray[i] = new int[M];

        // 将内存块清零,以模拟实际使用场景
        for (int j = 0; j < M; j++) {
            ptrArray[i][j] = 0;
        }
    }

    // 输出每个内存块的首地址,用于观察地址分布情况
    for (int i = 0; i < N; i++) {
        std::cout << "Block " << i + 1 << ": " << ptrArray[i] << std::endl;
    }

    // 释放动态分配的内存
    for (int i = 0; i < N; i++) {
        delete[] ptrArray[i];
    }

    return 0;
}

C++内存分区模型

参考:C 程序内存管理中,容易使内存碎片化的 <堆>——作者:考鼎录文章来源地址https://www.toymoban.com/news/detail-597872.html

到了这里,关于C++内存分区模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • c++内存的四大分区详解

    目录 前言: 1、程序的基本运行流程 2,为啥要分为四个区域? 3,分为哪四个区域? 4,4个区域详解 代码区: 为什么会设置这两个功能呢? 全局区: 栈区: 堆区:  new: 补充知识:new 总结: 这篇博客介绍 c++四大分区 的详解,其中也会涉及到有关 new 的知识

    2024年02月20日
    浏览(29)
  • C++核心编程——内存分区、引用、函数提高和函数重载

    本专栏记录C++学习过程包括C++基础以及数据结构和算法,其中第一部分计划时间一个月,主要跟着黑马视频教程,学习路线如下, 不定时更新,欢迎关注 。 当前章节处于: ---------第1阶段-C++基础入门 ---------第2阶段实战-通讯录管理系统, ==== 第3阶段-C++核心编程 , --------

    2024年01月22日
    浏览(26)
  • C/C++程序的内存分区

    正确理解C/C++程序的内存分区,对程序员来说是最基本的要求。 网络上流形两大版本内存分区,分别为: 五大内存分区:堆、栈、全局/静态存储区、自由存储区和常量存储区。 五大内存分区:堆、栈、全局/静态存储区、字符串常量区和代码区。 且不论以上两种分区孰是孰

    2023年04月09日
    浏览(24)
  • IsolarAB导出arxml到Matlab/Simulink生成模型并配置内存分区

    本文以一个简单的示例说明如何通过Isolar设计的SWC arxml导入到simulink中生成模型,并指定simulink生成的代码带内存分区信息。本文创建的SWC叫做ECAS_Sensor_SWC, 主要作用是对传感器信号进行处理和传递给后级SWC使用。 对IsolarAB AUTOSAR_Platform/BaseTypes(定义位于PlatformBase_Types.arxml)下面

    2024年04月09日
    浏览(34)
  • JVM内存管理、内存分区:堆、方法区、虚拟机栈、本地方法栈、程序计数器

    线程共享 堆         存放 实例 ,字符串常量(直接引用),静态变量,线程分配缓冲区(TLAB线程私有)。 垃圾收集器 管理的区域 方法区         非堆,和堆相对的概念。存储已被虚拟机加载的 类型信息 、常量、静态变量、即时编译器编译后的代码缓存等数据,内

    2024年02月10日
    浏览(33)
  • 【C++奇遇记】内存模型

    🎬 博客主页:博主链接 🎥 本文由 M malloc 原创,首发于 CSDN🙉 🎄 学习专栏推荐:LeetCode刷题集 数据库专栏 初阶数据结构 🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! 📆 未来很长,值得我们全力奔赴更美好的生活✨ 🐤本篇文章将讲授C++的内存模型相关的知识

    2024年02月11日
    浏览(22)
  • 编写C语言程序,模拟实现首次/最佳/最坏适应算法的内存块分配和回收,要求每次分配和回收后显示出空闲分区和已分配分区的情况。假设初始状态下,可用的内存空间为640KB。(江西师范大学软件学院 操作系统)

    为了实现动态分区分配,通常将系统中的空闲分区链接成一个链。所谓顺序查找是指依次搜索空闲分区链上的空闲分区,去寻找一个大小能满足要求的分区。 --------计算机操作系统(第四版) 可变分区也称动态分区,在指作业装入内存时,从可用的内存中划出一块连续的区域

    2024年02月08日
    浏览(33)
  • C++奇迹之旅:探索类对象模型内存的存储猜想

    上回我们学习了类的定义,初步了解了什么是类?类的定义,以及类的三个访问限定符: public , private , protected ,本小节将讲解类的实例化,类对象模型的猜想存储,及三种简单类的计算。 在 C++ 中,类的实例化是指创建一个类的对象。当我们定义了一个类之后,就可以根据

    2024年04月12日
    浏览(28)
  • C++执行程序计时函数详解

    通常计时函数主要有两个,分别是getTickCount()和getTickFrequency(). getTickCount()函数,返回的是CPU自某个时间(如启动电脑)以来走过的时钟周期数; getTickFrequency()函数,返回的是CPU一秒钟所走的时钟周期数,这样我们就可以轻松的以秒为单位对某运算进行计时; 将两个函数组合

    2024年02月13日
    浏览(33)
  • 【Java高级应用:深入探索Java编程的强大功能,JVM 类加载机制, JVM 内存模型,垃圾回收机制,JVM 字节码执行,异常处理机制】

    本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题 中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:

    2024年01月16日
    浏览(75)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包