一、嵌入式C 语言整数数据类型推荐使用
- 数值很大,超过 - 32768-32768 范围,推荐使用 long 型;
- 在 - 32768-32768 范围内,资源有限如很大数组或者很多结构体,可以使用 short 或者 char;
- 如果是无符号类型使用 unsigned 类型。
- 如果数值比较小的使用 unsigned char 或者 signed char 不用 short 或者 int
- exp:STM32 是 ARM 32 位内核,数据位宽 32 位,处理效率最高,每次访问内存,哪怕一个 byte,读写都是 32 位,因此定义 unsigned char 变量时候看内存小,实际内部会转换为 int,因此定义成 unsigned int 实际代码内存会更小。(8 位 CPU 例外)
- 系统级与原厂代码常用的 uint8_t/uint16_t/uint32_t 均为 typedef 定义的别名,其原始类型依次是 unsigned char、unsigned short、unsigned int,用于固定数据位数以保证跨平台一致性。
二、定义和声明
- 高手:0 err 0 warning
- 习惯声明外部函数加 extern
- .c 负责定义实现,.h 负责声明
- 禁止在同一工程中多处使用相同变量名,编译器可能认为是一个变量,可以通过前缀来区别
- static 定义变量在函数内部会导致变量存储在静态存储区,局部变量存储在函数栈空间,这里相当于 extern
函数指针
cpp
typedef int (*func)()
func p1,p2;
原生写法(无 typedef)
void (*fp)(int) = fun; // 定义 + 赋值
fp (123); // 间接调用
缺点:语法晦涩、复用差。
typedef 别名写法(工程推荐)
typedef void (*Fp)(int);
Fp fp = fun;
fp (123);
优点:简洁易读、适合批量 / 回调复用。
结构体挂载函数指针核心规则
- 结构体只存函数指针,不存函数实现;
- 函数实体单独写在 .c 外部;
- 运行时把函数地址赋值给结构体指针,完成绑定。
三、标准 结构体 + 回调 完整模板(重点)
cpp
#include <stdio.h>
// 1.定义回调函数指针类型
typedef void (*CallBack)(int);
// 2.结构体内嵌回调指针
struct Device
{
int data;
CallBack cb;
};
// 3.回调函数具体实现
void myCallback(int val)
{
printf("回调执行:%d\n", val);
}
// 4.业务函数:内部触发回调
void taskRun(struct Device *dev)
{
if(dev->cb)
{
dev->cb(dev->data);
}
}
// 5.使用:注册回调 + 触发
int main(void)
{
struct Device dev;
dev.data = 666;
dev.cb = myCallback; // 注册回调
taskRun(&dev); // 内部自动调用回调
return 0;
}
核心原理(一句话)
外部注册回调函数地址到结构体,内部业务函数通过函数指针间接反向调用,实现解耦、分层、多态(嵌入式 / 驱动通用)。
函数实现存放位置
- 简单项目:同文件 .c 内;
- 正式工程:实现放 .c,声明 / 结构体 / 指针类型放 .h。
const 有关用法
硬件平台
1. RAM:随机存取存储器
特点:可读、可写,断电数据丢失
- 中文:运行内存
- 单片机里:全局变量、局部变量、栈、堆 都在 RAM
2. ROM(单片机里就是 Flash):只读存储器
特点:断电不丢,一般只读,不能随便改写
- 代码、const 常量、字库、固件 都存在 Flash(ROM)
ARM 单片机一般有:I-Cache(指令缓存)、D-Cache(数据缓存)
Cache 既不是 RAM,也不是 ROM Cache 是 CPU 内部的一小块超高速缓存 ,比 RAM 还要快很多倍。
- 有 Cache:Cortex‑A / H7 高端 M 核,能优化到 Cache
- 无 Cache:F1/F4/M0/M3 普通单片机,最多优化到 RAM
编译器
Keil、GCC、IAR 三大嵌入式编译器全都支持
必要条件
- 必须开优化等级 O1/O2/Os;
- O0 关闭优化:所有 const 只读 Flash,不提速、不缓存。
核心
只有高频读取的 const 才会缓存;低频常量常驻 Flash。
- const 修饰值:值不可改,指针可改const int *p;
- const 修饰指针:指针不可改,值可改int *const p;
- 值、指针都不可改const int *const p;
- 普通常量(非指针)const int a;
const 在函数形参(只写声明 / 定义,无实现)
- 普通变量形参void fun (const int x);
- 指针形参(保护数据,最常用)// 不能通过指针修改内容void fun (const int *p);
- 字符串形参 标准写法void fun (const char *str);
四、复杂声明与 main 函数使用
- int *(*p [5])(int *, int *);p 是一个含 5 个元素的函数指针数组;每个指针指向:参数为两个 int 指针、返回值为 int 指针 的函数。
- .h 头文件:只放宏、结构体 /typedef、函数声明、extern 变量声明;不放变量定义、不放函数实现。
- .c 源文件:放变量定义、函数具体实现。
- 原因:头文件会被多个.c 重复包含,若在.h 写变量 / 函数定义,会造成重复定义报错。搭配头文件保护,就能彻底避免重复包含问题。
main 函数标准写法
- int main (void) ------ 标准无参,最正规
- int main (int argc, char **argv) ------ 标准带参,用于命令行传参
- int main () ------ 不严谨,弱定义无参,坑多
- void main (void) ------ 非标准旧语法,直接废弃
堆栈相关
- 所有函数内普通局部变量(含 main、自定义函数),都存储在栈区。
- 栈空间容量极小,系统严格限制。
- 函数内定义大数组、大型结构体,极易造成栈溢出,程序崩溃。
- 大件数据解决方案:全局变量、static 修饰、malloc 堆内存。
- 主流 CPU 均有硬件栈 + 专属栈指针寄存器。
- ARM:R13 固定为 SP(栈指针)。
- 汇编指令:PUSH 压栈,SP 自动偏移;POP 出栈,SP 自动复位
- x86:ESP 为栈指针,逻辑一致。
- 函数局部变量、现场保存,全靠硬件栈支撑。
堆栈配置文件
- .s 启动文件:startup_stm32xxxx.s
- 关键宏定义:Stack_Size EQU 0x400 ; 栈 默认 1KB
变量初始化
- 只读代码 / 常量 → Flash(code、rodata)
- 全局变量 → RAM(rwdata、zi)
- 局部变量 → 硬件栈 Stack
- 函数局部变量、malloc 内存,默认都是随机脏数据。
- memset:批量把指定内存整块清零 / 设为固定值。
- 常用用法:memset (内存地址,0, 长度),专门给堆内存、数组快速清 0。
字符串定义方式
- char *p = "ABC";字符串 ABC:存在 Flash rodata(只读)指针 p:局部在栈,全局在 RW/ZI RAM不可写!修改内容直接死机、硬件报错
- char p [] = "ABC";字符串拷贝到 RAM(栈 / 全局静态区)本体在内存,可读写修改占用 RAM 空间
- char p [] = {"ABC"};和第二种完全一样,语法写法区别,存储、属性无差异
五、结构体、联合体、枚举核心用法
结构体栈上和堆上使用场景
不需要 malloc 的场景(干净无内存问题)
-
特点:自动生命周期、不用手动清零、不用拷贝管理、无泄漏
cpp// 小结构体、临时用、不跨函数返回 struct Info { int flag; short val; }; void fun(void) { // 栈变量,自动回收 struct Info info = {0}; info.flag = 1; // 直接使用,完事函数结束自动销毁 }要点:小体积、无大缓冲区;不出函数、不返回指针;零 malloc /memset/free
必须 malloc 的 4 种细化场景(全覆盖你说的点)
场景 1:结构体内含大数组,栈会炸 → 堆分配 + memset 清零
cpp
struct BigData {
int len;
char buf[2048]; // 大缓冲区
};
void deal_data(const char *src_buf)
{
// 1. 堆申请
struct BigData *p = malloc(sizeof(struct BigData));
// 2. 必须手动清零(malloc 是脏内存)
memset(p, 0, sizeof(struct BigData));
// 3. 数据拷贝
p->len = strlen(src_buf);
memcpy(p->buf, src_buf, p->len);
// 4. 本函数临时用:用完立刻 free
free(p);
}
规则:函数内临时堆内存 → 谁 malloc,谁当场 free
场景 2:跨函数返回结构体指针 → 内存生命周期延长
栈绝对不能返回,只能堆:
cpp
// 提供者:申请内存
struct BigData* data_create(const char *src)
{
struct BigData *p = malloc(sizeof(struct BigData));
memset(p, 0, sizeof(struct BigData));
memcpy(p->buf, src, strlen(src));
return p; // 堆内存带出函数
}
// 使用者:负责释放
void main_func(void)
{
struct BigData *data = data_create("hello");
// 业务使用...
// 关键:谁接收,谁最终 free
free(data);
}
核心规则(工程铁律):谁申请,谁负责说明释放责任传出指针 = 移交释放权,上层用完必须 free忘了释放 → 内存泄漏,嵌入式长时间运行必崩
场景 3:全局长期持有(缓存 / 状态)→ 只申请,不频繁 free
cpp
static struct BigData *g_cache = NULL;
void cache_init(void)
{
g_cache = malloc(sizeof(struct BigData));
memset(g_cache, 0, sizeof(struct BigData));
}
// 整机退出/注销时才 free
void cache_deinit(void)
{
if(g_cache)
{
free(g_cache);
g_cache = NULL;
}
}
适用:常驻缓存、设备配置、后台常驻节点
场景 4:动态数量(链表 / 队列)→ 逐个 malloc,销毁统一 free
cpp
struct Node {
int data;
struct Node *next;
};
// 新建节点
struct Node* node_new(int v)
{
struct Node *n = malloc(sizeof(struct Node));
memset(n, 0, sizeof(struct Node));
n->data = v;
return n;
}
// 销毁整条链表,统一释放
void list_free(struct Node *head)
{
struct Node *tmp;
while(head)
{
tmp = head;
head = head->next;
free(tmp);
}
}
规则
- 栈结构体:小、临时、不出函数;不用 malloc、memset、free,= {0} 初始化即可
- 堆结构体(malloc)必做三件事:申请后必须 memset 清脏数据;数据搬运用 memcpy;用完必须 free
- 跨函数内存权责:
- 函数内自用:本函数内 free
- 返回指针给外部:移交释放,上层负责 free
- 全局静态常驻:初始化申请,退出统一释放
- 什么时候必选堆:含大数组 / 超大结构体(防栈溢出);需要返回指针、跨生命周期使用;动态节点:链表、队列、缓存
六、展开结构体
空结构体
- struct x{}; sizeof(x);
- 标准 C 不允许空结构体;
- GCC 空结构体占 1 字节,只为占位、保证唯一地址;
- 局部空结构体变量,正常在栈上分配内存;
柔性数组占位思想
cpp
#include <stdint.h>
#include <string.h>
// 修正后:无union,纯你要的结构
struct X
{
uint8_t head; // 1字节
uint16_t len; // 2字节
uint8_t buffer[0]; // 柔性数组,不占内存
};
// 两个uint8_t 数组 大小100
uint8_t receive_buffer[100];
uint8_t data_buffer[100];
void test(void)
{
// 数组首地址 强转为结构体指针
struct X *p_buffer = (struct X *)receive_buffer;
// 整段拷贝:receive_buffer → data_buffer
memcpy(data_buffer, receive_buffer, sizeof(receive_buffer));
}
补充关键知识点
- 结构体本身大小(无对齐):1 + 2 = 3 字节
- buffer [0] 柔性数组不计入结构体大小
举例
cpp
struct X
{
uint8_t head; // 1B
uint16_t len; // 2B
uint8_t buffer[0]; // 柔性数组
};
核心第一句:柔性数组 [0] 本身不占任何 sizeof 大小,sizeof (struct X) 只算前面成员 + 结构体对齐,不含 buffer
什么是占位思想
- head + len 是固定协议头;
- buffer [0] 只是占一个位置、标记起始地址;
- 真正的数据载荷,紧贴在结构体头部内存的后面;
- 不额外分配固定空间,用多少、占多少。内存连续排布:[head (1B)] [len (2B)] [buffer 真实数据区......],buffer [0] 就指向这里,它不是提前开一块固定数组,只是标记载荷起点。
柔性数组 内存特点
- 不占结构体体积;
- 必须放在结构体最后一位;
- 本身无内存,真实数据靠:栈数组强转(你现在的写法)或者 malloc 一口气申请「结构体头 + 载荷总长」
数组强转结构体工程优势
优势 1:不用逐个偏移算地址,直接点成员
cpp
// 原始字节数组
uint8_t receive_buffer[100];
// 强转直接解析协议头
struct X *p = (struct X *)receive_buffer;
p->head; // 直接拿第1字节
p->len; // 直接拿2~3字节
p->buffer; // 直接拿到后面载荷起始
不用手写偏移 buf [0]、buf [1]、buf [2],可读性爆炸、少 Bug。
优势 2:协议头和载荷内存连续,天然贴合串口 / CAN / 网络帧硬件收上来的原始数据流,本身就是一段连续内存,和柔性数组结构体内存布局完全匹配,天生适配。
优势 3:极度省内存如果写成固定数组:uint8_t buffer [64]; 不管数据是 5 字节还是 60 字节,永远占 64B。柔性数组:数据多长,就用多长,无冗余浪费。
优势 4:统一解包格式所有通信帧:包头 + 长度 + 载荷,全用这套柔性结构体模板,代码统一、好维护。
对比反例,为什么不用指针代替?
错误写法:
cpp
struct Bad
{
uint8_t head;
uint16_t len;
uint8_t *buf; // 指针
};
指针单独占 4/8 字节;指针是独立地址,不跟包头连续;不能直接数组强转解析;容易野指针、内存泄漏。柔性数组直接贴在头部后面,无额外开销、无指针坑。
极简笔记总结(直接抄)
- 柔性数组 [0] 不占用结构体大小,仅做地址占位;
- 核心思想:头部固定协议段 + 尾部动态变长载荷;
- 内存连续排布,适配原始数据流,支持数组强转直接解析;
- 优势:不用手动算偏移,代码简洁;动态长度、不浪费内存;无指针、无野指针、无泄漏;必须放在结构体最后一个成员。
cpp
// 纯协议头,无柔性数组
struct X
{
uint8_t head;
uint16_t len;
};
uint8_t receive_buffer[100];
uint8_t data_buffer[100];
void test(void)
{
struct X *p_buf = (struct X *)receive_buffer;
// 最后一个成员 len 的地址 + 1
uint8_t *payload = (uint8_t *)(&p_buf->len + 1);
memcpy(data_buffer, payload, p_buf->len);
}
数据选择性解析
cpp
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// ---------------------- 两个不同业务子结构体 ----------------------
// 类型0 对应结构体
typedef struct
{
uint16_t val1;
uint16_t val2;
} DataA_t;
// 类型1 对应结构体
typedef struct
{
uint32_t id;
uint8_t status;
} DataB_t;
// ---------------------- 带柔性数组的主结构体 ----------------------
typedef struct
{
uint8_t type; // 数据类型:0=解析A,1=解析B
uint8_t len; // 有效数据长度
uint8_t data[]; // 柔性数组
} MsgHead_t;
// ---------------------- 全局原始缓冲数组 ----------------------
// 原始源数据buf
uint8_t src_buf[64] =
{
// type=0, len=4, 后面4字节是DataA数据
0x00,0x04, 0x11,0x22,0x33,0x44
};
// 目标接收buf
uint8_t dst_buf[64] = {0};
// ---------------------- 核心逻辑 ----------------------
int main(void)
{
// 1.用结构体指针直接操作数组(强转)
MsgHead_t *p_src_msg = (MsgHead_t *)src_buf;
MsgHead_t *p_dst_msg = (MsgHead_t *)dst_buf;
// 2.整体拷贝:头部+柔性数组全部数据
uint32_t total_len = sizeof(MsgHead_t) + p_src_msg->len;
memcpy(p_dst_msg, p_src_msg, total_len);
// 3.定义两个子结构体,用来接收解析结果
DataA_t dataA;
DataB_t dataB;
// 4.根据type选择性解析
if (p_dst_msg->type == 0)
{
// 解析为 DataA
memcpy(&dataA, p_dst_msg->data, sizeof(DataA_t));
printf("解析DataA: val1=0x%04X, val2=0x%04X\n", dataA.val1, dataA.val2);
}
else if (p_dst_msg->type == 1)
{
// 解析为 DataB
memcpy(&dataB, p_dst_msg->data, sizeof(DataB_t));
printf("解析DataB: id=0x%08X, status=0x%02X\n", dataB.id, dataB.status);
}
else
{
printf("未知类型\n");
}
return 0;
}
结构体函数传地址 和 值
cpp
// 定义一个结构体
typedef struct
{
int x;
int y;
}Point;
// 传值:形参是结构体本身
void testValue(Point p)
{
p.x = 999; // 只改副本
p.y = 888;
printf("传值内部:x=%d, y=%d\n", p.x, p.y);
}
// 传址:形参是结构体指针
void testAddr(Point *p)
{
p->x = 666; // 直接修改原变量
p->y = 555;
printf("传址内部:x=%d, y=%d\n", p.x, p.y);
}
int main(void)
{
Point pt;
pt.x = 10;
pt.y = 20;
// 1.传值调用
testValue(pt);
printf("传值后原值:x=%d, y=%d\n\n", pt.x, pt.y);
// 2.传址调用
testAddr(&pt);
printf("传址后原值:x=%d, y=%d\n", pt.x, pt.y);
return 0;
}
核心总结
- 传值:拷贝整个结构体,临时副本;函数内修改不影响外面;结构体大时,内存开销大、效率低
- 传址 (指针):只传 4/8 字节地址,效率极高;通过 -> 访问成员;函数内修改直接改动原结构体;工程 / 嵌入式全部优先用传址
三层分离、文件拆分、ops 函数指针结构体放在设备抽象层、驱动纯硬件、应用零耦合
cpp
drv/
drv_uart.h 底层硬件驱动声明
drv_uart.c 底层寄存器/硬件实现
device/
dev_uart.h 【抽象层】函数指针结构体 统一接口
dev_uart.c 设备实例绑定、驱动注册
app/
main.c 应用层,只调用抽象接口,不碰硬件
底层驱动层 ------ drv_uart.h
cpp
#ifndef __DRV_UART_H
#define __DRV_UART_H
#include <stdint.h>
// 底层纯硬件函数声明
void uart1_hw_init(uint32_t baudrate);
void uart1_hw_send(uint8_t *buf, uint16_t len);
#endif
底层驱动层 ------ drv_uart.c
cpp
#include "drv_uart.h"
// 真实项目:配置时钟、GPIO、寄存器、DMA、中断
void uart1_hw_init(uint32_t baudrate)
{
// 硬件初始化逻辑
}
void uart1_hw_send(uint8_t *buf, uint16_t len)
{
// 底层串口发送逻辑
for(uint16_t i = 0; i < len; i++)
{
// 串口数据寄存器写入
}
}
设备抽象层 ------ dev_uart.h 【核心】
cpp
#ifndef __DEV_UART_H
#define __DEV_UART_H
#include <stdint.h>
// 统一操作接口:函数指针结构体
typedef struct
{
void (*init)(uint32_t baud);
void (*send)(uint8_t *buf, uint16_t len);
}UART_Ops_t;
// 对外暴露设备句柄
extern UART_Ops_t uart1_dev;
#endif
设备抽象层 ------ dev_uart.c
cpp
#include "dev_uart.h"
#include "drv_uart.h"
// 设备实例:挂载底层硬件函数
UART_Ops_t uart1_dev =
{
.init = uart1_hw_init,
.send = uart1_hw_send
};
应用层 ------ main.c
cpp
#include "dev_uart.h"
int main(void)
{
// 应用层:只调用统一抽象接口
uart1_dev.init(115200);
uint8_t tx_buf[] = "Hello World";
uart1_dev.send(tx_buf, sizeof(tx_buf));
while(1)
{
// 业务逻辑
}
}
设备配置 存 Flash 掉电保存的通用写法
核心需求总结
把 PID 参数 / 配置参数 定义成一个结构体,存放在 片内 Flash 固定分区,防止掉电丢失,上电先读取 Flash 里的结构体配置。
问题:新芯片、Flash 刚擦除、首次上电、固件升级后,Flash 里全是 0xFF / 无效数据,不能直接用。
行业标准做法:在配置结构体里,多加两个关键成员:magic 魔术校验码(关键标记)、crc 校验和(防损坏、防篡改)
上电逻辑:读 Flash 结构体 → 先判断 magic 是否合法 → 再校验 CRC,不合法 / 全 FF → 自动加载默认参数,再重新写入 Flash
标准结构体设计
cpp
// PID 配置 + 保存到Flash的配置结构体
typedef struct
{
// 1. 【关键】魔术码:用来判断是不是有效配置
uint32_t magic;
// 2. 业务参数:PID算子
float kp;
float ki;
float kd;
uint16_t dead_zone;
int16_t out_limit;
// 3. 【关键】CRC校验:防止数据损坏
uint16_t crc;
// 强制紧凑对齐,防止Flash读写字节错位
} __attribute__((packed)) Pid_Config_t;
magic:自己定一个固定数,比如 0xAA556677;Flash 空数据默认全是 0xFFFFFFFF,必然不等于合法 magic,直接就能判断:是有效配置 还是 空 / 未初始化
全局常量定义
cpp
// 自定义魔术码(随便写一个唯一固定值)
#define PID_CONFIG_MAGIC 0xAA556677
// Flash 存储地址(STM32 片内Flash最后一页)
#define PID_CONFIG_ADDR 0x0807F000
核心逻辑伪代码
1. 上电初始化流程
cpp
// 全局运行时PID配置
Pid_Config_t g_pid_cfg;
void PID_Config_Init(void)
{
// 1. 直接把Flash地址强转为结构体指针
Pid_Config_t *flash_cfg = (Pid_Config_t *)PID_CONFIG_ADDR;
// 2. 第一步:判断 magic 是否合法
if(flash_cfg->magic == PID_CONFIG_MAGIC)
{
// 3. 第二步:CRC 数据完整性校验
if(PID_Check_Crc(flash_cfg) == OK)
{
// 合法:直接拷贝Flash配置到运行内存
memcpy(&g_pid_cfg, flash_cfg, sizeof(Pid_Config_t));
return;
}
}
// 走到这里说明:
// ① 首次上电 ② Flash被擦除 ③ 数据损坏 ④ magic非法
// 加载默认参数
PID_Load_Default(&g_pid_cfg);
// 把默认参数重新写入Flash,下次上电就正常了
PID_Save_To_Flash(&g_pid_cfg);
}
2. 加载默认参数函数
cpp
void PID_Load_Default(Pid_Config_t *cfg)
{
cfg->kp = 2.5f;
cfg->ki = 0.1f;
cfg->kd = 0.05f;
cfg->dead_zone = 5;
cfg->out_limit = 1000;
// 写入合法魔术码
cfg->magic = PID_CONFIG_MAGIC;
}
3.保存结构体到 Flash(带 CRC 计算)
cpp
void PID_Save_To_Flash(Pid_Config_t *cfg)
{
// 先计算CRC,填充进去
cfg->crc = PID_Calc_Crc((uint8_t*)cfg,
sizeof(Pid_Config_t) - 2);
// STM32操作:Flash解锁 → 擦除页 → 编程写入 → 上锁
FLASH_Unlock();
FLASH_PageErase(PID_CONFIG_ADDR);
FLASH_WriteData(PID_CONFIG_ADDR, (uint8_t*)cfg, sizeof(Pid_Config_t));
FLASH_Lock();
}
4、CRC 校验、CRC 计算(简单示意)
cpp
// 校验:对比计算出来的crc和结构体里的crc
uint8_t PID_Check_Crc(Pid_Config_t *cfg)
{
uint16_t calc_crc = PID_Calc_Crc((uint8_t*)cfg,
sizeof(Pid_Config_t) - 2);
return (calc_crc == cfg->crc) ? 1 : 0;
}
核心问题解答
- 怎么判断结构体是空 / 无效?靠 magic 魔术码,新 Flash / 擦除后:magic = 0xFFFFFFFF,正常配置:magic = 固定约定值 0xAA556677
- 为什么还要加 CRC?防止电压波动、Flash 位翻转、意外断电导致参数半截损坏,光判断 magic 不够,必须做完整性校验
- 是不是工程标准写法?100% 就是 STM32 工业设备、控制器、变频器、温控仪通用写法,适用于掉电保存参数、首次上电默认值、固件升级不兼容、Flash 清空容错场景
STM32 内核硬件特性
结构体成员偏移<128B = 短指令、快访问;高频数据前置,是裸机实时控制、调速、PID 优化的常规标准写法。
结构体嵌套赋值
cpp
// 内层结构体
struct Info {
int age;
char sex;
};
// 外层结构体 嵌套 Info
struct User {
int id;
// 嵌套成员
struct Info info;
};
struct User u2 = {
20,
{18, 'F'} // 内层结构体整体初始化
};
结构体重定义
先写通用 Sensor 基础结构体,所有传感器公共属性:通道、延时、状态
cpp
// 通用传感器基础属性
typedef struct
{
uint16_t ch_val; // 通道采样值
uint16_t delay_ms; // 延时时间
uint8_t state; // 运行状态
} SensorBase_t;
// 给不同传感器单独起类型名,语义清晰
typedef SensorBase_t TempSensor_t; // 温度传感器
typedef SensorBase_t PressSensor_t; // 压力传感器
typedef SensorBase_t LightSensor_t; // 光照传感器
TempSensor_t temp_sen;
PressSensor_t press_sen;
LightSensor_t light_sen;
优势:公共参数抽通用结构体,所有传感器复用;用 typedef 起不同别名,区分设备语义;核心价值:解耦、易维护、易扩展、可读性强、少 BUG;
Linux 通过成员求结构首地址解析
cpp
#define offsetof(type, member) ((size_t)&((type *)0)->member)
#define container_of(ptr, type, member) \
({ \
const typeof(((type *)0)->member) *__mptr = ptr; \
(type *)((char *)__mptr - offsetof(type, member)); \
})
参数:ptr:已知的【成员指针】,你现在手里拿到的、结构体里某个成员的地址。
type:完整结构体类型,你最终要反向拿到的「整个大结构体」类型。member:ptr 对应的、该成员在结构体内的成员名。
typeof (((type ) 0)->member):GCC 扩展语法,自动识别这个成员的原生类型 char * 严格按 1 字节运算 注:结构体首地址 = 成员地址 - 成员偏移量,char 保证字节减法、typeof 保证类型安全。
typeof 和 sizeof 都是在编译处理
cpp
int fun(int a);
typeof(fun) * fptr; // int (*fptr)(int);
typeof(int *)a, b; // int *a, *b;
typeof(int) * a, b; // int *a, b;
结构体位于操作
1、真实位域 + 真实寄存器地址
cpp
// 引脚4位位域(真实寄存器划分)
typedef struct
{
uint32_t CNF:2;
uint32_t MODE:2;
}GPIO_PinBit;
// 真实 CRH 结构体映射
typedef struct
{
GPIO_PinBit PIN8;
GPIO_PinBit PIN9;
GPIO_PinBit PIN10;
GPIO_PinBit PIN11;
GPIO_PinBit PIN12;
GPIO_PinBit PIN13; // 常用PC13
GPIO_PinBit PIN14;
GPIO_PinBit PIN15;
}GPIO_CRH_REG;
// 关键:F103 真实寄存器绝对地址
#define GPIOC_CRH ((GPIO_CRH_REG *)0x40011004)
#define GPIOC_ODR ((uint32_t *)0x4001100C)
2、真实操作(PC13 点灯,工业裸机写法)
cpp
// 1. 配置 PC13 推挽输出(直接操作位域,无移位)
GPIOC_CRH->PIN13.CNF = 0;
GPIOC_CRH->PIN13.MODE = 1;
// 2. 直接操作输出位
*GPIOC_ODR |= (1 << 13); // 亮
*GPIOC_ODR &=~(1 << 13); // 灭
对比 传统寄存器 vs 位域
cpp
// 传统写法(要算移位、极易写错)
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x3 << 20);
// 位域写法(真实可用)
GPIOC_CRH->PIN13.CNF = 0;
GPIOC_CRH->PIN13.MODE = 1;
精简好处(背这 3 条)
- 寄存器按硬件真实位分段,不用手动算移位掩码;
- 只改当前引脚位,不影响其他引脚;
- 直接地址操作,比 HAL / 库函数更快、延时最小。
结构体位域控内存
cpp
// 对应寄存器 1字节 8位:bit0 ~ bit7
struct Reg8
{
uint8_t b0 : 1; // 先定义 → 绑定 bit0(最低位)
uint8_t b1 : 1; // 绑定 bit1
uint8_t b2 : 1; // 绑定 bit2
uint8_t b3 : 1;
uint8_t b4 : 1;
uint8_t b5 : 1;
uint8_t b6 : 1;
uint8_t b7 : 1; // 后定义 → 绑定 bit7(最高位)
};
STM32 / ARM 小端:位域先定义的成员 → 对应寄存器 低位 bit0;位域 :n :严格占用 n 个二进制位,压缩内存、贴合硬件。
7、展开联合体
多视角解读 32 位数据的联合体
cpp
typedef union {
uint32_t a; // 视角1:把这 4 字节看成一个 32 位整数
uint8_t b[4]; // 视角2:看成 4 个独立字节
uint16_t c[2]; // 视角3:看成 2 个 16 位半字
struct {
uint16_t x;
uint16_t y;
} d; // 视角4:看成两个 16 位成员的结构体
struct {
uint32_t arg1 : 5; // 视角5:按位域分解成 6 个独立字段
uint32_t arg2 : 6;
uint32_t arg3 : 5;
uint32_t arg4 : 3;
uint32_t arg5 : 11;
uint32_t arg6 : 2;
} e;
} x;
- 这个联合体的总大小永远是 4 字节(32 bit),所有成员共享同一块内存。
- 给 aaa.a = 0x12345678 赋值后,后面所有成员看到的,都是同一 4 字节数据,只是 "解读方式不同"。
逐段分析代码作用
- 用 uint32_t a 视角:aaa.a = 0x12345678; 直接把这 4 字节当成一个完整的 32 位无符号整数,数值就是 0x12345678。
- 用 uint8_t b [4] 视角(按字节拆分):printf ("0X%02x 0X%02x 0X%02x 0X%02x\r\n", aaa.b [3], aaa.b [2], aaa.b [1], aaa.b [0]);小端序(ARM/STM32 默认)下内存:地址低→高: 0x78 0x56 0x34 0x12,对应数组下标: b [0] b [1] b [2] b [3],打印结果:0X12 0X34 0X56 0X78,作用:查看内存字节序、网络字节序转换、按字节操作数据。
- 用 uint16_t c [2] 和 struct d 视角(按 16 位拆分):printf ("0X%04x 0X%04x\r\n", aaa.c [1], aaa.c [0]);printf ("0X%04x 0X%04x\r\n", aaa.d.x, aaa.d.y);小端序内存: 0x78 0x56 0x34 0x12,c [0] = 0x5678,c [1] = 0x1234,打印结果:0X1234 0X5678,d.x 和 d.y 和 c [0]、c [1] 完全等价,作用:把 32 位寄存器 / 数据拆成高低 16 位,单独操作。
- 用 struct e 视角(位域分解):struct 把 32 bit 分成 6 个不同长度字段,赋值后可直接读取每个字段,不用移位和掩码,作用:直接按位操作寄存器 / 协议字段。
代码的核心作用
一句话概括:一块 4 字节的内存,既可以看成一个整数,也可以看成字节数组、半字数组、位域结构,不用拷贝数据,就能从不同角度解读它,即联合体的多面性。
应用场景(嵌入式 / 单片机)
- 寄存器映射:一次性写整个寄存器,也可直接修改某一字段
- 网络协议 / 通信帧解析:直接按字段访问,无需手动解析
- 数据类型转换(无拷贝):类型双关,比强制转换更安全清晰
- 大小端转换、字节序处理:按字节重排数据
联合体通信协议经典用法
cpp
typedef union {
struct {
uint8_t header; // 帧头
uint8_t datatype; // 数据类型
uint16_t len; // 数据长度
uint16_t data_buf[0];// 柔性数组,存实际数据
} frame;
uint8_t buf[100]; // 字节数组,用来直接接收/发送原始数据
} data_frame;
- 联合体里的 frame 和 buf 共享同一块内存
- 接收数据:通过 x.buf 读原始字节流,解析用 x.frame 访问字段
- 核心作用:数据收发零拷贝、通信协议一键解析
- 应用场景:单片机串口 / RS485/CAN 通信、Modbus / 自定义物联网协议、低资源嵌入式系统
柔性数组联合体示例
cpp
typedef struct
{
uint8_t header;
uint8_t datatype;
uint16_t len;
uint16_t data_buf[0];
} data_frame;
uint8_t buf[100];
((data_frame *)(buf))->data_buf
data_buf [0] 是柔性数组,不占结构体额外空间,强转后自动指向结构体头后的数据部分
大小端判断
cpp
// 方法1:用联合体判断大小端
union {
uint16_t a;
uint8_t byte[2];
} val = {.a = 0x1234};
val.byte[0]
// 方法2:用指针强制转换判断大小端
uint16_t a = 0x1234;
((uint8_t *)&a)[0];
应用:串口 / 网络通信字节序转换
八、展开枚举
- 结构体 + 数组:用数字下标可读性差,调整顺序易出错
- #define 宏定义:文本替换,无类型检查,易重复定义、作用域污染
- enum 枚举(推荐):自动递增整数,有类型、作用域,安全规范
cpp
enum
{
LILEI, // 0
HANMEIMEI, // 1
MADONGMEI, // 2
LINING, // 3
LILU, // 4
TEILANGPU, // 5
BAIDENG // 6
};
核心结论:给数组下标赋予语义化名字,提升可读性和可维护性;#define 无类型检查,enum 是标准规范写法
命令码定义
cpp
enum
{
OPENDOOR = 1000,
CLOSEDOOR,
CLEAN,
RESET,
......
};
九、表达式和运算符进阶挑战
- A = 10;B = 10;B += B -= B * B + A;结果:-200
- float a = 1.0f;int b = 0; // 关键:b 默认考题里都是给 0
- printf ("% d", a + b);
- float 会被提升为 double
- C 语言可变参数(printf 里):只要是 float,一律默认隐式提升成 double
- a + b:int b 转 float → 相加 → 结果 float → 传入 printf → 自动升级为 double
- 关键②:double 内存布局 + % d 冲突% d 要求:拿低 32 位二进制当成 int 解析double 是 8 字节 (64 位) 标准布局:符号位 (1) + 指数 (11) + 尾数 (52)double 1.0 的二进制内存:高 32 位有指数、符号位,低 32 位全部都是 0
- uint8_t a = 100;uint8_t b = 100;打印 a*b 结果不是 10000,而是会发生截断
- uint8_t 取值范围 0~255 (2 的八次方减一)
- uint16_t 0~65535
- 最高位表示正负,所以 7 次方
- int8_t -128~127 (2 的七次方 - 1)
- int16_t -32768~32767
- 死循环
- int8_t a = 0;
- while (a <= 255){a++;}
- x << n 等价于 x * 2ⁿ
- 所以 1 * 8 等价于 1 << 3
- int8_t a = -3 的二进制(补码)
- a >> 1 结果
- 反码 +1(求补码):1111 1101
- 按位取反(求反码):1111 1100
- 正数 3 的 8 位原码:0000 0011
- 有符号右移 >> 1 的计算步骤核心规则:有符号数右移,高位补符号位(负数补 1)
- 原补码:1111 1101
- 右移 1 位(挤掉最右边的 1,左边补 1):1111 1110
- 把 1111 1110 转回十进制(补码逆运算):补码减 1:1111 1101按位取反:0000 0010(数值为 2)加负号:-2
- 负数左移 :就是严格乘 2,和正数规则一致
- 负数右移 :算术右移,向下取整,和 C 语言
/除法不一样 - STM32 加减乘都是几十 ps 的量级,但除法和取模直接冲到了 1200ps 左右,比加法慢了近 30 倍,比赋值慢了 1500 倍
- x / 位 %10 = x 对应位值每一位对应在数码管显示
负数取模
cpp
int array [] = {-3, -2, -1, 0, 1, 2, 3};
int i;
for (i = 0; i < 7; i++) {
// 判断是否为偶数
if (array [i] % 2 == 0) {
printf ("% d", array [i]);
}
}
不管正负,偶数对 2 取模的结果永远是 0,奇数才会得到 ±1。
- // 关键:每 8 个数字,就输出一次换行,否则输出空格
- printf ((((i+1)%8)?"":"\r\n"));
用三目运算符处理 "尾巴数据"
cpp
(12768%512) ? FLASH_Write_Sector (i, buf+i512, (12768%512)) : 0;
//这行的作用是:判断是否有剩余数据,如果有,就单独写一次;如果没有,就什么都不做。
- 把一个 0~255 的无符号字符型数据,转换成 0xXX 格式的十六进制字符串,并存入 str 中。
cpp
void Value2String (unsigned char value,char *str)
{
char *Hex_Char_Table="0123456789ABCDEF";
str [0]='0';str [1]='X';str [4]=0;
str [2]=Hex_Char_Table [value/16];
str [3]=Hex_Char_Table [value%16];
}
uint8_t 转二进制
cpp
uint8_t reg = 0X5A;
uint8_t i = 0;
char bstr[9];
printf("0b");
for(i=0; i<8; i++)
{
bstr[7-i] = "01"[reg%2];
reg /= 2;
}
printf(bstr);
比较浮点类型
cpp
typedef union
{
float f;
uint8_t bytes[4];
} FLOAT;
FLOAT uf;
FLOAT xf;
int i=0;
for(i=0;i<9;i++) uf.f += +0.0;
xf.f = -0.0;
if(xf.f == uf.f) printf("相等\r\n");
else printf("不相等\r\n");
printf("uf:%02X %02X %02X %02X\r\n",uf.bytes[3],uf.bytes[2],uf.bytes[1],uf.bytes[0]);
printf("xf:%02X %02X %02X %02X\r\n",xf.bytes[3],xf.bytes[2],xf.bytes[1],xf.bytes[0]);
- 在 IEEE 754 浮点数标准中,+0.0 和 -0.0 的二进制表示是不同的:+0.0:符号位为 0,十六进制为 00 00 00 00-0.0:符号位为 1,十六进制为 80 00 00 00
- 但在数值上,+0.0 == -0.0 的结果为真,因为它们代表的数学值是相等的。
- 作用:利用联合体的内存共享特性,既可以按 float 类型访问数值,也可以按 uint8_t bytes [4] 数组访问浮点数的原始二进制字节。
反向判断
cpp
int a=0;
if(1==a)
{
//代码
}
短路求值
cpp
int i = 10;
int j = 20;
int k, m, c;
// && 的短路(左边为假,右边不执行)
m = (i > j) && (j = 0);
printf("m = %d, j = %d\n", m, j);
// || 的短路(左边为真,右边不执行)
c = (i < j) || (k = 8);
printf("c = %d, k = %d\n", c, k);
结果:m = 0, j = 20 c = 1, k = 随机值
位运算规则
- & 全 1 为 1
- ^ 相同为 0 不同为 1
- | 有 1 为 1 全 0 为 0
- 1^2^2^3^4......n = 2
- 一个数组存放了若干个整数,一个数出现了奇数次,其余数都出现了偶数次,找出这个奇数次的数
cpp
int findOddNum(int *arr, int len)
{
int res = 0;
for(int i = 0; i < len; i++)
{
res ^= arr[i];
}
return res;
}
51 模拟 spi 驱动速度大约在 300~500kbps
cpp
#include <REG51.H>
// 定义 SPI 引脚(根据你的硬件修改)
sbit SPI_SCK = P1^0; // 时钟
sbit SPI_MOSI = P1^1; // 主机输出,从机输入(SI)
sbit SPI_MISO = P1^2; // 主机输入,从机输出(SO)
sbit SPI_CS = P1^3; // 片选
// 可位寻址变量,方便取位
unsigned char bdata spi_dat;
sbit spi_dat7 = spi_dat^7;
sbit spi_dat6 = spi_dat^6;
sbit spi_dat5 = spi_dat^5;
sbit spi_dat4 = spi_dat^4;
sbit spi_dat3 = spi_dat^3;
sbit spi_dat2 = spi_dat^2;
sbit spi_dat1 = spi_dat^1;
sbit spi_dat0 = spi_dat^0;
/**
* @brief SPI 全双工收发一个字节
* @param txdata: 要发送的字节
* @retval 接收到的字节
*/
unsigned char SPI_RW_Byte(unsigned char txdata)
{
unsigned char rxdata = 0;
spi_dat = txdata; // 把要发送的数据放到可位寻址变量
// 发送第7位,同时接收
SPI_MOSI = spi_dat7;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x80;
SPI_SCK = 1;
// 发送第6位,同时接收
SPI_MOSI = spi_dat6;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x40;
SPI_SCK = 1;
// 发送第5位,同时接收
SPI_MOSI = spi_dat5;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x20;
SPI_SCK = 1;
// 发送第4位,同时接收
SPI_MOSI = spi_dat4;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x10;
SPI_SCK = 1;
// 发送第3位,同时接收
SPI_MOSI = spi_dat3;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x08;
SPI_SCK = 1;
// 发送第2位,同时接收
SPI_MOSI = spi_dat2;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x04;
SPI_SCK = 1;
// 发送第1位,同时接收
SPI_MOSI = spi_dat1;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x02;
SPI_SCK = 1;
// 发送第0位,同时接收
SPI_MOSI = spi_dat0;
SPI_SCK = 0;
if(SPI_MISO) rxdata |= 0x01;
SPI_SCK = 1;
return rxdata;
}
/**
* @brief SPI 发送一个字节(只发不收)
*/
void SPI_Send_Byte(unsigned char dat)
{
SPI_CS = 0; // 拉低片选
SPI_RW_Byte(dat);
SPI_CS = 1; // 拉高片选
}
/**
* @brief SPI 接收一个字节(只收不发,发0xFF)
*/
unsigned char SPI_Recv_Byte(void)
{
unsigned char dat;
SPI_CS = 0;
dat = SPI_RW_Byte(0xFF);
SPI_CS = 1;
return dat;
}
实战:GPIO 位带
cpp
// 定义 PAout(n):直接操作 GPIOA->ODR 的第 n 位
#define PAout(n) BIT_ADDR((uint32_t)&GPIOA->ODR, n)
#define PAin(n) BIT_ADDR((uint32_t)&GPIOA->IDR, n)
// 用法
PAout(5) = 1; // PA5 置 1(硬件原子)
PAout(5) = 0; // PA5 清 0
if (PAin(5)) {} // 读 PA5 输入
- 原子操作:单指令完成,不会被中断打断,多任务 / 中断安全
- 代码极简:像 51 一样 X=1/X=0,可读性强
- 效率高:省去读‑改‑写,减少总线访问、CPU 周期
- 无竞争:适合频繁翻转 IO、标志位、中断标志
注意
- 只支持 Cortex‑M3/M4(M0/M0+ 没有位带)
- 仅限上述 2 个 1MB 区域,不是整个内存
- SRAM 位带区范围:0x20000000 ~ 0x200FFFFF(1MB)别名区:0x22000000 ~ 0x23FFFFFF(32MB)
- 外设位带区(GPIO、串口、定时器等寄存器)范围:0x40000000 ~ 0x400FFFFF(1MB)别名区:0x42000000 ~ 0x43FFFFFF(32MB)
- 写别名区任意值:最低位决定 0/1,高位忽略
- 现代 HAL 也有 HAL_GPIO_WritePin,但位带 更高效、更原子
闭包表达式
cpp
#define CAL_CHKSUM(buf, len) ({int i=0, sum=0; \
for(i=0;i<len;i++) sum+=buf[i];sum;})
说明:这是 GCC 扩展的 ** 语句表达式(statement expression)** 宏,整体放在一对圆括号 ({...}) 里,内部可以有多条语句、循环等,最后一个表达式 sum; 作为整个宏的 "返回值"。
C 预处理器的字符串化 # 和拼接 ##,属于高级宏技巧
cpp
#define RTM_EXPORT(symbol) \
const char __rtmsym_##symbol##_name[] = #symbol; \
const struct rt_module_symtab __rtmsym_##symbol SECTION("RTMSymTab") = { \
(void *)&symbol, \
__rtmsym_##symbol##_name \
};
SECTION 就是编译器会:专门开辟一块内存段,名字叫 RTMSymTab然后把这个结构体单独放进去。
位图
场景 1:一千万人的性别标记需求:标记 1000 万人的性别(0 = 男,1 = 女)。计算:1000 万 ÷ 32 ≈ 312500 个 int空间:约 1.25MB,相比直接用数组存节省了上千倍空间。操作:通过位运算(& | ^)快速读写单个状态。
场景 2:40 亿整数的存在性判断需求:给 40 亿个不重复的 unsigned int,快速判断一个数是否在其中。计算:unsigned int 范围是 0~2³²-1(约 42 亿),刚好可以用一个大小为 2³² bits 的位图覆盖。空间:2³² bits = 512MB,内存可承受。实现步骤:遍历 40 亿个数,把对应 bit 置 1。查询时,直接访问该数对应的 bit,0 表示不存在,1 表示存在
关键优势
- 极致省空间:1bit / 对象,处理海量数据时比数组 / 哈希表节省几个数量级的内存。
- 查询极快:时间复杂度 O (1),直接定位 bit 位判断状态。
- 操作高效:所有读写都可通过位运算完成,CPU 执行效率高。
适用场景
- 海量数据去重
- 快速存在性判断(黑名单、白名单)
- 状态标记(用户在线状态、任务完成状态)
- 排序(位图排序)
位图实现代码
cpp
#include <stdio.h>
#include <stdlib.h>
// 位图结构体
typedef struct {
unsigned int *data; // 数据数组,每个元素存32个bit
int size; // 能表示的最大元素个数
} BitMap;
// 1. 初始化位图,size为需要表示的最大数字
BitMap* bitmap_create(int size) {
BitMap *bm = (BitMap*)malloc(sizeof(BitMap));
if (!bm) return NULL;
// 计算需要多少个unsigned int(向上取整)
int array_size = (size + 31) / 32;
bm->data = (unsigned int*)calloc(array_size, sizeof(unsigned int));
bm->size = size;
return bm;
}
// 2. 把数字x标记为存在(置1)
void bitmap_set(BitMap *bm, int x) {
if (x < 0 || x >= bm->size) return;
int idx = x / 32; // 数组下标
int bit = x % 32; // 位偏移
bm->data[idx] |= (1U << bit);
}
// 3. 判断数字x是否存在(读bit)
int bitmap_test(BitMap *bm, int x) {
if (x < 0 || x >= bm->size) return 0;
int idx = x / 32;
int bit = x % 32;
return (bm->data[idx] & (1U << bit)) != 0;
}
// 4. 把数字x标记为不存在(置0)
void bitmap_clear(BitMap *bm, int x) {
if (x < 0 || x >= bm->size) return;
int idx = x / 32;
int bit = x % 32;
bm->data[idx] &= ~(1U << bit);
}
// 5. 销毁位图
void bitmap_destroy(BitMap *bm) {
if (bm) {
free(bm->data);
free(bm);
}
}
// 测试主函数
int main() {
// 比如:处理范围0~99的数字
BitMap *bm = bitmap_create(100);
// 标记几个数字
bitmap_set(bm, 10);
bitmap_set(bm, 25);
bitmap_set(bm, 99);
// 查询
printf("10 是否存在:%s\n", bitmap_test(bm, 10) ? "是" : "否");
printf("20 是否存在:%s\n", bitmap_test(bm, 20) ? "是" : "否");
printf("99 是否存在:%s\n", bitmap_test(bm, 99) ? "是" : "否");
// 清除10
bitmap_clear(bm, 10);
printf("清除后,10 是否存在:%s\n", bitmap_test(bm, 10) ? "是" : "否");
bitmap_destroy(bm);
return 0;
}
位图排序
cpp
#include <stdio.h>
#include <stdlib.h>
// 位图结构体
typedef struct {
unsigned int *data;
int size; // 位图可表示的最大数+1
} BitMap;
// 创建位图
BitMap* bitmap_create(int max_num) {
BitMap *bm = (BitMap*)malloc(sizeof(BitMap));
if (!bm) return NULL;
int array_size = (max_num + 31) / 32; // 向上取整
bm->data = (unsigned int*)calloc(array_size, sizeof(unsigned int));
bm->size = max_num + 1;
return bm;
}
// 设置bit
void bitmap_set(BitMap *bm, int x) {
if (x < 0 || x >= bm->size) return;
int idx = x / 32;
int bit = x % 32;
bm->data[idx] |= (1U << bit);
}
// 销毁位图
void bitmap_destroy(BitMap *bm) {
if (bm) {
free(bm->data);
free(bm);
}
}
// 位图排序(去重版)
void bitmap_sort(int arr[], int n, int max_num) {
BitMap *bm = bitmap_create(max_num);
if (!bm) return;
// 1. 标记所有数字
for (int i = 0; i < n; i++) {
bitmap_set(bm, arr[i]);
}
// 2. 按顺序输出
//idx:找到数字 i 存在哪个 int 里
//bit:找到在这个 int 的第几位
printf("排序结果:");
for (int i = 0; i <= max_num; i++) {
int idx = i / 32;
int bit = i % 32;
if ((bm->data[idx] & (1U << bit)) != 0)/*判断这个是否存在*/ {
printf("%d ", i);
}
}
printf("\n");
bitmap_destroy(bm);
}
int main() {
int arr[] = {5, 3, 7, 1, 3, 9, 0, 10};
int n = sizeof(arr) / sizeof(arr[0]);
int max_num = 10; // 已知最大值
bitmap_sort(arr, n, max_num);
return 0;
}
十、深度刨析 C 语言指针
- C 语言指针就是有类型的地址!!
- (uint8_t *)(0X12345678)uint8_t * 指向 1 字节0X12345678 32 位地址*(uint8_t *)(0X12345678) = 0X55
- 字符串、数组、结构体首地址是常量不能 ++、--
- 指针是变量可以 ++、--
共用体指针
cpp
typedef union
{
float a;
uint8_t bytes[4];
} data_uni;
data_uni x = {.a = 1.0};
data_uni *p = &x;
p->bytes[3] = 0X80;
printf("%f", p->a);
求 float 相反数结果 - 1
C 语言中动态内存分配(malloc)的使用原则与注意事项
- 优先静态分配:能用编译期就确定大小的静态数组(如 int array [100])就别用动态分配。
- 必须配对释放:用了 malloc 申请的内存,一定要用 free 释放,避免内存泄漏。
- 检查返回值:调用 malloc 后要判断是否返回 NULL,防止空指针访问崩溃。
- 避免频繁申请释放:频繁 malloc/free 会产生内存碎片,降低性能。
- 合理配置堆大小:根据实际需求设置堆(heap)的大小,避免资源不足或浪费。
STM32 单片机 IAP(在应用编程)中,bootloader 跳转到用户应用程序的标准流程代码,实现从 Bootloader 跳转到 APP 执行
cpp
// 定义一个无返回值、无参数的函数指针类型iapfun
typedef void (*iapfun)(void);
iapfun jump2app; // 声明一个该类型的函数指针变量
// 定义volatile修饰的uint32_t类型,用于防止编译器优化,确保每次都从内存读取
typedef volatile uint32_t vu32;
void MSR_MSP(uint32_t addr)
{
// set Main Stack value
__ASM volatile("MSR MSP, r0");
__ASM volatile("BX r14");
}
void iap_load_app(uint32_t appxaddr)
{
disable_irq(); // 关闭所有中断,防止跳转时中断干扰
// 1. 获取APP的复位入口地址(程序开始地址)
// 用户代码区第二个字(appxaddr+4)存放的是复位向量地址
jump2app = (iapfun)*(vu32*)(appxaddr + 4);
// 2. 初始化APP的栈顶指针
// 用户代码区第一个字(appxaddr)存放的是栈顶地址
MSR_MSP(*(vu32*)appxaddr);
// 3. 跳转到APP的复位入口,执行用户程序
jump2app();
}
MSR MSP, r0:把传入的 addr(r0 寄存器)的值写入主栈指针寄存器 MSP。
BX r14:函数返回,跳回调用处。
- disable_irq ():关闭全局中断,避免跳转过程中被中断打断,导致程序跑飞。
- 读取 appxaddr+4:APP 程序的向量表中,偏移 4 字节的位置是复位中断的入口地址。
- 读取 appxaddr:APP 向量表起始位置存放的是 APP 的栈顶地址,必须先设置 MSP。
- jump2app ():通过函数指针,跳转到 APP 的复位入口,开始执行用户程序。
柔性数组指针
cpp
typedef struct
{
uint8_t header;
uint16_t data_type;
uint8_t pbuf[]; // 柔性数组(长度可变)
} data_frame;
// 一次malloc,同时分配结构体和后面的数据缓冲区
data_frame *p = (data_frame *)malloc(sizeof(data_frame) + datalen);
// 从串口1接收数据,直接存入柔性数组对应的内存区域
uart1_receive_data(p->pbuf, datalen);
数组与字符串
cpp
int realarray[10];
int *array = &realarray[-1];
//取 realarray 地址往前偏移 1 个 int 后的地址
分割长字符串
cpp
unsigned char substr(unsigned char *pos, char *str)
{
unsigned char len = strlen(str);
unsigned char n = 0, i = 0;
// 遍历字符串,遇到空格时做标记
for (; i < len; i++)
{
if (str[i] == ' ')
{
str[i] = 0; // 把空格替换成'\0',让前面的子串成为独立字符串
pos[n++] = (i + 1); // 记录下一个子串的起始偏移量
}
}
return n; // 返回子串的总数
}
// 使用示例
unsigned char pos[10]; // 最多存10个子串的偏移量
char str[30];
strcpy(str, "abc 1000 50 off 2500");
unsigned char count = substr(pos, str);
// 访问拆分后的子串
str + pos[0]; // "abc"
str + pos[1]; // "1000"
str + pos[2]; // "50"
str + pos[3]; // "off"
str + pos[4]; // "2500"
同类写法
cpp
// 字符数组定义(注意:这里数组长度10明显不足以放下所有元素,存在笔误)
char str1[10] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
// 字符串定义(自动在末尾加上'\0'结束符)
char *str2 = "0123456789ABCDEF";
C 语言里,字符串 + 十六进制转义符 \x,可以表示任意 8 位数据序列,不局限于 ASCII 字符
cpp
// 1. 定义一个uint8_t数组,存任意字节数据
uint8_t buf[10] = {0x34, 0x78, 0x90, 0x12, 0x37, 0x27, 0x29, 0x10, 0x45, 0x38};
// 2. 用字符串+转义符表示完全相同的字节序列
"\x34\x78\x90\x12\x37\x27\x29\x10\x45\x38"
// 3. 直接把字符串当数组用
return "\x34\x89"[value]; // 当value=1时,返回0x89
printf("%f\n", ((float *)"\x1B\x0D\xA9\x41")[0]);
\x1B\x0D\xA9\x41 4 字节float * 指向 4 字节,
\x1B\x0D\xA9\x41 的浮点结果就是答案
数组区间初始化
cpp
#define MAX (10)
int buf[MAX] = {
0XAA, 0X55, // 前两个元素
[MAX-3] = 0X12, 0X34, 0X56 // 从索引 MAX-3 开始初始化
};
十一、函数
断言
cpp
#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif
- 执行 (void) 0,什么都不做
- 调用 assert_failed 函数,传入当前文件名 FILE 和行号 LINE,方便定位错误
cpp
void assert_failed(uint8_t* file, uint32_t line)
{
// 死循环,触发断点
while (1)
{
// 可在此处添加错误日志、串口打印等调试信息
}
}
STM32 外设参数合法校验
cpp
#define IS_GPIO_ALL_PERIPH(PERIPH) ( \
(((*(uint32_t*)&(PERIPH)) == GPIOA_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOB_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOC_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOD_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOE_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOF_BASE) || \
((*(uint32_t*)&(PERIPH)) == GPIOG_BASE)) \
)
// 典型用法
void GPIO_SetMode(GPIO_TypeDef *GPIOx, uint16_t Pin, uint8_t Mode)
{
// 用宏检查GPIOx是否合法
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
// ... 后续操作
}
printf 可以同时向多个串口(UART1/2/3)输出
cpp
// 全局变量:记录当前要输出的串口
unsigned char us=0;
// 重定向printf的底层字符输出函数
int fputc(int ch, FILE *f)
{
switch(us)
{
case 0: // UART1:调试串口
while((USART1->SR & 0x40) == 0); // 等待发送寄存器为空
USART1->DR = ch;
break;
case 1: // UART2:ESP8266通信
while((USART2->SR & 0x40) == 0);
USART2->DR = ch;
break;
case 2: // UART3:SIM800通信
while((USART3->SR & 0x40) == 0);
USART3->DR = ch;
break;
}
return ch;
}
// 宏定义:切换当前printf输出的目标串口
#define U_TO_DEBUG us=0;
#define U_TO_ESP8266 us=1;
#define U_TO_SIM800 us=2;
// 使用示例
int main(void)
{
// 向调试串口输出
U_TO_DEBUG
printf("hello world!");
// 向ESP8266发送AT指令
U_TO_ESP8266
printf("AT\r\n");
// 向SIM800发送AT指令
U_TO_SIM800
printf("AT\r\n");
}
可变参数宏实现
cpp
typedef char * va_list;
// 数据类型的长度计算,四字节对齐
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
// 把ap指针移动到v的最后面
#define va_start(ap, v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// 返回ap内的数值,然后将ap指针后移(图中未写全)
#define va_arg(ap, t) ( *(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 防止野指针的存在
#define va_end(ap) ( ap = (va_list)0 )
宏定义封装日志打印
cpp
#define LOG_ENABLE (1)
#define LOG(ARGS) { if(LOG_ENABLE) printf(ARGS); else 0; }
#define LOG_ENABLE (1)
#define LOG(fmt, ...) do { \
if(LOG_ENABLE) { \
printf("[LOG] %s:%d: " fmt "\r\n", __FILE__, __LINE__, ##__VA_ARGS__); \
} \
} while(0)
// 使用方式:和printf完全一样
LOG("初始化完成,值=%d", 100);
多级日志
cpp
#define LOG_LEVEL_NONE 0
#define LOG_LEVEL_ERROR 1
#define LOG_LEVEL_INFO 2
#define LOG_LEVEL_DEBUG 3
#define LOG_LEVEL LOG_LEVEL_DEBUG
#define LOG_ERROR(fmt, ...) do { if(LOG_LEVEL >= LOG_LEVEL_ERROR) printf("[ERROR] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_INFO(fmt, ...) do { if(LOG_LEVEL >= LOG_LEVEL_INFO) printf("[INFO] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_DEBUG(fmt, ...) do { if(LOG_LEVEL >= LOG_LEVEL_DEBUG) printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); } while(0)
内联函数
inline 是 C 语言的关键字,它的作用是建议编译器将函数的代码直接插入到调用位置用 inline 内联函数优化选择排序
cpp
// 内联函数:交换两个float变量的值
inline void swapf(float *p1, float *p2)
{
float tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 选择排序函数:对长度为n的float数组进行升序排序
void selection_sortf(float a[], int n)
{
int i, j, mini;
for (i = 0; i < n - 1; ++i)
{
mini = i; // 记录当前最小值的索引
// 从i后面的元素中找最小值
for (j = i + 1; j < n; j++)
{
if (a[j] < a[mini])
mini = j;
}
// 交换最小值元素和当前i位置的元素
swapf(a + i, a + mini);
}
}
// 错误:声明为 extern inline
extern inline void swapf(float *p1, float *p2);
程序编译过程
第一阶段:预处理(Preprocess)
文件后缀 :.c → .i做的事情:
- 删掉注释
- 展开
#include头文件 - 替换
#define宏定义 - 处理
#if/#else条件编译特点 :还是纯文本代码,没有翻译成机器码
第二阶段:编译(Compile)
文件后缀 :.i → .s做的事情:语法检查、语义分析,把 C 代码翻译成汇编语言 特点:变成汇编指令,还不是机器能直接跑的
第三阶段:汇编(Assemble)
文件后缀 :.s → .o(目标文件)做的事情:把汇编代码翻译成二进制机器码 特点 :已经是机器码,但不能独立运行,缺少函数、库的地址
第四阶段:链接(Link)
文件后缀 :.o → .exe / elf(可执行文件)做的事情:
- 合并多个目标文件
- 链接系统库(比如 printf、scanf)
- 分配内存地址、解析符号特点:完成后,就是可以直接运行的程序了
连接器的作用
编译器生成 .o 文件,链接器把它们拼成最终的 .bin 文件,并决定每个函数、变量在内存里的位置。ELF 是 Linux/Unix 系统下,可执行文件、目标文件、共享库的标准格式。你平时编译出来的 .o 目标文件、可执行程序、.so 动态库,本质上都是 ELF 格式。它包含了程序运行所需的全部信息:
- 机器指令代码(.text 段)
- 初始化数据(.data 段)
- 未初始化数据(.bss 段)
- 符号表、重定位信息、调试信息等
十二、在 Linux 下,程序从编译到运行的完整流程都离不开 ELF:
- 编译阶段:编译器生成 .o 目标文件,这是 ELF 格式。
- 链接阶段:链接器把多个 .o 文件整合成一个 ELF 格式的可执行文件。
- 运行阶段:操作系统内核加载器会解析 ELF 文件头,把代码段、数据段映射到内存,然后跳转到入口点执行。
ELF 就是程序在磁盘上的 "蓝图",它告诉操作系统怎么把程序加载到内存并运行。RT-Thread 内核通过 dlopen/dlsym 加载 ELF 格式的 .so 模块,再通过 symtable 符号表,让模块可以调用内核的 API,实现动态扩展。