int a = 7 / 2; // a = 3,不是 3.5
float b = 7 / 2; // b = 3.0!因为 7/2 先按整数算出 3,再转成 float
float c = 7.0 / 2; // c = 3.5 ✓ 只要有一个是小数,就按小数算
float d = (float)7 / 2; // d = 3.5 ✓ 或者强制转换
这个坑在算平均值、百分比时必踩。判断方法:看 / 两边是不是都是整数。
3.2 取余 % 的妙用
取余在单片机里用得极多,因为它是「循环」和「拆分数字」的核心工具:
// ① 让一个数在 0~7 之间循环(8 个 LED 轮流亮)
i = (i + 1) % 8; // 0,1,2,...,7,0,1,2,... 永远不越界// ② 拆出一个三位数的各位数字(数码管显示要用)
int n = 258;
int ge = n % 10; // 8
int shi = n / 10 % 10; // 5
int bai = n / 100 % 10; // 2
// 想表达「a 加 b 再乘 c」,但这样写是错的:
int r = a + b * c; // 实际是 a + (b*c)
int r = (a + b) * c; // ✓ 加括号// 判断「第 3 位是不是 1」——这是 Day 11 的内容,先看个眼熟
if (P1 & 0x08 == 0) // ✗ == 优先级高于 &,实际是 P1 & (0x08==0)
if ((P1 & 0x08) == 0) // ✓ 必须加括号
位运算(& | ^ << >>)的优先级比比较运算符还低,所以位运算永远要加括号。这个坑 Day 11 会重点打。
🧠 今天的自测
① 7 / 2 是多少?7.0 / 2 呢?为什么?
② 写出把 258 的百位、十位、个位分别取出来的三行代码。
③ if (a = 0) 和 if (a == 0) 有什么区别?哪个是 bug?
④ if (a & 1 == 0) 为什么不等于 if ((a & 1) == 0)?
① 写一段代码:输入一个 1~7 的整数,输出对应的星期几;其它数字输出「无效」。
② switch 里 break 能不能省?什么情况下故意省?
③ 下面哪句「永远执行」? if (x > 0) printf("a"); printf("b");
④ 为什么 else if 链里的条件要从严格排到宽松?
int i = 1;
while (i <= 5) {
printf("%d\n", i);
i++; // 千万别忘了改 i,否则死循环
}
/* 输出 1 2 3 4 5 */
🚨 死循环:忘了让条件变化
int i = 1;
while (i <= 5) {
printf("%d\n", i); // ✗ i 永远是 1 → 无限打印
}
// 按 Ctrl+C 可以强行中断
写循环时养成习惯:写完立刻检查「这个循环靠什么结束」。
5.2 for:最常用的循环
// for (初始化; 继续条件; 每轮结束做的事)
for (int i = 1; i <= 5; i++) {
printf("%d\n", i);
}
它和上面的 while 完全等价,但把「起点、终点、步进」都写在了一行里,不容易漏。
三种常见写法
// 从 0 数到 n-1(数组遍历的标准写法)
for (int i = 0; i < n; i++) { ... }
// 从 1 数到 n
for (int i = 1; i <= n; i++) { ... }
// 倒着数
for (int i = 10; i >= 1; i--) { ... }
// 每次加 2(只数奇数)
for (int i = 1; i <= 9; i += 2) { ... }
⚠️ < 还是 <=?差一个就出错
这是初学者最高频的 off-by-one 错误。看数组:如果 int a[5] 的下标是 0~4,那遍历就必须写 i < 5,写成 i <= 5 就越界了(读到了 a[5],那块内存不属于你)。
for (int i = 0; i < 5; i++) b[i] = 1; // ✓ 这才是全部设成 1
这个坑非常致命,因为它编译能过、能运行,只是结果全错。
6.4 遍历数组
#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, a[i]);
sum += a[i];
}
printf("总和 = %d,平均 = %d\n", sum, sum / 5);
return 0;
}
// 语法: 返回类型 函数名(参数列表) { 函数体 }
int add(int a, int b)
{
int sum = a + b;
return sum; // 把结果送回去
}
int main(void)
{
int x = add(3, 5); // 调用:传 3 和 5 进去,拿到 8
printf("%d\n", x); // 8
return 0;
}
void change(int x)
{
x = 100; // 改的是复制品
}
int main(void)
{
int a = 5;
change(a);
printf("%d\n", a); // 输出 5!不是 100
return 0;
}
⚠️ 函数改不了外面的变量
调用 change(a) 时,C 把 a 的值复制一份给 x。函数里改 x,跟外面的 a 毫无关系。
这叫值传递。那如果我就是想让函数修改外面的变量怎么办?—— 要把「地址」传进去。这是 Day 9 的核心,也是指针存在的最大理由。
7.5 函数要「先声明后使用」
// 写法一:把函数定义写在 main 前面(简单,适合小程序)
int add(int a, int b) { return a + b; }
int main(void) { ... }
// 写法二:先声明(也叫原型),定义放后面
int add(int a, int b); // 只写这一行,末尾有分号
int main(void) { int x = add(1,2); }
int add(int a, int b) { return a + b; } // 真正的定义
如果函数写在 main 后面又没声明,编译器会报 implicit declaration(隐式声明)警告或错误。第三周你会看到所有 51 程序都把函数声明放最上面,或者放进 .h 文件。
7.6 一个实用的例子:自己写延时
#include <stdio.h>
/* 打印一条分割线,长度由参数决定 */
void line(int n)
{
for (int i = 0; i < n; i++) {
printf("-");
}
printf("\n");
}
/* 求数组最大值 */
int max_of(int a[], int n) // 数组作为参数,要另外传长度
{
int m = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > m) m = a[i];
}
return m;
}
int main(void)
{
int data[6] = {3, 9, 2, 7, 5, 8};
line(30);
printf("最大值 = %d\n", max_of(data, 6));
line(30);
return 0;
}
#include <stdio.h>
int main(void)
{
int score[10];
int i, max, min, sum = 0, pass = 0;
for (i = 0; i < 10; i++) {
scanf("%d", &score[i]);
}
max = min = score[0]; // 先把第一个当成最大最小
for (i = 0; i < 10; i++) {
if (score[i] > max) max = score[i];
if (score[i] < min) min = score[i];
sum += score[i];
if (score[i] >= 60) pass++;
}
printf("最高分:%d\n", max);
printf("最低分:%d\n", min);
printf("平均分:%d\n", sum / 10);
printf("及格人数:%d\n", pass);
return 0;
}
注意 max = min = score[0]; —— 必须先拿一个真实数据做起点,不能设成 0(否则全是负数时就错了)。这个「用第一个元素初始化最值」的套路,以后天天用。
练习 2 用函数改写进阶
把上一题拆成三个函数:int find_max(int a[], int n)、int find_min(int a[], int n)、int count_pass(int a[], int n),让 main 只负责输入和打印。
看参考答案
#include <stdio.h>
int find_max(int a[], int n)
{
int m = a[0];
for (int i = 1; i < n; i++) if (a[i] > m) m = a[i];
return m;
}
int find_min(int a[], int n)
{
int m = a[0];
for (int i = 1; i < n; i++) if (a[i] < m) m = a[i];
return m;
}
int count_pass(int a[], int n)
{
int c = 0;
for (int i = 0; i < n; i++) if (a[i] >= 60) c++;
return c;
}
int main(void)
{
int s[10], i, sum = 0;
for (i = 0; i < 10; i++) scanf("%d", &s[i]);
for (i = 0; i < 10; i++) sum += s[i];
printf("最高分:%d\n", find_max(s, 10));
printf("最低分:%d\n", find_min(s, 10));
printf("平均分:%d\n", sum / 10);
printf("及格人数:%d\n", count_pass(s, 10));
return 0;
}
int a = 10;
int b = 20;
int *p;
p = &a; // p 指向 a
printf("%d\n", *p); // 10
p = &b; // 改成指向 b(指针本身可以被重新赋值)
printf("%d\n", *p); // 20
p = &a;
*p = *p + 5; // 把 a 加 5(*p 代替了 a 的位置)
printf("%d\n", a); // 15
/*==============================================================
功能:按一下 P3.2 的按键,P1.0 的 LED 翻转一次
硬件:LED 接 P1.0(低电平点亮),按键接 P3.2 到 GND
==============================================================*/
#include <reg51.h>
sbit KEY = P3^2;
sbit LED = P1^0;
void DelayMs(unsigned int ms)
{
unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 120; j++);
}
void main(void)
{
LED = 1; /* 初始熄灭(低电平点亮,所以 1 是灭) */
while (1) {
if (KEY == 0) {
DelayMs(15); /* 消抖 */
if (KEY == 0) {
LED = !LED; /* 翻转 */
while (KEY == 0); /* 等松手 */
}
}
}
}
✓ 试试这几个改动
① 把 DelayMs(15) 改成 DelayMs(1) → 观察是否开始「按一次跳好几次」(抖动漏出来了)。
② 删掉 while (KEY == 0); → 按住不放时灯会高速闪。
③ 把 LED = !LED; 改成 LED = 0; → 变成「按下才亮,松开就灭」(这是另一种做法,不需要松手检测)。
🧠 今天的自测
① 按键不按和按下时,引脚电平分别是什么?为什么可以不加外部上拉电阻?
② 什么是抖动?持续多久?为什么延时 15ms 就能解决?
③ while (KEY == 0); 这一行删掉会出现什么现象?
④ 延时确认法最大的缺点是什么?正规写法怎么改?
📖 想深入看理论
《51单片机入门前置知识》:2.6 节(按键与"抖动")、6.4 节(完整实战:按键控制 LED 含消抖)。