华为od 面试八股文_C语言_06_含答案
·
1:struct 和 union 的内存布局有什么区别?什么场景下必用 union?
回答要点:
- 内存布局区别:
struct(结构体):所有成员依次存储,每个成员都有自己独立的内存空间。结构体的大小 >= 所有成员大小之和(受内存对齐影响)。union(联合体):所有成员共享同一块内存,起始地址相同。联合体的大小 >= 最大成员的大小(同样受内存对齐影响)。
- 应用场景:
- 节省内存:当多个成员不会同时使用时(互斥)。
- 数据类型转换:例如解析网络协议包,或者判断机器的大小端模式(通过存
int取char)。 - 寄存器/硬件访问:在嵌入式开发中,用
union映射硬件寄存器的不同位域。
2:malloc 的底层实现原理是什么?它真的会立即分配物理内存吗?
回答要点:
- 底层系统调用:
- 对于小块内存(通常 < 128k),
malloc使用brk()或sbrk()系统调用,通过移动堆顶指针(program break)来扩大堆空间。 - 对于大块内存,
malloc使用mmap()系统调用,在进程的虚拟地址空间中寻找一块空闲区域进行映射。
- 对于小块内存(通常 < 128k),
- 物理内存分配:
- 不会立即分配。操作系统采用惰性分配 (Lazy Allocation) 策略。
malloc返回的只是虚拟地址。只有当程序真正去读写这块内存时,CPU 触发缺页中断 (Page Fault),操作系统才会分配实际的物理内存页,并建立虚拟地址到物理地址的映射。
- 内存池:
malloc(如 glibc 的 ptmalloc)通常维护一个内存池,释放的内存(free)不会立即还给操作系统,而是留作下次分配,以减少系统调用的开销。
3:详细讲讲 volatile 的三个核心应用场景?它能保证线程安全吗?
回答要点:
- 核心作用:告诉编译器不要对该变量进行优化。每次读取该变量时,必须直接从内存中读取,而不是从 CPU 寄存器或缓存中读取;每次写入都必须立即写回内存。
- 三大场景:
- 硬件寄存器访问:嵌入式中,状态寄存器的值可能由硬件自动改变,编译器无法感知。
- 中断服务程序 (ISR):中断中修改的全局变量,主程序中检测该变量时。
- 多线程共享标志位:一个线程修改,另一个线程轮询检测(注意:仅限简单的标志位)。
- 线程安全:不能。
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的实现中,src和dst指针通常被声明为restrict,告诉编译器它们不会重叠,从而生成更高效的汇编代码。
7:写宏 (Macro) 时有哪些经典陷阱?如何用 do {...} while(0) 避免?
回答要点:
- 陷阱:
- 副作用重复计算:
#define MAX(a, b) ((a) > (b) ? (a) : (b))。如果调用MAX(i++, j++),i或j会被执行两次。 - 优先级问题:
#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_t 和 int 有什么本质区别?为什么循环计数最好用 size_t?
回答要点:
- 本质区别:
int:通常是 32 位有符号整数(取决于平台),可能溢出,且有负数。size_t:是sizeof操作符返回的类型,通常是unsigned long或unsigned long long。它是无符号的,且保证能容纳当前平台上最大可能对象的字节大小(即能覆盖整个内存地址空间)。
- 推荐理由:
- 数组索引或内存大小永远不可能是负数。
- 在 64 位系统上,数组长度可能超过
INT_MAX(2GB),使用int做索引会导致溢出和无限循环,而size_t是安全的。
10:setjmp 和 longjmp 是做什么的?它们和 goto 有什么区别?
回答要点:
- 作用:提供非本地跳转 (Non-local Jump) 能力。
setjmp:保存当前的栈环境(寄存器、栈指针等)到jmp_buf缓冲区。longjmp:恢复之前保存的栈环境,直接跳转回setjmp调用的位置。
- 区别:
goto:只能在同一个函数内部跳转。setjmp/longjmp:可以在不同函数之间跳转(通常是从深层嵌套函数跳回顶层函数)。
- 应用:常用于 C 语言中的异常处理机制模拟(类似 try-catch),或者在嵌入式系统中处理严重错误复位。

更多推荐
所有评论(0)