自学内容网 自学内容网

嵌入式面试八股文(五)·一文带你详细了解程序内存分区中的堆与栈的区别

目录

1.  栈的工作原理

1.1  内存分配

1.2  地址生长方向

1.3  生命周期

2.  堆的工作原理

2.1  动态内存分配

2.1.1  malloc函数

2.1.2  calloc函数

2.1.3  realloc函数

2.1.4  free函数

2.2  生命周期管理

2.3  地址生长方向

3.  堆与栈区别

3.1  管理方式不同

3.2  空间大小不同

3.3  生长方向不同

3.4  分配方式不同

3.5  分配效率不同

3.6  存放内容不同


        堆(Heap)和栈(Stack)是计算机内存管理中两个重要的概念,它们在数据存储、生命周期和访问方式上有着显著的区别,在理解这两个概念时,需要放到具体的场景下,因为不同场景下,堆与栈代表不同的含义。一般情况下,有两层含义:
(1)程序内存布局场景下,堆与栈表示两种内存管理方式。
(2)数据结构场景下,堆与栈表示两种常用的数据结构。

        二者又有不同的使用场景:

栈(Stack):在函数调用时,参数和局部变量的存储;递归算法的实现;临时数据的快速处理。

堆(Heap):存储大规模数据结构,如动态数组、链表、树等;在运行时需要创建对象或数据结构但其大小无法预先确定的情况。

适合用于小规模、短生命周期的数据,自动管理,速度快,但空间有限。

适合用于大规模、长生命周期的数据,灵活性高,但需要手动管理。

1.  栈的工作原理

1.1  内存分配

        在函数调用时,操作系统会为该函数分配栈空间,用于存储局部变量、参数以及返回地址等。

        每当一个新的函数被调用时,一个新的栈帧(stack frame)会被创建,这个栈帧包含了该函数的所有局部变量和参数。

在编程中,每当一个函数被调用时,操作系统会为该函数分配一段栈空间,通常称为“栈帧”。

代码示例: 

#include <stdio.h>

void exampleFunction(int a, int b) {
    int sum = a + b; // 局部变量
    printf("Sum: %d\n", sum);
    printf("Address of a: %p\n", (void*)&a);
    printf("Address of b: %p\n", (void*)&b);
    printf("Address of sum: %p\n", (void*)&sum);
}

int main() {
    int x = 5;       // 主函数的局部变量
    int y = 10;      // 主函数的局部变量
    exampleFunction(x, y); // 调用函数
    return 0;
}

        对以上代码,当 exampleFunction 被调用时,操作系统为其分配一个栈帧。参数 a 和 b 会被压入栈中。在上面的例子中,x 和 y 的值(5 和 10)将被传递到函数的栈帧中。局部变量 sum 会在栈帧中分配一定的内存空间。在调用 exampleFunction 时,当前执行位置的地址会被保存到栈中,以便函数执行结束后能够返回到正确的位置。

        通常情况下,生长方向是向下(地址减小),这意味着新分配的栈空间地址会比之前的地址小。栈帧的简化示意图如下:

|----------------- |
|   sum (局部)     |  <- exampleFunction 的栈帧顶部
|------------------|
|     b (参数)     |
|------------------|
|     a (参数)     |
|------------------|
|  返回地址 (main) |  <- 上一个函数(main)的栈帧
|------------------|

        这里我们可以看到sum的内存地址比a和b的小,但是我们又会发现一个问题,不是说先分配的内存大吗?为什么b的要比a的大,这是因为编译器在生成代码时可能会根据调用约定、优化策略等因素,调整数据在栈中的布局。比如,有时候局部变量可能会按特定顺序排列,以提高访问效率,这就导致了b的内存地址比a的大。

1.2  地址生长方向

        通常,栈是向低地址生长的,也就是说,后定义的变量会占用较低的内存地址

        例如,在你的代码示例中,char s[] = "abc"; 会比 int b; 先被分配,因此 s 的地址会低于 b 的地址。

代码示例: 

#include <stdio.h>

int main() {
    int b;               // 栈变量
    char s[] = "abc";   // 栈变量,存放字符串
    char* p2;           // 栈变量,指针声明,未初始化
    printf("Address of b: %p\n", (void*)&b);
    printf("Address of s: %p\n", (void*)&s);
    printf("Address of p2: %p\n", (void*)&p2);
    return 0;
}

        这个并没有按照向低地址生长,具体原因同上,编译器为了优化或满足对齐要求,可能改变了变量在栈中的排列顺序。

1.3  生命周期

        栈中存储的变量在函数执行期间存在,一旦函数执行结束,栈帧被销毁,所有局部变量的内存会被释放,生命周期结束。

        这意味着栈中的数据不需要开发者手动管理,简单而且高效。

代码示例: 

#include <stdio.h>

void exampleFunction() {
    int localVar = 10; // 局部变量在栈中创建
    printf("Local variable value: %d\n", localVar);
}

int main() {
    exampleFunction(); // 调用函数
    // 在此处 localVar 不再可用
    // printf("%d\n", localVar); // 这行会导致编译错误
    return 0;
}

        当 exampleFunction 执行完毕后,其栈帧被销毁,localVar 的内存被释放。
        如果尝试在 main 中访问 localVar(如注释中的 printf),将会导致编译错误,因为 localVar 超出了作用域。

2.  堆的工作原理

2.1  动态内存分配

        动态内存分配是指在程序运行时根据需要申请和释放内存的能力,这对于处理不确定大小的数据结构非常重要。在 C 语言中,常用的动态内存管理函数包括 malloc、calloc、realloc 和 free。下面是这些函数的简要说明及其用法示例。

2.1.1  malloc函数

用途:分配指定大小的内存块。
返回值:返回指向分配内存块的指针,如果失败则返回 NULL。

int *arr = (int *)malloc(n * sizeof(int));

2.1.2  calloc函数

用途:分配内存块并初始化为零。
参数:第一个参数是要分配的元素个数,第二个参数是每个元素的大小。
返回值:返回指向分配内存块的指针,如果失败则返回 NULL。

int *arr = (int *)calloc(n, sizeof(int));

2.1.3  realloc函数

用途:重新调整已分配内存块的大小。
参数:第一个参数是之前分配的指针,第二个参数是新的大小。
返回值:返回指向新内存块的指针,如果失败则返回 NULL,原内存块保持不变。

arr = (int *)realloc(arr, newSize * sizeof(int));

2.1.4  free函数

用途:释放之前分配的内存块。
参数:要释放的指针。

free(arr);

代码示例: 

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("请输入元素的数量:");
    scanf("%d", &n);

    // 使用 malloc 分配内存
    int *arr = (int *)malloc(n * sizeof(int));//若内存分配失败,返回值为NULL 
    if (arr == NULL) //判断内存是否分配成功 
{
        printf("内存分配失败!\n");
        return 1; // 错误处理
    }

    // 初始化数组
    for (int i = 0; i < n; i++) 
{
        arr[i] = i + 1;
    }

    // 打印数组
    printf("数组元素: ");
    for (int i = 0; i < n; i++) 
{
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 重新分配内存
    printf("输入新大小: ");
    int newSize;
    scanf("%d", &newSize);
    arr = (int *)realloc(arr, newSize * sizeof(int));
    if (arr == NULL) 
{
        printf("重新分配失败!\n");
        return 1; // 错误处理
    }

    // 如果增加了大小,初始化新元素
    for (int i = n; i < newSize; i++) 
{
        arr[i] = 0; // 或其他值
    }

    // 打印新的数组
    printf("新数组元素: ");
    for (int i = 0; i < newSize; i++) 
{
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);

    return 0;
}

2.2  生命周期管理

手动管理内存:在堆中分配的内存块不会在程序结束时自动释放,开发者需要使用 free 函数显式释放不再需要的内存。

避免内存泄漏:如果未能适时释放已分配的内存,会导致内存泄漏,进而降低程序的性能,甚至导致系统崩溃。

最佳实践:在每次调用内存分配函数(如 malloc、calloc 或 realloc)后,确保在适当的时候使用 free 释放内存。

2.3  地址生长方向

        堆的内存地址生长方向与栈相反,由低到高,但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,即 p2 指向的地址并不一定大于 p1 所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。堆中存储的数据若未释放,则其生命周期等同于程序的生命周期。

3.  堆与栈区别

3.1  管理方式不同

:由操作系统自动分配和释放,无需程序员手动管理。
:由程序员手动申请和释放,容易导致内存泄漏。

3.2  空间大小不同

:通常较小,受限于每个进程的栈大小(例如,Windows 默认 1MB,Linux 默认 10MB)。
:理论上可以使用的空间较大,取决于虚拟内存的大小。

3.3  生长方向不同

:向下生长,地址从高到低。
:向上生长,地址从低到高。

3.4  分配方式不同

:支持静态和动态分配。静态分配用于局部变量,动态分配通过 alloca() 函数实现。
:仅支持动态分配,由程序员通过库函数或运算符进行管理。

3.5  分配效率不同

:由于有硬件支持和专门指令,分配和释放效率较高。
:由C/C++提供的库函数或运算符来完成申请与管理,实现机制复杂,频繁分配可能导致内存           碎片,效率较低。

3.6  存放内容不同

:栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者 BSS 段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。

简单点来说,存放函数返回地址、参数、局部变量等。每个函数调用会创建一个新的栈帧。

:存放由程序员动态分配的数据,具体内容由程序员控制。

C语言菜鸟入门·各种typedef用法超详细解析-CSDN博客 

千题千解·嵌入式工程师八股文详解_时光の尘的博客-CSDN博客


原文地址:https://blog.csdn.net/MANONGDKY/article/details/142727415

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!