一文搞懂C++内存布局:堆、栈及内存分区的完整指南

栈存储区

栈内存区主要用于存放局部变量函数调用信息、以及一些临时数据。以下是栈内存区中常见存放的数据类型:

栈内存区存放的数据:

  1. 局部变量

    • 这些是在函数内部声明的变量,例如基本类型变量(如 intfloat 等),它们的生命周期通常仅限于函数的执行期间。当函数执行结束时,这些变量会被自动释放。
    • 例如:
      void func() {
          int local_var = 10; // 局部变量,存储在栈区
      }
      
  2. 函数参数

    • 当一个函数被调用时,传递给函数的参数会被存储在栈中。这些参数在函数执行完毕后也会被自动释放。
    • 例如:
      void func(int param) {
          // 参数 `param` 存储在栈中
      }
      
  3. 返回地址

    • 当一个函数被调用时,CPU 会将调用函数后的下一条指令的地址(即返回地址)保存在栈中,以便在函数执行完后能返回到正确的位置继续执行程序。
  4. 临时变量

    • 编译器在一些复杂表达式或代码块中,可能会创建临时变量。这些变量同样存储在栈中,并且只在相关表达式求值期间存在。
  5. 函数调用帧(栈帧)

    • 每次函数调用时,系统会为该函数在栈上分配一块内存,称为栈帧,它包含了函数的局部变量、参数和返回地址等信息。当函数返回时,这个栈帧会被销毁。

栈区的特点:

  • 自动管理:栈中的内存是由编译器自动分配和释放的,不需要程序员手动管理。
  • 后进先出(LIFO):栈是按“后进先出”的原则进行操作的,最近压入栈的变量会最先被弹出。
  • 存储速度快:栈内存的分配和回收速度非常快,因为它只需调整栈指针即可。
  • 空间有限:栈的大小通常是有限的,尤其在嵌套递归调用较多时,可能会出现栈溢出(Stack Overflow)。

例子说明:

#include <iostream>

void func(int a) {
    int b = a + 10; // 局部变量 `b`,存储在栈中
    std::cout << "b: " << b << std::endl;
}

int main() {
    int x = 5;  // 局部变量 `x`,存储在栈中
    func(x);    // `x` 作为参数传递给 `func`,存储在栈中
    return 0;
}

在这个例子中:

  • x 是在 main 函数中声明的局部变量,它存储在栈中。
  • abfunc 函数中的局部变量,它们也存储在栈中。
  • x 作为参数传递给 func,此时 x 的值(5)被存储在栈中作为 a 的值。

因此,栈内存区主要存放函数的局部变量、函数调用参数、返回地址以及一些临时数据。

堆内存主要用于动态分配的数据。在程序运行时,堆内存中的数据可以通过程序员显式申请和释放,并且这些数据的生命周期通常由程序员控制,不受函数调用的限制。以下是堆内存中存储的数据类型及其特点:

堆存储区

堆内存区存放的数据:

  1. 动态分配的对象

    • 通过 newmalloc 等操作在堆中动态分配的内存。这些对象不会在函数结束时自动释放,而是需要程序员显式调用 deletefree 来释放它们。
    • 例如:
      int* ptr = new int(10); // 动态分配的整数,存储在堆中
      delete ptr; // 手动释放堆内存
      
  2. 动态数组

    • 动态分配的数组或容器数据类型,如 C++ 中通过 new 或 C 语言中的 malloc 函数动态分配的数组,这些数据存储在堆中。
    • 例如:
      int* array = new int[5]; // 动态分配的数组,存储在堆中
      delete[] array; // 手动释放数组内存
      
  3. 类对象实例

    • 使用 new 创建的类对象实例在堆中分配内存。与局部变量不同,堆中的对象可以在函数执行完毕后继续存在,直到程序员手动释放。
    • 例如:
      class MyClass {
      public:
          MyClass() { std::cout << "Constructor called!" << std::endl; }
      };
      
      int main() {
          MyClass* obj = new MyClass(); // 动态分配的对象,存储在堆中
          delete obj; // 手动释放对象内存
      }
      
  4. 复杂数据结构

    • 堆通常用于存储复杂数据结构,如链表、树、图等,因为它们的内存需求动态且不可预知,需要灵活的内存分配。
    • 例如,链表节点可以动态分配存储在堆中:
      struct Node {
          int data;
          Node* next;
      };
      
      Node* head = new Node(); // 动态分配链表头节点,存储在堆中
      head->next = new Node(); // 动态分配下一个节点
      delete head->next; // 手动释放内存
      delete head;
      

堆区的特点:

  1. 动态分配:堆中的内存可以在程序运行时动态分配和释放,大小由程序员决定。这允许在运行时根据需求申请内存,而不是在编译时确定大小。

  2. 手动管理:堆内存不像栈内存那样由系统自动管理,程序员必须手动释放内存。未释放的堆内存会导致内存泄漏,长期不释放的堆内存会逐渐耗尽系统资源。

  3. 生命周期不确定:与栈不同,堆中的内存可以跨越多个函数调用的生命周期。例如,函数中动态分配的内存在函数退出后依然存在,直到显式释放。

  4. 灵活性:堆内存允许程序动态申请大量数据存储,因此适合用于那些大小不确定或需要在运行时分配内存的数据结构。

  5. 效率较低:相比栈内存,堆内存的分配和释放需要更多的时间,尤其是随着频繁的分配和释放,可能会导致内存碎片化,影响性能。

堆内存的使用举例:

#include <iostream>

class Person {
public:
    std::string name;
    int age;
    Person(std::string n, int a) : name(n), age(a) {}
};

int main() {
    // 动态分配一个整数
    int* num = new int(42); // 动态分配,存储在堆中
    std::cout << "Number: " << *num << std::endl;

    // 动态分配一个数组
    int* array = new int[5]; // 动态分配的数组,存储在堆中
    array[0] = 1;
    array[1] = 2;
    std::cout << "Array[0]: " << array[0] << std::endl;

    // 动态分配一个类对象
    Person* p = new Person("Alice", 30); // 动态分配,存储在堆中
    std::cout << "Name: " << p->name << ", Age: " << p->age << std::endl;

    // 手动释放堆内存
    delete num;    // 释放整数
    delete[] array; // 释放数组
    delete p;      // 释放对象

    return 0;
}

总结:

  • 堆内存主要用于存放动态分配的内存,包括动态对象、数组和复杂数据结构。
  • 堆内存的特点是灵活性强、手动管理、生命周期长,适合用于需要动态内存分配的场景。
  • 使用堆内存时,程序员需要显式管理内存的分配和释放,避免内存泄漏或过度消耗系统资源。

其他内存

在代码运行时,除了之外,常见的内存分区还包括代码区数据区(全局区),以及一些操作系统管理的区域。这些区域各自负责存储不同类型的数据,具体分区如下:

1. 代码区(Code Segment / Text Segment)

  • 存放内容:存放程序的可执行代码,也就是编译后的机器指令。

  • 作用:代码区是只读的,用来防止程序无意中修改自己的指令。

  • 特点:代码区的内容通常是固定的(静态),在程序运行过程中不会改变。现代系统中,代码区通常是只读的,以避免程序修改自己的代码(自修改代码)。

    例如:

    void func() {
        // 函数指令在代码区中
    }
    

    在这个例子中,func 函数的机器指令存储在代码区中。

2. 全局区/数据区(Global Segment / Data Segment)

全局区通常被分为两个子区域:

a. 已初始化的数据区(Initialized Data Segment)
  • 存放内容:存放所有已初始化的全局变量和静态变量。这些变量在程序启动时被分配,并且在程序运行期间一直存在。

  • 特点:已初始化的数据在程序运行过程中可以被修改,且其生命周期从程序开始一直持续到程序结束。

    例如:

    int global_var = 10; // 已初始化的全局变量,存储在已初始化数据区
    
b. 未初始化的 BSS 区(Block Started by Symbol Segment)
  • 存放内容:存放所有未初始化的全局变量和静态变量。BSS 区用于占用内存但不分配实际的值,通常会被初始化为 0。

  • 特点:与已初始化的数据区相似,BSS 区中的变量也在程序运行期间保持可用,但这些变量的初始值默认为 0。

    例如:

    static int uninitialized_var; // 未初始化的静态变量,存储在 BSS 区,默认为 0
    

3. 常量区(Literal Pool / Read-Only Data Segment)

  • 存放内容:存放程序中定义的常量数据,例如字符串常量、const 修饰的变量、以及其它只读数据。

  • 特点:常量区通常是只读的,程序无法修改这些数据。

    例如:

    const int constant_var = 100; // 存储在常量区
    const char* str = "Hello";    // 字符串常量,存储在常量区
    

    这里,constant_var 和字符串 "Hello" 都会被存储在常量区中。

4. 操作系统管理区

在操作系统中,还存在一些由系统管理的内存区域。这些区域通常用于系统与程序之间的交互,不直接为用户编程所使用。

a. 栈外内存映射区(Memory-Mapped Segment)
  • 存放内容:通常用于映射文件或设备到内存空间。程序可以通过内存映射直接访问文件或设备的内容,而不需要显式读取或写入文件。这种方式在处理大文件或共享内存时非常高效。

    例如,在 Unix/Linux 系统中可以通过 mmap 函数来实现内存映射。

b. 内核空间(Kernel Space)
  • 存放内容:操作系统内核及其相关的数据结构。
  • 作用:这个区域对用户态的程序是不可见的,通常用于存储操作系统的核心代码和数据。
  • 特点:用户程序不能直接访问该区域,如果访问会导致权限错误(Segmentation Fault)。内核空间管理硬件、内存分配、进程调度等。

5. 堆栈外的动态链接库内存(Shared Library Memory)

  • 存放内容:动态链接库(例如 .so.dll 文件)的内容会在程序运行时被加载到内存中。
  • 作用:共享库用于节省内存,允许多个程序共享同一份库代码,而无需为每个程序各自加载一份。
  • 特点:当程序调用动态链接库时,操作系统会将库文件的内容映射到程序的地址空间中,使其可以像调用普通函数一样调用共享库中的函数。

内存布局示意图

以下是典型的程序内存布局示意图(从高地址到低地址):

-------------------------
|    内核空间 (不可访问)  |
-------------------------
|      栈区 (Stack)      |
-------------------------
|  堆区 (Heap, 动态分配)  |
-------------------------
|  BSS 段 (未初始化数据)  |
-------------------------
| 已初始化数据段 (Data)  |
-------------------------
|  常量区 (Read-Only)    |
-------------------------
|  代码区 (Text)         |
-------------------------

总结

  • 代码区:存储程序的可执行代码。
  • 全局区(已初始化和未初始化):存储全局变量和静态变量,已初始化和未初始化分别存储在不同的区域。
  • 常量区:存储常量和字符串常量,通常是只读的。
  • 堆区:存储动态分配的内存,程序员手动管理。
  • 栈区:存储局部变量、函数参数和返回地址,由系统自动管理。
  • 操作系统管理区:包括文件映射区域、内核空间和动态链接库内存等。

不同内存分区的划分确保了程序运行时数据的有序管理,既提高了内存的利用效率,也便于系统安全和稳定性维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值