跳到主要内容

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 变量的类型​

不同数据使用不同类型:

类型例子用途
intint a = 3;整数,例如次数、编号
floatfloat a = 25.0f;小数,例如温度、电压
doubledouble a = 3.1415926;精度通常比 float 更高的小数
charchar 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 = 3a == 3
把 3 存入 a判断 a 是否等于 3

逻辑:组合条件或取反

与 &&或 ||非 !
两个条件都成立至少一个条件成立把条件的真假反过来
if else判断的极简写法

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 括号里用两个分号分成三段:

  1. int i = 1:开始前执行一次。
  2. i <= 5:每次执行前检查,成立才进入循环。
  3. 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。

参考代码
main.c
#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 中。

参考代码
main.c
#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。

参考代码
main.c
#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。

参考代码
main.c
#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 输出。

参考代码
main.c
#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静态全局变量
  • 局部变量加上static,不改变作用域 但生存期会一直到程序结束
  • 全局变量加上static,表示只在当前文件使用
练习

用宏计算平方

在 my_code.h 中定义带参数的宏 SQUARE(x),计算一个数的平方。在 main 中输出 SQUARE(2 + 3) 的结果。

示例输出

square = 25
代码
正在准备编辑器…
运行结果
提示

宏会直接替换文本。给每个参数和整个表达式加括号:((x) * (x))。

参考代码
main.c
#include <stdio.h>
#include "my_code.h"

int main(void)
{
printf("square = %d\n", SQUARE(2 + 3));
return 0;
}
my_code.h
#ifndef MY_CODE_H
#define MY_CODE_H

#define SQUARE(x) ((x) * (x))

#endif
my_code.c
#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,结果才是 15 & 3 得到 1
按位或 |有一位是 1,结果就是 15 | 3 得到 7
按位异或 ^两位不同为 1,相同为 05 ^ 3 得到 6
按位取反 ~0变1,1变0a = ~a;
左移 <<向左移动,右边补 01 << 2 得到 4
右移 >>向右移动,移出的位丢弃8 >> 1 得到 4

8.3 volatile​

常用于中断标志位,为了防止编译器优化。

volatile uint8_t flag = 0;

volatile 告诉编译器,这个变量的值可能被其他地方改变,每次需要实实在在地去读取内存中地值,而不能偷懒用CPU中对应的缓存。