04 ©️C语言回顾
1. 程序与变量
C语言轻松上手
1.1 运行Hello World!程序
可以在下面输入printf试试
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
运行后会输出:
Hello, World!
先按顺序看懂这几处:
| 写法 | 在这个程序里做什么 |
|---|---|
#include <stdio.h> | 包含头文件,让编译器认识 printf |
int main(void) | 主函数,练习程序从这里开始执行 |
{ } | 括起来的语句属于这个函数 |
printf(...) | 输出括号中的内容 |
\n | 换行 |
return 0; | 结束主函数,返回 0 表示正常结束 |
把 Hello, World! 换成自己的名字,再运行一次。双引号里的内容变了,输出也会跟着变。
printf、return 这样的语句后面要有分号。#include 这一行不加分号。
1.2 变量
int 是数据类型(就是约定数据以什么样的方式存到内存里),count 是变量的名字,0 是初始值。
1.3 变量的类型
不同数据使用不同类型:
| 类型 | 例子 | 用途 |
|---|---|---|
int | int a = 3; | 整数,例如次数、编号 |
float | float a = 25.0f; | 小数,例如温度、电压 |
double | double a = 3.1415926; | 精度通常比 float 更高的小数 |
char | char a = 'A'; | 一个字符 |
25.0f 末尾的 f 表示这是一个 float 常量。
打印时,格式要与数据对应:
int a = 3;
float b = 25.0f;
char c = 'A';
printf("%d\n", a);
printf("%.1f\n", b);
printf("%c\n", c);
%d 输出整数,%f 输出小数,%.1f 保留一位小数,%c 输出一个字符。多个数据按顺序填写,例如:
printf("%d %.1f %c\n", a, b, c);
count++; // 相当于 count = count 加 1
count--; // 相当于 count = count 减 1
2. 判断与循环
判断与循环 就是程序的逻辑
2.1 if判断
#include <stdio.h>
int main(void)
{
int a;
if (a > 10) {
printf("程序1\n");
} else {
printf("程序2\n");
}
return 0;
}
比较:判断大小或是否相等
| 大于 / 小于 | 大于等于 / 小于等于 | 相等 / 不相等 |
|---|---|---|
> / < | >= / <= | == / != |
赋值:注意 = 和 == 的区别
a = 3 | a == 3 |
|---|---|
| 把 3 存入 a | 判断 a 是否等于 3 |
逻辑:组合条件或取反
与 && | 或 || | 非 ! |
|---|---|---|
| 两个条件都成立 | 至少一个条件成立 | 把条件的真假反过来 |
a > b ? a : b:条件成立取 a,否则取 b,这种写法叫条件运算符。
2.2 switch判断
int a;
switch (mode) {
case 0:
printf("程序1\n");
break;
case 1:
printf("程序2\n");
break;
default:
printf("程序3\n");
break;
}
case 后面是要匹配的整数值,default 处理都不匹配的情况,这里的 break 用来退出。
2.3 for循环
int sum = 0;
for (int i = 0; i < 10; i++) {
printf("程序\n");
}
printf("退出\n");
for 括号里用两个分号分成三段:
int i = 1:开始前执行一次。i <= 5:每次执行前检查,成立才进入循环。i++:每次循环结束后加 1,再回去检查条件。
2.4 while循环
int count = 3;
while (count > 0) {
printf("程序\n");
count--;
}
这样的while循环也可以写成for循环
for(int count = 3; count > 0; count--)
printf("程序\n");
}
| 写法 | 作用 |
|---|---|
break; | 退出当前这一层循环 |
continue; | 跳过本次循环剩余代码,开启下一次循环 |
do { ... } while (条件); | 先执行再判断,至少执行一次 |
求 1 到 100 中所有偶数的和
使用 for 循环逐个检查 1~100 的整数,把偶数累加到 sum,最后输出结果。完成后试着只改判断条件,计算奇数的和。
示例输出
sum = 2550
提示
i % 2 == 0 表示 i 是偶数。sum 需要在循环外初始化为 0。
参考代码
#include <stdio.h>
int main(void)
{
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
sum += i;
}
}
printf("sum = %d\n", sum);
return 0;
}
3. 函数与作用域
函数方便复用
3.1 把加法 写成 add函数
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int result = add(3, 5);
printf("%d\n", result);
return 0;
}
| 部分 | 含义 |
|---|---|
前面的 int | 返回一个整数 |
add | 函数名 |
int a, int b | 接收两个整数,叫作参数 |
执行到 return,当前函数立即结束。返回类型是 void 时表示不返回数据;参数写 void 表示不接收参数。
3.2 先声明 后调用
上面的 add 写在 main 前面,编译器已经认识它。如果想把函数实现{}放在后面,需要先写一条声明:
int add(int a, int b); // 声明:提前告诉编译器张啥样
-
声明只有函数的名字、参数和返回类型,末尾有分号;
-
定义则带有
{ },里面是真正执行的代码。
3.3 作用域 生存期
对变量的使用而言,我们需要从以下两个角度去分析它:
| 名称 | 定义 |
|---|---|
| 作用域 | 代码中哪些位置可以使用这个变量 |
| 生存期 | 程序运行时,这个变量存在多久 |
3.4 局部变量 全局变量
局部变量:定义在函数内部或某个 { } 代码块中的变量,只能在对应范围内使用。
全局变量:定义在所有函数外面的变量,可以供多个函数使用。
int total = 0; // 全局变量
void record(void)
{
int step = 1; // 局部变量
total += step;
}
从作用域 生存期角度去分析:
-
局部变量作用域在内层
{ }中,离开这一层代码块时生命周期就结束,不必等整个函数返回。 -
全局变量作用域在全部文件,生存期一直到程序结束,但若要在其他
.c文件中使用,还需要相应的extern声明。 -
给局部变量加上
static,不改变作用域,但生存期会一直存在到程序结束。
for (int i = 0; i < 3; i++){} 中定义的 i,也只在这个 for{} 范围内有效。
3.5 static静态局部变量
连续调用三次 count_calls(),看看加不加 static 有什么区别。
不加 static:每次重新开始
void count_calls(void)
{
int count = 0;
count++;
printf("%d\n", count);
}
每次调用都会创建本次的 count,执行 count = 0 的初始化,加 1 后输出。函数返回时,这个变量的生命周期结束,所以下一次仍从 0 开始,三次输出为 1、1、1。
加上 static:保留上次的值
void count_calls(void)
{
static int count = 0;//加上static
count++;
printf("%d\n", count);
}
count 只初始化一次。函数返回后,它仍然存在,下一次调用会接着上次的值加 1,三次输出为 1、2、3。
| 对比 | int count = 0; | static int count = 0; |
|---|---|---|
| 初始化 | 每次调用时初始化 | 程序启动前初始化一次 |
| 函数返回后 | 本次变量的生存期结束 | 变量继续存在,保留当前值 |
static 延长了这里局部变量的生存期,但没有扩大它的作用域,函数外仍然不能直接用这个 count。
写一个返回较大值的函数
补全 max2 函数,返回两个整数中较大的一个。在 main 中分别用 12、7 和 -3、5 调用它,并输出结果。
示例输出
12
5
提示
函数只负责比较参数并 return,打印结果放在 main 中。
参考代码
#include <stdio.h>
int max2(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
int main(void)
{
printf("%d\n", max2(12, 7));
printf("%d\n", max2(-3, 5));
return 0;
}
4. 数组与字符串
同类型数据放一起
4.1 数组
int value[4] = {1, 3, 6, 9};//数组初始化
-
int value[4]中的[4]表示可以存 4 个整数。 -
元素的编号叫下标,从 0 开始,所以有效下标是 0、1、2、3。
4.2 遍历
数组下标可以使用变量。让 i 从 0 走到 3,就能依次访问每个元素:
#include <stdio.h>
int main(void)
{
int value[] = {1, 3, 6, 9};
for (int i = 0; i < 4; i++) {
printf("%d\n", value[i]);
}
return 0;
}
也可以这样求数组中元素的数量:
sizeof(value) / sizeof(value[0]);
sizeof(value) 是整个数组占的字节数,sizeof(value[0]) 是一个元素占的字节数,相除就是元素个数。
4.3 字符串
要保存 Hello 这五个字母,可以写:
char text[] = "Hello";
printf("%s\n", text);
-
它实际保存了6个字符:
'H'、'e'、'l'、'l'、'o'、'\0'。最后的'\0'是结束标记。 -
%s从开头输出字符,遇到'\0'就停止。
4.4 二维数组
二维数组可以按行、列来理解:
int table[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
//双层for循环 遍历输出二维数组
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d\n", value[i][j]);
}
}
-
[2][3]表示 2 行、每行 3 个元素。两个下标同样从 0 开始。 -
table[1][2]就是第二行、第三列。 -
它在内存中仍然连续排列:先第一行,再第二行。
用字符数组输出字符串
定义字符数组 text,保存 "Hello, STM32!",再用 printf 输出它。
示例输出
Hello, STM32!
提示
用 char text[] = "Hello, STM32!"; 定义数组,输出字符串用 %s。
参考代码
#include <stdio.h>
int main(void)
{
char text[] = "Hello, STM32!";
printf("%s\n", text);
return 0;
}
5. 指针与地址
存地址的变量叫做指针
5.1 数值与地址
int count = 0;
printf("%d\n", count);
printf("%p\n", &count);
count 保存的数值是 10;
&count 就是存放这份数据的内存地址。
5.2 指针变量
int count = 0;
int *p = &count;
-
变量count装着0,指针p指变量count的地址 -
int *表示它指向的内存空间是一个int类型的数据。
| 写法 | 表示什么 |
|---|---|
count | 变量里保存的整数 10 |
&count | 变量的地址 |
p | 指针里保存的地址,这里就是 &count |
*p | 找到 p 指向的位置,访问那里的整数 |
5.3 指针传参
#include <stdio.h>
void init(int *p)
{
*p = 0;
}
int main(void)
{
int count = 10;
init(&count);
printf("%d\n", count);
return 0;
}
-
&count取出地址,交给参数p; -
*p = 0修改这个内存地址里的数据; -
回到
main,count就变成了 0,通过这样函数就能修改main里的变量
指针要先指向有效的数据,才能使用 *p。只写 int *p; 就直接使用,或者访问空指针 NULL,都会出问题。
5.4 指针与数组
int value[4] = {1, 3, 6, 9};
int *p = value;
printf("%d\n", *p); // 1
printf("%d\n", *(p + 1)); // 3
-
这里的
value表示首元素地址,等同于&value[0]。 -
p + 1向后移动一个元素,所以指向第二个数 20。
用指针交换两个变量
补全 swap 函数,交换 main 中 x = 3 和 y = 8 的值。练习结束后,说明为什么调用时要写 &x 和 &y。
示例输出
x = 8, y = 3
提示
先用临时变量保存 *a,再把 *b 写入 *a,最后把临时变量写入 *b。
参考代码
#include <stdio.h>
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main(void)
{
int x = 3;
int y = 8;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y);
return 0;
}
6. 结构体与枚举
不同类型的数据放一起
6.1 结构体
struct person {
char* name;
int age;
};
-
struct person是定义的结构体类型; -
name和age是它的成员,分别保存姓名的地址和年龄;
接着创建结构体变量,并用点号访问成员:
struct person student = {"Xiao Ming", 18};
printf("%s: %d\n", student.name, student.age);
初值按成员顺序填写,所以姓名是 Xiao Ming,年龄是 18。
char* name是字符指针,保存字符串首字符的地址;int age直接保存整数。
实例化后结构体变量的名字就是第一个成员的地址。
6.2 typedef类型别名
每次写 struct person 有些长,可以给它起一个简洁一点的别名person_TypeDef
typedef struct person person_TypeDef;
person_TypeDef 是结构体类型的别名,也可以把结构体定义和别名合在一起写:
typedef struct {
char* name;
int age;
} person_TypeDef;
//等价于
struct person {
char* name;
int age;
};
typedef struct person person_TypeDef;
typedef 给类型起别名,不会实例化变量。STM32 中的 GPIO_InitTypeDef、ADC_HandleTypeDef 也是这样的类型名。
6.3 结构体指针
person_TypeDef *p = &student;
指针p 保存 student 的地址,就可以通过指针操作结构体。
person_TypeDef *p = &student;
p->age = 10;
person_TypeDef student;
student.age = 10;
- 通过
结构体变量操作成员,用.; - 通过
结构体指针变量操作成员,用->。
void update_age(person_TypeDef *p, int age)//结构体传参等价于:person_TypeDef *p = &student;
{
p->age = age;
}
STM32 中的 hadc->Instance 也是一样:hadc 是结构体指针,Instance 是它指向的结构体成员。
6.4 enum枚举
以STM32 中用枚举给 GPIO 的低电平和高电平起名字为例子
typedef enum {
GPIO_PIN_RESET = 0u,
GPIO_PIN_SET
} GPIO_PinState;
// 等价于
enum GPIO_State {
GPIO_PIN_RESET = 0u,
GPIO_PIN_SET
};
typedef enum GPIO_State GPIO_PinState;
-
GPIO_PIN_RESET表示 0(低电平),0u中的u表示无符号整数; -
GPIO_PIN_SET没有填写数值,后面的都按顺序来; -
如果第一个成员也不填写数值,默认从 0 开始;
用结构体保存姓名和年龄
下面已准备好 person 结构体。定义一个 student 变量,用点号给 name 赋值 "Xiao Ming"、age 赋值 18,再输出这两个成员。
示例输出
Xiao Ming: 18
提示
变量写成 struct person student;,通过 student.name 和 student.age 访问成员。姓名用 %s,年龄用 %d 输出。
参考代码
#include <stdio.h>
struct person {
char* name;
int age;
};
int main(void)
{
struct person student;
student.name = "Xiao Ming";
student.age = 18;
printf("%s: %d\n", student.name, student.age);
return 0;
}
7. 头文件与宏定义
代码多了 分文件整理
7.1 头文件写声明
新建 my_code.h:
#ifndef MY_CODE_H
#define MY_CODE_H
int max(int a, int b);
#endif
int max(int a, int b); 是函数声明,告诉编译器函数的参数和返回类型。
头文件里的#ifndef、#define、#endif 用来防止头文件重复声明同一个源文件。
7.2 源文件写实现
新建 my_code.c:
#include "my_code.h"
int max(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
.c 文件里写函数的具体代码,开头包含自己的头文件。
然后在 main.c 中使用:
#include <stdio.h>
#include "my_code.h"//调用
int main(void)
{
printf("max = %d\n", max(12, 7));
return 0;
}
#include 引入头文件:
-
C语言标准库的头文件用尖括号
<stdio.h>; -
自己的头文件用双引号
"my_code.h"。
7.3 编译和链接
-
main.c和my_code.c分别编译,生成目标文件; -
链接器把目标文件和用到的库合在一起,生成可执行程序。
7.4 宏定义
#define SAMPLE_COUNT 8
编译前,SAMPLE_COUNT 会被替换成 8。#define 末尾不加分号。
宏也可以带参数:
#define SQUARE(x) ((x) * (x))
printf("%d\n", SQUARE(2 + 3));
SQUARE(2 + 3) 通过宏定义展开成 ((2 + 3) * (2 + 3)),最终结果是 25。
编译开关常这样写(方便调试用):
#define DEBUG_CODE 1
#if DEBUG_CODE
/* 需要调试时才保留的代码 */
#endif
把 DEBUG_ENABLED 改为 0,中间的代码就不参与编译。
7.5 extern全局变量声明
如果一个变量确实需要多个文件共用,在一个 .c 中定义一次:
int sample_count = 0;
在供其他文件包含的头文件里只写声明,在前面加extern:
extern int sample_count;//增加extern关键字
7.6 static静态全局变量
如果一个全局变量或函数只给当前文件使用,在前面加 static:
static int count = 0;
static void helper(void);
main()
{
/* ... */
}
- 局部变量加上
static,不改变作用域 但生存期会一直到程序结束 - 全局变量加上
static,表示只在当前文件使用
用宏计算平方
在 my_code.h 中定义带参数的宏 SQUARE(x),计算一个数的平方。在 main 中输出 SQUARE(2 + 3) 的结果。
示例输出
square = 25
提示
宏会直接替换文本。给每个参数和整个表达式加括号:((x) * (x))。
参考代码
#include <stdio.h>
#include "my_code.h"
int main(void)
{
printf("square = %d\n", SQUARE(2 + 3));
return 0;
}
#ifndef MY_CODE_H
#define MY_CODE_H
#define SQUARE(x) ((x) * (x))
#endif
#include "my_code.h"
8. C语言标准库
8.1 嵌入式常用标准库
CMSIS 内核头文件core_cm*.h 里面 #include 了 <stdint.h>,然后被 stm32xx.h / stm32xx_hal.h 间接包含,所以我们写STM32代码的时候经常用到<stdint.h>但很少需要主动包含它
| 头文件 | 常用内容 | 用途 |
|---|---|---|
<stdint.h> | uint8_t、uint16_t、uint32_t | 定义固定宽度的整数 |
<stdbool.h> | bool、true、false | 表示开关和状态 |
<stdio.h> | printf、sprintf、snprintf | 打印信息、格式化文字 |
<string.h> | memcpy、memset、strlen、strcmp | 处理缓冲区和字符串 |
<stdlib.h> | strtol、abs、malloc、free | 数值转换、动态内存管理 |
<math.h> | fabsf、sqrtf、sinf、cosf | 绝对值、开方、三角函数 |
在 STM32 中,printf 输出到串口需要重定向;sprintf 写入字符数组时要留够空间;malloc、free 需要包含对应头文件。
复制 清空一块数据
int source[3] = {10, 20, 30};
int target[3];
memcpy(target, source, sizeof(source));
memset(target, 0, sizeof(target));
-
memcpy把source的内容复制到target; -
memset把target的每个字节填成 0;
8.2 位运算
可以打开电脑上的计算器,切到程序员模式,对照十进制和二进制。
位从右往左、从 0 开始编号
| 运算 | 规则 | 最简示例 |
|---|---|---|
按位与 & | 两位都是 1,结果才是 1 | 5 & 3 得到 1 |
按位或 | | 有一位是 1,结果就是 1 | 5 | 3 得到 7 |
按位异或 ^ | 两位不同为 1,相同为 0 | 5 ^ 3 得到 6 |
按位取反 ~ | 0变1,1变0 | a = ~a; |
左移 << | 向左移动,右边补 0 | 1 << 2 得到 4 |
右移 >> | 向右移动,移出的位丢弃 | 8 >> 1 得到 4 |
8.3 volatile
常用于中断标志位,为了防止编译器优化。
volatile uint8_t flag = 0;
volatile 告诉编译器,这个变量的值可能被其他地方改变,每次需要实实在在地去读取内存中地值,而不能偷懒用CPU中对应的缓存。