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

news2024/11/27 22:23:59

目录

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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2192555.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

海南聚广众达电子商务咨询有限公司助力商家业绩飙升

在这个短视频与直播风靡的时代&#xff0c;抖音电商无疑成为了众多商家竞相追逐的新风口。作为电商服务领域的佼佼者&#xff0c;海南聚广众达电子商务咨询有限公司凭借其专业的团队、创新的策略与丰富的实战经验&#xff0c;正引领着一批又一批商家在抖音平台上破浪前行&#…

顺序表及其代码实现

目录 前言1.顺序表1.1 顺序表介绍1.2 顺序表基本操作代码实现 总结 前言 顺序表一般不会用来单独存储数据&#xff0c;但自身的优势&#xff0c;很多时候不得不使用顺序表。 1.顺序表 1.1 顺序表介绍 顺序表是物理结构连续的线性表&#xff0c;支持随机存取&#xff08;底层…

Leetcode—139. 单词拆分【中等】

2024每日刷题&#xff08;173&#xff09; Leetcode—139. 单词拆分 dp实现代码 class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {int n s.size();unordered_set<string> ust(wordDict.begin(), wordDict.end());vector<b…

探索基于基于人工智能进行的漏洞评估的前景

根据2023年的一份报告 网络安全企业据估计&#xff0c;到 10.5 年&#xff0c;网络犯罪每年将给世界造成 2025 万亿美元的损失。每年记录在案的网络犯罪数量都会创下新高。这要求对传统的安全测试流程进行重大改变。这就是漏洞评估发挥作用的地方。 漏洞评估对于识别系统中的弱…

双指针_有效三角形个数三数之和四数之和

有效三角形个数 思路&#xff1a; 我们可以通过暴力枚举&#xff0c;三重for循环来算但&#xff0c;时间复杂度过高。 有没有效率更高的算法呢&#xff1f; 我们知道如果两条较短的边小于最长的一条边&#xff0c;那么就可以构成三角形。 如果这个数组是升序的&#xff0c;两…

负压DC-DC开关电源设计

负压DC-DC开关电源设计 与常见的正压输出BUCK电路对比&#xff0c;区别就在于将 原芯片接GND的网络接到了负压输出。 电感一接sW引脚&#xff0c;另外一接到了OV-GND。 注意几点如下: 芯片耐压选择 EN引脚耐压 输入滤波电容的选择 拓扑结构 BOOST模式&#xff1a;当NMO…

NXP i.MX8系列平台开发讲解 - 4.2.3 摄像头篇(三) - 摄像头MIPI 接口

专栏文章目录传送门&#xff1a;返回专栏目录 Hi, 我是你们的老朋友&#xff0c;主要专注于嵌入式软件开发&#xff0c;有兴趣不要忘记点击关注【码思途远】 文章目录 关注星号公众号&#xff0c;不容错过精彩 作者&#xff1a;HywelStar 1. 概述 MIPI是Mobile Industry Pr…

论文阅读:InternVL v1.5| How Far Are We to GPT-4V? 通过开源模型缩小与商业多模式模型的差距

论文地址&#xff1a;https://arxiv.org/abs/2404.16821 Demo&#xff1a; https://internvl.opengvlab.com Model&#xff1a;https://huggingface.co/OpenGVLab/InternVL-Chat-V1-5 公开时间&#xff1a;2024年4月29日 InternVL1.5&#xff0c;是一个开源的多模态大型语言模…

【无人机设计与控制】基于matlab的无人机FMCW(频率调制连续波)毫米波高度计雷达仿真

摘要 本文介绍了一种基于FMCW&#xff08;频率调制连续波&#xff09;雷达技术的无人机毫米波高度计的仿真。FMCW雷达通过测量发射信号与回波信号之间的频差来确定目标的距离和速度。在本项目中&#xff0c;我们使用MATLAB仿真无人机毫米波雷达的性能&#xff0c;展示其在不同…

TS1 order set分析

如下图&#xff0c;所示为TS1 order序列。该序列有16个symbol组成。 常见的symbol有&#xff0c;PAD和COM等。PAD是K symbol&#xff0c;还有D symbol。下文先给出COM symbol的解读。读协议文档可知COM常被称为K28.5。K是symbol的类型&#xff0c;注意symbol是编码过的数据。K…

六、Java 基础语法(下)

一、变量 1、变量的定义与使用 变量就是内存中的存储空间&#xff0c;空间中存储着经常发生改变的数据变量定义格式&#xff1a; 数据类型 变量名 数据值使用时根据变量名使用举例如下&#xff0c;上面是代码&#xff0c;下面是输出 2、变量的注意事项 变量名不允许重复…

Dyna-slam复现(保姆级详细图文版,百分百成功)

因最近论文要和这些算法做对比,故配置了一下,在此记录 因为是老的算法,cuda版本现在的显卡都不能使用,所以笔者找的电脑是华硕飞行堡垒17年的电脑,1080的显卡 深度学习及maskrcnn配置 先将dyna-slam git下来,终端执行 git clone https://github.com/BertaBescos/Dyna…

Arduino UNO R3自学笔记21 之 Arduino电机的闭环控制

注意&#xff1a;学习和写作过程中&#xff0c;部分资料搜集于互联网&#xff0c;如有侵权请联系删除。 前言&#xff1a;上篇写了电机速度测定&#xff0c;这篇主要是讲测定出的速度用于反馈&#xff0c;使得实际速度快速响应到需要的速度。 1.控制系统介绍 分2大类&#x…

ECML PKDD 2024 | 时空数据(Spatial-Temporal)和时间序列(Time series)论文总结

ECML PKDD 2024于9月9号-9月13号在立陶宛维尔纽斯举行&#xff08;Vilnius&#xff09; 本文总结了ECML PKDD 2024有关时空数据&#xff08;spatial-temporal data&#xff09;的相关论文&#xff0c;主要包含交通预测&#xff0c;预训练&#xff0c;迁移学习等内容&#xff0…

latex本地运行(MiKTeX+VScode)-20241006

1、安装 LaTex 主流的分发版本应该就是 TeXLive 和 MikTeX 了,这里使用 MikTex(只有几百M)—— TeXLive 太大了、默认安装全部包,可选自选部分安装单实在有些许麻烦,MikTeX 则方便得多,需要的时候可以自动安装全部包 点击跳转到 MiKTeX 官网,直接下载即可:不用担心什…

jQuery——事件委托

1、事件委托(委派/代理): 将多个子元素的事件监听委托给父辈元素处理监听回调是加在了父辈元素上当操作任何一个子元素 时&#xff0c;事件会冒泡到父辈元素父辈元素不会直接处理事件&#xff0c;而是根据 event.target 得到发生事件的子元素&#xff0c;通过这个子元素调用事…

微信第三方开放平台接入本地消息事件接口报错问题java.security.InvalidKeyException: Illegal key size

先看报错&#xff1a; java.security.InvalidKeyException: Illegal key sizeat javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039)at javax.crypto.Cipher.implInit(Cipher.java:805)at javax.crypto.Cipher.chooseProvider(Cipher.java:864)at javax.crypto.Cipher.in…

Spring MVC__@RequestMapping注解、获取请求参数、域对象共享数据、视图、Restful

目录 一、RequestMapping注解1、RequestMapping注解的功能2、RequestMapping注解的位置3、RequestMapping注解的value属性4、RequestMapping注解的method属性5、RequestMapping注解的params属性&#xff08;了解&#xff09;6、RequestMapping注解的headers属性&#xff08;了解…

登 Nature 子刊!论文一作详解蛋白质语言模型的小样本学习方法,解决湿实验数据匮乏难题

在「Meet AI4S」系列直播第三期中&#xff0c;我们有幸邀请到了上海交通大学自然科学研究院 & 上海国家应用数学中心博士后周子宜&#xff0c; 他所在的上海交通大学洪亮课题组研究方向主要为 AI 蛋白和药物设计、分子生物物理。该课题组研究成果颇丰&#xff0c;截止目前共…

小程序图片资源等使用阿里服务链接更新问题

同名更换图片&#xff0c;小程序无需发版本更新&#xff0c;存在图片缓存问题解决方法 修改Cache-Control参数即可