公司动态

嵌入式笔试C语言高频代码解析:从宏定义到状态机

📅 2026/8/31 22:42:23
嵌入式笔试C语言高频代码解析:从宏定义到状态机
嵌入式笔试的 C 语言题翻来覆去就是那几类宏定义、关键字语义、指针、内存对齐、位操作、链表、状态机、通信协议。很多同学不是不会写代码而是没把“面试官真正要考的点”吃透。明明一段代码看着眼熟一追问就卡住分数自然就丢在基础题上。这篇文章直接把 20 段高频 C 代码整理成一条学习主线每段都给出了可运行的代码、考点拆解和面试官追问方向。重点覆盖嵌入式 Linux、单片机、驱动开发岗位笔试中最常见的考察面适合正在找嵌入式软件工程师、嵌入式开发岗位的应届生和转行者。你不需要背八股只需要把下面这些代码在本地敲一遍、跑一遍、改一遍笔试丢分点就能少掉一大部分。1. 核心能力速览能力项说明内容定位嵌入式 C 笔试高频考点代码解析代码数量20 段每段都可单独编译验证覆盖范围宏、const/static/volatile、指针、数组、结构体对齐、位操作、大小端、链表、环形缓冲区、状态机、CRC、硬件寄存器映射运行环境标准 C 环境Linux 用 gccWindows 用 MinGW 或 Keil MDK硬件要求无纯软件环境即可测试编译方式gcc -Wall -stdc99 命令行编译适合岗位嵌入式软件工程师、单片机开发、嵌入式 Linux 驱动、物联网开发学习方式自己敲代码 跑代码 回答追问从知识结构看这 20 段代码基本覆盖了嵌入式笔试 70% 以上的 C 语言考察点。接下来先从底层环境开始再逐段拆解。2. 嵌入式 C 笔试考点地图嵌入式笔试的 C 语言题很少考“这个算法怎么写”更多考“这段代码能不能跑、为什么能跑、为什么不能跑”。真正拉开差距的考点集中在三类第一类是语言基础。宏展开的副作用、const 修饰对象、static 生命周期、volatile 编译优化、typedef 和 #define 的区别。这些问题单独拿出来都不难但组合在选择题和程序分析题里错误率非常高。第二类是内存模型。数组和指针的退化、sizeof的语义、结构体对齐、动态内存分配、返回局部变量的陷阱。嵌入式开发直接面对栈、堆、只读区、数据段面试官默认你清楚这些区域的划分。第三类是硬件交互。位操作、大小端、位域寄存器、环形缓冲区、状态机、CRC 校验、外设寄存器地址映射。这类题在嵌入式笔试中的比例明显高于互联网后端笔试。下面按这三大类把 20 段代码拆开讲。3. 本地编译环境准备在开始看代码前先把编译环境准备好。这 20 段代码全部是标准 C理论上任何 C 编译器都能编译。3.1 Linux 或 WSL 环境直接在终端里写代码测试# 安装 gcc通常系统自带 sudo apt install gcc # 编译单文件程序 gcc -Wall -stdc99 -o demo demo.c # 运行 ./demo-Wall会打开所有常见警告笔试刷题时建议一直开着。很多代码陷阱在编译阶段就会爆出警告比如类型不匹配、变量未使用、指针加减越界风险。3.2 Windows 环境Windows 下最简单的方案是装 MinGW-w64或者在 VSCode 里配置 C/C 编译环境。如果已经在用 Keil MDK 做单片机开发也可以把代码丢进 Keil 的仿真工程里跑Keil 对嵌入式 C 语法检查更严格反而更容易发现笔试代码里的坑。VSCode 里写 C 代码时装好 C/C 插件后把编译器路径指到 gcc就可以直接在终端里编译gcc -Wall -stdc99 -o demo.exe demo.c .\demo.exe3.3 建议的刷题姿势不要直接复制下面的大段代码。先把考点自己写一遍写不出来再对照代码。跑通之后再尝试修改参数、改变边界条件比如把字符串逆序函数改成逆序打印、把链表反转改成递归实现。这个步骤做扎实笔试手写代码基本不会慌。4. 第一类代码关键字与宏定义高频题这段是嵌入式笔试选择题的重灾区。宏定义展开、const 修饰、static 生命周期、volatile 编译优化每一类都能单独出道题。4.1 01 宏定义 MAX/MIN副作用是面试官第一刀#define MAX(a, b) ((a) (b) ? (a) : (b))考点在于宏展开是文本替换不是函数调用。MAX(i, j)会被展开成((i) (j) ? (i) : (j))i 和 j 会被自增两次结果完全不符合预期。这就是宏的副作用问题。嵌入式笔试经常把这种代码放进选择题问执行后 i、j 的值是多少。追问方向为什么宏参数外面要加括号GCC 下有没有更安全的写法可以用__typeof__或内联函数。宏和内联函数的区别是什么推荐回答思路先说清楚括号是防止优先级陷阱再指出参数副作用是宏的天然问题最后说明在嵌入式工程里更常用内联函数或带类型检查的写法来替代复杂宏。4.2 02 位操作宏寄存器操作的看家本领#define SET_BIT(x, n) ((x) | (1U (n))) #define CLR_BIT(x, n) ((x) ~(1U (n))) #define TOGGLE_BIT(x, n) ((x) ^ (1U (n))) #define GET_BIT(x, n) (((x) (n)) 1U)位操作是嵌入式笔试必考没有例外。这四个宏分别对应置位、清零、翻转、读取。笔试题目通常会换一层皮来考比如“请写出将寄存器第 7 位清零、同时保留其他位的语句”。需要特别注意两点1U是无符号整数避免有符号右移的未定义行为n 的取值不能超过类型位宽否则是未定义行为。追问方向如何一次置位多个 bit如何将某个 bit 取反后写回GET_BIT 返回的值是什么类型推荐回答x | (1U 3) | (1U 7); x ~((1U 2) | (1U 5));4.3 03 const 修饰到底谁不能变const int a 10; int b 20; const int *p b; // p 指向 const int不能通过 p 修改 b int * const q b; // q 是 const 指针不能修改 q 本身 const int * const r b; // 两者都不能改笔试最常见的考法是给出几段声明问你哪一行能通过编译。记忆口诀是“const 修饰谁谁就不能变”。const int *pconst 修饰*p所以通过 p 不能改值。int * const qconst 修饰 q所以指针变量本身不能改。const int * const r两者都固定。追问方向函数参数用const char *s有什么好处嵌入式里 const 变量放在什么内存区域参考答案const 修饰的函数参数可以防止函数内部误修改外部数据嵌入式里 const 变量通常放在只读数据区或 Flash节省 RAM 空间。4.4 04 static 三大作用局部变量、文件作用域、生命周期#include stdio.h int counter(void) { static int count 0; return count; } int main(void) { printf(%d , counter()); printf(%d , counter()); return 0; }输出结果是1 2。count 虽然定义在函数内部但用 static 修饰后生命周期延长到整个程序运行结束并且只初始化一次。普通局部变量每次调用都会在栈上重新创建。static 还有另外两个作用修饰全局变量时限制变量只能在当前文件内访问修饰函数时限制函数只在当前文件内链接。这在多文件嵌入式工程中非常重要。追问方向static 局部变量存储在哪个内存区域static 全局变量和普通全局变量的区别是什么中断函数里能不能调用带 static 局部变量的函数回答要点static 局部变量存储在数据段或 BSS 段不在栈上static 全局变量的链接属性是内部链接普通全局变量是外部链接。4.5 05 volatile嵌入式最容易丢分的编译优化坑volatile unsigned int *reg (volatile unsigned int *)0x40000000; *reg 0x01;volatile 告诉编译器这个变量的值可能在当前代码上下文之外被改变不要优化到 CPU 寄存器里每次使用都必须从内存重新读取。嵌入式里三类场景必须加 volatile硬件外设寄存器映射地址。中断服务程序和主循环共享的全局变量。多线程或 RTOS 任务间共享的变量。笔试经典题是int flag 0; while (flag 0) { // 等待中断置位 flag }如果 flag 没有 volatile编译器可能把flag 0优化成死循环因为编译器认为循环体里没有修改 flag直接从寄存器取值永远不会重新读内存。追问方向const 和 volatile 能不能同时修饰一个变量volatile 能解决并发安全吗参考答案const 和 volatile 可以同时使用典型场景是只读硬件状态寄存器volatile 只保证“每次从内存读”不解决多线程原子性所以不能替代互斥锁。4.6 06 typedef 与 #define看似一样实际差很多#define INT_PTR int* INT_PTR a, b; // 等价于 int *a, b; b 是 int不是 int* typedef char *CHAR_PTR; CHAR_PTR c, d; // c 和 d 都是 char*#define 是预处理阶段文本替换typedef 是编译阶段类型声明。笔试选择题非常喜欢用INT_PTR a, b;来考很多人会因为惯性以为 a 和 b 都是指针。typedef 在嵌入式工程里的常见用法是给结构体、函数指针、寄存器映射结构体起别名让代码简洁。但要注意不要滥用 typedef 隐藏原本类型降低代码可读性。追问方向typedef struct 和 struct 直接定义的区别在哪里在 C 语言中typedef void (*callback_t)(int);是什么意思参考答案typedef void (*callback_t)(int);定义了一个函数指针类型指向返回值是 void、参数是 int 的函数。这在事件驱动框架和回调注册里很常用。5. 第二类代码指针、数组与字符串指针是嵌入式 C 笔试的绝对核心几乎每一份笔试卷都会出现。这里挑了三段最高频的代码。5.1 07 数组与指针等价但不完全相同#include stdio.h int main(void) { int arr[5] {1, 2, 3, 4, 5}; 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(sizeof(arr) %zu\n, sizeof(arr)); printf(sizeof(p) %zu\n, sizeof(p)); return 0; }三种访问方式等价输出都是3。但sizeof(arr)是整个数组的大小sizeof(p)是指针变量的大小。在 32 位系统上 p 的大小通常是 4 字节在 64 位系统上是 8 字节。数组名在绝大多数表达式中会退化成首元素地址但放在sizeof操作符里不会退化。这是笔试里最高频的 sizeof 考点之一。追问方向arr 1指向什么位置数组作为函数参数时为什么sizeof(arr)会变成指针大小回答要点arr 1的步长是整个数组指向数组末尾之后数组作为函数参数时编译器会把参数声明调整为指针所以sizeof退化成指针大小。5.2 08 函数指针与回调事件驱动的基础#include stdio.h int add(int a, int b) { return a b; } int calc(int (*op)(int, int), int a, int b) { return op(a, b); } int main(void) { printf(add via func ptr: %d\n, calc(add, 3, 5)); return 0; }函数指针是嵌入式驱动开发中非常常用的语法。定时器回调、按键矩阵扫描、串口命令表、任务调度器底层都依赖函数指针。笔试里手写函数指针声明的正确率高很多人一碰到就卡壳。推荐先把int (*op)(int, int)这个声明读法练熟op是一个指针指向返回值是 int、参数是两个 int 的函数。追问方向函数指针数组怎么声明中断回调函数里能直接调用阻塞函数吗参考答案函数指针数组声明是int (*ops[4])(int, int);可以用于命令表驱动中断回调里不能做耗时或阻塞操作要快速进快速出否则会破坏实时性。5.3 09 字符串逆序笔试手写题第一梯队#include stdio.h #include string.h void str_reverse(char *s) { if (!s) return; char *left s; char *right s strlen(s) - 1; while (left right) { char tmp *left; *left *right; *right tmp; left; right--; } } int main(void) { char buf[] embedded; str_reverse(buf); printf(%s\n, buf); return 0; }输出结果是deddembe。这个函数用双指针从两端向中间遍历原地交换字符。注意传入的必须是可写字符数组不能直接传字符串常量这个坑下面还会讲。笔试手写时最容易犯三个错误忘记判断空指针、strlen(s)对空串返回 0 后 right 变成s - 1、最后没有写\0。其实原地交换不会改变字符串长度所以不需要重新补\0。追问方向不允许修改原字符串时怎么逆序输出递归版本怎么写中文字符串逆序和 ASCII 字符串逆序有什么区别回答要点不允许修改原串时可以新开一块缓冲区倒序拷贝递归写法简单但嵌入式里链表过长时可能导致栈溢出中文字符串在 UTF-8 下是可变长编码按字节逆序会把字符拆碎需要按字符边界处理。6. 第三类代码内存、对齐与结构体内存问题是嵌入式笔试区分度最高的部分。结构体对齐、动态内存、返回局部变量三个题一旦出现错误率普遍偏高。6.1 10 字符串长度sizeof 与 strlen 的区别#include stdio.h #include string.h int main(void) { char arr[] hello; char *p arr; printf(strlen(arr) %zu\n, strlen(arr)); printf(sizeof(arr) %zu\n, sizeof(arr)); printf(sizeof(p) %zu\n, sizeof(p)); return 0; }标准答案strlen(arr)是 5sizeof(arr)是 6包含结尾的\0sizeof(p)在 32 位系统是 4在 64 位系统是 8。笔试选择题经常把sizeof(p)和sizeof(arr)混在一起考。很多人在函数参数里传数组名以为sizeof能算出数组大小结果算出的是指针大小。追问方向手写一个strlen函数。sizeof是在编译期还是运行期计算的手写strlen的参考实现size_t my_strlen(const char *s) { const char *p s; while (*p) { p; } return (size_t)(p - s); }6.2 11 结构体内存对齐笔试送分题还是送命题#include stdio.h typedef struct { char a; int b; char c; } S1; typedef struct { char a; char c; int b; } S2; int main(void) { printf(sizeof(S1) %zu\n, sizeof(S1)); printf(sizeof(S2) %zu\n, sizeof(S2)); return 0; }在 32 位系统上输出通常是sizeof(S1) 12、sizeof(S2) 8。两个结构体成员完全一样只是顺序不同大小就差了 4 字节。对齐规则是每个成员按照自身对齐数对齐结构体的总大小必须是最大对齐数的整数倍。int要对齐到 4 字节所以char a后面会填充 3 个字节char c后面再填充 3 个字节。嵌入式里结构体经常用于协议数据包、寄存器映射把成员顺序排好可以省 RAM还能避免一些平台上的非对齐访问错误。如果需要紧凑布局可以用#pragma pack(1) typedef struct { char a; int b; char c; } PackedS1; #pragma pack()追问方向为什么要内存对齐pack(1) 的代价是什么如何判断结构体实际占用大小回答要点对齐是为了 CPU 访问效率某些 ARM 架构非对齐访问会直接触发硬件异常pack(1) 节省空间但可能降低访问速度且可移植性变差。6.3 12 返回局部变量栈指针的典型陷阱char *get_string(void) { char buf[32]; snprintf(buf, sizeof(buf), hello); return buf; // 错误返回栈地址 }函数返回后栈帧被释放buf 指向的内存内容不确定属于未定义行为。笔试机试里很多人机试跑不出结果就是因为这个。正确写法有三种// 方法一调用方传入输出缓冲区 void get_string(char *out, size_t size) { snprintf(out, size, hello); } // 方法二返回静态缓冲区注意线程不友好 char *get_string(void) { static char buf[32]; snprintf(buf, sizeof(buf), hello); return buf; } // 方法三malloc 动态分配调用方负责 free char *get_string(void) { char *buf malloc(32); if (buf) { snprintf(buf, 32, hello); } return buf; }追问方向返回局部数组名和返回局部结构体有什么区别栈空间一般多大、堆空间一般多大回答要点返回结构体通常按值拷贝不是返回地址所以可以栈空间在单片机工程里可能只有几 KB堆空间受链接脚本和内存池配置限制动态内存使用要格外小心。7. 第四类代码位操作、大小端与寄存器映射嵌入式笔试和硬件交互最紧密的部分就是这一块。不懂位操作和字节序很难写出能跑在真实板子上的驱动代码。7.1 13 大小端判断一个函数解决问题#include stdio.h #include stdint.h int is_little_endian(void) { uint32_t x 1; uint8_t *p (uint8_t *)x; return *p 1; } int main(void) { if (is_little_endian()) { printf(little-endian\n); } else { printf(big-endian\n); } return 0; }小端模式低字节保存在低地址。x86 和大多数 ARM Cortex-M 默认是小端。大端模式高字节保存在低地址网络字节序就是大端。这个函数利用指针类型转换把uint32_t的首字节取出来。如果首字节是 1说明低字节在低地址是小端。笔试还可能用联合体来写typedef union { uint32_t word; uint8_t bytes[4]; } EndianTest; int is_little_endian(void) { EndianTest t; t.word 1; return t.bytes[0] 1; }追问方向大小端对通信协议有什么影响如何把结构体数据写入字节流回答要点设计通信帧时要把数值都转成网络字节序再按字节发送接收端再转回主机字节序避免不同平台解析错位。7.2 14 字节序转换手动实现 htons/htonl#include stdint.h uint16_t swap_bytes16(uint16_t x) { return (uint16_t)((x 8) | (x 8)); } uint32_t swap_bytes32(uint32_t x) { return ((x 0xFF000000U) 24) | ((x 0x00FF0000U) 8) | ((x 0x0000FF00U) 8) | ((x 0x000000FFU) 24); }swap_bytes16(0x1234)结果是0x3412swap_bytes32(0x12345678)结果是0x78563412。嵌入式网络通信里协议栈通常提供htons、htonl、ntohs、ntohl底层原理就是这种字节交换。笔试不要求背库函数但要求你理解怎么通过移位和掩码重新组合字节。追问方向从串口收到 4 字节怎么拼成一个uint32_t为什么不能用强制类型转换直接转回答要点拼接时可以(uint32_t)d[0] 24 | (uint32_t)d[1] 16 | ...按协议字节序拼接直接强转会受本机大小端影响可移植性差。7.3 15 位域定义寄存器看起来方便坑也不少#include stdint.h typedef struct { uint32_t b0 : 1; uint32_t b1 : 1; uint32_t rsvd : 2; uint32_t data : 12; uint32_t crc : 16; } RegBits; volatile RegBits *reg (volatile RegBits *)0x40000000;位域能直接用成员名操作寄存器里的 bit 段可读性很好。但位域的分配方向由编译器决定标准没有规定 bit 是从低到高还是从高到低排列所以跨平台移植时容易出问题。嵌入式 Linux 和单片机驱动里更常见的做法是宏定义偏移量和掩码配合 readl/writel 或直接指针访问显式控制位布局。追问方向位域和移位宏相比哪个可移植性更好ARM Cortex-M 上位域布局默认是按小端分配吗回答要点移位宏的可移植性通常更好因为布局完全由代码控制位域依赖编译器的内存布局规则使用前要查对应编译器手册。7.4 20 硬件寄存器地址映射结构体指针访问外设#include stdint.h #define UART_BASE 0x40004000U typedef struct { volatile uint32_t SR; // 状态寄存器 volatile uint32_t DR; // 数据寄存器 } UART_TypeDef; volatile UART_TypeDef *UART (volatile UART_TypeDef *)UART_BASE; void uart_send_char(char c) { UART-DR (uint32_t)c; }这是嵌入式驱动里非常典型的写法把外设基地址强制转换成结构体指针然后用成员访问寄存器。volatile保证每次读写都直接访问物理地址而不是使用缓存值。笔试遇到这种题重点看你会不会写结构体定义、会不会用volatile、会不会区分外设基地址和偏移量。追问方向结构体成员顺序必须和硬件寄存器偏移一致吗为什么寄存器地址要加 volatile回答要点外设寄存器的地址偏移是硬件决定的结构体成员顺序必须按硬件手册排列加 volatile 是为了防止编译器优化掉对寄存器的读写操作。8. 第五类代码链表、环形缓冲区、状态机与通信协议最后这几段是嵌入式笔试里的大题高频区。链表、环形缓冲区、状态机、CRC每一个都是实际工程里天天在用的东西。8.1 16 链表反转笔试高频手写题typedef struct Node { int data; struct Node *next; } Node; Node *reverse_list(Node *head) { Node *prev NULL; Node *cur head; while (cur) { Node *next cur-next; cur-next prev; prev cur; cur next; } return prev; }核心是三个指针依次往后走。先保存cur-next再把cur-next指向前一个节点最后整体后移。如果漏掉保存 next 这一步链表会断掉。笔试手写链表反转最容易出错的是循环结束后返回的节点。循环结束时cur为 NULLprev指向原来的最后一个节点也就是新链表的头节点所以返回prev。追问方向如何判断链表是否有环递归反转链表有什么坑参考答案快慢指针可以判断环慢指针每次走一步快指针每次走两步如果相遇则有环递归反转在链表很长时容易栈溢出嵌入式里更推荐迭代写法。8.2 17 环形缓冲区串口和通信队列的标配数据结构#include stdint.h #define RBUF_SIZE 64 typedef struct { uint8_t buf[RBUF_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; int rb_write(RingBuffer *rb, uint8_t data) { uint16_t next (rb-head 1) % RBUF_SIZE; if (next rb-tail) { return -1; // 缓冲区满 } rb-buf[rb-head] data; rb-head next; return 0; } int rb_read(RingBuffer *rb, uint8_t *data) { if (rb-head rb-tail) { return -1; // 缓冲区空 } *data rb-buf[rb-tail]; rb-tail (rb-tail 1) % RBUF_SIZE; return 0; }环形缓冲区用两个指针 head 和 tail 管理读写位置。最常见的设计是预留一个空位来区分“满”和“空”否则 head tail 既可能是空也可能是满。嵌入式 UART 接收、DMA 双缓冲、网络协议栈接收队列底层都有环形缓冲区的身影。笔试喜欢考察满判断、空判断和连续读写的位置关系。追问方向生产者和消费者都操作同一个环形缓冲区时怎么保证安全环形缓冲区大小为什么常用 2 的幂回答要点单生产者单消费者场景可以用关中断或在临界区保护读写下标大小用 2 的幂时取模操作可以优化成 (size - 1)效率更高。8.3 18 状态机框架按键、协议解析、菜单系统都用它typedef enum { STATE_IDLE, STATE_RUNNING, STATE_ERROR } State_t; State_t state STATE_IDLE; void state_machine_run(uint8_t start_event, uint8_t error_event, uint8_t reset_event) { switch (state) { case STATE_IDLE: if (start_event) { state STATE_RUNNING; } break; case STATE_RUNNING: if (error_event) { state STATE_ERROR; } break; case STATE_ERROR: if (reset_event) { state STATE_IDLE; } break; default: state STATE_IDLE; break; } }状态机是嵌入式软件架构的核心思想按键消抖、通信协议解析、充电管理、菜单切换都是状态机。笔试经常要求补全状态转移条件或者问你某事件发生后状态变成什么。嵌入式笔试里的状态机题要重点关注“事件是否会被吞掉”和“状态机是否有默认分支”。很多题会在 default 分支上挖坑如果没有 default非法状态会导致程序跑飞。追问方向状态机里能不能用阻塞延时状态机的事件怎么产生回答要点状态机里尽量不用阻塞延时用 tick 轮询或事件标志判断超时事件可以由中断、定时器扫描、通信解析产生状态机代码本身只处理“当前状态 输入事件 - 下一状态”。8.4 19 CRC 校验通信协议里的数据完整性保障#include stdint.h uint8_t crc8(uint8_t *data, uint16_t len) { uint8_t crc 0; while (len--) { crc ^ *data; for (int i 0; i 8; i) { if (crc 0x80) { crc (uint8_t)((crc 1) ^ 0x07); } else { crc (uint8_t)(crc 1); } } } return crc; }这段代码实现的是多项式0x07的 CRC-8 逐位计算。每个字节先异或到 crc再按位处理遇到最高位为 1 就与多项式异或。笔试通常不会让你背出完整的 CRC 计算表但会考你理解什么叫“按位异或 多项式反馈”。如果题目给出查表法代码要能看懂表和渐次计算的关系。追问方向CRC 和累加和有什么区别为什么通信协议里常用 CRC 而不是简单累加和回答要点CRC 的检错能力更强尤其是对突发错误和连续 bit 翻转累加和实现简单但碰撞概率高可靠性不如 CRC。9. 编译运行与自测方法这 20 段代码不要只“看明白”要真去编译运行。这里给一个统一的自测模板把可运行的几段代码拼在一个 main 函数里。#include stdio.h #include string.h #include stdint.h #define MAX(a, b) ((a) (b) ? (a) : (b)) int is_little_endian(void) { uint32_t x 1; uint8_t *p (uint8_t *)x; return *p 1; } uint16_t swap_bytes16(uint16_t x) { return (uint16_t)((x 8) | (x 8)); } void str_reverse(char *s) { if (!s) return; char *left s; char *right s strlen(s) - 1; while (left right) { char tmp *left; *left *right; *right tmp; left; right--; } } int main(void) { char buf[] embedded; str_reverse(buf); printf(reverse: %s\n, buf); printf(little_endian: %d\n, is_little_endian()); printf(swap16(0x1234): 0x%04X\n, swap_bytes16(0x1234)); int x 10, y 20; printf(MAX(10, 20): %d\n, MAX(x, y)); return 0; }编译命令gcc -Wall -stdc99 -o test_demo test_demo.c ./test_demo预期输出reverse: deddembe little_endian: 1 swap16(0x1234): 0x3412 MAX(10, 20): 20如果输出和预期一致说明基础环境没问题。链表、环形缓冲区、状态机、CRC 这几段代码可以单独建一个文件封装成测试函数再在 main 里加assert判断结果。一个推荐的自测思路是把每段代码的“输入 - 预期输出 - 实际输出”写成注释运行后逐项比对。这样做笔试前的查漏补缺效率很高也能在准备“嵌入式八股文”时积累真实运行结论。10. 常见问题与排查方法问题现象可能原因排查方式解决方案宏展开后结果不对宏参数没有加括号或参数带副作用展开宏后再看表达式参数全部加括号改用内联函数字符串逆序程序崩溃传入的是字符串常量写入只读区检查传入参数类型改用可写字符数组如char buf[] hellosizeof(数组) 结果变成 4/8数组传入函数后退化成指针打印 sizeof 检查位置在函数外计算大小或额外传入长度参数结构体大小和预期不一致内存对齐导致填充字节用 sizeof 打印实际大小调整成员顺序必要时使用 #pragma pack(1)链表反转死循环或断链没有保存 cur-next手动画出链表节点推导先保存 next再改 cur-next再移动指针大小端判断结果相反对大小端概念理解反了打印每个字节的地址和值记住“低字节在低地址是小端”位域寄存器位布局错乱位域分配顺序依赖编译器查阅编译器手册或对比反汇编改用移位宏 掩码状态机跑到未知状态switch 缺少 default 分支检查状态枚举范围增加 default 分支恢复安全状态volatile 声明漏掉编译器优化掉寄存器读写查看反汇编或观察行为异常硬件寄存器指针统一加 volatileCRC 结果和上位机不一致多项式或初始值不匹配对比通信协议文档参数统一多项式初值、输入反射、输出异或参数这里最值得提前关注的两个坑是字符串常量不能原地修改以及结构体对齐导致协议帧长度错误。这两个问题在笔试机试和项目开发中都非常常见。11. 嵌入式 C 笔试学习建议第一上面每段代码都先自己手写一遍再对照答案改。手写和看是两码事。很多同学概念都知道一上机写struct Node *next都会漏掉struct这种基础错误在笔试里很致命。第二把代码跑起来后再自问“如果改一个条件会怎样”。比如数组变成 10 个元素结构体成员换个顺序宏参数改成i状态机事件连续来两次结果怎么变。这类变式题在笔试里出现频率极高平时不多练考场上很吃亏。第三结合真实工程代码来记忆。比如在串口中断里写环形缓冲区在协议栈里看字节序转换在驱动里找 volatile 寄存器访问在按键驱动里看状态机。看得多了笔试题目会变得非常直白。第四不要只看“嵌入式八股文”要能解释为什么。八股给的是答案模板但面试官通常会追问原理比如“为什么加 volatile”“为什么对齐会有填充字节”“为什么返回局部数组会出错”。先把原理讲清楚再背模板也不迟。12. 总结这 20 段 C 代码基本覆盖了嵌入式笔试里最高频的语言基础、内存模型、硬件交互知识点。宏定义的副作用、const/static/volatile 语义、数组和指针的区别、结构体对齐、链表反转、环形缓冲区、状态机、CRC、大小端转换每一段都值得反复练到能直接默写。如果时间有限优先吃透这几段宏定义、volatile、数组和 sizeof、结构体对齐、字符串逆序、链表反转、状态机框架。这几段是最常出现在选择题和手写题里的内容。建议把文章里的代码复制到本地工程里逐一编译运行再对照追问方向自测一遍。代码跑通了笔试题里很多看起来很绕的选项一眼就能识别出真正的考点。