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 原始来源
- C语言 1000 问导航:https://v.flomoapp.com/mine/?memo_id=MjQ1NzM0NTk5
- NULL/野/悬空指针:https://v.flomoapp.com/mine/?memo_id=MjUyNTg3ODk1
- 内存泄漏与悬垂指针:https://v.flomoapp.com/mine/?memo_id=MjU2MzEwOTA0
- 函数指针回调:https://v.flomoapp.com/mine/?memo_id=MjUyNTg3OTA0
- 二级指针:https://v.flomoapp.com/mine/?memo_id=MjUyNTg3OTY0
- 字符串函数安全性:https://v.flomoapp.com/mine/?memo_id=MjUyNTg4MTE2
- 二维数组内存布局:https://v.flomoapp.com/mine/?memo_id=MjUyNTg4MTI3
- 结构体传值与传指针:https://v.flomoapp.com/mine/?memo_id=MjU2MzA5NDQx
- 结构体对齐:https://v.flomoapp.com/mine/?memo_id=MjU2MzA4OTE1
- 文件定位:https://v.flomoapp.com/mine/?memo_id=MjU2NDUwNTY4
- 位图:https://v.flomoapp.com/mine/?memo_id=MjU2MzEyNTA0