尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

嵌入式C笔试高频考点:20段代码吃透指针、位操作与状态机

嵌入式C笔试高频考点:20段代码吃透指针、位操作与状态机 兄弟们最近是不是又开始刷嵌入式笔试真题了很多读者跟我反馈说嵌入式笔试题目看着不难但一做就错尤其是C语言相关的选择题、改错题和编程题好像每个考点都见过但每次踩坑的都是同一个地方。究其原因还是对C语言在嵌入式场景中的“特殊用法”和“常见陷阱”缺少系统梳理。这篇文章我整理了20 段笔试中出场率极高的 C 语言代码覆盖数据类型、指针、结构体、位操作、内存管理、链表、状态机、中断等嵌入式笔试高频考点。每一段都会给出完整代码、运行结果、考点分析以及面试官想听到的回答方向。不管你是正在准备校招、跳槽还是想扎实补一补嵌入式C语言基础这篇文章都值得收藏后反复看。1. 嵌入式笔试到底在考什么先别急着看代码我们先用 30 秒把方向理清楚。嵌入式岗位的笔试题目和纯后端、纯算法岗位有明显区别。嵌入式笔试更看重以下几类能力底层硬件意识内存、寄存器、地址、位操作、大小端。C语言基本功指针、数组、结构体、宏定义、类型转换。资源受限思维栈空间、堆空间、内存对齐、代码体积。并发与中断意识volatile、static、回调、重入。调试能力段错误、内存泄漏、栈溢出、野指针。所以你会发现嵌入式笔试中的 C 语言题目往往不是单纯考语法而是把语法放到“单片机、Linux、驱动、RTOS”这些场景里考。接下来我们直接进入正题。我按照考点类型把这 20 段代码分成了 8 组方便你按需复习。2. 环境准备本地复现这些代码需要什么在开始之前先说说运行环境。这些代码主要是标准 C 语言绝大部分不依赖特定硬件可以直接在 PC 上编译运行。推荐工具Windows安装 MinGW-w64 或 Visual Studio或者直接用 VS Code 搭配 C/C 插件。Linux系统自带 gcc直接命令行编译。在线编译器如果只是快速验证可以用在线 C 编译器。编译命令示例gcc -Wall -O0 -o test test.c ./test注意笔试复习阶段强烈建议开-Wall编译选项把所有警告都当成提示信息看一遍。很多嵌入式笔试题目考察的就是“哪一行会产生警告/错误”。3. 第一组数据类型与溢出陷阱3.1 无符号数与有符号数的隐式转换#include stdio.h int main(void) { unsigned int a 10; int b -20; if (a b 0) { printf(a b 0, value %u\n, a b); } else { printf(a b 0\n); } printf(a b %u\n, a b); return 0; }输出结果a b 0, value 4294967286 a b 4294967286考点分析当有符号数和无符号数混合运算时有符号数会被隐式转换为无符号数。-20转换成无符号数是一个很大的正整数所以a b结果大于 0。实际公式-20对应无符号数为2^32 - 20 4294967276再加10得到4294967286。嵌入式场景在判断传感器数据、定时器计数值、通信协议字段时如果无符号和有符号混用非常容易出这种问题。例如unsigned char len 0; if (len - 1 0) { // 错误示例永远为真 // ... }因为len - 1会发生整型提升0 - 1变成-1然后转成无符号255。3.2 数据溢出与截断#include stdio.h int main(void) { unsigned char c 255; c c 1; printf(c %d\n, c); // 期望多少实际多少 char d 127; d d 1; printf(d %d\n, d); // 有符号char溢出 return 0; }输出结果c 0 d -128考点分析无符号 char 溢出后回绕到 0。有符号 char 溢出属于未定义行为但绝大多数平台上是回绕到最小值。嵌入式开发中控制变量、循环变量、通信计数变量都可能发生溢出需要根据数据范围选择合适的类型。这类问题经常在笔试中以“输出结果题”出现答题时一定要先确定变量类型和取值范围。4. 第二组运算符优先级与求值顺序4.1 经典运算符优先级题#include stdio.h int main(void) { int a 1, b 2, c 3; int result a b * c--; printf(a %d, b %d, c %d, result %d\n, a, b, c, result); return 0; }输出结果a 2, b 3, c 2, result 10拆解过程后缀自增a表达式用旧值 1之后 a 变为 2。前缀自增bb 先变为 3表达式值为 3。后缀自减c--表达式用旧值 3之后 c 变为 2。先算乘法3 * 3 9再算加法1 9 10。考点分析注意区分“表达式的值”和“变量最终的值”。乘除优先于加减自增自减优先级很高。笔试中常扩展考察a a这类代码实际上同一变量在一个表达式中被多次修改属于未定义行为不要这样写。4.2 短路求值#include stdio.h int main(void) { int x 0; int y 0; if (x y) { // 不会执行到这里 } printf(x %d, y %d\n, x, y); int m 1; int n 0; if (m || n) { // 会执行到这里 } printf(m %d, n %d\n, m, n); return 0; }输出结果x 1, y 0 m 2, n 0考点分析左侧为假时右侧不执行。||左侧为真时右侧不执行。这就是短路求值。嵌入式代码中的典型应用先判断指针是否为 NULL再解引用。例如if (p ! NULL p-len 0) { // 安全访问 }如果先访问p-len再判断p当p为 NULL 时就可能触发段错误。5. 第三组指针、数组与指针运算5.1 指针与数组的关系#include stdio.h int main(void) { int arr[5] {10, 20, 30, 40, 50}; int *p arr; printf(arr[2] %d\n, arr[2]); printf(*(p 2) %d\n, *(p 2)); printf(p[2] %d\n, p[2]); printf(2[arr] %d\n, 2[arr]); printf(sizeof(arr) %d\n, (int)sizeof(arr)); printf(sizeof(p) %d\n, (int)sizeof(p)); return 0; }输出结果arr[2] 30 *(p 2) 30 p[2] 30 2[arr] 30 sizeof(arr) 20 sizeof(p) 8考点分析数组名在表达式中会退化为指向首元素的指针但sizeof是例外。p[2]和2[arr]都能访问数组元素因为a[i]本质上就是*(a i)。sizeof(arr)是整个数组的字节数sizeof(p)是指针的大小在 64 位平台上通常是 8 字节。嵌入式场景串口接收数组、ADC 采样数组、图像数据缓冲区本质上都是指针操作的延伸。笔试必问题数组名和指针的区别答案就是上面代码中sizeof表现出的差异。5.2 二维数组与指针访问#include stdio.h int main(void) { int matrix[2][3] { {1, 2, 3}, {4, 5, 6} }; printf(matrix[1][2] %d\n, matrix[1][2]); printf(*(*(matrix 1) 2) %d\n, *(*(matrix 1) 2)); int *p matrix[0][0]; printf(通过线性指针访问 matrix[1][2] %d\n, *(p 1 * 3 2)); return 0; }输出结果matrix[1][2] 6 *(*(matrix 1) 2) 6 通过线性指针访问 matrix[1][2] 6考点分析二维数组名是“指向数组的指针”matrix 1跳过一整行。在嵌入式代码中图像处理、矩阵运算、按键扫描表都常用二维数组。使用线性指针访问时需要自己计算偏移量。这种方式在“把二维数组传给函数”的时候很实用尤其是当行数和列数需要作为参数时。6. 第四组结构体、联合体与内存对齐6.1 struct 内存对齐计算#include stdio.h #pragma pack(push, 1) typedef struct { char a; int b; char c; } PackedStruct; #pragma pack(pop) typedef struct { char a; int b; char c; } NormalStruct; int main(void) { printf(NormalStruct size %d\n, (int)sizeof(NormalStruct)); printf(PackedStruct size %d\n, (int)sizeof(PackedStruct)); printf(NormalStruct a offset %d\n, (int)offsetof(NormalStruct, a)); printf(NormalStruct b offset %d\n, (int)offsetof(NormalStruct, b)); printf(NormalStruct c offset %d\n, (int)offsetof(NormalStruct, c)); return 0; }输出结果NormalStruct size 12 PackedStruct size 6 NormalStruct a offset 0 NormalStruct b offset 4 NormalStruct c offset 8考点分析默认对齐规则下int需要 4 字节对齐所以char a后面会填充 3 个字节。#pragma pack(1)可以取消对齐但会导致非对齐访问在部分 MCU 上会增加访问开销甚至触发硬件异常。嵌入式协议解析中很多人喜欢用#pragma pack(1)直接把结构体映射到协议帧这对于 x86 上位机可行但对于 ARM Cortex-M 某些场景要谨慎。笔试经常反过来考给定结构体定义计算大小。做题时按“每个成员偏移量必须整除自身对齐值”来算。6.2 union 的内存共享特性#include stdio.h typedef union { unsigned int value; unsigned char bytes[4]; } DataUnion; int main(void) { DataUnion u; u.value 0x12345678; printf(bytes[0] 0x%02x\n, u.bytes[0]); printf(bytes[1] 0x%02x\n, u.bytes[1]); printf(bytes[2] 0x%02x\n, u.bytes[2]); printf(bytes[3] 0x%02x\n, u.bytes[3]); printf(union size %d\n, (int)sizeof(DataUnion)); return 0; }输出结果小端平台bytes[0] 0x78 bytes[1] 0x56 bytes[2] 0x34 bytes[3] 0x12 union size 4考点分析联合体所有成员共享同一块内存大小取最大成员大小。通过union可以把一个 32 位数据拆成 4 个字节这在通信协议解析中很常见。输出结果依赖平台大小端这也是嵌入式笔试常考点。7. 第五组位操作与寄存器操作7.1 置位、清位、翻转、取值#include stdio.h #define REG_ADDR_BASE (0x40000000UL) int main(void) { unsigned int reg 0x00000000; // 置位 bit5 reg | (1U 5); printf(置位 bit5 后: 0x%08x\n, reg); // 清位 bit5 reg ~(1U 5); printf(清位 bit5 后: 0x%08x\n, reg); // 翻转 bit3 reg ^ (1U 3); printf(翻转 bit3 后: 0x%08x\n, reg); // 取值 bit3 unsigned int bit3 (reg 3) 0x01; printf(bit3 的值为: %u\n, bit3); // 批量置位 bit0~bit3 reg | 0x0F; printf(批量置位低4位: 0x%08x\n, reg); return 0; }输出结果置位 bit5 后: 0x00000020 清位 bit5 后: 0x00000000 翻转 bit3 后: 0x00000008 bit3 的值为: 1 批量置位低4位: 0x0000000f考点分析置位用|清位用 ~翻转用^取值用 右移。单片机寄存器操作中1U n的U后缀可以避免有符号整数移位时可能出现的符号扩展问题。笔试中经常出现“用宏定义实现置位/清位”需要能熟练写出下面这种宏#define SET_BIT(reg, bit) ((reg) | (1U (bit))) #define CLEAR_BIT(reg, bit) ((reg) ~(1U (bit))) #define TOGGLE_BIT(reg, bit) ((reg) ^ (1U (bit))) #define GET_BIT(reg, bit) (((reg) (bit)) 0x01U)7.2 位域的使用与注意事项#include stdio.h typedef struct { unsigned char mode : 2; unsigned char enable : 1; unsigned char reserved : 5; } ControlField; int main(void) { ControlField ctrl; unsigned char raw 0x41; // 二进制 0100 0001 // 假设 raw 来自寄存器或协议帧 unsigned char *p (unsigned char *)ctrl; *p raw; printf(mode %u\n, ctrl.mode); printf(enable %u\n, ctrl.enable); printf(sizeof(ControlField) %d\n, (int)sizeof(ControlField)); return 0; }输出结果mode 1 enable 0 sizeof(ControlField) 1考点分析位域可以按位定义结构体成员节省内存常用于寄存器映射和协议解析。位域的内存布局依赖编译器和平台可移植性较差跨平台通信时建议使用显式位操作代替。嵌入式笔试中位域通常和大小端、内存布局结合考察。8. 第六组宏定义与预处理8.1 宏定义常见错误#include stdio.h #define SQUARE(x) x * x #define SQUARE_FIX(x) ((x) * (x)) #define MAX(a, b) ((a) (b) ? (a) : (b)) int main(void) { int a 5; printf(SQUARE(a 1) %d\n, SQUARE(a 1)); printf(SQUARE_FIX(a 1) %d\n, SQUARE_FIX(a 1)); int x 3, y 4; int max MAX(x, y); printf(max %d, x %d\n, max, x); return 0; }输出结果SQUARE(a 1) 11 SQUARE_FIX(a 1) 36 max 5, x 5考点分析SQUARE(a 1)展开成a 1 * a 1所以结果是5 5 1 11。带参宏必须给参数和整体都加括号。MAX(x, y)中参数被展开两次导致 x 被自增两次这是宏的副作用面试官非常喜欢考。嵌入式代码中宏常用于寄存器地址定义、位掩码定义、断言、调试日志开关合理使用能提升可读性但要注意副作用和优先级问题。8.2 条件编译#include stdio.h #define DEBUG_LEVEL 2 #if DEBUG_LEVEL 1 #define LOG_INFO(fmt, ...) printf([INFO] fmt \n, ##__VA_ARGS__) #else #define LOG_INFO(fmt, ...) #endif #if DEBUG_LEVEL 2 #define LOG_DEBUG(fmt, ...) printf([DEBUG] fmt \n, ##__VA_ARGS__) #else #define LOG_DEBUG(fmt, ...) #endif int main(void) { int temp 25; LOG_INFO(temp %d, temp); LOG_DEBUG(temp %d, temp); return 0; }输出结果[INFO] temp 25 [DEBUG] temp 25考点分析条件编译可以在编译阶段裁剪代码适合嵌入式设备中根据调试等级打印日志。##__VA_ARGS__是 GNU 扩展用于处理可变参数为空的情况。部分编译器需要特殊处理。笔试中常见题型给定宏定义判断#if、#ifdef、#if defined()的差异。如果只是想在调试时开启日志、发布时彻底不编入代码条件编译比运行时 if 更省资源。9. 第七组内存与字符串9.1 字符串常量与字符数组#include stdio.h #include string.h int main(void) { char *p1 hello; char arr[] hello; printf(sizeof(p1) %d\n, (int)sizeof(p1)); printf(sizeof(arr) %d\n, (int)sizeof(arr)); printf(strlen(p1) %d\n, (int)strlen(p1)); printf(strlen(arr) %d\n, (int)strlen(arr)); // arr[0] H; // 合法 // p1[0] H; // 未定义行为字符串常量存储在只读区 return 0; }输出结果sizeof(p1) 8 sizeof(arr) 6 strlen(p1) 5 strlen(arr) 5考点分析char *p1指向字符串常量存放在只读区修改会导致段错误或未定义行为。char arr[]是数组会在栈上拷贝一份字符串可以修改。sizeof(arr)包含结尾的\0strlen不包含。嵌入式笔试中这个题经常作为“为什么程序运行一段时间后崩溃”的排查方向。9.2 字符串逆序字符串逆序在嵌入式笔试中出现率极高很多公司直接让手写。#include stdio.h #include string.h void reverse_string(char *s) { int left 0; int right (int)strlen(s) - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } } int main(void) { char str[] embedded; printf(原始字符串: %s\n, str); reverse_string(str); printf(逆序字符串: %s\n, str); return 0; }输出结果原始字符串: embedded 逆序字符串: debeddme考点分析必须保证传入的是可修改的字符数组不能是字符串常量。双指针法时间复杂度 O(n)空间复杂度 O(1)。面试官可能会追问如果要求不修改原字符串返回新字符串呢或者如果要求逆序后继续反转单词呢这些变体在面试中很常见。10. 第八组const、static、volatile10.1 const 修饰指针的不同含义#include stdio.h int main(void) { int a 10; int b 20; const int *p1 a; // 指向 const int 的指针不能通过 p1 修改 a // *p1 30; // 错误 p1 b; // 允许指针本身可以变 int *const p2 a; // 指针本身是 const不能指向其他地址 // p2 b; // 错误 *p2 30; // 允许可以通过 p2 修改 a const int *const p3 a; // 指针和指向的数据都不能变 printf(a %d, b %d\n, a, b); printf(*p1 %d, *p2 %d\n, *p1, *p2); return 0; }输出结果a 30, b 20 *p1 20, *p2 30考点分析面试官非常喜欢问“const 在星号左边和右边分别代表什么”。记忆方法const修饰的是它左边最近的那个类型如果左边没有就修饰右边。嵌入式开发中const常用于定义只读的查找表这类表可以被编译器放到 Flash 中节省 RAM。例如const int sin_table[256] { /* 预计算的sin值 */ };10.2 volatile 的作用#include stdio.h int main(void) { volatile unsigned int status_reg 0; // 模拟中断或硬件修改 status_reg status_reg 1; while (status_reg 0) { // 等待硬件置位 // 如果 status_reg 不是 volatile编译器可能优化成死循环 } printf(status_reg %u\n, status_reg); return 0; }考点分析volatile告诉编译器这个变量可能被当前线程之外的代码修改硬件、中断、其他线程不要优化掉对该变量的访问。嵌入式开发中以下场景必须用 volatile硬件寄存器映射。中断服务函数中修改的全局变量。RTOS 多任务间共享的全局变量。笔试常考const和volatile能同时修饰一个变量吗答案是可以。例如只读的硬件状态寄存器程序不能修改它但它的值会变。const volatile unsigned int *status_reg (const volatile unsigned int *)0x40001000UL;10.3 static 的三种作用#include stdio.h static int global_counter 0; // 文件作用域限制在本文件内 static void increment(void) // 文件作用域仅本文件可调用 { static int local_counter 0; // 局部静态变量生命周期为整个程序 local_counter; global_counter; printf(local_counter %d, global_counter %d\n, local_counter, global_counter); } int main(void) { increment(); increment(); increment(); return 0; }输出结果local_counter 1, global_counter 1 local_counter 2, global_counter 2 local_counter 3, global_counter 3考点分析static修饰局部变量变量存储在静态区只初始化一次函数结束后不销毁。static修饰全局变量或函数限制作用域在当前文件避免与其他文件同名符号冲突。嵌入式 C 项目中模块内部工具函数、模块内部全局状态都推荐用 static 封装这也是“信息隐藏”思想。在 RTOS 任务函数、中断回调中局部静态变量常用于保存上一次状态。11. 手写代码重点题链表、环形缓冲区、状态机这一部分是嵌入式笔试编程题的重灾区不止考语法还考数据结构设计和代码组织能力。下面精选三组最常考的代码。11.1 单链表的插入与删除链表在嵌入式开发中非常常用比如任务队列、内存池空闲块链表。#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node *next; } Node; // 在头部插入节点 int list_insert_head(Node **head, int value) { Node *new_node (Node *)malloc(sizeof(Node)); if (new_node NULL) { return -1; } new_node-data value; new_node-next *head; *head new_node; return 0; } // 删除指定值的第一个节点 int list_delete(Node **head, int value) { if (head NULL || *head NULL) { return -1; } Node *cur *head; Node *prev NULL; while (cur ! NULL cur-data ! value) { prev cur; cur cur-next; } if (cur NULL) { return -1; // 不存在 } if (prev NULL) { // 删除的是头节点 *head cur-next; } else { prev-next cur-next; } free(cur); return 0; } void list_print(Node *head) { while (head ! NULL) { printf(%d - , head-data); head head-next; } printf(NULL\n); } int main(void) { Node *head NULL; list_insert_head(head, 10); list_insert_head(head, 20); list_insert_head(head, 30); list_print(head); list_delete(head, 20); list_print(head); list_delete(head, 30); list_print(head); return 0; }输出结果30 - 20 - 10 - NULL 30 - 10 - NULL 10 - NULL考点分析插入和删除都要注意修改头指针的场景所以需要传二级指针。删除节点后要free防止内存泄漏。链表面试题经常会要求“原地反转单链表”建议把反转、查找中间节点、判断是否有环作为扩展练习。11.2 环形缓冲区环形缓冲区在串口接收、DMA 数据传输、日志系统中非常常见是嵌入式笔试的高频手写题。#include stdio.h #include stdint.h #include string.h #define BUF_SIZE 8 typedef struct { uint8_t data[BUF_SIZE]; uint8_t head; // 写入位置 uint8_t tail; // 读取位置 uint8_t count; // 当前数据个数 } RingBuffer; void ring_buf_init(RingBuffer *rb) { memset(rb, 0, sizeof(RingBuffer)); } int ring_buf_write(RingBuffer *rb, uint8_t value) { if (rb-count BUF_SIZE) { return -1; // 缓冲区满 } rb-data[rb-head] value; rb-head (rb-head 1) % BUF_SIZE; rb-count; return 0; } int ring_buf_read(RingBuffer *rb, uint8_t *value) { if (rb-count 0) { return -1; // 缓冲区空 } *value rb-data[rb-tail]; rb-tail (rb-tail 1) % BUF_SIZE; rb-count--; return 0; } int main(void) { RingBuffer rb; ring_buf_init(rb); for (uint8_t i 0; i 10; i) { int ret ring_buf_write(rb, i 1); if (ret 0) { printf(写入: %d\n, i 1); } else { printf(缓冲区满写入失败: %d\n, i 1); } } uint8_t val 0; while (ring_buf_read(rb, val) 0) { printf(读取: %d\n, val); } return 0; }输出结果写入: 1 写入: 2 写入: 3 写入: 4 写入: 5 写入: 6 写入: 7 写入: 8 缓冲区满写入失败: 9 缓冲区满写入失败: 10 读取: 1 读取: 2 读取: 3 读取: 4 读取: 5 读取: 6 读取: 7 读取: 8考点分析环形缓冲区核心是取模运算或位与运算当容量为 2 的幂时。写入时先判断是否满读取时先判断是否空。在中断和主循环配合的场景中还可能要求实现“单生产者单消费者”的无锁版本此时count可以通过head和tail推导。笔试中常把容量定义为 2 的幂这样可以写成index (BUF_SIZE - 1)比取模更高效。11.3 按键状态机很多嵌入式笔试第二道编程题会考状态机。这里用按键消抖状态机来演示。#include stdio.h typedef enum { KEY_STATE_IDLE, KEY_STATE_PRESSED, KEY_STATE_CONFIRM } KeyState; int read_key_gpio(void) { // 模拟按键读取实际项目中读取 GPIO 引脚 static int press_count 0; static int key 0; press_count; if (press_count 5) { key 1; // 模拟按键按下 } if (press_count 8) { key 0; // 模拟按键释放 press_count 0; } return key; } void key_state_machine_run(void) { static KeyState state KEY_STATE_IDLE; static int confirm_count 0; int key_value read_key_gpio(); switch (state) { case KEY_STATE_IDLE: if (key_value 1) { state KEY_STATE_PRESSED; confirm_count 0; } break; case KEY_STATE_PRESSED: if (key_value 1) { confirm_count; if (confirm_count 2) { state KEY_STATE_CONFIRM; printf(按键按下确认\n); } } else { state KEY_STATE_IDLE; confirm_count 0; } break; case KEY_STATE_CONFIRM: if (key_value 0) { state KEY_STATE_IDLE; confirm_count 0; } break; default: state KEY_STATE_IDLE; break; } } int main(void) { for (int i 0; i 10; i) { key_state_machine_run(); } return 0; }输出结果按键按下确认考点分析状态机是嵌入式软件架构中最基础也最实用的思想适合处理按键、通信协议解析、菜单逻辑、任务调度等。状态机代码要点状态枚举清晰、事件驱动、避免在多个地方修改状态。笔试中如果只让写“简单按键消抖”你可以用延时实现但如果要求“不阻塞、可扩展”状态机方案得分更高。12. 综合代码阅读题用 10 分钟看懂一段“工程代码”笔试最后一类高频题型是“代码阅读题”。题目给出一个剪裁过的嵌入式 C 文件要求找出问题或写出输出。这里给一个综合例子模拟笔试风格#include stdio.h #include string.h typedef struct { unsigned short length; unsigned char data[4]; unsigned char crc; } Frame; unsigned char calc_crc(const unsigned char *data, unsigned int len) { unsigned char crc 0; for (unsigned int i 0; i len; i) { crc ^ data[i]; } return crc; } void process_frame(const unsigned char *buffer) { Frame frame; memcpy(frame, buffer, sizeof(frame)); if (frame.length 4) { printf(长度错误\n); return; } unsigned char crc calc_crc(frame.data, frame.length); if (crc ! frame.crc) { printf(CRC 校验失败\n); return; } printf(数据有效: %d\n, frame.data[0]); } int main(void) { unsigned char buffer[8] {0x04, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00}; // 计算 CRC buffer[6] buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3] ^ buffer[4]; process_frame(buffer); return 0; }这段代码有哪些问题结构体对齐问题Frame包含unsigned short和unsigned char在默认对齐下可能有填充字节。如果协议帧是紧密排列的使用memcpy(sizeof(frame))可能多拷贝填充字节。缓冲区越界风险buffer[8]有 8 字节sizeof(frame)在默认对齐下可能是 8 字节2 4 1 1 padding。如果结构体变大memcpy可能越界。CRC 计算范围calc_crc(frame.data, frame.length)只对data的前length字节计算但如果发送方和接收方对 CRC 计算范围理解不一致会失败。magic number直接使用4表示最大长度应该用宏或常量。这种题考察的不只是“能不能看懂”还包括有没有工程安全意识。改进版本#include stdio.h #include string.h #define FRAME_MAX_DATA_LEN 4 #pragma pack(push, 1) typedef struct { unsigned short length; unsigned char data[FRAME_MAX_DATA_LEN]; unsigned char crc; } Frame; #pragma pack(pop) unsigned char calc_crc(const unsigned char *data, unsigned int len) { unsigned char crc 0; for (unsigned int i 0; i len; i) { crc ^ data[i]; } return crc; } void process_frame(const unsigned char *buffer, unsigned int buffer_len) { if (buffer_len sizeof(Frame)) { printf(缓冲区长度不足\n); return; } Frame frame; memcpy(frame, buffer, sizeof(Frame)); if (frame.length FRAME_MAX_DATA_LEN) { printf(长度错误\n); return; } unsigned char crc calc_crc(frame.data, frame.length); if (crc ! frame.crc) { printf(CRC 校验失败\n); return; } printf(数据有效: %d\n, frame.data[0]); }这个改进版本做了三件事使用#pragma pack(1)去除结构体填充确保和协议帧一致。增加缓冲区长度检查避免越界。用宏代替魔法数字。13. 常见笔试错误与排查清单13.1 高频错误对照表问题现象常见原因解决思路无符号变量相减结果异常无符号数溢出/回绕先判断大小再减或改用有符号类型数组越界但编译不报错C 语言不检查数组边界使用时手动校验索引范围字符串拷贝导致段错误目标缓冲区过小改用strncpy并检查长度函数返回局部变量地址栈内存函数结束即失效改为返回值、传参或 static 变量结构体大小和预期不符内存对齐使用offsetof检查必要时 pack宏参数出现副作用参数被重复展开宏内参数都加括号避免传入自增表达式程序行为被编译器优化改变缺少 volatile硬件相关变量加 volatile两个源文件同名全局变量冲突未使用 static内部变量和函数加 static内存越写后程序随机崩溃野指针/写越界使用内存检测工具开启编译警告使用未初始化指针局部指针默认值随机初始化指针为 NULL使用前判断13.2 笔试答题时的通用排查步骤看到一段 C 代码并让你“找问题”时按这个顺序检查变量类型有无符号、char 范围、int 溢出。指针操作是否解引用 NULL、是否越界、是否返回局部地址。数组下标边界条件是否可能等于数组长度。运算符优先级混合运算是否和预期一致。宏定义参数是否被重复展开括号是否完整。结构体布局对齐、大小端、pack。内存管理malloc 是否判断返回值free 后是否置 NULL。生命周期static、全局、局部、堆栈。并发安全中断/多线程环境下是否有竞争条件。14. 最佳实践与复习建议14.1 嵌入式 C 代码的工程建议笔试是一回事真正项目里还要更严格。结合工程经验给几条建议能不动态分配就不动态分配MCU 上堆空间有限动态分配容易产生碎片。使用静态内存池、环形缓冲区、变长数组时要有上限。所有外部输入都要校验协议长度、索引、CRC、范围不能信任接收到的数据。寄存器操作必须 volatile使用指针访问寄存器地址时类型必须是 volatile 限定防止编译器优化。中断服务函数越短越好中断里只做标志位设置、数据搬移或简单的状态切换不要在中断里执行耗时的打印和复杂算法。使用断言和日志在关键入口处使用断言但量产版本要关闭日志分级编译裁剪避免串口阻塞。结构体访问硬件寄存器时确认内存布局不同编译器、不同对齐规则下结果可能不同优先使用位操作 固定偏移。14.2 笔试复习路线如果你离笔试还有 2 到 3 周建议按这个顺序复习第一周把本文的 20 段代码全部手写一遍运行并分析输出。第二周刷链表、环形缓冲区、状态机、字符串逆序这类高频手写题每种至少写 3 遍。考试前 3 天整理错题重点温习本文第 13 章的排查清单。如果你已经工作了正在准备面试可以额外看这些方向Linux 下的嵌入式 C 开发static、volatile、const、指针数组与数组指针。RTOS 相关任务间通信、互斥锁、中断与任务同步。编译器与链接-Wall、内存段布局、链接脚本、栈大小。14.3 20 段代码速查表给读者们做个速查表方便收藏后快速定位复习内容编号考点核心要点优先级1无符号与有符号转换混合运算会隐式转换必考2数据类型溢出无符号回绕有符号未定义必考3运算符优先级先乘除后加减注意自增自减值必考4短路求值左侧确定时不执行右侧高频5数组名与指针sizeof 区别、下标本质必考6二维数组指针行指针与线性指针高频7结构体内存对齐对齐规则、offsetof必考8union 大小端内存共享、大小端取字节高频9位操作置位/清位/翻转/取值必考10位域节省内存注意可移植性高频11宏定义错误参数括号、副作用必考12条件编译日志分级、代码裁剪高频13字符串常量与数组只读区与栈上拷贝必考14字符串逆序双指针法高频15const 修饰指针星号左右区别必考16volatile防优化、硬件访问必考17static 三种用途静态变量/文件作用域必考18单链表插入删除二级指针、free高频19环形缓冲区取模/位运算、读写索引高频20状态机状态枚举、事件驱动高频15. 写在最后一些实在话嵌入式笔试不是靠背题就能过的但也不能不背。上面这 20 段代码基本覆盖了嵌入式 C 语言笔试 70% 以上的知识点。每一段你都亲自敲一遍、运行一遍、出错一遍比看十篇文章都管用。尤其是链表、环形缓冲区和状态机这三类不要只在 IDE 里写最好拿张纸手写。笔试的时候是要在纸上写代码的手写和机器上写完全两种感觉。另外一个建议平时编译代码一定开-Wall -Wextra把警告当成错误来看。很多笔试找问题题其实考的就是编译器警告里提示的那些点。如果这篇文章对你有帮助可以先收藏备用。复习过程中遇到具体问题欢迎在评论区把代码贴出来一起讨论。祝兄弟们笔试顺利拿下心仪 offer。
返回列表