C语言1000问:指针安全、数组布局、结构体与位图

本篇迁移 C 语言 1000 问第四批 10 张完整问答卡,覆盖指针生命周期、内存错误、函数指针、二级指针、字符串安全、二维数组、结构体传参、内存对齐、文件定位和位图。

31. NULL 指针、野指针和悬空指针有什么区别?

答案:

  • NULL 指针:表示当前不指向有效对象/函数的空指针值。C 标准不要求其底层位模式一定是地址 0。
  • 野指针:未初始化或被破坏的指针,值不确定。
  • 悬空指针:原来指向有效对象,但对象生命周期已结束或内存已释放,指针值仍被保留。

防御:声明时初始化;明确对象所有权和生命周期;释放后清理所有相关引用;使用前检查 API 约定的空值;配合 ASan、静态分析和边界检查。将某一个指针置 NULL 并不会自动清除其他别名。

32. 内存泄漏、野指针和悬空指针分别是什么?

答案: 内存泄漏是动态分配的内存失去可达引用但未释放;野指针是指针值不确定或非法;悬空指针是指针指向的对象已失效。三者根源通常是所有权和生命周期管理不清。

flowchart TD
  A[内存管理错误] --> B[泄漏:申请后丢失引用]
  A --> C[野指针:未初始化 / 被破坏]
  A --> D[悬空指针:对象已失效]
  B --> E[资源持续耗尽]
  C --> F[非法访问 / UB]
  D --> F
  G[所有权 / 生命周期 / 配对释放] --> H[预防与审查]

嵌入式长期运行系统尤其要定义谁申请、谁释放、对象何时失效,以及错误路径如何清理。

33. 函数指针作为回调时,编译器帮忙做了什么?

答案: 函数指针保存可调用函数的地址/引用。编译器依据函数指针类型检查返回类型和参数类型,并生成间接调用指令;链接器负责解析可见函数符号。回调常搭配 void * context 传递用户上下文。

回调的执行上下文由调用方决定:若在 ISR 中调用,回调必须满足中断上下文的时序和重入约束;若在任务中调用,则要考虑锁和阻塞。

34. 什么情况下必须使用二级指针?

答案: 当函数需要修改调用方的指针变量本身时,需要传入该指针变量的地址。例如动态分配后回传对象、链表头指针可能改变的插入/删除操作。

int create_buffer(uint8_t **out, size_t size)
{
    if (out == NULL) {
        return -1;
    }
    *out = malloc(size);
    return (*out != NULL) ? 0 : -1;
}

这里一级指针只会修改形参副本;二级指针才允许函数写回调用方的指针变量。生产代码还需明确释放责任和错误码约定。

35. strcpy、strcat、sprintf 为什么容易造成缓冲区溢出?

答案: 这些接口不接收目标缓冲区容量,无法自动确认目标空间足够。源字符串或格式化结果过长时会写越界。可优先使用带容量参数的 snprintf,并检查返回值是否发生截断;strncpy 也不保证总是 NUL 结尾,不能把它当成万能安全替代。

接口设计最好显式传入 buffer、capacity 和实际数据长度,并统一规定截断/失败策略。

36. C 的二维数组如何排列?为什么按行遍历通常更快?

答案: C 的多维数组采用行优先布局(row-major)。对于 int a[3][4],每一行的 4 个元素连续存储,下一行紧随其后。按行遍历使访问地址连续,更容易利用缓存行和突发内存访问;按列遍历会以整行跨度跳跃。

for (size_t i = 0; i < rows; ++i) {
    for (size_t j = 0; j < cols; ++j) {
        use(a[i][j]);
    }
}

对大数组,访问顺序会明显影响性能;具体收益依赖缓存、内存总线和编译器优化。

37. 结构体作为函数参数,传值和传指针如何选择?

答案: 传值会按 ABI 规则复制结构体值(可能使用寄存器或内存);传指针只传地址,避免大对象拷贝,但引入间接访问和别名关系。

  • 小结构体:传值可能更清晰且不慢。
  • 大结构体:常用 const T * 避免复制。
  • 需要修改调用方对象:传 T *。
  • 结构体含指针成员时,传值只是浅拷贝指针,不会复制其指向的数据。

最终应按目标 ABI、对象大小、优化结果和接口语义测量,而不是规定“所有结构体一律传指针”。

38. 结构体内存对齐和填充字节是怎么回事?

答案: 每个成员有对齐要求,编译器会在成员之间或结构体尾部插入 padding,使成员地址和整体大小满足实现要求。这样有助于目标 CPU 高效或正确访问,但会增加内存占用。

不要把某个例子中的偏移和 sizeof 结果当作所有平台固定值;应使用 sizeof、offsetof 和目标 ABI 文档验证。使用 packed/pragma pack 会改变布局,可能导致未对齐访问、性能下降或硬件异常;协议序列化优先显式按字节打包,不依赖原生结构体布局。

39. fseek、ftell、rewind 如何使用?文本和二进制模式有什么差异?

答案:

  • ftell:取得当前文件位置指示器对应的位置值。
  • fseek:移动文件位置,成功返回 0。
  • rewind:回到文件开头并清除 EOF/错误指示器。

二进制流中位置通常可用于字节级定位;文本流受换行转换和实现约束,不能假设 ftell 一定等于真实字节偏移。可移植程序应遵守标准对文本流定位的限制;需要任意字节偏移时使用二进制模式。超大文件还要使用平台提供的大文件定位接口。

40. 位图(bitmap)如何实现?嵌入式有哪些用途?

答案: 位图用一个 bit 表示一个布尔状态。N 个状态只需约 ceil(N/8) 字节。用 i/8 定位字节,用 i%8 定位该字节内的 bit。

#define BIT_SET(a, i)   ((a)[(i) / 8U] |=  (uint8_t)(1U << ((i) % 8U)))
#define BIT_CLR(a, i)   ((a)[(i) / 8U] &= (uint8_t)~(1U << ((i) % 8U)))
#define BIT_TEST(a, i)  (((a)[(i) / 8U] & (uint8_t)(1U << ((i) % 8U))) != 0U)

常见用途:资源分配、任务就绪表、事件标志、状态压缩和配置开关。要处理越界、并发修改和原子性;多任务/中断共享同一字节时需要同步保护。

Flomo 原始来源