【嵌入式C语言题库】嵌入式工程应用中的 C 语言指针选择题001(附详细解析与工程实践要点)
本文通过10道嵌入式开发中的指针实战题,系统讲解了指针在寄存器访问、缓冲区操作、协议解析等场景的应用与常见陷阱。重点解析了结构体对齐(题2)、内存拷贝与端序(题6)、野指针生命周期(题8)和双重指针设计(题10)等核心问题。文章强调指针运算本质、内存对齐规则、端序处理技巧,并提供工程实践建议:用memcpy代替强制类型转换确保可移植性,避免返回栈地址防止野指针,API设计要明确内存所有权。最后指出
在嵌入式开发中,指针几乎无处不在:访问寄存器、操作环形缓冲区、协议解析、DMA 搬运、任务调度……
下面通过 10 道场景化选择题,系统回顾指针在实战中的用法与“坑点”。第 2、6、8、10 题给出加长版解析,帮助你从工程角度真正吃透。
题目
1. 串口缓冲区访问
uint8_t rx_buf[128];
uint8_t *p = rx_buf;
p += 5;
此时 p
指向的是:
A. rx_buf[0]
B. rx_buf[4]
C. rx_buf[5]
D. rx_buf[6]
2. 指针与结构体偏移(对齐重点题)
typedef struct {
uint16_t id;
uint32_t data;
uint8_t flag;
} Frame;
Frame *pf = (Frame*)0x20001000;
则 &(pf->data)
的地址是:
A. 0x20001000
B. 0x20001002
C. 0x20001004
D. 0x20001006
3. 指针传递与数组
void process(uint16_t *data, int len) {
for(int i=0; i<len; i++) {
data[i] = data[i] >> 1;
}
}
int main() {
uint16_t adc[4] = {100,200,300,400};
process(adc, 4);
}
执行后 adc[2]
的值是:
A. 100 B. 150 C. 200 D. 300
4. 函数指针调用
typedef void (*TaskFn)(void);
void TaskA(void){ printf("A"); }
void TaskB(void){ printf("B"); }
int main() {
TaskFn f = TaskA; f();
f = TaskB; f();
}
程序输出为:
A. AA
B. AB
C. BA
D. BB
5. 指针运算与字长
uint32_t arr[4] = {1,2,3,4};
uint32_t *p = arr;
p = p + 2;
此时 *p
的值是:
A. 1 B. 2 C. 3 D. 4
6. 内存拷贝与端序(端序重点题)
uint8_t buf[6] = {1,2,3,4,5,6};
uint16_t value;
memcpy(&value, buf+2, 2); // 小端 CPU
则 value
的十进制结果是:
A. 771 (0x0303
)
B. 1027 (0x0403
)
C. 1284 (0x0504
)
D. 1541 (0x0605
)
7. 指针数组与二维数组
const char *menu[] = {"File", "Edit", "Exit"};
printf("%s", menu[1]);
输出为:
A. File
B. Edit
C. Exit
D. 编译错误
8. 野指针与生命周期(生命周期重点题)
以下哪种情况最容易导致野指针?
A. 全局数组指针指向静态内存
B. 动态申请内存后未释放
C. 函数返回局部变量的地址
D. 使用 malloc
返回值前先判断是否为 NULL
9. 结构体指针与成员访问
输出为:
A. 1 B. 100 C. 随机值 D. 编译错误
10. 双重指针与设计(设计与线程安全重点题)
void init(uint8_t **pp) {
static uint8_t buffer[4] = {10,20,30,40};
*pp = buffer;
}
int main() {
uint8_t *p;
init(&p);
printf("%d", p[2]);
}
输出为:
A. 10 B. 20 C. 30 D. 40
答案
1.C 2.C 3.B 4.B 5.C 6.B 7.B 8.C 9.B 10.C
解析
1. 串口缓冲区访问 —— 指针加法按元素大小步进
uint8_t*
按字节步进,p += 5
指向 rx_buf[5]
。
工程提示:环形缓冲区中常用
(head + n) % size
做下标回绕,指针版可用“哨兵+范围判断”避免取模带来的除法成本。
2. 指针与结构体偏移(对齐重点题)
答案:C(0x20001004)
为什么不是 0x20001002?
结构体成员会按 ABI 的对齐规则排列。典型 32 位/ARM 平台上:
-
uint16_t id
占 2 字节,起于0x20001000
; -
uint32_t data
需要 4 字节对齐,因此在id
后会插入 2 字节填充(padding),使其从0x20001004
开始; -
uint8_t flag
在data
之后,地址为0x20001008
。
快速验证:
#include <stddef.h> printf("%zu\n", offsetof(Frame, data)); // 期望输出 4
工程实践要点:
跨平台/协议打包要显式控制对齐
使用
#pragma pack(push, 1)
/__attribute__((packed))
仅在确需紧凑布局(如协议帧、外设寄存器镜像)时使用;否则默认对齐利于访问性能与总线对齐。禁用未定义填充区读写
不要通过“裸内存复制+指针偏移”去触碰 padding 字节,跨编译器/平台可能引入 UB 或移植性问题。
与 DMA/外设映射对齐
DMA/寄存器映射常要求自然对齐;未对齐访问可能触发硬 Fault 或隐形性能损耗。
3. 指针传递与数组 —— 就地修改
传入
uint16_t*
,右移一位等价除以 2,300 >> 1 = 150
。工程提示:对 ADC 原始数据做位运算时要注意饱和/舍入策略;固定点算法建议统一封装宏或内联函数,保证一致性。
4. 函数指针调用 —— 调度表常见写法
先调用
TaskA
输出A
,再切换到TaskB
输出B
。工程提示:任务调度表用
static const TaskFn table[]
可上 ROM;结合状态机能做表驱动设计。
5. 指针运算与字长 —— 步进按元素尺寸
uint32_t*
步进单位为 4 字节,p+2
指向arr[2]
,值为 3。工程提示:在内存映射寄存器数组上做指针运算要确认寄存器间距与数组声明匹配,避免“看起来对齐、实则地址错位”。
6. 内存拷贝与端序(端序重点题)
答案:B(1027, 即 0x0403)
memcpy(&value, buf+2, 2)
拷贝字节{3, 4}
到value
的存储单元。
在 小端 CPU 中(常见于 x86/大多 ARM 配置):
低地址存放 低有效字节(LSB)
因而
value
的内存布局为:0x03
(低地址)0x04
(高地址),数值就是0x0403 = 1027
端序速记:小端 = 小字节在前(低地址);网络字节序是大端,需要显式转换(
htons/ntohs
)。工程实践要点:
协议/外设寄存器跨端序要显式转换
处理网络包、外采模块或多 MCU 混编时,用
htons/ntohs/htonl/ntohl
或自定义bswap16/32
保证可读性与可维护性。不要用类型转换代替 memcpy
直接
(uint16_t)*(uint16_t*)(buf+2)
可能踩到未对齐访问和严格别名规则,移植性差;memcpy
是标准且优化友好的方式。结构体打包读取的两层坑
既有对齐(padding)问题,又有端序问题;跨 MCU 传输结构体一般禁止直接裸发,应该按字段序列化。
7. 指针数组与二维数组 —— 文本菜单常用法
menu[1]
是指向字面量"Edit"
的指针,printf
输出Edit
。工程提示:GUI/CLI 菜单表建议
const char * const menu[]
,避免被误改。
8. 野指针与生命周期(生命周期重点题)
答案:C(函数返回局部变量地址)
为什么 C 最危险?
局部变量位于栈帧,函数返回后其生命周期结束,栈空间会被后续调用复用。返回其地址/引用会形成悬空指针(dangling pointer),表现为“有时能用、有时崩”的隐蔽时序错误。其他选项为什么不是:
B “动态申请后未释放” → 属于泄漏,但指针仍然有效(直到你释放它)。变成野指针的是“释放后继续用”(use-after-free)。
A 指向静态/全局区,一般安全(除非被错误改写或越界)。
D 在用前检查
NULL
是好事,不会导致野指针。工程实践要点(如何规避):
禁止返回栈上对象地址
若需返回缓冲区:
方案 A:static(见第 10 题,注意线程安全/重入);
方案 B:堆上分配(
malloc
,由调用方释放);方案 C:调用方先分配,callee 通过输出参数**填充(推荐嵌入式):
bool get_data(uint8_t *out, size_t cap, size_t *out_len);
加工具扫描与防御
开启 ASan/UBSan(能开则开)、静态分析、MISRA-C 规则;
将“返回栈地址”加入code review 清单。
API 文档说明所有权
统一约定“谁分配谁释放”,避免双重释放或忘记释放。
9. 结构体指针与成员访问 —— 正常取值
p->count
指向info.count
,输出 100。工程提示:跨端序数据从总线搬入结构体成员前,先做字节序转换;避免“结构体看起来对,数值全反”的坑。
10. 双重指针与设计(设计与线程安全重点题)
答案:C(30)
init
用 二级指针把内部缓冲区地址“返回”给调用方:
buffer
用static
修饰,具有静态存储期(程序整个生命周期有效),因此*pp = buffer;
后在main
中可安全访问。
p[2] == 30
。工程实践要点(深入):
为什么不用返回局部数组地址?
uint8_t buffer[4];
(无 static)是栈上对象,函数返回后失效 → 悬空指针(见第 8 题)。static 的隐患:
不可重入/非线程安全:多次调用或多线程共享同一块内存,数据会被覆盖。
状态难以追踪:隐藏全局状态,测试与维护成本高。
更健壮的 API 设计(推荐优先级):
首选:由调用方提供缓冲区(零内存分配、最可控)
void init(uint8_t *out, size_t cap) { static const uint8_t src[4] = {10,20,30,40}; size_t n = cap < 4 ? cap : 4; memcpy(out, src, n); }
次选:返回堆内存并约定释放权
uint8_t *init_heap(size_t *out_len){ uint8_t *p = malloc(4); if (!p) return NULL; uint8_t tmp[4] = {10,20,30,40}; memcpy(p, tmp, 4); *out_len = 4; return p; // 调用方 free(p) }
若必须用 static:在文档中显式声明非线程安全,必要时加锁或提供句柄化多实例接口(将状态放入对象实例)。
小结与建议
指针 + 对齐 + 端序 + 生命周期 是嵌入式里最容易“看得懂写不好”的四件套。
做协议与寄存器映射时,优先显式序列化与显式对齐/打包;
API 设计上明确内存所有权与线程安全,比“一时写快”更值钱。
更多推荐
所有评论(0)