【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编译器交代的“遗嘱”开始工作和分配“栈区”的。因此,“栈区”的“临时分配”非真正严格意义上的“临时分配”。
第三个疑问:
问:函数内部所定义的局部变量总数不超过单片机的“栈”区的RAM数量,那,万一超过了“栈”区的RAM数量,后果严重吗?
答:后果特别严重。这种情况,专业术语叫“爆栈”。程序会出现异常,而且是莫名其妙的异常。为了避免这种情况,一般在编写程序的时候,函数内部都不能定义大数组的局部变量,局部变量的数量不能定义太多太大,尤其要避免刚才所说的定义开辟大数组局部变量这种情况。大数组的定义应该定义成全局变量,或者定义成“静态的局部变量”(“静态”这部分相关的内容后面章节会讲到)。有一些C编译器,遇到“爆栈”的情况,会好心跟你提醒让你编译不过去,但是也有一些C编译器可能就不会给你提醒,所以大家以后做项目写函数的时候,要对“爆栈”心存敬畏。
【54.5 全局变量和局部变量的优先级。】
刚才说到,全局变量的作用域是永久性并且不受范围限制的,而局部变量的作用域就是它所在函数的内部范围,那么问题来,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?这个问题就涉及到优先级。注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。为了深刻理解“全局变量和局部变量的优先级”,强烈建议大家必须仔细看完下面列举的三个练习例子。
【54.6 例程练习和分析。】
请看下面第一个例子:
/\*---C语言学习区域的开始。-----------------------------------------------\*/
unsigned char a=5; //此处第1个a是全局变量。
void main() //主函数
{
unsigned char a=2; //此处第2个a是局部变量。跟上面全局变量的第1个a重名了!
View(a); //把a发送到电脑端的串口助手软件上观察。
while(1)
{
}
}
/\*---C语言学习区域的结束。-----------------------------------------------\*/
分析:
上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是2?正确的答案是2。因为在函数内部,函数内部的局部变量比全局变量的优先级更加高。此时View(a)是第2个局部变量的a,而不是第1个全局变量的a。虽然这里的两个a重名了,但是它们的内存模型不一样,第1个全局变量的a是分配在“全局数据区”是具有唯一的地址的,而第2个局部变量的a是被分配在临时的“栈”区的,寄生在main函数内部。
再看下面第二个例子:
/\*---C语言学习区域的开始。-----------------------------------------------\*/
void HanShu(void); //函数声明
unsigned char a=5; //此处第1个a是全局变量。
void HanShu(void) //函数定义
{
unsigned char a=3; //此处第2个a是局部变量。
}
void main() //主函数
{
unsigned char a=2; //此处第3个a也是局部变量。
HanShu(); //子函数被调用
View(a); //把a发送到电脑端的串口助手软件上观察。
while(1)
{
}
}
/\*---C语言学习区域的结束。-----------------------------------------------\*/
分析:
上述例子,有3个变量重名了!其中一个是全局变量,另外两个是局部变量。此时输出显示的结果是5还是3还是2?正确的答案是2。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量a)是在执行View(a)语句的时候就消亡不存在了,所以此时View(a)的a是第3个局部变量的a(在main函数内部定义的局部变量的a)。
再看下面第三个例子:
/\*---C语言学习区域的开始。-----------------------------------------------\*/
void HanShu(void); //函数声明
unsigned char a=5; //此处第1个a是全局变量。
void HanShu(void) //函数定义
{
unsigned char a=3; //此处第2个a是局部变量。
}
void main() //主函数
{
HanShu(); //子函数被调用
View(a); //把a发送到电脑端的串口助手软件上观察。
while(1)
{
}
}
/\*---C语言学习区域的结束。-----------------------------------------------\*/
分析:
上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是3?正确的答案是5。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量)是在执行View(a)语句的时候就消亡不存在了,同时,因为此时main函数内部也没有定义a的局部变量,所以此时View(a)的a是必然只能是第1个全局变量的a(在main函数外面定义的全局变量的a)。
【54.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
- 首页
- 第一节:我的价值观
- 第二节:初学者的疑惑
- 第三节:单片机最重要的一个特性
- 第四节:平台软件和编译器软件的简介
- 第五节:用Keil2软件关闭,新建,打开一个工程的操作流程
- 第六节:把.c源代码编译成.hex机器码的操作流程
- 第七节:本节预留
- 第八节:把.hex机器码程序烧录到单片机的操作流程
- 第九节:本节预留
- 第十节:程序从哪里开始,要到哪里去?
- 第十一节:一个在单片机上练习C语言的模板程序
- 第十二节:变量的定义和赋值
- 【TODO】第十三节:赋值语句的覆盖性
- 【TODO】第十四节:二进制与字节单位,以及常用三种变量的取值范围
- 【TODO】第十五节:二进制与十六进制
- 【TODO】第十六节:十进制与十六进制
- 【TODO】第十七节:加法运算的5种常用组合
- 【TODO】第十八节:连加、自加、自加简写、自加1
- 【TODO】第十九节:加法运算的溢出
- 【TODO】第二十节:隐藏中间变量为何物?
- 【TODO】第二十一节:减法运算的5种常用组合。
- 【TODO】第二十二节:连减、自减、自减简写、自减1
- 【TODO】第二十三节:减法溢出与假想借位
- 【TODO】第二十四节:借用unsigned long类型的中间变量可以减少溢出现象
- 【TODO】第二十五节:乘法运算中的5种常用组合
- 【TODO】第二十六节:连乘、自乘、自乘简写,溢出
- 【TODO】第二十七节:整除求商
- 【TODO】第二十八节:整除求余
- 【TODO】第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
- 【TODO】第三十节:逻辑运算符的“与”运算
- 【TODO】第三十一节:逻辑运算符的“或”运算
- 【TODO】第三十二节:逻辑运算符的“异或”运算
- 【TODO】第三十三节:逻辑运算符的“按位取反”和“非”运算
- 【TODO】第三十四节:移位运算的左移
- 【TODO】第三十五节:移位运算的右移
- 【TODO】第三十六节:括号的强制功能---改变运算优先级
- 【TODO】第三十七节:单字节变量赋值给多字节变量的疑惑
- 【TODO】第三十八节:第二种解决“运算过程中意外溢出”的便捷方法
- 【TODO】第三十九节:if判断语句以及常量变量的真假判断
- 【TODO】第四十节:关系符的等于“==”和不等于“!=”
- 【TODO】第四十一节:关系符的大于“>”和大于等于“>=”
- 【TODO】第四十二节:关系符的小于“<”和小于等于“<=”
- 【TODO】第四十三节:关系符中的关系符:与“&&”,或“||”
- 【TODO】第四十四节:小括号改变判断优先级
- 【TODO】第四十五节: 组合判断if...else if...else
- 【TODO】第四十六节: 一维数组
- 【TODO】第四十七节: 二维数组
- 【TODO】第四十八节: while循环语句
- 【TODO】第四十九节: 循环语句do while和for
- 【TODO】第五十节: 循环体内的continue和break语句
- 【TODO】第五十一节: for和while的循环嵌套
- 【TODO】第五十二节: 支撑程序框架的switch语句
- 【TODO】第五十三节: 使用函数的三要素和执行顺序
- 【TODO】第五十四节: 从全局变量和局部变量中感悟“栈”为何物
- 【TODO】第五十五节: 函数的作用和四种常见书写类型
- 【TODO】第五十六节: return在函数中的作用以及四个容易被忽略的功能
- 【TODO】第五十七节: static的重要作用
- 【TODO】第五十八节: const(./book/或code)在定义数据时的作用
- 【TODO】第五十九节: 全局“一键替换”功能的#define
- 【TODO】第六十节: 指针在变量(./book/或常量)中的基础知识
- 【TODO】第六十一节: 指针的中转站作用,地址自加法,地址偏移法
- 【TODO】第六十二节: 指针,大小端,化整为零,化零为整
- 【TODO】第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用
- 【TODO】第六十四节: 指针让函数具备了多个相当于return的输出口
- 【TODO】第六十五节: 指针作为数组在函数中的入口作用
- 【TODO】第六十六节: 指针作为数组在函数中的出口作用
- 【TODO】第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用
- 【TODO】第六十八节: 为函数接口指针“定向”的const关键词
- 【TODO】第六十九节: 宏函数sizeof(./book/)
- 【TODO】第七十节: “万能数组”的结构体
- 【TODO】第七十一节: 结构体的内存和赋值
- 【TODO】第七十二节: 结构体的指针
- 【TODO】第七十三节: 结构体数据的传输存储和还原
- 【TODO】第七十四节: 结构体指针在函数接口处的频繁应用
- 【TODO】第七十五节: 指针的名义(例:一维指针操作二维数组)
- 【TODO】第七十六节: 二维数组的指针
- 【TODO】第七十七节: 指针唯一的“单向输出”通道return
- 【TODO】第七十八节: typedef和#define和enum
- 【TODO】第七十九节: 各种变量常量的命名规范
- 【TODO】第八十节: 单片机IO口驱动LED
- 【TODO】第八十一节: 时间和速度的起源(指令周期和晶振频率)
- 【TODO】第八十二节: Delay“阻塞”延时控制LED闪烁
- 【TODO】第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁
- 【TODO】第八十四节: 中断与中断函数
- 【TODO】第八十五节: 定时中断的寄存器配置
- 【TODO】第八十六节: 定时中断的“非阻塞”延时控制LED闪烁
- 【TODO】第八十七节: 一个定时中断产生N个软件定时器
- 【TODO】第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)
- 【TODO】第八十九节: 跑马灯的三种境界
- 【TODO】第九十节: 多任务并行处理两路跑马灯
- 【TODO】第九十一节: 蜂鸣器的“非阻塞”驱动
- 【TODO】第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)
- 【TODO】第九十三节: 独立按键鼠标式的单击与双击
- 【TODO】第九十四节: 两个独立按键构成的组合按键
- 【TODO】第九十五节: 两个独立按键的“电脑键盘式”组合按键
- 【TODO】第九十六节: 独立按键“一键两用”的短按与长按
- 【TODO】第九十七节: 独立按键按住不松手的连续均匀触发
- 【TODO】第九十八节: 独立按键按住不松手的“先加速后匀速”的触发
- 【TODO】第九十九节: “行列扫描式”矩阵按键的单个触发(原始版)
- 【TODO】第一百节: “行列扫描式”矩阵按键的单个触发(优化版)
- 【TODO】第一百零一节: 矩阵按键鼠标式的单击与双击
- 【TODO】第一百零二节: 两个“任意行输入”矩阵按键的“有序”组合触发
- 【TODO】第一百零三节: 两个“任意行输入”矩阵按键的“无序”组合触发
- 【TODO】第一百零四节: 矩阵按键“一键两用”的短按与长按
- 【TODO】第一百零五节: 矩阵按键按住不松手的连续均匀触发
- 【TODO】第一百零六节: 矩阵按键按住不松手的“先加速后匀速”触发
- 【TODO】第一百零七节: 开关感应器的识别与软件滤波
- 【TODO】第一百零八节: 按键控制跑马灯的启动和暂停和停止
- 【TODO】第一百零九节: 按键控制跑马灯的方向
- 【TODO】第一百一十节: 按键控制跑马灯的速度
- 第一百一十一节: 工业自动化设备的开关信号的运动控制
- 【TODO】第一百一十二节: 数码管显示的基础知识
- 【TODO】第一百一十三节: 动态扫描的数码管显示数字
- 【TODO】第一百一十四节: 动态扫描的数码管显示小数点
- 【TODO】第一百一十五节: 按键控制数码管的秒表
- 【TODO】第一百一十六节: 按键控制数码管的倒计时
- 【TODO】第一百一十七节: 按键切换数码管窗口来设置参数
- 【TODO】第一百一十八节: 按键让某位数码管闪烁跳动来设置参数
- 【TODO】第一百一十九节: 一个完整的人机界面的程序框架的脉络
- 【TODO】第一百二十节: 按键切换窗口切换局部来设置参数
- 【TODO】第一百二十一节: 可调参数的数码管倒计时
- 【TODO】第一百二十二节: 利用定时中断做的“时分秒”数显时钟
- 【TODO】第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序
- 【TODO】第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯
- 【TODO】第一百二十五节: “双线”的肢体接触通信
- 【TODO】第一百二十六节: “单线”的肢体接触通信
- 【TODO】第一百二十七节: 单片机串口接收数据的机制
- 【TODO】第一百二十八节: 接收“固定协议”的串口程序框架
- 【TODO】第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架
- 【TODO】第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架
- 【TODO】第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架
- 【TODO】第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架
- 【TODO】第一百三十三节:常用的三种串口发送函数
- 【TODO】第一百三十四节:“应用层半双工”双机串口通讯的程序框架