# 第十二节:变量的定义和赋值
## 【12.1 学习 C 语言的建议和方法】
先提一些学 C 语言的建议和方法,帮大家删繁就简,去掉一些初学者常见的思想包袱。现阶段我们的学习是使用单片机,把单片机当做一个成品,把单片机当做一个忠诚的士兵,学习 C 语言就是学习如何使用单片机,如何命令单片机,如何让单片机听懂我们的话并且听我们指挥。单片机内部太细节的构造原理暂时不用过多去关注,只要知道跟我们使用相关的几个特征就可以,这样初学者的学习包袱就没有那么重,就可以把重点放在使用上的,而不是好奇于根本原理的死磕到底。学 C 语言跟学习英语的性质是一样的,都是在学习一门外语,只是 C 语言比英语的语法要简单很多,非常容易上手,词汇量也没有英语那么多,C 语言常用单词才几十个而已。学习任何一门语言的秘诀在于练习,学习 C 语言的秘诀是多在单片机上练习编程。本教程后面几乎每个章节都有例程,这个例程很重要,初学者即使看懂了,我也强烈建议要把 “C 语言学习区域” 的那部分代码亲自上机敲键盘练习一遍,并且看看实验现象是否如你所愿。
## 【12.2 变量定义和赋值的感性认识】
这些年我用过很多单片机,比如 51,PIC,LPC17 系列,STM8,STM32 等单片机。尽管各类单片机有一些差异,但是在选单片机时有 3 个参数我们一定会关注的,它们分别是:工作频率,数据存储器 RAM,程序存储器 ROM。工作频率跟晶振和倍频有关,决定了每条指令所要损耗的时间,从而决定了运算速度。RAM 跟代码里所定义变量的数量有关。ROM 跟程序代码量的大小有关。程序是什么?程序就是由对象和行为两者构成的。对象就是变量,就是变量的定义,就是 RAM,RAM 的大小决定了一个程序允许的对象数量。行为就是赋值,判断,跳转,运算等语法,就是 ROM,ROM 的大小决定了一个程序允许的行为程度。本节的标题是 “变量的定义和赋值”,其中 “定义” 就是对象,“赋值” 就是行为。
## 【12.3 变量的定义】
变量的定义。一个程序最大允许有多少个对象,是由 RAM 的字节数决定的 (字节是一种单位,后面章节会讲到)。本教程的编译环境是以 AT89C52 芯片为准,AT89C52 这个单片机有 256 个字节的 RAM,但是并不意味着程序就一定要全部占用这些 RAM。程序需要占用多少 RAM,完全是根据程序的实际情况来决定,需要多少就申请多少。这里的 “对象” 就是变量,这里的 “申请” 就是变量的定义。
定义变量的关键字。常用有 3 种容量的变量,每种变量的取值范围不一样。第一种是 `unsigned char` 变量,取值范围从 0 到 255,占用 RAM 一个字节,比喻成一房一厅。第二种是`unsigned int` 变量,取值范围从 0 到 65535,占用 RAM 两个字节,比喻成两房一厅。第三种是 `unsigned long` 变量,取值范围从 0 到 4294967295,占用 RAM 四个字节,比喻成四房一厅。`unsigned char`, `unsigned int` 和 `unsigned long` 都是定义变量的关键字,所谓关键字也可以看成是某门外语的单词,需要大家记忆的,当然不用死记硬背,只要多上机练习就自然熟记于心,出口成章。多说一句,上述的变量范围是针对本教程所用的单片机,当针对不同的单片机时上述变量的范围可能会有一些小差异,比如在 STM32 单片机中,`unsigned int` 的字节数就不是两个字节,而是四个字节,这些都是由所选的编译器决定的,大家暂时有个初步了解就可以。
定义变量的语法格式。定义变量的语法格式由 3 部分组成:关键字、变量名、分号。比如: ` unsigned char a; `
其中 unsigned char 就是关键字,a 就是变量名,分号 `;` 就是一条语句的结束符号。
变量名的命名规则。变量名的第一个字符不能是数字,必须是字母或者下划线,字母或者下划线后面可以带数字,一个变量名之间的字符不能带空格,两个独立变量名之间也不能用空格隔开(但是两个独立变量名之间可以用逗号隔开)。变量名不能跟编译器已征用的关键字重名,不能跟函数名重名,这个现象跟古代要求臣民避讳皇帝的名字有点像。哪些名字是合法的,哪些名字是不合法的?现在举一些例子说明:
```c
unsigned char 3a; // 不合法,第一个字符不能是数字。
unsigned char char; // 不合法,char 是编译器已征用的关键字。
unsigned char a b; // 不合法,ab 是一个变量名,a 与 b 的中间不能有空格。
unsigned char a,b; // 合法,a 和 b 分别是一个独立的变量名,a 与 b 的中间可以用逗号隔开。
unsigned char a; // 合法。
unsigned char abc; // 合法。
unsigned char _ab; // 合法。
unsigned char _3ab; // 合法。
unsigned char a123; // 合法。
unsigned char a12ced; // 合法。
```
定义变量与 RAM 的内在关系。当我们定义一个变量时,相当于向单片机申请了一个 RAM 空间。C 编译器会自动为这个变量名分配一个 RAM 空间,每个字节的 RAM 空间都有一个固定唯一的地址。把每个字节的 RAM 空间比喻成房间,这个地址就是房号。地址是纯数字编号,不利于我们记忆,C 语言编译器为了降低我们的工作难度,不用我们记每个变量的地址,只需要记住这个变量的名称就可以了。操作某个变量名,就相当于操作某个对应地址的 RAM 空间。变量名与对应地址 RAM 空间的映射关系是 C 编译器暗中悄悄帮我们分配好的。比如:
```c
unsigned char a; //a 占用一个字节的 RAM 空间,这个空间的地址由 C 编译自动分配。
unsigned char b; //b 占用一个字节的 RAM 空间,这个空间的地址由 C 编译自动分配。
unsigned char c; //c 占用一个字节的 RAM 空间,这个空间的地址由 C 编译自动分配。
```
上述 a,b,c 三个变量各自占用一个字节的 RAM 空间,同时被 C 编译器分配了 3 个不同的 RAM 空间地址。
变量定义的初始化。变量定义之后,等于被 C 编译器分配了一个 RAM 空间,那么这个空间里面存储的数据是什么?如果没有刻意给它初始化,RAM 空间里面存储的数据是不太确定的,是默认的。有些场合,需要在给变量分配 RAM 空间时就给它一个固定的初始值,这就是变量定义的初始化。变量初始化的语法格式由 3 部分组成:关键字,变量名赋值,分号。比如:
```c
unsigned char a=9;
```
其中 unsigned char 就是关键字。
其中 a = 9 就是变量名赋值。a 从被 C 编译器分配 RAM 空间那一刻起,就默认是预存了一个 9 的数据。
分号 `;` 就是一条语句的结束符号。
## 【12.4 变量的赋值】
赋值语句的含义。把右边对象的内容复制一份给左边对象。赋值语句有一个很重要的特性,就是覆盖性,左边对象原来的内容会被右边对象复制过来的新内容所覆盖。比如,左边对象是变量 a,假设原来 a 里面存的数据是 3,右边对象是数据 6,执行赋值语句后,会把右边的 6 赋值给了对象 a,那么 a 原来的数据 3 就被覆盖丢失了,变成了 6。
赋值语句的格式。赋值语句的语法格式由 4 部分组成:左边对象,关键字,右边对象,分号。比如:
`a = b;`
其中 a 就是左边对象。
其中 `=` 就是关键字。写法跟我们平时用的等于号是一样,但是在 C 语言里不是等于的意思,而是代表赋值的意思,它是代表中文含义的 “给”,而不是用于判断的 “等于”,跟等于号是两码事(C 语言的等于号是 “==”,这个后面章节会讲到)。
其中 b 就是右边对象。
其中分号 `;` 代表一条语句的结束符。
赋值语句与 ROM 的关系。赋值语句是行为的一种,所以编译会把赋值这个行为翻译成对应的指令,这些指令在下载程序时最终也是以数据的形式存储在 ROM 里,指令也是以字节为单位 (字节是一种单位,后面章节会讲到)。本教程的编译环境是以 AT89C52 芯片为准,AT89C52 这个单片机有 8K 的 ROM 容量,也就是有 8192 个字节的 ROM(8 乘以 1024 等于 8192),但是并不意味着程序就一定要全部占用这些 ROM。程序需要占用多少 ROM,完全是根据程序的行为程度决定,也就是通常所说的你的程序容量有多大,有多少行代码。多说一句,在单片机或者我们常说的计算机领域里,存储容量是以字节为单位,而每 K 之间的进制不是我们日常所用的 1000,而是 1024,所以刚才所说的 8K 不是 8000,而是 8192,这个是初学者很容易迷惑的地方。刚才提到,赋值语句是行为,凡是程序的行为指令都存储在单片机的 ROM 区。C 编译器会把一条赋值语句翻译成对应的一条或者几条机器码,机器码指令也是以字节为单位的。下载程序的时候,这些机器码就会被下载进单片机的 ROM 区。比如以下这行赋值语句:
```c
unsigned char a;
unsigned char b = 3;
a = b;
```
经过 C 编译器编译后会生成以字节为单位的机器码。这些机器码记录着这些信息:变量 a 的 RAM 地址,变量 b 的 RAM 地址和初始化时的预存数据 3,以及把 b 变量的内容赋值给 a 变量的这个行为。所有这些信息,不管是 “数据” 还是 “行为”,本质都是以 “数据”(或称数字,数码都可以)的形式存储记录的,单位是字节。
## 【12.5 例程的分析和练习】
接下来练习一个程序实例。直接复制前面章节中第十一节的模板程序,练习代码时只需要更改 “C 语言学习区域” 的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的 51 学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。本章节在 “C 语言学习区域” 练习的代码如下:
```c
/*---C语言学习区域的开始。-----------------------------------------------*/
void main() //主函数
{
unsigned char a; //定义的变量a被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
unsigned char b; //定义的变量b被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
unsigned char c; //定义的变量c被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
unsigned char d=9; //定义的变量d被分配了一个字节的RAM空间,保存的数据被初始化成9.
b=3; //把3赋值给变量b,b由原来不确定的默认数据变成了3。
c=b; //把变量b的内容复制一份赋值给左边的变量c,c从不确定的默认值变成了3。
View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
while(1)
{
}
}
/*---C语言学习区域的结束。-----------------------------------------------*/
```
在电脑串口助手软件上观察到的程序执行现象如下:
> 开始...
> 第1个数
> 十进制:255
> 十六进制:FF
> 二进制:11111111
> 第2个数
> 十进制:3
> 十六进制:3
> 二进制:11
> 第3个数
> 十进制:3
> 十六进制:3
> 二进制:11
> 第4个数
> 十进制:9
> 十六进制:9
> 二进制:1001
分析:
第 1 个数 a 居然是 255,这个 255 从哪来?因为 a 我们一直没有给它初始值,也没有给它赋值,所以它是不确定的默认值,这个 255 就是所谓的不确定的默认值,是编译器在定义变量 a 时分配的,带有不确定的随机性,不同的编译器可能分配的默认值都会存在差异。根据我的经验,unsigned char 类型定义的默认值往往是 0 或者 255(255 是十六进制的 0xff,十六进制的内容后续章节会讲到)。
- 首页
- 第一节:我的价值观
- 第二节:初学者的疑惑
- 第三节:单片机最重要的一个特性
- 第四节:平台软件和编译器软件的简介
- 第五节:用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】第一百三十四节:“应用层半双工”双机串口通讯的程序框架