在嵌入式开发中,指针几乎无处不在:访问寄存器、操作环形缓冲区、协议解析、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 flagdata 之后,地址为 0x20001008

快速验证:

#include <stddef.h>
printf("%zu\n", offsetof(Frame, data)); // 期望输出 4

工程实践要点:

  1. 跨平台/协议打包要显式控制对齐

    • 使用 #pragma pack(push, 1) / __attribute__((packed)) 仅在确需紧凑布局(如协议帧、外设寄存器镜像)时使用;否则默认对齐利于访问性能与总线对齐。

  2. 禁用未定义填充区读写

    • 不要通过“裸内存复制+指针偏移”去触碰 padding 字节,跨编译器/平台可能引入 UB 或移植性问题。

  3. 与 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)。

工程实践要点:

  1. 协议/外设寄存器跨端序要显式转换

    • 处理网络包、外采模块或多 MCU 混编时,用 htons/ntohs/htonl/ntohl 或自定义 bswap16/32 保证可读性与可维护性。

  2. 不要用类型转换代替 memcpy

    • 直接 (uint16_t)*(uint16_t*)(buf+2) 可能踩到未对齐访问严格别名规则,移植性差;memcpy 是标准且优化友好的方式。

  3. 结构体打包读取的两层坑

    • 既有对齐(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 是好事,不会导致野指针。

工程实践要点(如何规避):

  1. 禁止返回栈上对象地址

    • 若需返回缓冲区:

      • 方案 A:static(见第 10 题,注意线程安全/重入);

      • 方案 B:堆上分配malloc,由调用方释放);

      • 方案 C:调用方先分配,callee 通过输出参数**填充(推荐嵌入式):

        bool get_data(uint8_t *out, size_t cap, size_t *out_len);
        

  2. 加工具扫描与防御

    • 开启 ASan/UBSan(能开则开)、静态分析、MISRA-C 规则;

    • 将“返回栈地址”加入code review 清单

  3. API 文档说明所有权

    • 统一约定“谁分配谁释放”,避免双重释放或忘记释放。


9. 结构体指针与成员访问 —— 正常取值

p->count 指向 info.count,输出 100。

工程提示:跨端序数据从总线搬入结构体成员前,先做字节序转换;避免“结构体看起来对,数值全反”的坑。


10. 双重指针与设计(设计与线程安全重点题)

答案:C(30)

init二级指针把内部缓冲区地址“返回”给调用方:

  • bufferstatic 修饰,具有静态存储期(程序整个生命周期有效),因此 *pp = buffer; 后在 main 中可安全访问。

  • p[2] == 30

工程实践要点(深入):

  1. 为什么不用返回局部数组地址?

    • uint8_t buffer[4];(无 static)是栈上对象,函数返回后失效 → 悬空指针(见第 8 题)。

  2. static 的隐患:

    • 不可重入/非线程安全:多次调用或多线程共享同一块内存,数据会被覆盖。

    • 状态难以追踪:隐藏全局状态,测试与维护成本高。

  3. 更健壮的 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 设计上明确内存所有权线程安全,比“一时写快”更值钱。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐