从单片机基础到程序框架(全集 2019pdf版).pdf - 第229页
第五十四节: 从全局变量和局部变量中感悟“栈”为何物 。 【54.1 本节阅读前的名词约定。 】 变量可以粗 略的分成两 类, 一类是全局 变量, 一类是局部 变量。 如果更深一 步精细划分 , 全局变量还 可 以分成“普通 全局变量”和 “静态全局变 量” ,局 部变量也可以 分成“普通 局部变量” 和“静态局部变 量” , 也就是说,若精细划分,可以分成四类 。其中 “静态全局变 量”和 “静态局部变 量”多了一个前 缀“静态” ,…
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C 语言学习区域”的代码就可以了,
其它部分的代码不要动。编译后,把程序下载进带串口的 51 学习板,通过电脑端的串口助手软件就可以观
察到不同的变量数值,详细方法请看第十一节内容。

第五十四节: 从全局变量和局部变量中感悟“栈”为何物。
【54.1 本节阅读前的名词约定。】
变量可以粗略的分成两类,一类是全局变量,一类是局部变量。如果更深一步精细划分,全局变量还可
以分成“普通全局变量”和“静态全局变量”,局部变量也可以分成“普通局部变量”和“静态局部变量”,
也就是说,若精细划分,可以分成四类。其中“静态全局变量”和“静态局部变量”多了一个前缀“静态”,
这个前缀“静态”是因为在普通的变量前面多加了一个修饰关键词“static”,这部分的内容后续章节会讲
到。本节重点为了让大家理解内存模型的“栈”,暂时不考虑“静态变量”的情况,人为约定,本节所涉及
的“全局变量”仅仅默认为“普通全局变量”,“局部变量”仅仅默认为“普通局部变量”。
【54.2 如何判定全局变量和局部变量?】
全局变量就是在函数外面定义的变量,局部变量就是在函数内部定义的变量,这是最直观的判定方法。
下面的例子能很清晰地说明全局变量和局部变量的判定方法:
unsigned char a; //在函数外面定义的,所以是全局变量。
void main() //主函数
{
unsigned char b; //在函数内部定义的,所以是局部变量。
b=a;
while(1)
{
}
}
【54.3 全局变量和局部变量的内存模型。】
单片机内存包括 ROM 和 RAM 两部分,ROM 存储的是单片机程序中的指令和一些不可更改的常量数据,而
RAM 存放的是可以被更改的变量数据,也就是说,全局变量和局部变量都是存放在 RAM,但是,虽然都是存
放在 RAM,全局变量和局部变量之间的内存模型还是有明显的区别的,因此,分了两个不同的 RAM 区,全局
变量占用的 RAM 区称为“全局数据区”,局部变量占用的 RAM 区称为“栈”,因为我后面会用宾馆来比喻“栈”,
为了方便记忆,大家可以把“栈”想象成 “客栈”来记忆。它们的内存模型到底有什么本质的区别呢?“全
局数据区”就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住(假设你还没结婚的时候),
而且是永久的,所以说每个全局变量都有唯一对应的 RAM 地址,不可能重复的。而“栈”就像宾馆客栈,一
年下来每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址一年下
来每天可能住进不同的人,不是唯一的。“全局数据区”的全局变量拥有永久产权,“栈”区的局部变量只能
临时居住在宾馆客栈,地址不是唯一的,有期限的。全局变量像私人区,局部变量像公共区。“栈”的这片
公共区,是给程序里所有函数内部的局部变量共用的,函数被调用的时候,该函数内部的每个局部变量就会
被分配对应到“栈”的某个 RAM 地址,函数调用结束后,该局部变量就失效,因此它对应的“栈”的 RAM 空
间就被收回以便给下一个被调用的函数的局部变量占用。请看下面这个例子,我借用“宾馆客栈”来比喻局
部变量所在的“栈”。
void HanShu(void); //子函数的声明

void HanShu(void) //子函数的定义
{
unsigned char a; //局部变量
a=1;
}
void main() //主函数
{
HanShu() ; //子函数的调用
}
分析:上述例子,单片机从主函数 main 往下执行,首先遇到 HanShu 子函数的调用,所以就跳到 HanShu
函数的定义那里开始执行,此时的局部变量 a 开始被分配在 RAM 的“栈区”的某个地址,相当于你入住宾馆
被分配到某个房间。单片机执行完子函数 HanShu 后,局部变量 a 在 RAM 的“栈区”所分配的地址被收回,
局部变量 a 消失,被收回的 RAM 地址可能会被系统重新分配给其它被调用的函数的局部变量,此时相当于你
离开宾馆,从此你跟那个宾馆的房间没有啥关系,你原来在宾馆入住的那个房间会被宾馆老板重新分配给其
他的客人入住。全局变量的作用域是永久性不受范围限制的,而局部变量的作用域就是它所在函数的内部范
围。全局变量的“全局数据区”是永久的私人房子(这里的“永久”仅仅是举一个例子,别拿“70 年产权”
来抬杠),局部变量的“栈”是临时居住的“客栈”。重要的事情说两遍,再次总结如下:
(1)每定义一个新的全局变量,就意味着多开销一个新的 RAM 内存。而每定义一个局部变量,只要在函
数内部所定义的局部变量总数不超过单片机的“栈”区,此时的局部变量不开销新的 RAM 内存,因为局部变
量是临时借用“栈”区的,使用后就还给“栈”,“栈”是公共区,可以重复利用,可以服务若干个不同的函
数内部的局部变量。
(2)单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化,全局变量
是一直保存之前最后一次更改的值。
【54.4 三个常见疑问。】
第一个疑问:
问:“全局数据区”和“栈区“是谁在幕后分配的,怎么分配的?
答:是 C 编译器自动分配的,至于怎么分配,谁分配多一点,谁分配少一点,C 编译器会有一个默认的
比例分配,我们一般都不用管。
第二个疑问:
问:“栈”区是临时借用的,子函数被调用的时候,它内部的局部变量才会“临时”被分配到“栈”区
的某个地址,那么问题来了,谁在幕后主持“栈区”这些分配的工作,难道也是 C 编译器?C 编译器不是在
编译程序的时候一次性就做完了编译工作然后就退出历史舞台了吗?难道我们程序已经在单片机内部运转
的时候,编译器此时还在幕后指手画脚的起作用?
答:单片机已经上电开始运行程序的时候,编译器是不可能起作用的。所以,真相只有一个,“栈区”
分配给函数内部局部变量的工作,确实是 C 编译器做的,唯一需要注意的地方是,它不是“现炒现卖”,而
是在单片机上电前,C 编译器就把所有函数内部的局部变量的分配工作就规划好了,都指定了如果某个函数
一旦被调用,该函数内部的哪个局部变量应该分到“栈区”的哪个地址,C 编译器都是事先把这些“后事”
都交代完毕了才“结束自己的生命”,后面,等单片机上电开始工作的时候,虽然 C 编译器此时“不在”了,
但是单片机都是严格按照 C 编译器交代的“遗嘱”开始工作和分配“栈区”的。因此,“栈区”的“临时分
配”非真正严格意义上的“临时分配”。