C语言基础

2026-08-11 5586 字 约 13 分钟 0 次浏览
目录

C语言基础

🎯 主要学习:数据类型、宏定义、typedef、结构体、枚举、循环、数组、指针

前五项是 C 语言的"语法地基"(怎么描述数据、怎么组织代码),后三项是嵌入式开发的核心三件套:循环负责重复任务(如轮询传感器),数组负责批量数据(如采样缓冲),指针负责直接操作硬件地址(寄存器、DMA、外设)。


🔢 一、数据类型(Data Types)

1. 基本数据类型

类型关键字32位平台大小取值范围嵌入式用途
字符型char1字节-128127 或 0255存 ASCII、小整数
短整型short2字节-32768~32767省内存的小整数
整型int4字节-2^31~2^31-1通用整数
长整型long4字节同 int兼容历史代码
单精度浮点float4字节1e-381e38小数计算(STM32 无FPU时很慢)
双精度浮点double8字节精度更高一般不用,占内存

⚠️ 嵌入式里尽量用固定宽度类型(见下),不要依赖 int 的大小,因为不同平台位数不同。

2. 嵌入式推荐:固定宽度整数类型(stdint.h)

STM32 标准库和 HAL 库都用这套,大小明确、跨平台一致:

类型大小范围常用场景
uint8_t1字节无符号0~255寄存器、GPIO 引脚号、缓冲区索引
int8_t1字节有符号-128~127小范围有符号数
uint16_t2字节无符号0~65535ADC 值(0~4095)、PWM 比较值
int16_t2字节有符号-32768~32767温度、加速度等有符号传感器
uint32_t4字节无符号0~2^32-1定时器计数、地址、时间戳
int32_t4字节有符号-2^31~2^31-1通用计算
#include <stdint.h>
uint8_t  led_state = 0;       // LED 状态,非0即1
uint16_t adc_value = 0;       // ADC 采样值
uint32_t tick = 0;            // 系统滴答计数

3. 数据类型完整对照表(含 ST 库别名)

C语言数据类型完整对照表

🔑 这张表把 C 原生关键字 / 位数 / 取值范围 / stdint 类型 / ST 库类型 五列对齐,是嵌入式开发最常用的速查表。

ST 库别名(标准外设库 SPL 里大量使用):

  • u8 / s8 = uint8_t / int8_t(无符号/有符号 8 位)
  • u16 / s16 = uint16_t / int16_t
  • u32 / s32 = uint32_t / int32_t

读 STM32 标准库源码时经常看到 u32、u16 这类写法,本质就是 typedef 出来的固定宽度类型,和 uint32_t 完全等价。新项目推荐用 stdint.h 的标准写法(可移植性更好),但要能看懂 ST 库的别名。

4. 修饰符

关键字作用嵌入式场景
const定义常量,不可修改查表数据、字模(省 RAM,放 Flash)
static函数内:保留值不销毁;文件内:限制作用域状态变量、私有函数
volatile禁止编译器优化,每次都从内存读寄存器、中断变量、多线程共享(必加!)
unsigned无符号计数、地址、位运算
const uint8_t font[][16] = {...};   // 字模放 Flash,不占 RAM
static uint8_t state = 0;           // 函数调用间保留状态
volatile uint8_t uart_rx_flag = 0;  // 中断里会改,必须 volatile

🏷️ 二、宏定义(#define)

1. 基本用法

#define PI 3.14159
#define BUF_SIZE 64
#define LED_PIN GPIO_Pin_13

🔑 宏定义是纯文本替换,在预处理阶段完成,不占内存、没有类型检查。末尾不要加分号(加了分号会把分号也替换进去)。

2. 带参数的宏(类似函数,但更快)

#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#define MIN(a, b)   ((a) < (b) ? (a) : (b))
#define ABS(x)      ((x) > 0 ? (x) : -(x))

uint16_t m = MAX(adc1, adc2);   // 取较大值

⚠️ 每个参数都要加括号,防止运算符优先级出问题。宏没有类型检查,复杂逻辑优先用 inline 函数。

3. 嵌入式常用宏

// 位操作宏(操作寄存器最常用)
#define SET_BIT(reg, bit)   ((reg) |=  (1 << (bit)))
#define CLR_BIT(reg, bit)   ((reg) &= ~(1 << (bit)))
#define TGL_BIT(reg, bit)   ((reg) ^=  (1 << (bit)))
#define GET_BIT(reg, bit)   (((reg) >> (bit)) & 1)

// 条件编译(调试开关、多平台适配)
#define DEBUG 1
#if DEBUG
    #define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
    #define LOG(fmt, ...)
#endif

4. 头文件保护(防止重复包含)

#ifndef __LED_H
#define __LED_H

// 头文件内容...

#endif

每个 .h 文件都要有这个结构,否则重复包含会报重定义错误。


🔤 三、typedef(类型别名)

1. 基本用法

typedef unsigned char uint8;     // 给 unsigned char 起个短名
typedef int (*FuncPtr)(int);     // 给函数指针类型起别名

🔑 typedef 是给已有的类型起别名,不是定义新类型。和 #define 不同,typedef 有类型检查,由编译器处理。

2. 嵌入式中最常见:给结构体起别名

typedef struct
{
    uint16_t x;
    uint16_t y;
} Point_t;     // 以后直接用 Point_t 定义变量,不用写 struct

Point_t p1 = {10, 20};

STM32 库里到处都是这种写法:GPIO_InitTypeDef、TIM_TimeBaseInitTypeDef 都是 typedef 出来的结构体类型。

3. typedef vs #define

对比typedef#define
处理时机编译器预处理器(文本替换)
类型检查✅ 有❌ 无
指针类型typedef char *pchar; 正确#define pchar char * 会出问题

📦 四、结构体(struct)

1. 定义与使用

// 定义结构体类型
struct Sensor
{
    uint16_t temp;     // 温度
    uint16_t humi;     // 湿度
    uint32_t timestamp;
};

struct Sensor s1;       // 定义变量(要写 struct)
s1.temp = 250;          // 用 . 访问成员
s1.humi = 600;

2. 配合 typedef(嵌入式标准写法)

typedef struct
{
    uint16_t temp;
    uint16_t humi;
    uint32_t timestamp;
} Sensor_t;

Sensor_t s2 = {250, 600, 1000};   // 初始化按顺序
Sensor_t s3 = {.temp = 250, .humi = 600};  // 指定成员初始化(C99,推荐)

3. 结构体指针与 -> 运算符

Sensor_t *p = &s2;
p->temp = 300;        // 指针用 -> 访问成员,等价于 (*p).temp
p->timestamp = 2000;

🔑 结构体作函数参数时,传指针比传值好——传值会拷贝整个结构体(占栈、费时间),传指针只传 4 字节地址。STM32 库的 GPIO_Init(GPIOx, &gpio) 就是传指针。

4. 结构体嵌套(描述复杂对象)

typedef struct
{
    uint16_t x;
    uint16_t y;
} Point_t;

typedef struct
{
    Point_t pos;       // 嵌套另一个结构体
    uint8_t id;
    uint8_t type;
} Device_t;

Device_t dev;
dev.pos.x = 100;      // 逐层访问

5. 结构体在嵌入式中的用途

用途示例
封装外设配置STM32 的 GPIO_InitTypeDef
描述一个设备传感器数据 + 状态 + 配置
链表节点数据队列、任务管理
寄存器映射用结构体描述一组寄存器地址

🔢 五、枚举(enum)

1. 定义与使用

// 定义枚举类型
typedef enum
{
    LED_OFF = 0,
    LED_ON  = 1,
    LED_BLINK = 2
} LED_State_t;

LED_State_t state = LED_ON;

🔑 枚举本质是整数,第一个成员默认从 0 开始,依次 +1。显式赋值后,后面的成员在前一个基础上 +1。

2. 默认值规则

typedef enum
{
    RED,      // = 0
    GREEN,    // = 1
    BLUE = 5, // = 5
    YELLOW    // = 6(自动+1)
} Color_t;

3. 嵌入式典型用途:状态机

typedef enum
{
    STATE_IDLE,       // 空闲
    STATE_READING,    // 读取中
    STATE_PROCESS,    // 处理中
    STATE_ERROR       // 错误
} SystemState_t;

SystemState_t g_state = STATE_IDLE;

void State_Machine(void)
{
    switch(g_state)
    {
        case STATE_IDLE:    /* ... */ break;
        case STATE_READING: /* ... */ break;
        case STATE_PROCESS: /* ... */ break;
        case STATE_ERROR:   /* ... */ break;
    }
}

💡 枚举比 #define 一堆常量更清晰,还能把相关的状态归为一组,编译器有时还能帮你检查 switch 是否漏了分支。

4. 枚举 vs 宏定义

对比enum#define
类型有类型(整数)无类型(文本替换)
作用域有作用域全局(直到 #undef)
自动编号✅ 支持❌ 需手动
调试可见✅ 调试器能显示枚举名❌ 只显示数字

🔁 六、循环(Loop)

1. 三种循环对比

循环语法适用场景
forfor(初始;条件;更新){}已知次数(遍历数组、固定延时)
whilewhile(条件){}次数不定(等待某标志位)
do-whiledo{}while(条件);至少执行一次(菜单、初始化重试)

2. for 循环(已知次数)

结构:for(初始化表达式; 循环条件; 更新表达式) { 循环体 }

执行顺序:①初始化(只跑一次)→ ②判断条件 → ③执行循环体 → ④更新表达式 → 回到②

// 遍历数组求和(最常用)
uint16_t sum = 0;
for(uint8_t i = 0; i < 10; i++)   // i 从 0 到 9
{
    sum += adc_buf[i];
}

// 固定次数的软件延时
for(uint32_t i = 0; i < 1000000; i++)
{
    __NOP();   // 空操作,占一个时钟周期
}

// 倒序遍历(如数据搬移)
for(uint8_t i = 9; i > 0; i--)
{
    buf[i] = buf[i - 1];
}

💡 for 的三个表达式都可省略,但分号不能省:for(;;){} 等价于 while(1) 死循环。

3. while 循环(次数不定 / 先判断后执行)

结构:while(循环条件) { 循环体 }

特点:先判断条件,为真才执行;条件一开始就为假则一次都不跑。

// 等待某个标志位(如等待转换完成)
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET)
{
    // 什么都不做,原地等
}

// 统计数据直到遇到结束符 0xFF
uint8_t i = 0;
while(rx_buf[i] != 0xFF)
{
    data[i] = rx_buf[i];
    i++;
}

// 主循环(嵌入式最常见)
while(1)   // 条件恒真,永不退出
{
    // 任务...
}

4. do-while 循环(至少执行一次 / 先执行后判断)

结构:

do
{
    循环体
} while(循环条件);   // ⚠️ 末尾必须有分号!

特点:先执行一次循环体,再判断条件;无论条件真假,至少跑一次。

// 菜单程序:先显示一次菜单,再根据输入决定是否继续
uint8_t key;
do
{
    Show_Menu();          // 显示菜单(至少显示一次)
    key = Key_Scan();     // 扫描按键
    Handle_Key(key);      // 处理按键
} while(key != KEY_EXIT); // 按退出键才离开

// 初始化重试:先尝试一次,失败再重试
uint8_t retry = 0;
do
{
    status = Sensor_Init();   // 先初始化一次
    retry++;
} while(status != 0 && retry < 3);  // 失败且未超3次则重试

// 字符串长度计算(至少处理一个字符)
uint8_t len = 0;
do
{
    len++;
} while(str[len] != '\0');

5. 三种循环怎么选

场景推荐原因
已知循环次数(遍历数组、固定延时)for初始化/条件/更新集中在一起,清晰
次数不定、可能一次都不执行(等标志位)while先判断,条件不满足直接跳过
必须至少执行一次(菜单、重试、协议交互)do-while先执行后判断,保证跑一次

🔑 三者可以互相改写,但选对了代码更易读。嵌入式里 while(1) 主循环、while(标志) 等待、do-while 菜单/重试 是最典型的三种用法。

6. 嵌入式中最典型的循环 —— 主循环

int main(void)
{
    System_Init();          // 初始化(只跑一次)
    while(1)                // 主循环:程序永不退出
    {
        Read_Sensor();      // 读取传感器
        Process_Data();     // 处理数据
        Update_Display();   // 刷新显示
    }
}

💡 单片机没有操作系统时,while(1) 就是程序的"心脏",所有任务在里面轮流执行(轮询式架构)。

7. break / continue

关键字作用
break立即跳出整个循环
continue跳过本次剩余语句,进入下一次循环
for(int i = 0; i < 10; i++)
{
    if(sensor[i] == 0)  continue;   // 该点无效,跳过
    if(sensor[i] > 100) break;      // 超阈值,停止扫描
    sum += sensor[i];
}

8. ⚠️ 循环延时的坑

// 阻塞式延时:简单但会卡死主循环,实际项目慎用
void Delay(uint32_t n) { while(n--); }

阻塞延时期间 CPU 什么都干不了。工程中优先用定时器 + 标志位做非阻塞延时(见 STM32F103C8T6 定时器部分)。


📦 七、数组(Array)

1. 定义与访问

uint8_t buf[10];              // 声明 10 个元素,下标 0~9
uint8_t rx[5] = {0};          // 全部初始化为 0
uint8_t cmd[] = {0xAA, 0x55}; // 自动推断长度为 2

buf[0] = 25;                  // 下标从 0 开始!
uint8_t last = buf[9];        // 最后一个是 [9] 不是 [10]

⚠️ 最常见 bug:越界访问。buf[10] 已经越界,会覆盖相邻内存,在单片机上可能直接跑飞。

2. 数组在嵌入式中的核心用途

用途示例
接收缓冲区uint8_t uart_rx[64]; 存串口数据
采样数据uint16_t adc_val[100]; 存ADC采样
滑动滤波存最近 N 次采样求平均,去毛刺
查表法用数组存正弦表/字模,省计算

3. 数组名的本质

uint8_t buf[10];
// buf 本身就是"首元素地址",即 &buf[0]
// 所以 buf[i] 等价于 *(buf + i)

🔑 这一点是数组与指针的桥梁,也是理解下一节的关键。

4. 二维数组(如OLED字模、矩阵键盘)

uint8_t keymap[4][4] = {       // 4x4 矩阵键盘
    {1, 2, 3, 10},
    {4, 5, 6, 11},
    {7, 8, 9, 12},
    {15, 0, 14, 13}
};
uint8_t key = keymap[row][col];

🎯 八、指针(Pointer)—— 嵌入式的灵魂

1. 基本概念

int a = 10;
int *p = &a;    // p 存放 a 的地址(& 取地址)
*p = 20;        // *p 访问 p 指向的值(* 解引用)→ a 变成 20
符号含义
&a取变量 a 的地址
*p取指针 p 指向的值
int *p声明一个指向 int 的指针

2. 指针与数组

uint8_t buf[5] = {10, 20, 30, 40, 50};
uint8_t *p = buf;   // 数组名即首地址,无需 &

*p       // = buf[0] = 10
*(p+1)   // = buf[1] = 20
p[2]     // = buf[2] = 30   (指针也能用下标!)

3. 指针作为函数参数(传址 vs 传值)

// ❌ 传值:函数内改不了外部变量
void add(int x) { x += 1; }

// ✅ 传址:通过指针修改外部变量
void add(int *x) { (*x) += 1; }

int n = 5;
add(&n);   // n 变成 6

💡 数组作参数时天然"传址"(传的是首地址),所以函数里能修改整个数组。这也是 Read_Buffer(uint8_t *buf, uint16_t len) 这类函数的原理。

4. 🔥 指针操作硬件寄存器(嵌入式核心)

STM32 的每个外设寄存器本质是一个固定的内存地址,用指针就能直接读写:

// GPIOC 端口输出寄存器地址(示意)
#define GPIOC_ODR  (*(volatile uint32_t *)0x4001100C)

GPIOC_ODR |= (1 << 13);   // PC13 输出高
GPIOC_ODR &= ~(1 << 13);  // PC13 输出低
关键字为什么需要
volatile告诉编译器"这个值可能被硬件改变",禁止优化,寄存器/中断变量必须加
(uint32_t *)把地址数字强转成指针类型
*(...)解引用,直接读写该地址

🔑 库函数(如 GPIO_SetBits())底层就是这样操作寄存器的,只是封装得更安全易读。

5. ⚠️ 指针三大坑

坑说明对策
野指针未初始化就使用声明即赋 NULL 或有效地址
空指针解引用*p 时 p 是 NULL用前判断 if(p != NULL)
越界/悬空指向已释放或数组外严格控制范围

📝 九、综合示例:串口接收 + 滑动滤波(三者结合)

#define BUF_SIZE 8
uint16_t adc_buf[BUF_SIZE] = {0};   // 数组:采样缓冲
uint8_t  idx = 0;

// 传入指针,返回滤波后平均值
uint16_t Filter_Add(uint16_t *buf, uint16_t new_val)
{
    buf[idx] = new_val;             // 存入
    idx = (idx + 1) % BUF_SIZE;     // 循环下标(环形缓冲)

    uint32_t sum = 0;
    for(uint8_t i = 0; i < BUF_SIZE; i++)  // 循环求和
        sum += buf[i];
    return sum / BUF_SIZE;          // 平均值
}

一个函数里同时用到了 循环(求和)、数组(缓冲)、指针(传参),是嵌入式最常见的组合。


✅ 十、学习检查清单

  • 会用 uint8_t/uint16_t/uint32_t 等固定宽度类型
  • 理解 const/static/volatile 三个修饰符的作用
  • 会写 #define 常量、带参宏、位操作宏、头文件保护
  • 会用 typedef 给结构体起别名(XXX_t 命名)
  • 会定义和使用结构体,理解 . 和 -> 的区别
  • 会用枚举写状态机
  • 能手写 while(1) 主循环结构
  • 掌握 for / while / do-while 三种循环的完整结构与区别
  • 知道 do-while 末尾必须加分号、至少执行一次
  • 会根据场景选对循环(已知次数用 for、等标志用 while、菜单/重试用 do-while)
  • 理解数组下标从 0 开始、避免越界
  • 分清 &(取址)和 *(解引用)
  • 理解数组名 == 首地址
  • 会用指针作函数参数修改外部变量
  • 理解 volatile 在寄存器/中断中的作用
  • 能看懂库函数底层的指针操作

📚 十一、主要学习资料

主要跟浙江大学翁恺老师的课,科班级别的系统讲解,适合打牢基础。

翁恺 —— 浙江大学

  • 课程:翁恺教你C语言程序设计!C语言基础入门!
  • 特点:浙大计算机学院老师,讲解清晰、循序渐进,从变量到指针层层递进,概念讲得透,不只是教语法
  • 对应重点:循环、数组、指针(本笔记三大核心)都有系统章节
  • 学习建议:配合本笔记的嵌入式场景示例一起看——翁恺讲清"语言是什么",本笔记补充"在单片机里怎么用"

🔗 相关链接

  • STM32F103C8T6 - 应用这些基础到实际开发板
  • 自我介绍 - C语言是我的学习重点(作为嵌入式核心语言)

最后更新:2026-09-05

相关阅读