简体中文
cover

所有程序都存储在内存中。当一个程序开始运行时,操作系统会为其创建一个进程。进程是系统进行资源分配和调度的基本单位,它拥有独立的虚拟地址空间。

拿一个 4GB 内存的电脑来说,在 32 位进程的虚拟地址空间中,可寻址范围从 0x00000000 到 0xFFFFFFFF。通常低地址存放代码段与数据段,高地址附近为栈,堆则用于满足程序运行时动态分配内存的需求。

每个区域在运行时都有自己的用途,了解内存布局有助于有效管理内存并避免泄漏和堆栈溢出等问题。

内存四区

C++ 程序的内存布局主要分为四个区域:代码区、全局/静态区、栈区和堆区。

内存四区

1. 代码区(Code Segment)

代码区存储程序编译的二进制代码,由操作系统进行管理,通常为只读以提高安全性,防止程序意外修改自身代码。

比如你学习 C++ 时写的所有代码,包括英文字母、数字、符号、中文注释等等,这些代码在编译后会被转换为二进制机器码(010101),存储在代码区中。

程序的可执行机器指令越多,代码区占用的空间就越大;而注释、模板元编程产生的未实例化代码并不会进入最终的可执行文件,因此不会增加代码区体积。

2. 全局/静态区(Data Segment)

全局/静态区存储程序员定义的全局变量和静态变量。此段位于代码区的正上方,分为两部分:

A. 已初始化数据段(.data)

存储在声明时分配值的全局变量和静态变量。

// Global variable
int a = 50;

// Static variable
static int b = 100;

a 和 b 都 存储在初始化的数据段中

B. 未初始化数据段(.bss)

保存尚未显式初始化的全局变量和静态变量。系统在运行时自动将这些值设置为零。

// Global variable
int c;
// Static variable
static int d;

c 和 d 放置在 BSS 段中。

3. 堆区(Heap Segment)

堆是用于运行时动态分配的内存区域。堆中的内存使用 new/delete 等运算符或 malloc()/free() 等函数手动管理。

#include <iostream>
using namespace std;

int main() {

    // Dynamically allocate array on heap
    int* arr = new int[10];
    delete[] arr;
    return 0;
}

使用 new[] 分配的数组,必须对应使用 delete[] 释放。arr 指向的内存驻留在堆中。

4. 栈区(Stack Segment)

栈区用于:局部变量、函数参数、返回地址

void func()
{
    int a = 10; // a 是一个在函数中声明的变量
    cout << a << endl;
}

每个函数调用都会创建一个栈帧(Stack Frame),该帧被推送到栈。当函数完成时,他会自动释放

存储持续性

了解了内存四区后,你可能对于不同类型的数据有了更深入地理解,C++ 使用三种存储持续性(不讨论并行编程)

  1. 自动存储持续性(Automatic Storage Duration)
  2. 静态存储持续性(Static Storage Duration)
  3. 动态存储持续性(Dynamic Storage Duration)

这些方案的区别顾名思义就在于数据存储的持续时间,再来回顾下内存四区,从下到上依次为代码区、全局/静态区、堆区和栈区

其中对应的存储持续性为

内存区域存储持续性
栈区自动存储持续性
全局/静态区静态存储持续性
堆区动态存储持续性

1. 自动存储持续性(栈区)

栈区存放的是在函数中声明的变量(包括函数参数)。这些变量在函数调用时创建,并在函数返回时销毁,存储持续性为自动的。

#include <iostream>
using namespace std;

void func()
{
    int a = 10; // a 是一个自动变量
    cout << a << endl;
}

int main()
{
    func(); // 函数调用时,a 被创建,函数返回时,a 被销毁
    cout << a << endl; // 错误:'a' 未在此作用域内声明 (其生命周期也在 func 函数结束时终结)
    return 0;
}

2. 静态存储持续性(全局/静态区)

在全局/静态区中存放的是在函数定义外定义的变量和使用 static 关键字声明的变量。他们在程序运行期间一直存在,存储持续性为静态的。

#include <iostream>
using namespace std;

int a = 10;
static int b = 20;

void func()
{
    cout << a << endl; // 正确,a 在这里可访问
    cout << b << endl; // 正确,b 在这里可访问
}

int main()
{
    func();
    cout << a << endl; // 正确,a 在这里可访问
    cout << b << endl; // 正确,b 在这里可访问
    return 0;
}

代码区(Code Segment)通常被视为具有静态存储持续性,因为它的内容在程序的整个生命周期中都存在且不变。

3. 动态存储持续性(堆区)

堆区存放的是使用 new 运算符创建的变量,分配的内存将会一直存在,直到使用 delete 运算符删除该变量将其释放或者程序结束为止。这种存储持续性为动态的。

#include <iostream>
using namespace std;

int * func()
{
    int* p = new int(10); // p 指向一个动态分配的整数
    return p;
}

int main()
{
    int* p = func();
    cout << *p << endl; // 输出 10
    delete p;
    cout << *p << endl; // 出错

    return 0;
}

程序验证四区分布

#include <iostream>
#include <iomanip>
#include <cstdint>

/* ================= 全局/静态区 ================= */
int                 global_a = 1;
const int           const_global_a = 2;
static int          static_global_a = 3;
static const int    static_const_global_a = 4;

/* ================= 代码区示例函数 ================= */
void code_sample() {}

int main()
{
    /* ================= 栈区 ================= */
    int              local_a = 10;
    const int        const_local_a = 11;

    /* ================= 全局/静态区 ================= */
    static int       static_local_a = 12;
    static const int static_const_local_a = 13;

    /* ================= 堆区 ================= */
    int* heap_p = new int(20);

    /* ================= 统一打印 ================= */
    auto print = [](const char* region, const char* name, const void* addr) {
        std::cout << std::left << std::setw(10) << region
            << std::setw(30) << name
            << reinterpret_cast<uintptr_t>(addr) << '\n';
        };

    std::cout << "区域      变量名                        十进制地址\n";
    std::cout << "---------------------------------------------\n";

    /* 栈区 */
    print("栈区", "local_a", &local_a);
    print("栈区", "const_local_a", &const_local_a);

    /* 堆区 */
    print("堆区", "heap_p", heap_p);

    /* 全局/静态区 */
    print("数据段", "global_a", &global_a);
    print("数据段", "const_global_a", &const_global_a);
    print("数据段", "static_global_a", &static_global_a);
    print("数据段", "static_const_global_a", &static_const_global_a);
    print("数据段", "static_local_a", &static_local_a);
    print("数据段", "static_const_local_a", &static_const_local_a);

    /* 代码区 */
    print("代码段", "code_sample 函数入口",
        reinterpret_cast<void*>(&code_sample));

    delete heap_p;
    return 0;
}
运行结果

可以看出,同一区域的地址挨着很近,但是前文说过,代码区是低地址,栈区是高地址,但是这里的地址按照 10 进制来看,代码区的地址比栈区的还大,这是什么原因呢?

这是因为现代操作系统使用了虚拟内存技术,程序看到的地址是虚拟地址,并不是真实的物理地址。操作系统会将虚拟地址映射到物理地址上,这个映射关系是动态变化的。

所以我们看到的地址并不一定反映内存的实际布局。

结语

了解了不同的存储类别后,相信你对 C++ 程序的内存布局有了更深入的理解。合理使用不同的存储类别,可以帮助你编写出更高效、更安全的代码。

文章分类在笔记
0
0
0
0