1:structunion 的内存布局有什么区别?什么场景下必用 union

回答要点:

  • 内存布局区别
    • struct(结构体):所有成员依次存储,每个成员都有自己独立的内存空间。结构体的大小 >= 所有成员大小之和(受内存对齐影响)。
    • union(联合体):所有成员共享同一块内存,起始地址相同。联合体的大小 >= 最大成员的大小(同样受内存对齐影响)。
  • 应用场景
    • 节省内存:当多个成员不会同时使用时(互斥)。
    • 数据类型转换:例如解析网络协议包,或者判断机器的大小端模式(通过存 intchar)。
    • 寄存器/硬件访问:在嵌入式开发中,用 union 映射硬件寄存器的不同位域。

2:malloc 的底层实现原理是什么?它真的会立即分配物理内存吗?

回答要点:

  • 底层系统调用
    • 对于小块内存(通常 < 128k),malloc 使用 brk()sbrk() 系统调用,通过移动堆顶指针(program break)来扩大堆空间。
    • 对于大块内存,malloc 使用 mmap() 系统调用,在进程的虚拟地址空间中寻找一块空闲区域进行映射。
  • 物理内存分配
    • 不会立即分配。操作系统采用惰性分配 (Lazy Allocation) 策略。
    • malloc 返回的只是虚拟地址。只有当程序真正去读写这块内存时,CPU 触发缺页中断 (Page Fault),操作系统才会分配实际的物理内存页,并建立虚拟地址到物理地址的映射。
  • 内存池malloc(如 glibc 的 ptmalloc)通常维护一个内存池,释放的内存(free)不会立即还给操作系统,而是留作下次分配,以减少系统调用的开销。

3:详细讲讲 volatile 的三个核心应用场景?它能保证线程安全吗?

回答要点:

  • 核心作用:告诉编译器不要对该变量进行优化。每次读取该变量时,必须直接从内存中读取,而不是从 CPU 寄存器或缓存中读取;每次写入都必须立即写回内存。
  • 三大场景
    1. 硬件寄存器访问:嵌入式中,状态寄存器的值可能由硬件自动改变,编译器无法感知。
    2. 中断服务程序 (ISR):中断中修改的全局变量,主程序中检测该变量时。
    3. 多线程共享标志位:一个线程修改,另一个线程轮询检测(注意:仅限简单的标志位)。
  • 线程安全不能volatile 只保证可见性(防止编译器优化),不保证原子性。对于 i++ 这种操作,依然需要加锁或使用原子操作。

4:什么是位域 (Bit-field)?它有什么跨平台移植性问题?

回答要点:

  • 定义:在 struct 中,允许定义只占用特定位数的成员,例如 unsigned int a : 1; 表示 a 只占 1 个 bit。
  • 用途:极度节省内存,或精确匹配硬件协议/寄存器格式。
  • 移植性陷阱
    • 内存布局不确定:C 标准未规定位域的存储顺序(是从低位到高位,还是高位到低位),这取决于编译器实现和大小端模式。
    • 跨边界对齐:当一个位域跨越了字节或字边界时,编译器可能将其拆分,也可能填充 padding,导致结构体大小不一致。
    • 结论:在编写跨平台代码或网络协议解析时,尽量避免直接使用位域,建议用位运算(&, |, <<)替代。

5:C 语言中常见的“未定义行为” (Undefined Behavior, UB) 有哪些?为什么它们很危险?

回答要点:

  • 定义:C 标准未规定编译器该如何处理的代码行为。编译器可以报错、忽略、生成垃圾代码,甚至让程序崩溃。
  • 常见 UB
    • 有符号整数溢出:例如 int a = INT_MAX + 1;
    • 解引用空指针或野指针。
    • 除以零
    • 移位数量非法:例如 1 << 32(在 32 位 int 上)。
    • 修改字符串字面量char *s = "hello"; s[0] = 'H';
  • 危险性:编译器会假设 UB 永远不会发生,并基于此进行激进优化。例如,编译器可能认为 a + 1 > a 永远为真(忽略溢出),导致死循环或安全漏洞。

6:restrict 关键字有什么用?它如何帮助编译器优化?

回答要点:

  • 作用restrict 是 C99 引入的类型限定符,只能修饰指针。
  • 承诺:它向编译器承诺,该指针是访问其所指向内存的唯一入口。在当前作用域内,不会有其他指针(别名)指向同一块内存。
  • 优化:有了这个承诺,编译器可以放心地进行指令重排或缓存优化,而不用担心数据依赖问题。例如在 memcpy 的实现中,srcdst 指针通常被声明为 restrict,告诉编译器它们不会重叠,从而生成更高效的汇编代码。

7:写宏 (Macro) 时有哪些经典陷阱?如何用 do {...} while(0) 避免?

回答要点:

  • 陷阱
    • 副作用重复计算#define MAX(a, b) ((a) > (b) ? (a) : (b))。如果调用 MAX(i++, j++)ij 会被执行两次。
    • 优先级问题#define MUL(a, b) a * b。调用 MUL(1+2, 3) 会变成 1+2*3。解决办法是给每个参数和整个表达式都加括号。
    • 多语句宏:如果在 if 后面调用包含多条语句的宏,可能会因为缺少花括号导致逻辑错误。
  • do {...} while(0) 惯用法
    • 将宏包裹在 do-while(0) 中,可以确保宏展开后是一个独立的语法单元(语句块),并且可以安全地在 if 语句中使用(包括吃掉末尾的分号)。

8:数组作为函数参数时,sizeof 会返回什么?为什么?

回答要点:

  • 现象:在函数内部对数组参数使用 sizeof,返回的是指针的大小(32 位系统是 4,64 位系统是 8),而不是整个数组的大小。
  • 原因数组退化 (Array Decay)。在 C 语言中,数组名作为函数参数传递时,会退化为指向数组首元素的指针。函数无法知道数组的实际长度,因此必须显式传递数组长度参数。

9:size_tint 有什么本质区别?为什么循环计数最好用 size_t

回答要点:

  • 本质区别
    • int:通常是 32 位有符号整数(取决于平台),可能溢出,且有负数。
    • size_t:是 sizeof 操作符返回的类型,通常是 unsigned longunsigned long long。它是无符号的,且保证能容纳当前平台上最大可能对象的字节大小(即能覆盖整个内存地址空间)。
  • 推荐理由
    • 数组索引或内存大小永远不可能是负数。
    • 在 64 位系统上,数组长度可能超过 INT_MAX (2GB),使用 int 做索引会导致溢出和无限循环,而 size_t 是安全的。

10:setjmplongjmp 是做什么的?它们和 goto 有什么区别?

回答要点:

  • 作用:提供非本地跳转 (Non-local Jump) 能力。
    • setjmp:保存当前的栈环境(寄存器、栈指针等)到 jmp_buf 缓冲区。
    • longjmp:恢复之前保存的栈环境,直接跳转回 setjmp 调用的位置。
  • 区别
    • goto:只能在同一个函数内部跳转。
    • setjmp/longjmp:可以在不同函数之间跳转(通常是从深层嵌套函数跳回顶层函数)。
  • 应用:常用于 C 语言中的异常处理机制模拟(类似 try-catch),或者在嵌入式系统中处理严重错误复位。

在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐