分享

【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节

 小张学AI 2024-05-10 发布于山东

大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。

重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。**争取让你每天用5-10分钟,了解一些以前没有注意到的细节。**


久闻C和C++内存分区大名,常用堆和栈,但是你真的懂里面的一些细节吗?真的知道程序中的每个变量每个函数都在内存中的哪个地方吗?本文我们详细学习下内存分区和其中的一些细节内容。

0. 内存分区

C++程序在执行时,将内存划分为4个区域(有的教程是5个区域,都差不多):

(1)代码区:存放函数体的二进制代码,由操作系统管理,只读,共享

(2)全局区:存放全局变量,静态变量,以及常量(字符常量和const修饰的全局变量),程序结束后由操作系统释放

(3)栈区:由编译器自动分配释放,存放函数的参数值,局部变量,返回值等,该部分空间不大,超过空间大小后会栈溢出。

(4)堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

多说一句:应该重点关注的是各个内存区域的管理者不同,生命周期不同,性质也不同。代码区只读、共享,堆区由程序员自行分配和释放。

1. 代码区

代码区在程序编译后,生成了可执行程序,在程序被真正执行前被加载到内存,在程序执行期间,不会被修改和释放。由于代码区是只读的,所以会被多个进程共享。在多个进程同时执行同一个程序时,操作系统只需要将代码段加载到内存中一次,然后让多个进程共享这个内存区域即可。

2. 全局区

全局区也是在程序被真正执行前被加载到内存。

下面以一段程序来看全局区的数据存储,各类型内存的存储连续性。

2.1 测试程序

#include <iostream>

//全局变量
int g_a = 10;
int g_b = 10;

//const修饰的全局变量,全局常量
const int c_g_a = 10;
const int c_g_b = 10;

static int s_g_a = 10;
static int s_g_b = 10;

int g_c;
int g_d;

static int s_g_c;
static int s_g_d;

int main(int argc, char *argv[])
{
    // 全局变量
    std::printf("全局变量g_a的地址为:%p\n", &g_a);
    std::printf("全局变量g_b的地址为:%p\n", &g_b);
    std::printf("未初始化全局变量g_c的地址为:%p\n", &g_c);
    std::printf("未初始化全局变量g_d的地址为:%p\n", &g_d);

    // 全局常量
    std::printf("全局常量c_g_a的地址为:%p\n", &c_g_a);
    std::printf("全局常量c_g_b的地址为:%p\n", &c_g_b);

    // 全局静态变量
    std::printf("全局静态变量s_g_a的地址为:%p\n", &s_g_a);
    std::printf("全局静态变量s_g_b的地址为:%p\n", &s_g_b);
    std::printf("未初始化全局静态变量s_g_c的地址为:%p\n", &s_g_c);
    std::printf("未初始化全局静态变量s_g_d的地址为:%p\n", &s_g_d);

    // 局部静态变量
    static int s_a = 10;
    static int s_b = 10;
    std::printf("局部静态变量s_a的地址为:%p\n", &s_a);
    std::printf("局部静态变量s_b的地址为:%p\n", &s_b);

    // 字符常量
    std::printf("字符常量的地址:%p\n", &("hello"));

    const int c_a = 10;
    const int c_b = 10;
    std::printf("局部常量c_a的地址为:%p\n", &c_a);
    std::printf("局部常量c_b的地址为:%p\n", &c_b);


    return 0;
}

测试程序中包含了 全局变量、全局常量、全局静态变量、字符常量、局部静态变量、局部常量、未初始化的全局变量、未初始化的局部静态变量等类型。

2.2 运行结果及分析

运行结果如下:

文字版运行结果:

全局变量g_a的地址为:00007ff76481a000
全局变量g_b的地址为:00007ff76481a004
未初始化全局变量g_c的地址为:00007ff76481f030
未初始化全局变量g_d的地址为:00007ff76481f034
全局常量c_g_a的地址为:00007ff76481b000
全局常量c_g_b的地址为:00007ff76481b004
全局静态变量s_g_a的地址为:00007ff76481a008
全局静态变量s_g_b的地址为:00007ff76481a00c
未初始化全局静态变量s_g_c的地址为:00007ff76481f038
未初始化全局静态变量s_g_d的地址为:00007ff76481f03c
局部静态变量s_a的地址为:00007ff76481a010
局部静态变量s_b的地址为:00007ff76481a014
字符常量的地址:00007ff76481b228
局部常量c_a的地址为:00000000005ffe5c
局部常量c_b的地址为:00000000005ffe58

从结果来看:

(1)局部常量明显与其它地址不是一起的。所以,局部常量不在全局区

(2)全局变量、全局静态变量、局部静态变量内存是连续的,所以这三个是一起的

(3)全局常量单独放在一块

(4)未初始化的全局变量和局部静态变量内存连续,是一起的

(5)字符常量单独放在一块

2.3 总结

全局区的存储方式大概如下图:

特别注意的是,局部常量不属于全局区。

思考题:

  1. 1. 为什么局部静态变量属于全局区而局部常量却不属于全局区?

  2. 2. const static 局部静态常量在哪?

3. 栈区

由编译器自动分配释放,存放函数的参数值,局部变量,函数调用的上下文等。下面以一段测试代码来看下栈的内容。

3.1 测试代码

#include <iostream>

void functionB()
{
    int bb;
    std::printf("\n函数B中局部变量bb的地址:%p\n", &bb);
    return;
}

int funcitionA(int a)
{
    int b = a;
    std::printf("\n形参a的地址:%p\n", &a);
    std::printf("函数A中局部变量b的地址:%p\n", &b);
//    std::printf("functionB的地址:%p\n", (void*)functionB);
    functionB();
    return b;
}

int main(int argc, char *argv[])
{
    int a = 10;
    int b = 20;
    std::printf("\n局部变量a的地址:%p\n", &a);
    std::printf("局部变量b的地址:%p\n", &b);
//    std::printf("functionA的地址:%p\n", (void*)funcitionA);
    int c = funcitionA(a);
    std::printf("\n返回值c的地址:%p\n", &c);

    char aa[3];
    std::printf("局部变量数组aa[0]的地址:%p\n", aa);
    std::printf("局部变量数组aa[1]的地址:%p\n", aa + 1);
    std::printf("局部变量数组aa[2]的地址:%p\n", aa + 2);

    return 0;
}

输出结果:

3.2 栈的方向

从输出结果看,局部变量 a,b,c的地址是连续的,但是连续递减(54 ---> 50 ---> 4c),所以,栈的生长方向是高地址到低地址。

值得注意的是,对于数组来说,数组元素的地址是连续的,从低到高存储。这是因为数组在分配内存时,是根据数组大小分配一整块内存,然后将数组元素整体存入。

3.3 不能返回栈区的局部变量指针

上篇指针的文章中,我们也写到了这一点。细节可以看我的这篇文章:【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节

3.4 栈的大小有限

栈的大小因平台而异,但一般比较小。

比如Windows系统下,栈的一般空间大小为2M。Linux下一般为8M,可以通过命令ulimit -s 来更改大小。

在有数据结构非常复杂和大的自定义类型时,注意估算这类自定义类型的大小,避免超过2M造成栈溢出。

3.5 函数调用模型

栈不光存储局部变量,还会存储函数调用的上下文等信息。函数的调用过程都是通过栈空间来进行组织和记录的。

这篇文章从汇编的角度详细解释了函数调用过程中堆栈空间的分配和变化。

去掉寄存器那些特别底层的东西,简单点来看,函数调用的过程如下:当调用函数时,分配给该函数一块栈空间,存储函数的参数,记录调用函数的位置(返回的位置)等。当函数运行结束退出后,该栈空间出栈销毁。在函数没有被调用前,是不分配给该函数栈空间的

4. 堆区

由程序员分配和释放,若程序员不释放,会一直存留到程序结束时由操作系统释放并回收。

分配的方式:一般由 malloc 函数或 new 来在堆上开辟内存。也可以使用STL中的 std::vector 等容器,这些容器内部自己在堆上开辟内存,其实底层也是用了malloc。

4.1 堆的方向

堆的方向为向上增长,由低地址到高地址。

4.2 堆的大小

堆的空间一般很大,例如Windows平台中可能为2G。

堆的内存是由程序员手动创建并释放的,这些创建的内存空间可能不是连续的,很容易产生一些内存碎块。如何提高内存空间的利用率是性能优化的一个重要方向,比如内存池等的设计都需要考虑如何提高内存利用率。

4.3 操作堆的函数

能在堆上开辟内存空间的函数为 new 或 malloc,当然还有一些其它的类型例如 std::vector,底层也会自己开辟内存空间。

后面咱们细说。

5. 思考题答案

以我的个人理解来回答下文中的思考题:

  1. 1. 为什么局部静态变量属于全局区而局部常量却不属于全局区?

静态变量在程序的生命周期内保持其值。在函数或方法内部定义的静态变量,即使函数执行结束,其值也不会被清除。在下一次调用函数时,静态变量将保持上次调用结束时的值。所以,其不能保存在栈区,因为栈区在函数退出后会被释放,而堆区由程序员自己维护,所以最好的方法就是全局区。

局部常量,只在这个函数内起作用,函数执行结束后消亡也没关系。所以可以保存在栈区,没必要保存在全局区。

  1. 1. const static 局部静态常量在哪?

const static修饰的局部静态常量,在全局区,属于全局常量那一块内存。测试结果如下:

局部静态常量的地址:00007ff6dc11b008

其实,局部静态常量,其实就是给全局常量加了一个作用域。它的值是永远存在且不可更改的,只是作用域只能在声明的函数中。

6. 参考

如果觉得本文对你有帮助,麻烦点个赞和关注呗~~~点击上方公众号,关注↑↑↑


  • · 大家好,我是 同学小张,日常分享AI知识和实战案例

  • · 欢迎 点赞 + 关注 👏,持续学习持续干货输出

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多