从嵌入式编程中感悟「栈」为何方神圣?
ID:技术让梦想更伟大
作者:李肖遥
何为变量?
变量一般可以细分为如下图:
本节重点为了让大家理解内存模型的“栈”,暂时不考虑“静态变量” 的情况,并约定如下:
“全局变量”仅仅默认为“普通全局变量”;
“局部变量”仅仅默认为“普通局部变量”。
如何判定全局变量和局部变量?
简单直观的来说,全局变量就是在函数外面定义的变量,局部变量就是在函数内部定义的变量,下面的例子能很清晰地说明全局变量和局部变量的判定方法:
unsigned char a; //在函数外面定义的, 所以是全局变量。
void main() //主函数
{
unsigned char b; //在函数内部定义的, 所以是局部变量。
b=a;
while(1)
{
}
}
全局变量和局部变量的内存模型
单片机内存包括ROM
和RAM
两部分,ROM
存储的是单片机程序中的指令和一些不可更改的常量数据,而 RAM
存放的是可以被更改的变量数据;
也就是说,全局变量和局部变量都是存放在RAM
,但是,虽然都是存放在 RAM
,全局变量和局部变量之间的内存模型还是有明显的区别的。
因此,分了两个不同的RAM
区,全局变量占用的 RAM
区称为全局数据区, 局部变量占用的 RAM
区称为栈。
它们的内存模型到底有什么本质的区别呢?
全局数据区就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住(假设你还是单身狗的时候),而且是永久的(sorry),所以说每个全局变量都有唯一对应的 RAM 地址, 不可能重复的。
栈就像客栈, 一年下来每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址一年下来每天可能住进不同的人,不是唯一的。
全局数据区的全局变量拥有永久产权,栈区的局部变量只能临时居住在宾馆客栈, 地址不是唯一的, 有期限的。
栈是给程序里所有函数内部的局部变量共用的,函数被调用的时候,该函数内部的每个局部变量就会被分配对应到栈的某个RAM
地址,函数调用结束后,该局部变量就失效。
因此它对应的栈 的RAM
空间就被收回,以便给下一个被调用的函数的局部变量占用。
举例借用“宾馆客栈”来比喻局部变量所在的“栈”。
void function(void); //子函数的声明
void function(void) //子函数的定义
{
unsigned char a; //局部变量
a=1;
}
void main() //主函数
{
function() ; //子函数的调用
}
我们看到单片机从主函数 main 往下执行, 首先遇到function()
子函数的调用, 所以就跳到function()
函数的定义那里开始执行, 此时的局部变量 a 开始被分配在 RAM
的“栈区” 的某个地址, 相当于你入住宾馆被分配到某个房间。
单片机执行完子函数function()
后,局部变量 a 在 RAM
的栈区
所分配的地址被收回, 局部变量a 消失,被收回的RAM
地址可能会被系统重新分配给其它被调用的函数的局部变量。
此时相当于你离开宾馆,从此你跟那个宾馆的房间没有啥关系, 你原来在宾馆入住的那个房间会被宾馆老板重新分配给其他的客人入住。
全局变量的作用域是永久性不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。全局变量的全局数据区
是永久的私人房子,局部变量的栈
是临时居住的客栈。
总结如下
-
每定义一个新的全局变量,就意味着多开销一个新的
RAM
内存。而每定义一个局部变量,只要在函数内部所定义的局部变量总数不超过单片机的栈
区,此时的局部变量不开销新的RAM
内存, 因为局部变量是临时借用栈
的, 使用后就还给栈
,栈
是公共区, 可以重复利用,可以服务若干个不同的函数内部的局部变量。 -
单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化, 全局变量是一直保存之前最后一次更改的值。
有哪些常见疑问?
全局数据区和栈区是谁在幕后分配的, 怎么分配的?
是C编译器自动分配的, 至于怎么分配,谁分配多一点,谁分配少一点,C 编译器会有一个默认的比例分配, 我们一般都不用管。
栈区是临时借用的,子函数被调用的时候,它内部的局部变量才会“临时” 被分配到“栈” 区的某个地址,那么问题来了,谁在幕后主持“栈区” 这些分配的工作?
单片机已经上电开始运行程序的时候,编译器已经不起作用,“栈区” 分配给函数内部局部变量的工作,确实是 C 编译器做的,但这是在单片机上电前。
C 编译器就把所有函数内部的局部变量的分配工作就规划好了,都指定了如果某个函数一旦被调用,该函数内部的哪个局部变量应该分到“栈区” 的哪个地址,C 编译器都是事先把这些“后事” 都交代完毕了才结束自己的生命。
等单片机上电开始工作的时候,虽然C编译器此时不在了,但是单片机都是严格按照C编译器交代的遗嘱开始工作和分配“栈区”的。因此,“栈区” 的“临时分配” 非真正严格意义上的“临时分配”。
函数内部所定义的局部变量总数不超过单片机的“栈” 区的 RAM 数量, 那, 万一超过了“栈” 区的 RAM数量, 后果严重吗?
这种情况专业术语叫爆栈。程序会出现莫名其妙的异常,后果特别严重。
为了避免这种情况, 一般在编写程序的时候, 函数内部都不能定义大数组的局部变量, 局部变量的数量不能定义太多太大,尤其要避免刚才所说的定义开辟大数组局部变量这种情况。
大数组的定义应该定义成全局变量,或者定义成 静态的局部变量。
有一些C编译器,遇到“爆栈” 的情况,会好心跟你提醒让你编译不过去,但是也有一些 C 编译器可能就不会给你提醒,所以大家以后做项目写函数的时候,要对爆栈心存敬畏。
全局变量和局部变量的优先级
刚才说到,全局变量的作用域是永久性并且不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。
那么问题来了,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?
这个问题就涉及到优先级。
注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。
我们来举一些例子
请看下面第一个例子
unsigned char a=5; //此处第 1 个 a 是全局变量
void main() //主函数
{
unsigned char a=2; //此处第2个a是局部变量,跟上面全局变量的第1个a重名了
print(a); //把a发送到电脑端的串口助手软件上观察
while(1)
{
}
}
正确的答案是 2。在函数内部的局部变量比全局变量的优先级更加高。
虽然这里的两个a重名了, 但是它们的内存模型不一样,第1个全局变量的a是分配在全局数据区
,是具有唯一的地址的,而第2个局部变量的a是被分配在临时的栈区
的,寄生在 main 函数内部。
再看下面第二个例子
void function(void); //函数声明
unsigned char a=5; //此处第1个 a 是全局变量
void function(void) //函数定义
{
unsigned char a=3; //此处第 2 个 a 是局部变量。
}
void main() //主函数
{
unsigned char a=2; //此处第 3 个 a 也是局部变量。
function(); //子函数被调用
print(a); //把 a 发送到电脑端的串口助手软件上观察。
while(1)
{
}
}
正确的答案是2。因为,function这个子函数是被调用结束之后,才执行 print(a)的, 就意味函数内部的局部变量(第2个局部变量 a)是在执行 print(a)语句的时候就消亡不存在了, 所以此时print(a)的a是第3个局部变量的a(在 main 函数内部定义的局部变量的 a)。
再看下面第三个例子
void function(void); //函数声明
unsigned char a=5; //此处第1个a是全局变量
void function(void) //函数定义
{
unsigned char a=3; //此处第2个a 是局部变量
}
void main() //主函数
{
function(); //子函数被调用
print(a); //把a发送到电脑端的串口助手软件上观察
while(1)
{
}
}
正确的答案是5。因为function这个子函数是被调用结束之后,才执行print(a)的,就意味function函数内部的局部变量(第2个局部变量)是在执行function(a)语句的时候就消亡不存在了。
同时,因为此时main函数内部也没有定义a的局部变量,所以此时function(a)的a是必然只能是第1个全局变量的a(在main函数外面定义的全局变量的a)。
最后
看到本文之后,相信大家已经对栈有了一些基础的认识,在嵌入式编程中,我们也要时刻注意,避免爆栈;如果有错误欢迎指出,我们下一期,再见。
嵌入式编程专辑 Linux 学习专辑
C/C++编程专辑
关注 微信公众号『技术让梦想更伟大』,后台回复“ m ”查看更多内容,回复“ 加群 ”加入技术交流群。
长按前往图中包含的公众号关注
本文分享自微信公众号 - 技术让梦想更伟大(gh_f7effb2fbc1c)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
推荐系统基础:使用PyTorch进行矩阵分解进行动漫的推荐
我们一天会遇到很多次推荐——当我们决定在Netflix/Youtube上看什么,购物网站上的商品推荐,Spotify上的歌曲推荐,Instagram上的朋友推荐,LinkedIn上的工作推荐……列表还在继续!推荐系统的目的是预测用户对某一商品的“评价”或“偏好”。这些评级用于确定用户可能喜欢什么,并提出明智的建议。 推荐系统主要有两种类型: 基于内容的系统:这些系统试图根据项目的内容(类型、颜色等)和用户的个人资料(喜欢、不喜欢、人口统计信息等)来匹配用户。例如,Youtube可能会根据我是一个厨师的事实,以及/或者我过去看过很多烘焙视频来推荐我烹饪视频,从而利用它所拥有的关于视频内容和我个人资料的信息。 协同过滤:他们依赖于相似用户喜欢相似物品的假设。用户和/或项目之间的相似性度量用于提出建议。 本文讨论了一种非常流行的协同过滤技术——矩阵分解。 矩阵分解 推荐系统有两个实体-用户和物品(物品的范围十分广泛,可以是实际出售的产品,也可以是视频,文章等)。假设有m个用户和n个物品。我们推荐系统的目标是构建一个mxn矩阵(称为效用矩阵),它由每个用户-物品对的评级(或偏好)组成。最初,这...
- 下一篇
Code Review最佳实践
我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题。包括像Google、微软这些公司,Code Review都是基本要求,代码合并之前必须要有人审查通过才行。 然而对于我观察到的大部分软件开发团队来说,认真做Code Review的很少,有的流于形式,有的可能根本就没有Code Review的环节,代码质量只依赖于事后的测试。也有些团队想做好代码审查,但不知道怎么做比较好。 网上关于如何做Code Review的文章已经有很多了,这里我结合自己的一些经验,也总结整理了一下Code Review的最佳实践,希望能对大家做好Code Review有所帮助。 Code Review有什么好处? 很多团队或个人不做Code Review,根源还是不觉得这是一件有意义的事情,不觉得有什么好处。这个问题要从几个角度来看。 首先是团队知识共享的角度 一个开发团队中,水平有高有低,每个人侧重的领域也有不同。怎么让高水平的帮助新人成长?怎么让大家都对自己侧重领域之外的知识保持了解?怎么能有人离职后其他人能快速接手?这些都是团队管...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程
- Hadoop3单机部署,实现最简伪集群
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19