面试题库
当前收录 117 道题,来自已发布章节的 interview.yaml(单一权威数据源,不在 MDX 重复维护答案);P0 全部 38 章题库已完整收录,后续随 P1/P2 内容继续扩充。
一个 C 模块的头文件应该放什么、不应该放什么?高频C/C++ · medium
要点:放:类型定义、宏、函数原型、extern 变量声明;不放:普通全局定义、非 inline 函数实现。
头文件是接口契约:使用者 include 它即可获得编译期所需的一切(类型、原型、宏)。普通全局定义 (int x = 0;)放进头文件后,每个包含它的 .c 都会生成一份定义,链接期 multiple definition; 函数实现同理。例外:static inline 函数与 static const 常量(内部链接、每翻译单元独立副本)可以在 头文件中出现,但要说清语义。判断标准一句话:同一头文件被多个 .c 包含后能否无冲突地链接。
追问:- 追问:extern int x; 与 static int x; 放头文件各是什么语义?
评分要点:- 接口契约定义
- multiple definition 风险
- inline/static 例外与语义
关联章节:1.3.10 API 封装与工程目录规范
#ifndef 守卫和 #pragma once 有什么区别?工程里怎么选?高频C/C++ · medium
要点:都是防重复包含:#ifndef 是标准三行写法;#pragma once 一行但非标准。工程默认
#ifndef GUARD_H / #define GUARD_H / ... / #endif 由 C 标准保证,任何编译器都工作;缺点是需要 保证宏名唯一(模块前缀即可)。#pragma once 只需一行,主流编译器(gcc/clang/msvc)都支持且 行为一致,但它是预处理指令的编译器扩展,严格可移植代码用 #ifndef。嵌入式工程编译器集通常固定, 两种都能用;对外发布的库建议 #ifndef(对任何消费者都成立)。
追问:- 追问:#ifndef 的宏名冲突会怎样?(守卫失效、重复包含报错;用模块前缀避免)
评分要点:- 两者机制
- 可移植性取舍
关联章节:1.3.10 API 封装与工程目录规范
模块内部函数为什么要加 static?不加会有什么工程问题?高频C/C++ · medium
要点:static 给内部链接,函数只在定义它的 .c 内可见:隐藏实现细节、避免跨模块符号冲突。
static 内部链接的三点收益:① 封装——使用者只见 .h 的公开 API,内部实现(缓冲区管理、表查找等) 不可见不可调用,改实现不影响外部;② 命名空间——两个模块都有同名内部函数(如 sort)互不冲突; ③ 优化机会——编译器可见完整调用图时可内联/死代码消除。不加 static 的全局函数会进符号表, 多模块工程里同名符号冲突、接口面积失控、重构成本上升。
追问:- 追问:static 变量与 static 函数的作用域/链接属性分别是什么?
评分要点:- 封装与符号冲突
- 链接属性准确
关联章节:1.3.10 API 封装与工程目录规范
数组名和指针有什么区别?sizeof 在两种情况下各得到什么?高频C/C++ · medium
要点:数组名是首元素地址的常量表达式(不可赋值);指针是变量(可改指向)。sizeof(数组名) 是整个数组大小,sizeof(指针) 是指针本身大小。
数组名在表达式中多数场合退化为首元素地址,但它本身是常量——不能 arr = 别的地址; 指针是普通变量,可以重新赋值。sizeof 是二者最直观的分水岭:sizeof(arr) 是整个数组 字节数(如 int a[10] 为 40),sizeof(ptr) 是指针大小(本机 8)。另外数组名作为函数 参数时退化为指针,函数内 sizeof 得到的也是指针大小——这是必须把长度 n 一并传入的原因。
追问:- 追问:&arr 与 arr 有什么区别?(值相同、类型不同:&arr 是指向整个数组的指针,+1 步长为整个数组)
评分要点:- 常量 vs 变量
- sizeof 差异
- 参数退化与长度传递
关联章节:1.2.5 数组
数组越界访问会发生什么?为什么说"测试通过"不代表没有越界?高频C/C++ · medium
要点:越界是未定义行为(UB):可能读到垃圾、可能崩溃、可能看似正常;不同编译器/优化级别/环境结果不同,所以一次测试通过不能证明安全。
C11 标准(J.2、6.5.6)把下标越界规定为未定义行为——标准不保证任何结果。实际中: 读到相邻内存可能"恰好是 0",也可能破坏相邻变量造成后续奇怪 bug,可能触发段错误。 正因为"看似正常"是合法表现之一,靠运行测试无法排除越界;正确姿势是边界审查 (0..n-1、i < n)+ 工具辅助(AddressSanitizer 在 5.6 展开)。面试加分:主动提出 用 ASan/静态检查而不是"多跑几遍"。
追问:- 追问:如何用工具暴露越界?(-fsanitize=address 编译运行)
评分要点:- UB 定义准确
- 三种可能表现
- 测试不能证明安全
关联章节:1.2.5 数组
二维数组在内存中如何存放?m[1][2] 在 m[0][0] 之后的第几个位置?C/C++ · medium
要点:C 数组行优先:按行连续存放;对 int m[2][3],m[1][2] 是第 6 个元素(下标 5),地址 = 首地址 + 5*sizeof(int)。
行优先:第一行的 3 个元素先连续放,再放第二行的 3 个(ex2 实测相邻地址依次 +4)。 寻址公式:m[i][j] 相对首地址偏移 (i * 列数 + j) * sizeof(元素)。所以 m[1][2] 是第 6 个元素(偏移 5 个 int)。行优先是 C 的硬性规定,与 1.3.2 的指针算术 (m[i] 即第 i 行首地址)完全一致。加分:能解释为什么必须指定列数(类型 int[3] 才能算出一行步长)。
追问:- 追问:为什么二维数组作参数时必须给出列数(int m[][3])?
评分要点:- 行优先与偏移公式
- 与指针算术一致性
- 列数必需的原因
关联章节:1.2.5 数组
C89、C99、C11 有哪些常用差异?C/C++ · medium
要点:C99 引入 // 注释、for 内声明、stdint.h/_Bool/VLA、long long;C11 引入 _Generic/_Static_assert/_Atomic 并把 VLA 降为可选;C17 是 C11 勘误版。
回答抓住"常用差异 + 版本定位":C89 是 1989 年老标准(声明在块头、只有 /* */ 注释), 现在基本只当历史;C99(1999)带来今天最顺手的写法——// 注释、for 循环内声明、 stdint.h/stdbool.h、long long、VLA;C11(2011)加入 _Generic、_Static_assert、 _Atomic 与线程库,同时把 VLA 从必选降为可选;C17(2018)只是 C11 的勘误版,无新特性。 加分项:指出工程上用 -std 显式锁标准、用 -Wpedantic 拦扩展写法的实践。
追问:- 追问:为什么工程要锁 -std?(避免开发者无意识用扩展,跨编译器/跨平台一致)
评分要点:- 各版本代表特性准确
- C17 与 C11 关系
- 工程锁标准意识
关联章节:1.2.1 C 程序结构与标准演进
标准语法、编译器扩展、平台实现差异,三者怎么区分?举例说明。高频C/C++ · medium
要点:标准语法所有编译器支持;扩展是特定编译器独有(如 0b 字面量、__attribute__);平台差异是标准留给实现决定的部分(如 int 位数、字节序)。
三层概念:① 标准语法——C 标准规定、任何符合标准的编译器都支持,锁 -std 后保证可用; ② 编译器扩展——超出标准的独有功能,如 gcc 的 0b 二进制字面量(C23 才进标准)、 __attribute__((packed)),-Wpedantic 会警告,换编译器即失效; ③ 平台实现差异——标准明确留给实现决定的部分,如 int 是 16/32 位、字节序大端小端、 NULL 的实际值,写代码要查平台文档而不能背死数。 举例:sizeof(int*) 在 32/64 位平台分别为 4/8(平台差异);__attribute__ 只在 GCC 系 可用(扩展);printf 的 %d(标准)。工程意义:能用标准的不用扩展,能用固定宽度类型 (int32_t)的不裸用 int。
追问:- 追问:为什么嵌入式代码里推荐 stdint.h 的 uint32_t 而不是 unsigned int?
评分要点:- 三层概念定义准确
- 每层有真实例子
- 工程选型意识
关联章节:1.2.1 C 程序结构与标准演进
一个规范的 C 程序文件有哪些组成部分?各部分职责是什么?高频C/C++ · easy
要点:头文件包含(声明)、预处理指令(宏/条件编译)、类型与全局声明、函数定义、main 入口;main 标准形式 int main(void)。
典型结构从上到下:① 文件头注释(版权/用途);② #include 头文件(引入标准库或自己 模块的声明,编译期文本展开);③ 预处理指令(宏定义、条件编译,如头文件守卫); ④ 类型定义/宏/函数原型(多文件工程里放 .h,1.3.10 展开);⑤ 函数定义;⑥ main 入口函数,标准形式 int main(void),return 0 表示成功、非 0 表示失败(0.2.4 已练)。 加分项:说出"声明放头文件、定义放源文件"的多文件组织原则与单一职责。
追问:- 追问:为什么 main 的返回值有意义?(给 shell/父进程判断程序成败,echo $?)
评分要点:- 结构完整
- main 标准形式
- 声明/定义分离意识
关联章节:1.2.1 C 程序结构与标准演进
volatile 和 _Atomic 有什么区别?多线程共享变量用哪个?高频C/C++ · medium
要点:volatile 管"访问不被优化",不提供原子性与内存序;_Atomic 提供原子读写与顺序保证。多线程共享变量用 _Atomic(单变量)或锁(复合操作)。
volatile 对编译器的约束是:每次访问都从内存读写、不缓存、不与其他 volatile 访问重排—— 适用场景是内存映射寄存器和信号处理共享标志。它不保证原子性:volatile int x 的 x++ 仍是 读-改-写三步,并发下会丢更新;也不提供内存序(其他线程何时看到写结果无保证)。 C11 _Atomic 类型保证单个操作(load/store/fetch_add 等)原子且带顺序语义; 多变量或"检查再改"的复合操作仍需要互斥锁。面试高频误区题,分层结论: volatile=访问约束,_Atomic=单变量原子,锁=复合互斥。
追问:- 追问:volatile 用在 MMIO 寄存器时为什么必须?(防止编译器把两次读优化成一次、把写删掉)
评分要点:- volatile 语义边界
- 原子性/内存序区分
- 分层选型结论
关联章节:1.3.11 C11 嵌入式相关特性
_Static_assert 在嵌入式工程里怎么用?举一个真实场景。高频C/C++ · medium
要点:编译期校验平台/布局假设:寄存器偏移、结构体大小、枚举值、sizeof 类型宽度,不满足直接编译失败。
场景举例:外设寄存器结构体按手册偏移排布,写 _Static_assert(offsetof(REG, CR2) == 0x04, "CR2 offset") 防止结构体布局被改动后悄悄错位;协议帧头写 _Static_assert(sizeof(frame_header) == 8, ...); 平台依赖写 _Static_assert(sizeof(uint32_t) == 4, ...)。价值:把"注释里的约定"变成"编译器强制的契约", 换工具链/换板卡时错误在编译期暴露,而不是烧进设备后现场排查。
追问:- 追问:和运行时 assert 相比收益在哪?(零运行时开销、越早失败成本越低)
评分要点:- 真实场景具体
- 编译期 vs 运行时
关联章节:1.3.11 C11 嵌入式相关特性
为什么寄存器定义推荐 uint32_t 而不是 unsigned int?高频C/C++ · easy
要点:uint32_t 宽度由标准保证为 32 位;unsigned int 宽度是实现定义的,寄存器/协议布局会随平台漂移。
C 标准只规定 int 至少 16 位,实际宽度随平台/工具链变化(16/32/64 位 MCU 不同), 而外设寄存器与二进制协议要求精确宽度。uint32_t 由 stdint.h 保证(平台支持时)恰好 32 位, 跨平台一致。工程习惯再加两道保险:宏集中定义寄存器地址;_Static_assert 校验关键假设。 相关点:uint32_t 是 typedef 而不是新类型,不改变运算语义,仍是 unsigned 的算术规则。
追问:- 追问:平台不提供 uint32_t 时会怎样?(stdint.h 不定义该类型,编译期即可发现)
评分要点:- 宽度保证 vs 实现定义
- typedef 语义
关联章节:1.3.11 C11 嵌入式相关特性
CMake 和 Makefile 是什么关系?各自解决什么问题?高频C/C++ · medium
要点:Makefile 直接描述"怎么编译";CMake 用 CMakeLists.txt 描述"工程长什么样",再生成 Makefile/Ninja/VS 工程等构建脚本。
Makefile 是构建规则(目标/依赖/命令),与平台和工具强绑定——Windows 的 nmake、Linux 的 make、 生成器的语法都不同。CMake 是更高一层的工程描述:CMakeLists.txt 声明目标、源文件、依赖与使用要求, 由 cmake 按生成器(-G)产出对应构建脚本(Unix Makefiles/MinGW Makefiles/Ninja/Visual Studio)。 分层收益:换平台/换生成器不改工程描述;同一份 CMakeLists 可同时维护 Makefile 与 VS 工程两种构建。
追问:- 追问:既然 CMake 生成 Makefile,为什么不直接用 Makefile?(跨平台描述与生成器抽象、自动依赖分析、模块与测试生态)
评分要点:- 描述 vs 规则的层次关系
- 生成器抽象与跨平台
关联章节:1.1.5 CMake 入门与多目录项目
target_link_libraries 里的 PRIVATE、PUBLIC、INTERFACE 分别是什么意思?高频C/C++ · medium
要点:控制"使用要求"的传播方向:PRIVATE 只自己用;PUBLIC 自己用且传给下游;INTERFACE 只传给下游(自己不使用)。
现代 CMake 的依赖围绕 target 展开:一个库的"使用要求"包括头文件路径、编译宏、链接的库。 PRIVATE:这些要求只影响本目标编译(内部实现细节,下游不可见);PUBLIC:本目标编译需要, 且任何链接本目标的下游也自动获得(如头文件路径随链接关系传播);INTERFACE:本目标不产生 编译单元(纯头文件库/仅传递配置),要求只给下游。举例:mymath 的头文件路径必须 PUBLIC, 否则 app 链接 mymath 后依然 #include 不到 mymath.h。
追问:- 追问:为什么现代 CMake 推荐 target 级命令而不用全局 include_directories?(避免目录级污染、依赖显式化)
评分要点:- 三种传播方向定义准确
- 能举头文件路径的例子
关联章节:1.1.5 CMake 入门与多目录项目
一个工程如何在同一个源码目录下同时维护 Debug 与 Release 两个构建?C/C++ · medium
要点:out-of-source 构建:同一份源码分别 cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug 与 -B build-release -DCMAKE_BUILD_TYPE=Release,两套产物互不干扰。
out-of-source 构建把构建产物与源码分离,因此同一源码可以生成任意多个构建目录,每个目录一份 独立的 CMake 缓存与产物。单配置生成器(Makefiles/Ninja)通过配置时的 -DCMAKE_BUILD_TYPE 区分; 多配置生成器(Visual Studio/Xcode)则在构建或打开 IDE 时选择配置。切回 in-source 会污染源码目录 且无法共存多配置——这是社区默认 out-of-source 的原因。
追问:- 追问:多配置生成器下 -DCMAKE_BUILD_TYPE 还有效吗?(无效,构建时选配置;可用 CMAKE_CONFIGURATION_TYPES 控制可选集合)
评分要点:- out-of-source 与多构建目录
- 单/多配置生成器差异
关联章节:1.1.5 CMake 入门与多目录项目
else if 链和 switch 各适合什么场景?为什么 switch 的分支值有类型限制?高频C/C++ · easy
要点:连续区间/复杂条件用 else if;离散整型值的多分支用 switch(更清晰、编译器可优化为跳转表)。switch 的表达式必须是整型,case 必须是整型常量表达式。
else if 适合区间判断(成绩分层)与任意表达式条件;switch 适合"一个整型值对应一个动作" (菜单、状态机),可读性更好,编译器对连续 case 可生成跳转表。限制来源:case 标签是编译期 常量,需要与表达式做精确整型比较;浮点有精度问题且区间语义不清晰,C 标准只允许整型。 补充:漏写 break 的穿透是高频考点,默认行为就是穿透。
追问:- 追问:状态机(如网络协议状态)用 switch 怎么写更稳?(default 兜底 + 每个状态显式 break)
评分要点:- 选型依据(区间 vs 离散)
- switch 类型限制的原因
- 提到穿透陷阱
关联章节:1.2.4 控制结构
do-while 和 while 有什么本质区别?举一个 do-while 最自然的应用场景。高频C/C++ · easy
要点:do-while 先执行后判断、至少执行一次;while 先判断后执行、可能一次不执行。最自然场景:交互菜单(先显示再等输入)。
区别在判断时点:while 在循环体前判断(可能 0 次),do-while 在循环体后判断(至少 1 次)。 菜单是典型场景——必须先显示选项,再读取用户输入,输入为 0 才退出。 加分:结合输入校验(scanf 返回非 1 时 break)说明循环出口设计。
追问:- 追问:如何保证 do-while 不会因为输入流结束而卡死?(检查 scanf 返回值并在循环内 break)
评分要点:- 判断时点差异
- 至少执行一次
- 菜单场景
关联章节:1.2.4 控制结构
循环条件写 `<` 还是 `<=` 有什么区别?结合数组遍历说说典型的越界事故。高频C/C++ · medium
要点:n 个元素下标为 0..n-1,遍历条件应写 i < n;写 i <= n 会访问第 n 个位置,是数组越界(未定义行为)。
边界事故的根因是把"元素个数"当成"最大下标"。写循环前先在纸上代边界值:i=0 时访问第一个、 i=n-1 时访问最后一个、i=n 时已经越界。防御性做法:固定用"下标从 0 到 n-1,条件 i < n"的 习惯;遍历范围不是从 0 开始时单独声明起止。越界读取是未定义行为(可能读到垃圾、可能崩溃, 结果不可依赖)——1.2.5 详细展开。
追问:- 追问:如何验证自己的循环边界正确?(用最小规模 n=0/1/2 的输入实测所有分支)
评分要点:- 0..n-1 与 i<n 的对应
- 越界 = 未定义行为
- 边界代入法
关联章节:1.2.4 控制结构
sizeof(int) 为什么不能背成固定 4 字节?高频C/C++ · easy
要点:sizeof 结果由实现/平台(数据模型、ABI)决定:标准只保证最小范围与相对关系;本机 MinGW 的 long 是 4 字节、Linux x86_64 是 8 字节,嵌入式可能不同。
C 标准对整型只规定最小范围(如 int 至少 16 位)与"short ≤ int ≤ long ≤ long long"的相对关系, 具体字节数由实现的数据模型决定:Windows/MinGW 用 LLP64(long=4、指针=8),Linux x86_64 用 LP64(long=8)。 所以跨平台代码一律用 sizeof 实测 + limits.h 边界值(INT_MAX 等),不硬编码、不背表。 唯一恒定的只有 sizeof(char)=1。
追问:- 追问:sizeof 是运行时算的吗?(不,编译期运算符)
评分要点:- 平台相关而非语言规定
- sizeof/limits.h 的用法
- char 恒 1
关联章节:1.2.2 数据类型与常量变量
const 和 volatile 分别解决什么问题?可以同时修饰一个变量吗?高频C/C++ · medium
要点:const 表达"只读"(编译器强制不可再赋值);volatile 表达"每次访问都真实读写内存"(防优化)。可以同时用:const volatile 典型是"只读的硬件状态寄存器"。
const:语义是"初始化后不再修改",编译器可据此优化与静态检查赋值错误(const T * 承诺不修改指向对象)。 volatile:语义是"每次访问必须从内存读写",编译器不得把访问缓存/合并/删除——用于硬件寄存器、 信号/中断与主流程共享的变量。两者正交:const volatile 表示"这个对象外部会变(volatile), 但程序不许写它(const)",如只读状态寄存器。误区:volatile 不是多线程同步手段(不保证原子性/内存序)。
追问:- 追问:volatile 能防止多线程数据竞争吗?(不能,需要锁/原子操作)
评分要点:- const 语义与优化
- volatile 语义与场景
- const volatile 组合
- volatile 误区
关联章节:1.2.2 数据类型与常量变量
int 和 unsigned int 一起比较,为什么结果常常"反直觉"?高频C/C++ · medium
要点:常规算术转换会把 int 提升为 unsigned int:-1 变成 4294967295,与 10 比较为假。
混合算术/比较时按"常规算术转换"把低等级转为高等级:有符号 int 与无符号 unsigned int 同宽时, 有符号操作数被转换为无符号——int 的 -1 变为 4294967295(模 2³²),于是 i < u 为假。 这是 C 语言设计的历史包袱,编写时要显式处理:比较前统一类型,或避免有符号/无符号混用; 编译器 -Wall -Wextra(-Wsign-compare)会给出警告。
追问:- 追问:怎么消除这类隐患?(统一类型/显式转换/用 size_t 与带符号循环变量时小心)
评分要点:- 常规算术转换规则
- 具体例子 -1 vs 10
- 规避方法
关联章节:1.2.2 数据类型与常量变量
realloc 为什么不能直接写 p = realloc(p, size)?正确写法是什么?高频C/C++ · medium
要点:realloc 失败返回 NULL 且原块保持不变;直接覆盖会让原指针丢失(泄漏+数据不可达)。正确写法:tmp = realloc(p, n); 判空后赋回。
realloc 尝试在原块上扩展,空间不足时分配新块、拷贝数据并释放旧块,返回新地址; 失败时返回 NULL,旧块仍有效。直接 p = realloc(p, n) 在失败时把 NULL 写进 p: 旧块地址丢失(内存泄漏),数据也无法再访问。正确模式: int *tmp = realloc(p, n * sizeof(int)); if (tmp == NULL) { /* p 仍有效:继续用旧块或 free(p) 后报错 */ } else p = tmp; 这是动态内存面试的最高频考点之一,能讲清"失败语义"与"泄漏机理"即合格。
追问:- 追问:realloc(ptr, 0) 的行为是什么?(实现定义:可能等价 free,不推荐依赖,应显式 free)
评分要点:- realloc 失败语义(NULL + 原块不变)
- 直接覆盖的泄漏机理
- 完整安全模式代码
关联章节:1.3.9 动态内存管理与常见错误
free(p) 之后把 p 置为 NULL 有什么作用?有什么局限?高频C/C++ · medium
要点:防止对同一指针重复 free 与直接误用;但只能保护这一个指针,其他副本指针依然悬空。
free 后置 NULL 让该指针不再指向已释放内存:if (p) free(p) 的幂等写法防 double free, 解引用前判 NULL 也能拦住一部分误用(NULL 解引用会立即崩溃,比悬空指针的随机行为好定位)。 局限:如果存在多个指针指向同一块(副本/结构体字段),置 NULL 只影响其中一个, 其他副本仍是悬空指针;且 free(NULL) 本身是安全的空操作,所以关键还在所有权唯一—— 一个堆块只有一个"主人",释放后所有引用方都应视为失效(文档写明)。
追问:- 追问:free(NULL) 合法吗?(合法,C 标准规定空操作)
评分要点:- 两个作用(防重复释放/防误用)
- 副本指针局限与所有权唯一
关联章节:1.3.9 动态内存管理与常见错误
堆和栈有什么区别?什么情况下必须用堆?高频C/C++ · medium
要点:栈自动管理、速度快、大小有限(MB 级)、生命周期随作用域;堆手动管理、大小受系统限制、生命周期由程序决定。数据量运行时才知道或超出栈容量时必须用堆。
栈:编译器自动分配释放(函数进出),分配开销极小(移动栈指针),大小有限 (Linux 默认线程栈 8MB 量级、嵌入式任务栈常只有几 KB),生命周期绑定作用域—— 函数返回即失效,因此不能把指向栈变量的指针传出去使用。 堆:malloc/calloc/realloc/free 手动管理,分配慢(维护空闲链表),容量受系统内存限制, 生命周期由程序决定——"函数返回后还要用的数据"、"运行时才知道大小的数据" (如读取文件行数后再开数组)、"超过栈容量的大对象"必须用堆。 嵌入式补充:MCU 堆通常很小且中断中禁 malloc,能静态就静态。
追问:- 追问:把指向栈变量的指针返回给调用者会发生什么?(悬空指针——栈帧回收后内存被复用)
评分要点:- 三组对比(管理/大小/生命周期)
- 必须用堆的三类场景
关联章节:1.3.9 动态内存管理与常见错误
项目里为什么用枚举而不是高频C/C++ · medium
要点:枚举在编译期保留符号信息:调试器可见成员名、有作用域与类型提示、改值只需改一处;宏只是预处理文本替换。
#define 在预处理阶段就被替换成裸数字:调试时只能看到数字、没有作用域(跨文件易冲突)、 编译器也无法给出"这个值可能不属于该集合"的提示。typedef enum 的成员是编译器可见的常量: gdb 里显示 ST_RUNNING 而不是 1;成员处于枚举的作用域内,配合前缀命名(ST_/ERR_) 可以避免冲突;部分工具链还能基于枚举给出告警与补全。代价是枚举成员是 int、 非强类型——需要强类型时用 C++ enum class。工程结论:状态集、错误码、协议字段值一律枚举。
追问:- 追问:什么场景下 #define 仍然合适?(宏函数、字符串拼接 #/##、条件编译标志——枚举替代不了这些)
评分要点:- 调试符号/作用域/类型提示三点
- 宏的适用场景边界
C 语言枚举的底层类型是什么?能保证占 4 个字节吗?高频C/C++ · medium
要点:底层类型是实现定义的、能容纳全部成员值的整数类型(通常 int,本机 gcc sizeof=4),但标准不保证;需要固定宽度用 stdint.h。
C11 6.7.2.2 规定枚举的底层类型是实现选择的、能表示全部成员值的整数类型—— 绝大多数编译器选 int,因此 sizeof 通常为 4,但这属于实现定义行为而非语言保证。 涉及结构体序列化、寄存器对齐、跨 ABI 传输时,不要把枚举写死为 4 字节: 用 sizeof 实测,或者直接用 uint8_t/uint32_t 等固定宽度类型表达值域 (枚举成员本身的值域可用手动赋值约束)。这正是不把"当前平台行为"当成"语言规则"的例子。
追问:- 追问:枚举成员可以是负数吗?(可以,如 ERR_BUSY = -1,成员是 int 常量)
评分要点:- 实现定义 vs 标准保证的区分
- 固定宽度场景的正确做法
typedef 解决了什么问题?和高频C/C++ · easy
要点:typedef 是编译期的类型别名(编译器理解、可用于复杂声明如函数指针);#define 是预处理文本替换,可能产生语法歧义。
typedef 给类型起别名:typedef int (*binop_t)(int,int); 之后 binop_t f = add; 简洁且 类型检查完整。典型场景:typedef struct {...} point_t; 省去每次写 struct 关键字; 函数指针类型简化;跨平台类型抽象(如项目内 size_type 统一替换)。 #define 也能"起别名",但它是文本替换:#define PTR int* 后 PTR a, b; 展开为 int* a, b; ——b 是 int 而不是指针,产生隐蔽错误;typedef int* PTR; 则 a、b 都是指针。 结论:起类型别名一律 typedef,宏只用于宏函数与条件编译。
追问:- 追问:typedef 和 struct 标签(tag)是什么关系?(struct point {...}; 可保留标签用于自引用链表节点,typedef 只是再起别名)
评分要点:- 编译期别名 vs 文本替换
- 复杂声明与宏展开歧义的例子
写出"指向接收两个 int、返回 int 的函数的指针"的 typedef,并说明函数指针最常见的用途。高频C/C++ · medium
要点:typedef int (*op_fn)(int, int); 最常用于回调:把函数作为参数交给框架/库,由其在恰当时机调用(如 qsort 比较函数、事件处理、中断服务)。
typedef int (*op_fn)(int, int); 之后 op_fn 即该函数指针类型。用途核心是回调(callback): 调用方不知道、也不该知道被调用方的具体业务,只约定接口(签名),被调用方注册一个符合接口的函数, 框架在事件发生时触发。标准库例子:qsort 的比较函数、bsearch;工程例子:事件循环、插件机制; 嵌入式例子:中断向量表存放服务函数地址、驱动"操作集"结构体(struct of function pointers)。
追问:- 追问:qsort 比较函数为什么参数是 const void * 而不是 int *?(通用性:qsort 不知道元素类型,void* 可接受任意类型指针)
评分要点:- typedef 写法正确
- 回调两要素(注册/触发)
- 至少一个具体场景
关联章节:1.3.4 函数指针与回调
为什么说"驱动开发大量使用回调"?请结合中断解释。高频嵌入式 · medium
要点:硬件事件何时发生是异步不可知的,驱动把处理函数地址注册进中断向量表,事件发生时硬件/内核直接跳转调用——这就是回调:你注册、事件触发、框架调用。
中断的本质是异步事件:CPU 不知道按键/定时器/串口数据什么时候来。驱动开发的做法是把"中断来了做什么" 写成服务函数,把函数地址注册到中断向量表(注册),事件发生时硬件按表跳转执行(触发)。内核的 驱动模型同样如此:file_operations 结构体里注册 open/read/write 等函数指针,内核在合适时机调用—— 驱动作者只实现业务,调用时机交给框架。这正是回调思想在系统层的体现。
追问:- 追问:中断回调函数与普通函数在编写上有什么特殊约束?(必须短小、不可阻塞、注意重入)
评分要点:- 异步事件与注册-触发
- 中断向量表/操作集例子
关联章节:1.3.4 函数指针与回调
用 qsort 对一个 int 数组升序排序,比较函数怎么写?为什么不能直接返回 a - b?高频C/C++ · medium
要点:返回 (a > b) - (a < b);a - b 在极端值下会溢出(如 INT_MAX 与 -1 相减),溢出后符号错误导致排序结果错误。
比较函数签名固定 int cmp(const void *pa, const void *pb),函数体内先转换: const int a = *(const int*)pa; const int b = *(const int*)pb; 然后 return (a > b) - (a < b); 该写法保证只返回 -1/0/1 且不会溢出。a - b 的隐患:INT_MAX - (-1) 在数学上是 2147483648, 超出 int 范围发生有符号溢出(未定义行为),实际可能得到负数,比较结果符号反转 → 排序错误。 面试加分:顺带说 bsearch 与 qsort 共用同一套比较函数约定。
追问:- 追问:如果想降序排序怎么办?(交换 a、b 的角色:return (b > a) - (b < a))
评分要点:- 签名与转换
- 溢出风险与安全写法
关联章节:1.3.4 函数指针与回调
C 语言里函数参数是传值还是传引用?"传址"是怎么回事?高频C/C++ · medium
要点:C 一律传值:形参是实参的副本。"传址"是把变量的地址值复制进指针形参,函数内通过解引用间接修改调用者的变量——本质仍是传值。
传值意味着函数内对形参的任何赋值都不影响实参(swap(int a,int b) 交换失败的原因)。 "传址"(swap(int *a,int *b) + 调用传 &x,&y)只是把地址这个"值"复制给了形参, 函数内 *a 解引用访问的是调用者的变量,所以能交换成功。严格表述: C 不存在引用传递(那是 C++ 的 &);"传址"= 传地址值 + 间接访问。 面试加分项:想修改调用者的指针变量本身,需要传指针的地址(二级指针)。
追问:- 追问:如何在一个函数里修改调用者的 int *p 本身?(int **pp + &p,或返回新指针)
评分要点:- 传值本质
- 传址=传地址值+解引用
- 与 C++ 引用的区别
关联章节:1.3.3 函数与递归
写递归函数要注意什么?递归有什么风险?高频C/C++ · medium
要点:三要素:基准情形、递归步骤、规模递减;风险:每层调用占栈帧,深度大时栈溢出;性能与栈开销高于等价迭代。
三要素是正确性保证:① 基准情形让递归终止;② 递归步骤把问题分解为子问题; ③ 每次调用参数向基准逼近(如 n-1),否则无限递归。 风险:调用栈每层一个栈帧(局部变量、返回地址),深度×帧大小=栈消耗; Linux 默认栈 8MB、嵌入式任务栈常仅几 KB,深递归直接栈溢出(崩溃)。 工程选型:规模小、定义天然递归(树遍历、汉诺塔)用递归;大规模/嵌入式栈小改迭代; 不依赖尾调用优化(标准不保证)。斐波那契递归还有指数级时间复杂度的额外问题(可提 memo 或迭代)。
追问:- 追问:为什么嵌入式里尽量避免深递归?(任务栈 1-16KB)
评分要点:- 三要素
- 栈帧与栈溢出
- 迭代选型
关联章节:1.3.3 函数与递归
函数声明和函数定义有什么区别?为什么头文件里只放声明?高频C/C++ · easy
要点:声明(原型)只有签名;定义含函数体。头文件只放声明可被多个 .c 安全包含;定义重复会出现链接期重定义错误。
声明告诉编译器"存在这样的函数"(返回类型、参数类型),支持在定义之前调用; 定义给出实现(函数体),一个函数全程序只能有一份定义(内联/静态函数等特例另说)。 头文件被多个源文件包含时,若放定义则每个 .c 编译出副本,链接时报 multiple definition; 放声明则每个 .c 只引用同一份定义。工程惯例:.h 放原型与类型,.c 放实现(1.3.10 的 API 规范展开)。
追问:- 追问:static 函数和 inline 函数在这个规则上有什么不同?(static 内部链接可放定义;inline 有特殊规则)
评分要点:- 声明/定义区分
- 多重定义问题
- 头文件职责
关联章节:1.3.3 函数与递归
从 .c 源文件到可执行文件经历了哪些阶段?每个阶段的产物是什么?高频C/C++ · medium
要点:预处理(.i)→ 编译(.s)→ 汇编(.o)→ 链接(可执行文件);头文件与宏在预处理阶段展开消失。
预处理:展开 #include/宏、处理条件编译,产出 .i(gcc -E);编译:把 .i 翻译成汇编,产出 .s(gcc -S); 汇编:把汇编翻译成机器码目标文件 .o,含符号表(gcc -c);链接:把多个 .o 与库合并、解析符号引用 (undefined reference 就出在这里),产出可执行文件。补充亮点:头文件在预处理后就不存在了—— 所以改头文件必须重新编译所有包含它的 .c(工程上用构建系统处理依赖,阶段 2.1 展开)。
追问:- 追问:-E/-S/-c 分别让 gcc 停在哪一步?
- 追问:改了一个头文件,为什么多个 .c 都要重新编译?
评分要点:- 四阶段名称与顺序
- 各阶段产物(.i/.s/.o/可执行)
- 链接阶段职责(符号解析)
关联章节:1.1.1 GCC 编译四阶段与常用选项
undefined reference to 'xxx' 是什么错误?如何排查?高频C/C++ · medium
要点:链接阶段的符号缺失:有声明/引用但找不到实现。排查实现文件是否参与编译、库是否链接、-l 顺序、C/C++ 混编名字修饰。
编译阶段只检查语法与声明,链接阶段才把"对 xxx 的引用"与"xxx 的实现"对上号;对不上就报 undefined reference。排查顺序:① 实现所在的 .c 是否被一起编译/链接;② 是否忘了链接库 (如数学库 -lm)或 -l 顺序不对(被依赖的库要放后面);③ C/C++ 混编时 C 函数被 C++ 名字修饰 (extern "C" 解决);④ 声明与定义签名不一致(如参数个数)。可以顺带提 nm 查看 .o 的符号表。
追问:- 追问:为什么 C 与 C++ 混编容易报 undefined reference?
- 追问:nm 命令能帮什么忙?
评分要点:- 阶段归属(链接而非编译)
- 至少两类排查手段
- extern "C" 加分
关联章节:1.1.1 GCC 编译四阶段与常用选项
-O0 与 -O2 有什么区别?开发调试和发布分别应该怎么选?C/C++ · easy
要点:-O0 不优化、编译快、便于调试;-O2 开启大量优化、程序更快但与源码对应关系弱;调试用 -O0 -g,发布用 -O2。
-O0 关闭优化,机器码与源码一一对应,断点/单步行为直观,适合开发调试(配合 -g 带调试信息); -O1/-O2/-O3 逐级开启更多优化(常量折叠、死代码消除、内联、向量化等),代码更快但可能被重排、 变量被优化掉,调试体验变差。关键点:优化不改变符合标准的程序的语义——如果开 -O2 后结果变了, 优先怀疑程序依赖了未定义行为(如未初始化变量、越界)。工程实践:Debug 用 -O0 -g,Release 用 -O2 (嵌入式资源受限场景还会配合 -Os 减小体积)。
追问:- 追问:为什么开 -O2 后某个变量在调试器里看不见了?
- 追问:-Os 与 -O2 的区别?
评分要点:- O0/O2 的行为差异
- 调试与发布的选择
- 优化不改语义 + 未定义行为意识
关联章节:1.1.1 GCC 编译四阶段与常用选项
线上程序崩溃了,现场没有调试器,你怎么定位?高频C/C++ · medium
要点:用 core dump:配置 ulimit -c 后崩溃会留 core 文件,gdb ./prog core 回放现场,bt 看崩溃调用栈。
核心思路是"事后回放":崩溃时内核把进程内存现场写成 core 文件,之后任何时间用 gdb ./prog core 打开,bt 就能看到崩溃时的完整调用栈(等价于崩溃瞬间按下暂停)。 前提:① 编译发布版也要带 -g(调试信息可单独剥离给符号服务器,不影响性能); ② ulimit -c unlimited 或按发行版配置 core_pattern。没有 core 时退而求其次: 日志 + 复现 + 二分注释定位。Windows 对应机制是 WER/minidump,思路相同。
追问:- 追问:-g 会影响发布版性能吗?(调试信息在独立段,不参与执行,不影响运行速度)
评分要点:- core dump 机制与 gdb 回放
- ulimit 配置
- 无 core 时的降级方案
关联章节:1.1.6 GDB 调试实战
段错误(SIGSEGV)的定位思路是什么?高频C/C++ · medium
要点:复现 → gdb run 停在崩溃点 → bt 从栈顶往下找第一帧自己的代码 → 检查指针(NULL/野指针/越界)与数组下标。
段错误 = 访问了不该访问的内存。标准流程:① 在 gdb 里 run 复现,崩溃时 gdb 自动暂停并显示 SIGSEGV 与行号;② bt 看调用栈——栈顶几帧可能是库函数,从栈顶往下找第一帧"自己写的函数" 才是问题入口;③ 在那里 print 相关指针与下标:NULL?已 free?越界?未初始化? 常见根因:解引用 NULL/野指针、数组越界写坏返回地址、栈溢出(大局部数组)、double free。 本章 ex1/ex2 就是这套流程的最小演示(store 里对 NULL 解引用,bt 精确到行)。
追问:- 追问:为什么数组越界有时不是立刻崩溃?(写坏相邻内存,可能到函数返回时才炸)
评分要点:- 复现与 bt 定位流程
- 栈顶找自己代码
- 常见根因归类
关联章节:1.1.6 GDB 调试实战
调试版和发布版编译选项有什么不同?-O2 会给调试带来什么麻烦?C/C++ · medium
要点:调试用 -O0 -g(不优化+调试信息);发布用 -O2(深度优化)。-O2 会导致断点行号漂移、变量显示 <optimized out>,单步顺序与源码不一致。
优化会重排/合并/消除代码:行号不再与源码一一对应(断点打在循环内却停在函数末尾,本章 ex3 实测),中间变量被常量折叠后连内存位置都没有(print 显示 <optimized out>)。 工程实践:开发构建 -O0 -g 或 -Og(为调试友好的轻优化,GCC 提供);发布构建 -O2 (必要排查时也带 -g,调试信息不影响运行性能)。回答要点:选项组合 + 两个具体症状 + 对策。
追问:- 追问:-Og 是什么?(GCC 专为调试设计的优化级别,性能损失小、变量基本可看)
评分要点:- -O0 -g vs -O2 组合
- 行号漂移与 optimized out 两个症状
- 发布版带 -g 的工程常识
关联章节:1.1.6 GDB 调试实战
为什么 Makefile 的命令行必须以 Tab 开头?高频C/C++ · easy
要点:历史语法设计:make 用行首 Tab 区分"命令行"与"规则/变量等其他行",空格缩进会报 missing separator。
Makefile 语法里,规则由"目标: 依赖"行和后续命令行组成,make 靠行首 Tab 判断哪些行属于 该规则的命令(变量赋值、include、目标行等都不以 Tab 开头)。这是 1970 年代延续至今的 设计,也是新手最高频的错误:编辑器把 Tab 替换成空格后报 makefile:4: *** missing separator. Stop.(本机实测错误信息)。实践:编辑器关闭"Tab 转空格", 或用支持 Makefile 模式的编辑器高亮检查。
追问:- 追问:.RECIPEPREFIX 可以改这个字符吗?(GNU make 可以,但极少用,可提一句加分)
评分要点:- Tab 区分命令行与其他行
- missing separator 报错
make 的增量编译原理是什么?有什么局限?C/C++ · medium
要点:比较目标文件与依赖文件的时间戳,目标比所有依赖新就跳过;局限:依赖没写全会漏编、时间戳被回拨会误判、跨目录递归要正确传递依赖。
原理:对每条规则,make 比较目标与依赖的修改时间,只要有一个依赖比目标新就执行命令重建, 否则跳过(Nothing to be done)。局限:①依赖关系必须写全——改了头文件没写进依赖行就不会 重编,需 -MMD 自动生成依赖;②时间戳语义——系统时间回拨或时钟同步会导致漏编/误编; ③伪目标冲突——clean 这类不产文件的目标要 .PHONY,否则同名文件会让它 up to date; ④分布式构建需要更精确的依赖图(这也是 CMake/Ninja 出现的原因之一)。
追问:- 追问:为什么改了系统时间可能不重编?(时间戳比较失去单调性)
评分要点:- 时间戳比较机制
- 依赖缺失/时间回拨/伪目标三类局限
make 与直接写 shell 脚本构建、以及 CMake,有什么区别?各适合什么场景?高频C/C++ · medium
要点:shell 脚本顺序全量执行、无依赖分析;make 有依赖图与增量编译但工程大时手写维护难;CMake 是更上层的工程描述,生成 Makefile/Ninja 等后端,适合跨平台与大型工程。
三个层次:①shell 脚本——按顺序把命令跑一遍,最简单,但没有"只重编改动部分"的能力, 全量编译浪费时间;②Makefile——声明目标与依赖,make 自动推导增量编译、支持 -j 并行, 适合中小型 C/C++ 工程;③CMake——用 CMakeLists.txt 描述目标、依赖、选项,再生成 Makefile/Ninja/VS 工程,跨平台与依赖查找能力强,是现代大型工程主流。 面试要点:说明三者不是替代关系而是抽象层次递进,能举出"多文件手动 gcc → Makefile → CMake"的演进路径,并知道 make 仍是嵌入式 SDK(如 Linux 内核、U-Boot)的常见构建入口。
追问:- 追问:内核为什么还用 Makefile?(递归 make 与 kbuild 体系的历史与定制需求)
评分要点:- 依赖分析与增量能力
- 三者层次关系
- 场景选择
运算符优先级和求值顺序(evaluation order)是一回事吗?高频C/C++ · medium
要点:不是。优先级/结合性决定表达式结构(谁先与谁结合);求值顺序决定操作数"何时被求值",而函数调用等求值顺序多数是未指定的。
优先级回答"结构问题":a + b * c 中 * 先与 b、c 结合;结合性回答"同级谁先结合"(赋值右结合)。 求值顺序回答"副作用先后":f() + g() * h() 中 f/g/h 谁先被调用,标准未指定(C11 只有少数 序列点/顺序保证:&&、||、?:、逗号等)。面试关键句:优先级≠求值顺序;想控制顺序就拆行。 更危险的是 UB:i++ + i++ 同一表达式多次修改同一变量且无序列点,标准不保证任何结果。
追问:- 追问:哪些运算符保证求值顺序?(&&、||、?: 只求值被选分支、逗号从左到右)
评分要点:- 优先级与求值顺序的区分
- 未指定 vs 未定义行为
- 保证顺序的少数运算符
关联章节:1.2.3 运算符与优先级
短路求值(short-circuit)是什么?写一个实战中用它的例子。高频C/C++ · easy
要点:左边为假、|| 左边为真时,右边不再求值。实战例子:p != NULL && *p > 0(先判空再解引用)。
短路是 && 与 || 的语言级保证:结果已定即停。它的工程价值在于"安全前置检查": p != NULL && *p > 0(p 为空时右边不执行,不会解引用空指针); i < n && arr[i] > 0(越界保护)。注意 & 和 | 没有短路(两边都求值), 把 & 当 && 用是常见错误(既算错语义又失去保护)。副作用函数(f() && g())依赖短路时要注意可读性。
追问:- 追问:为什么位与 & 与逻辑与 && 不能混用?(位与 vs 逻辑与;无短路 vs 短路)
评分要点:- 短路语义
- 安全前置检查实例
- 位与(&)与逻辑与(&&)的区别
关联章节:1.2.3 运算符与优先级
嵌入式代码里常写 SET_BIT(reg, 3) 这类宏,解释它的实现和注意点。高频嵌入式 · medium
要点:SET_BIT(reg,n) = (reg) | (1u << (n)):1u 左移 n 位后与 reg 相或。注意参数必须加括号、用 1u 不用 1(符号位/位宽问题)。
位操作宏四件套:SET_BIT=(reg)|(1u<<(n));CLEAR_BIT=(reg)&~(1u<<(n));TOGGLE_BIT=(reg)^(1u<<(n)); IS_SET=((reg)>>(n))&1u。三个注意点:① 宏参数与整体都要加括号(否则 SET_BIT(x, a+b) 展开出错); ② 用 1u 而非 1——int 符号位(第 31 位)左移是 UB,unsigned 才干净; ③ 移位位数 0 ≤ n < 位宽(超出是 UB)。寄存器操作常配合 volatile(1.2.2)使用。
追问:- 追问:CLEAR_BIT 为什么是 & ~(...)?
评分要点:- 四宏实现
- 括号纪律
- 1u 与位宽边界
关联章节:1.2.3 运算符与优先级
常量指针与指针常量的区别?高频C/C++ · easy
要点:const 在 * 左侧锁数据(常量指针),在 * 右侧锁指针本身(指针常量),两侧都有则全锁。
`const int *p`:p 可以重新指向别的变量,但不能通过 p 修改它所指向的数据——适合"只读遍历"(如函数参数加 const 保护); `int *const p`:p 一旦指向某个变量就不能再指向别处,但可以通过 p 修改数据——适合"固定句柄"(如硬件寄存器地址); `const int *const p`:两者都不可改。记忆口诀:const 离谁近就锁谁。
追问:- 追问:给函数传结构体指针时为什么要加 const?
- 追问:const int *p 与 int const *p 等价吗?(等价,const 位置在 * 左侧即修饰数据)
评分要点:- 两种写法各给出正确声明代码
- 说出 const 在 * 左/右侧的锁定对象
- 各举一个应用场景
关联章节:1.3.1 指针基础与 const 指针
野指针是怎么产生的?如何避免?高频C/C++ · easy
要点:未初始化、free 后未置空、越界访问都会产生野指针;对策是初始化、判空、free 后置 NULL。
野指针 = 指向未知/无效地址的指针。三大来源:①定义后未初始化就使用;②free(p) 后继续使用(悬挂指针); ③数组越界导致指针指向非法区域。解引用野指针是未定义行为——可能崩溃、可能读到垃圾值、也可能"看起来正常"(原讲义 "*p 可以查看"的表述已被本课程勘误,见 err-7.3-11)。避免方法:定义即初始化(NULL 或有效地址)、使用前判空、 free 后立即置 NULL、不保存指向栈上临时变量的指针。
追问:- 追问:free(p) 后把 p 置 NULL 为什么能防重复 free?
评分要点:- 说出至少两种成因
- 强调未定义行为(读也不行)
- 给出至少两条工程对策
关联章节:1.3.1 指针基础与 const 指针
sizeof(指针) 是多少?为什么?C/C++ · easy
要点:与平台地址宽度相关:64 位系统通常 8 字节、32 位系统 4 字节,与指针指向的类型无关。
指针变量里存的是地址,所以指针的大小由"地址需要多少位"决定,而不是由指向的数据类型决定。 因此 `sizeof(int*) == sizeof(char*) == sizeof(void*)`。回答时务必加上平台前提(32/64 位), 并说明应该用 sizeof 实测而不是硬编码 8 或 4。
评分要点:- 说出平台相关性(32 位 4 字节 / 64 位 8 字节)
- 说明与指向类型无关
关联章节:1.3.1 指针基础与 const 指针
int *p[3] 和 int (*p)[3] 有什么区别?各自用在什么场景?高频C/C++ · medium
要点:前者是"指针数组"(3 个 int* 元素);后者是"数组指针"(指向 int[3] 的一个指针)。前者用于存储多个字符串/对象地址(如 argv);后者用于以行遍历二维数组。
结合优先级决定类型:`int *p[3]` 中 `p[3]` 先结合 → p 是数组,元素为 int*(常用于字符串数组、指针表); `int (*p)[3]` 中 `(*p)` 先结合 → p 是指针,指向"3 个 int 的数组",p+1 步长为 sizeof(int[3]), 正好一行——因此二维数组 int a[N][3] 可以直接赋给 int (*p)[3] 逐行遍历。 判别口诀不背也罢,把握"从内向外读类型 + 步长验证"(sizeof(*p))。
追问:- 追问:二维数组 int a[2][3] 的数组名 a 是什么类型?(int (*)[3],即数组指针)
评分要点:- 两种类型准确定义
- 应用场景各举一例
关联章节:1.3.2 指针与数组/字符串/多级指针
函数里想修改调用者传入的指针本身(例如重新分配内存),应该怎么做?为什么只传指针不行?高频C/C++ · medium
要点:参数用二级指针(如 void init(int **pp)),调用传 &p;因为 C 参数一律传值,传指针只是复制了地址值,改不了调用者的指针变量本身。
C 只有传值:形参是实参的副本。传 int *p 时,函数内 p = 新地址 只是改副本, 调用者变量不变(常见于"在函数里 malloc 后返回给调用者"失败场景)。 正确做法:参数 int **pp,函数内 *pp = 新地址(解引用一级得到调用者的指针变量本身), 调用传 &p。或者返回新指针让调用者赋值。注意表述:这不是"引用传递", 而是"把指针的地址复制进形参,间接修改所指的指针变量"。
追问:- 追问:用返回值方案和二级指针方案各有什么取舍?(返回值单一但直观;二级指针可同时返回状态码)
评分要点:- 传值本质讲清
- 二级指针方案与返回值替代方案
关联章节:1.3.2 指针与数组/字符串/多级指针
数组名和指针变量有什么区别?哪些场合数组名不会退化为指针?高频C/C++ · medium
要点:数组名是"数组对象"的名字(首元素地址的常量,不可赋值、sizeof 得到整个数组大小);指针是变量。sizeof(a)、&a、字符串字面量初始化等场合不衰减。
三点区别:① 数组名不可作左值(不能 a = 新地址),指针变量可以;② sizeof(数组名) 是整个数组 字节数,sizeof(指针) 是指针本身大小;③ &a 的类型是"指向整个数组的指针"(int (*)[N]), 数值上与 &a[0] 相同但类型不同。例外场合:sizeof(a)、&a、_Alignof、字符串字面量初始化 char s[]="..." 等处不发生转换;函数形参里 int a[] 等价 int *a(衰减不可避免)。
追问:- 追问:&a 与 &a[0] 数值相同,那写 &a+1 会发生什么?(步长是整个数组)
评分要点:- 不可赋值/sizeof/& 三区别
- 不衰减的例外场合
关联章节:1.3.2 指针与数组/字符串/多级指针
宏与函数有什么区别?使用函数式宏有什么风险?高频C/C++ · medium
要点:宏是预处理阶段的文本替换、无类型检查、参数可能多次求值;函数是运行时调用。风险:优先级错误、副作用、无类型安全、难以调试。
宏在预处理阶段把调用处替换成宏体(-E 可见),不产生调用开销、还能做字符串化(#)与拼接(##); 函数在运行时压栈调用、参数只求值一次、有类型检查。宏的风险:① 不加括号导致优先级错误 (SQUARE(2+3) 反例);② 参数带副作用被多次求值(MAX(x++,y));③ 多语句宏破坏 if/else 结构 (需要 do-while(0));④ 无类型检查、调试器里看不到宏。现代 C/C++ 常用 static inline 函数 替代函数宏,兼顾零开销与类型安全;字符串化/拼接仍只能靠宏。
追问:- 追问:宏真的比函数快吗?(inline 与编译器内联的权衡)
- 追问:哪些场景仍然必须用宏?
评分要点:- 文本替换 vs 运行时调用的本质区别
- 至少两类风险(优先级/副作用/结构破坏)
- inline 替代意识
关联章节:1.1.2 预处理指令深入
包含多条语句的宏为什么要用 do { ... } while (0) 包裹?高频C/C++ · medium
要点:保证宏在任何使用位置(尤其 if/else)都作为单条语句展开,避免悬空 else 与多余分号。
宏是纯文本替换。若宏体是 { stmt1; stmt2; },调用处 if (cond) LOG(x); 展开后变成 if (cond) { ... }; ——结尾多出的分号把 if 语句截断,后面的 else 找不到配对的 if,直接编译报错 (悬空 else)。do { ... } while (0) 展开后是"一条完整的语句",后跟分号恰好结束,任何位置 (if/else、for 体内)都安全;且 while(0) 保证只执行一次、编译器优化后零开销。这是内核与 各大库的标准惯用法,也是本课程 ex1/ex2 宏的写法。
追问:- 追问:写出不用 do-while(0) 时 if/else 悬空的具体展开结果。
- 追问:do { } while (0) 会有运行时开销吗?
评分要点:- 文本替换导致的结构破坏场景
- 悬空 else / 多余分号的展开示例
- 零开销与惯用法
关联章节:1.1.2 预处理指令深入
#pragma once 与 #ifndef 头文件守卫有什么区别?各自优缺点?C/C++ · easy
要点:两者都防重复包含;#pragma once 简洁但非标准(编译器扩展),#ifndef 是标准写法、可移植但需要手动起唯一宏名。
#ifndef X_H / #define X_H / #endif 是 C 标准方式:第一次包含时定义守卫宏,后续包含被条件编译 跳过,所有编译器一致支持,缺点是守卫宏名靠人起(重名/复制粘贴错误会导致头文件被跳过); #pragma once 由编译器记住文件身份直接跳过,写起来简单、不易出错,且能处理符号链接等边界, 但它不在标准里(MSVC/GCC/Clang 均支持),极端可移植场景有人避开。工程实践:两害相权, 追求最大可移植性用 #ifndef,内部项目用 #pragma once 或两者都写(很多大型项目即如此)。
追问:- 追问:为什么大型项目经常两种都写?
- 追问:守卫宏名撞车会发生什么?
评分要点:- 两者防重复包含的原理
- 标准 vs 扩展的取舍
- 守卫宏名唯一性问题
关联章节:1.1.2 预处理指令深入
scanf 的返回值有什么用?输入残留(缓冲区里遗留的换行符)会造成什么现象?高频C/C++ · medium
要点:返回值 = 成功匹配的项数,用于判断读取成败;输入残留指上次读取后留在缓冲区的内容(如 \n)被后续 %c/gets 读走,产生"跳过输入"现象。
scanf 返回成功匹配并赋值的项数:scanf("%d %d", &a, &b) 返回 2 才是完全成功; 返回 0 说明格式不匹配(且输入未被消费,仍留在缓冲区);返回 EOF 说明输入结束。 输入残留的典型场景:scanf("%d") 读走数字后,行尾 \n 还在缓冲区, 紧接着的 scanf("%c") 会直接把这个 \n 读走——用户感觉"跳过了输入"。 解决:%c 前加空格(" %c" 跳过空白);或改用 fgets 整行读取再解析(工程推荐)。
追问:- 追问:scanf("%d") 遇到 'abc' 会发生什么?(返回 0,'abc' 残留,死循环风险)
- 追问:为什么更推荐 fgets + sscanf?(行语义清晰、不依赖缓冲状态)
评分要点:- 返回值语义
- 残留机理与现象
- fgets+sscanf 的工程替代
sprintf 和 snprintf 有什么区别?生产代码为什么用 snprintf?高频C/C++ · medium
要点:sprintf 无边界检查,目标小于内容即缓冲区溢出;snprintf 受 n 限制、必写 \0,且返回值是"本应写入"长度可判断截断。
sprintf(dst, fmt, ...) 不知道 dst 的容量,内容超出即越界写——与 gets 同类风险。 snprintf(dst, n, fmt, ...) 最多写 n-1 个字符并保证结尾 \0;返回值是"若空间充足本应写入" 的字符数(不含 \0):返回值 >= n 即说明发生截断,可据此做错误处理或扩容。 生产代码一律用 snprintf(或 asprintf 等带边界的变体);sprintf 仅在被证明安全的 固定缓冲区场景谨慎使用。加分项:提到 -Wformat-truncation 编译告警能在编译期拦截部分截断。
追问:- 追问:snprintf 返回负值是什么情况?(编码错误;POSIX 另有具体 errno)
评分要点:- sprintf 无边界 → 溢出
- snprintf 三特性(限长/必 \0/返回值语义)
- 工程结论
printf("%.2f", f) 输出的是浮点数的精确值吗?为什么?高频C/C++ · medium
要点:不是。二进制浮点不能精确表示多数十进制小数,%.2f 只是按四舍五入规则显示的近似值。
double 是 IEEE 754 二进制浮点:0.1、0.2 等十进制小数在二进制中是无限循环小数, 存储时被舍入为最接近的可表示值。printf 的 %f 系列只是把这个近似值再按 十进制格式化显示(%.2f = 四舍五入保留两位)。所以 3.14159 显示为 3.14 是 显示规则的结果,不是"精确到两位"。工程上金额等场景要用定点整数(分)或 十进制库,比较浮点用容差(fabs(a-b) < epsilon)而不是 ==。
追问:- 追问:为什么 0.1 + 0.2 不等于 0.3?(二进制舍入误差的经典演示)
评分要点:- 二进制浮点的表示局限
- 显示舍入 vs 存储精度
- 工程对策(定点/容差)
static 在 C 里有哪几种用法?各自的含义是什么?高频C/C++ · medium
要点:三种:static 局部变量(静态存储期、保持值);static 全局变量(内部链接、文件私有);static 函数(内部链接、模块私有)。
static 修饰局部变量:改变存储期——从自动变为静态,只初始化一次,函数退出后值保留,作用域仍是块内; static 修饰全局变量:改变链接属性——从外部链接变为内部链接,只有本文件可见,可用于模块私有状态; static 修饰函数:同样改变链接属性为内部链接,函数只在定义它的文件内可用,是实现模块封装的常规手段。 面试要点:能区分"改存储期"与"改链接属性"两种情况,而不是笼统说"静态的"。
追问:- 追问:一个 static 局部变量的初始化语句执行几次?(一次;语义上在第一次执行到该语句时初始化,之后跳过)
评分要点:- 三种用法全覆盖
- 区分存储期与链接属性
extern 声明和变量定义有什么区别?多文件共享一个全局变量的正确姿势是什么?高频C/C++ · medium
要点:extern 声明不分配存储,只告诉编译器"定义在别处";定义分配存储且只能有一处。正确姿势:头文件放 extern 声明,定义只写在一个 .c。
extern int x; 是声明:引入一个外部链接标识符,不产生存储;int x = 0; 是定义:分配存储并初始化。 多文件共享的工程姿势:shared.h 里写 extern int x;(可被任意多个 .c 包含),只在某一个 .c 里写 int x = 0;。 反例:把定义放进头文件,被两个 .c 包含后链接期报 multiple definition of 'x'(本章 ex3 实测)。 加分:static 与 extern 互补——不想共享的模块内状态用 static 限制可见性,必须共享的才用 extern。
追问:- 追问:函数声明需要 extern 吗?(不需要,函数原型默认外部链接;static 函数才需要特别说明)
评分要点:- 声明 vs 定义
- 头文件放置规则
- 多重定义报错
作用域(scope)和生命周期/存储期(storage duration)有什么区别?举一个两者不一致的例子。高频C/C++ · medium
要点:作用域是标识符可见的代码范围,存储期是对象存活的时长;static 局部变量就是典型:作用域只在函数内,但存储期是整个程序。
作用域回答"在哪里能写出这个名字"(块/文件/函数原型);存储期回答"对象从创建到销毁持续多久" (自动:进入块创建、离开块销毁;静态:程序启动创建、程序结束销毁)。两者正交: 函数内的 static int count 作用域仍是该函数块内(外面访问不到),但存储期是静态的 (函数返回后对象仍存在、值保留)。反之,自动局部变量两者一致:作用域与存储期都限于块。 面试加分:再提分配存储期(malloc)与线程存储期(_Thread_local)作为第三、四种。
追问:- 追问:把 static 局部变量挪到函数外面去,行为上等价吗?(基本等价于改名前缀的全局变量,但可见范围不同——挪出去后整个文件可见)
评分要点:- 两个概念定义准确
- static 局部变量的正交例子
静态库和动态库有什么区别?分别适用于什么场景?高频C/C++ · medium
要点:静态库编译时整体拷入可执行文件,运行时自包含但体积大、更新需重编;动态库运行时按需加载共享,体积小、可热更新,但存在版本依赖与找不到库的问题。
四个维度对比:①体积——静态库把代码拷进每个可执行文件,动态库多程序共享一份;②更新—— 静态库更新必须重新链接所有使用方,动态库替换 .so 即可(ABI 兼容前提下);③启动速度—— 静态链接启动略快(少一次动态解析与重定位),动态链接首次加载需解析符号;④部署——静态 自包含单文件好分发,动态需保证目标机有对应版本的库(版本地狱)。 场景:核心高频小模块、嵌入式受限环境倾向静态;系统级公共库(libc/libm)、插件机制、 需要热更新的场景用动态。嵌入式补充:交叉编译时静态链接更省事,避免目标板库版本不匹配。
追问:- 追问:什么是 ABI 兼容?(改实现不改接口与布局)
- 追问:链接时 -ladd 同时存在 libadd.a 与 libadd.so 会选哪个?(默认优先动态)
评分要点:- 四维度对比齐全
- 运行时依赖/版本问题
- 场景选择有依据
关联章节:1.1.3 静态库与动态库
程序启动报 error while loading shared libraries: libadd.so: cannot open shared object file,怎么排查与解决?高频C/C++ · medium
要点:先用 ldd 确认缺失的库与已解析路径;再按三方案解决:LD_LIBRARY_PATH 临时指定、/etc/ld.so.conf.d + ldconfig 系统级、安装到 /usr/local/lib 约定路径。
区分两个阶段:链接时找不到库报 cannot find -ladd(用 -L 解决),运行时找不到才是本题症状, 说明动态链接器(ld.so)在运行时搜索路径里没找到 .so。排查:ldd ./app 列出依赖库与实际 解析路径(not found 即缺失)。解决三方案:①临时——export LD_LIBRARY_PATH=$PWD(注意 变量名写全 PATH 后缀);②系统级——把路径写入 /etc/ld.so.conf.d/xxx.conf 后 sudo ldconfig 刷新缓存;③约定路径——安装到默认搜索路径之一如 /usr/local/lib 再 ldconfig。 面试加分:提到 ldd、ldconfig -p 查看缓存、readelf -d 查看 RPATH/RUNPATH。
追问:- 追问:LD_LIBRARY_PATH 对链接阶段生效吗?(不,只对运行时)
- 追问:-rpath 是什么?(把运行搜索路径烧进 ELF)
评分要点:- 区分链接时/运行时
- ldd 排查
- 三方案
关联章节:1.1.3 静态库与动态库
gcc 链接参数 -L 和 -l 有什么区别?C/C++ · easy
要点:-L 指定库搜索路径(目录),-l 指定库名(自动补 lib 前缀与 .a/.so 后缀);只有 -l 没有对应 -L 时链接器在默认路径找,找不到报 cannot find -lxxx。
-L<dir> 是"去哪找",可多次出现;-l<name> 是"找哪个",按命名约定展开为 lib<name>.a 或 lib<name>.so。默认搜索路径不含当前目录,所以本机演示必须 -L.。另两个易混点:-I 是头文件 路径(编译期),与链接无关;静态库链接时 -l 的顺序有讲究——被依赖的库要放在引用它的 目标文件之后(本机实测:-ladd 放 main.c 前会报 undefined reference to 'add')。
追问:- 追问:为什么默认不搜当前目录?(安全与可重现构建考虑)
评分要点:- -L 路径 / -l 库名区分
- lib 前缀后缀映射
- -I 易混点
关联章节:1.1.3 静态库与动态库
为什么 gets() 危险?它的正确替代是什么?高频C/C++ · medium
要点:gets 无法指定长度,超长输入必然缓冲区溢出;C11 已移除。替代:fgets(buf, sizeof(buf), stdin) 并处理其保留的换行符。
gets 只有一个参数(缓冲区指针),写入长度完全由输入决定:输入超过缓冲区容量时继续越过边界 写入相邻内存(其他局部变量、返回地址),即缓冲区溢出——历史上最著名的安全漏洞类型之一 (1988 年 Morris 蠕虫利用的正是 gets)。C11 已将其从标准移除。替代方案 fgets(buf, n, stdin) 最多读 n-1 个字符并保留行尾换行符,写入受 sizeof(buf) 限制;使用后按需去掉换行符。 加分项:提到 snprintf 的截断判断、以及 strncpy 不保证 \0 的坑。
追问:- 追问:fgets 和 gets 在换行符处理上有什么区别?(fgets 保留 \n,gets 丢弃)
- 追问:如果必须用可变长度读行怎么办?(POSIX 的 getline,或循环 fgets 拼接)
评分要点:- 无长度检查 → 溢出机理
- fgets 替代与换行处理
- C11 移除的事实
关联章节:1.2.6 字符串与安全函数
strncpy 安全吗?它有什么坑?高频C/C++ · medium
要点:不完全安全:src 长度达到 n 时不写结尾 \0,目标可能不是合法字符串,后续按字符串使用会越界读。
strncpy(dst, src, n) 复制最多 n 个字符:若 src 长度 >= n,则不会写入结尾 \0, dst 就不再是合法 C 字符串——后续 strlen/printf("%s") 会一路读到越界为止。 此外若 src 比 n 短,strncpy 会用 \0 填满剩余部分(性能问题)。安全的替代: ① 先保证 dst 有足够空间再 strcpy;② snprintf(dst, n, "%s", src)(必写 \0,还能判断截断); ③ 用 strlcpy(BSD 系,非标准)。如果用了 strncpy,务必手动补 dst[n-1] = '\0'。
追问:- 追问:snprintf 和 strncpy 在截断判断上有什么不同?(snprintf 返回值即'本应写入'长度)
评分要点:- \0 不保证的机理
- 手动补 \0 或换 snprintf
- 填零的性能副作用
关联章节:1.2.6 字符串与安全函数
strlen 和 sizeof 在字符串上下文里有什么区别?高频C/C++ · easy
要点:strlen 是运行期函数,返回到结尾 \0 为止的字符数(不含 \0);sizeof 是编译期运算符,返回对象占用的字节数(含 \0 与未用空间)。
char s[10] = "hello":strlen(s) = 5(运行时数到 \0),sizeof(s) = 10(数组声明大小)。 典型错误:用 sizeof(s) 判断字符串长度(把未用空间算进去);对指针 char *p,sizeof(p) 是指针大小(64 位系统 8 字节)而不是字符串长度——数组退化为指针后 strlen 才能得到长度。 另外中文按 UTF-8 编码时每个汉字 3 字节,strlen 数的是字节数不是"字符数"。
追问:- 追问:sizeof("abc") 是多少?(4:字符串字面量含结尾 \0)
评分要点:- 运行期 vs 编译期
- 指针与数组的 sizeof 区别
- 字节数与字符数(UTF-8)
关联章节:1.2.6 字符串与安全函数
为什么 sizeof(struct) 常常大于各成员 sizeof 之和?能用什么手段观察和改变它?高频C/C++ · medium
要点:因为内存对齐:成员按自身对齐要求摆放在整数倍地址上,产生填充字节;用 offsetof 观察各成员偏移,用
每个类型有对齐要求(如本机 int 对齐 4),成员地址必须是其对齐值的整数倍,成员之间与 结构体末尾会插入填充字节(padding),因此 sizeof(struct) 可能大于成员之和 (如 char+int+char 本机为 12 而非 6)。观察手段:offsetof 打印每个成员的偏移。 改变手段:#pragma pack(push,1) 取消填充(体积变小),但可能造成非对齐访问—— 性能下降、部分平台异常、可移植性降低,只在网络报文/寄存器映射等明确布局场景使用。 注意对齐规则是实现定义的,不同平台数值不同,不能假设。
追问:- 追问:为什么对齐后访问更快?(CPU 按对齐字长取数,跨边界要两次取数或直接异常)
评分要点:- 对齐与填充成因
- offsetof 与 pack 两种手段及代价
- 实现定义与不可假设
关联章节:1.3.6 结构体与位段
位段(bit-field)适合做什么?它的可移植性边界在哪里?高频C/C++ · medium
要点:适合寄存器映射与协议字段(按位打包);布局(位顺序/跨单元/填充)是实现定义的,跨平台不可假设。
位段用 unsigned int 把多个小字段打包进一个存储单元,适合"8 位寄存器:1 位使能+2 位模式 +4 位分频+1 位保留"这类硬件寄存器映射,以及协议头部的位字段。 可移植性边界:C11 只保证类型限制与"按声明顺序分配存储",位顺序(低位在前还是高位在前)、 是否允许跨存储单元、填充都是实现定义的(err-7.3-13 修正了"最多 31 位"的表述——那是某 实现的限制)。工程做法:映射寄存器前用已知值自检本平台位顺序;或用移位+掩码宏显式表达, 完全摆脱实现定义;位段结构体不用于序列化传输。
追问:- 追问:为什么用 unsigned int 位段而不是 int?(避免符号位参与造成意外与实现差异)
评分要点:- 寄存器映射用途
- 实现定义清单(位顺序等)
- 自检或移位掩码替代方案
关联章节:1.3.6 结构体与位段
函数参数用结构体传值和传指针有什么区别?分别适合什么场景?高频C/C++ · easy
要点:传值整体拷贝(大结构体代价高、函数内修改不影响实参);传指针只拷贝地址(省拷贝、可改实参、有被修改风险)。
传值:实参被完整复制进函数栈帧,函数内修改的是副本,调用方不受影响;结构体越大拷贝越贵。 传址:只传一个指针(4/8 字节),函数内通过 -> 修改的是实参本身;代价是"谁都能改"—— 只读场景应加 const(const struct X *p)表达"不改"并防误改。 选型:小结构体(如点坐标)传值简单直观;大结构体/需要修改实参/嵌入式栈空间紧张时传指针; 只读大结构体传 const 指针是默认选择。另外注意:传值是浅拷贝——内含指针成员时, 只复制指针本身不复制指向的数据(配合 1.3.9 动态内存理解深浅拷贝)。
追问:- 追问:结构体含指针成员时,传值会复制指向的数据吗?(不会,浅拷贝只复制指针)
评分要点:- 拷贝代价对比
- 修改语义与 const
- 浅拷贝提示
关联章节:1.3.6 结构体与位段
union 和 struct 有什么区别?union 适合哪些场景?高频C/C++ · easy
要点:struct 成员各占内存、同时有效;union 成员共享同一块内存(大小=最大成员)、任一时刻只有一个成员有效。union 适合寄存器双视角、字节序判断、协议字段多形态解析等字节级重解释场景。
struct:成员顺序存放、各自独立,sizeof 为各成员之和(加对齐填充),所有成员同时有效。 union:所有成员从同一地址开始共享内存,sizeof 等于最大成员的大小(含对齐), 任一时刻至多一个成员有值(读非最后写入成员是实现定义行为)。 典型场景:① 硬件寄存器以"整体 32 位 + 分字段"双视角访问;② 字节序自检 (写整型读字节数组);③ 协议包"同一段字节按不同类型解析"。 注意可移植性:跨平台严格代码用 memcpy 逐字节替代(见本章 ex2/ex3)。
追问:- 追问:为什么说 union 是省内存而不是多存数据?(共享而非叠加)
评分要点:- 存储模型区别(叠加 vs 共享)
- 至少一个典型场景
- 实现定义/可移植性提示
关联章节:1.3.7 共用体与大小端实战
什么是大小端?怎么用 C 判断本机字节序?高频C/C++ · medium
要点:大小端是多字节整数在内存中的字节排列方向;写 0x11223344 后读首字节(0x44 小端 / 0x11 大端)即可判断。
大小端(endianness):大端=低地址存高位字节("从左到右",网络字节序);小端=低地址 存低位字节("从右到左",x86/ARM 默认)。判断:union { unsigned int u; unsigned char b[4]; } 或指针法——存 0x11223344 后看首字节是 0x44(小端)还是 0x11(大端)。 必须说清边界:读非最后写入的 union 成员是实现定义行为(自检用途可接受); 且字节序是平台属性,代码不应把"所有板子都小端"当假设——网络传输一律 htonl/ntohl 显式转换。
追问:- 追问:网络字节序是大端还是小端?(大端)
评分要点:- 大小端定义准确
- 判断代码思路
- 实现定义与网络字节序提示
关联章节:1.3.7 共用体与大小端实战
int 与字节数组互转,为什么推荐 memcpy 而不是直接指针强转或 union 双关?高频C/C++ · medium
要点:指针强转违反严格别名规则,union 双关是实现定义行为;memcpy 逐字节复制是标准保证的,且编译器会优化回等价高效代码。
三种写法功能相同(把 int 的字节序列拆出来/拼回去),可移植性不同: ① 指针强转(*(unsigned char*)&v)——类型不同的左值访问对象,涉及严格别名规则 (C11 6.5 p6/p7),严格可移植性无保证;② union 双关——标准明确允许 union 成员访问 作为别名例外,但读非最后写入成员属实现定义行为(6.5.2.3 注 95),且受字节序影响; ③ memcpy——把 v 的字节逐字节复制进数组,标准保证行为,现代编译器能把它优化成 与直接访问等价的高效代码(没有性能损失)。因此"安全 + 不损失性能"的默认选择是 memcpy, 再叠加显式字节序转换(htonl/ntohl)完成跨机传输。
追问:- 追问:memcpy 之后字节数组里是什么顺序?(本机字节序;跨机传输前转网络字节序)
评分要点:- 三种写法的可移植性差异
- 严格别名与实现定义两个标准依据
- 优化无损失的认识
关联章节:1.3.7 共用体与大小端实战
计算机里负数为什么用补码而不是原码表示?高频C/C++ · easy
要点:补码让减法变成加法、且 0 只有一种表示;原码有两个 0 且减法电路复杂。
原码:符号位+绝对值,简单直观,但有两个 0(1000 0000 与 0000 0000),且加减法要区分符号, 电路复杂。反码解决了部分问题但仍有 +0/-0。补码:负数=正数取反加一,加法和减法共用一套电路, 0 唯一(全 0),且能多表示一个数(8 位是 -128)。这就是所有现代 CPU 采用补码的原因。
追问:- 追问:为什么 8 位补码范围是 -128~127 而不是 -127~127?
- 追问:0xFFFFFFFF 作为 int 是多少?(-1)
评分要点:- 0 唯一
- 减法统一为加法
- 多一个负数的范围说明
关联章节:0.1.3 原码/反码/补码与溢出
什么是溢出?有符号和无符号加法如何判断?高频C/C++ · medium
要点:结果超出表示范围即溢出;有符号看"同号相加得异号",无符号看进位标志。
溢出=运算结果超出该类型的表示范围。有符号加法判据:正+正得负、负+负得正(异号相加不可能溢出); 无符号看进位(最高位向上进位,CF=1)。硬件层面 CPU 同时给出进位标志与溢出标志, C 语言层面有符号溢出是未定义行为(编译器可能做激进优化),无符号回绕则是明确定义的。
追问:- 追问:为什么 C 语言有符号溢出是未定义行为?
- 追问:写代码如何避免溢出?(检查范围/用更大类型/编译器告警 -ftrapv)
评分要点:- 溢出定义
- 有符号判据
- 无符号进位
关联章节:0.1.3 原码/反码/补码与溢出
0x80 作为 int8_t 是多少?为什么?嵌入式 · easy
要点:-128。因为 1000 0000 是 8 位补码能表示的最小负数。
8 位补码范围 -128~127:1000 0000 按"取反加一"会回到自身,因此约定为 -128(这也是补码比 原码多一个负数的来源)。这个特值在嵌入式很常见:0x80 常作为"最小值/无效值"哨兵、I2C 等 协议中的标志位。回答时注意区分 int8_t(有符号)与 uint8_t(无符号,0x80=128)。
追问:- 追问:uint8_t 的 0x80 呢?(128)
评分要点:- 说出 -128
- 解释 1000 0000 特例
- 区分 int8_t/uint8_t
关联章节:0.1.3 原码/反码/补码与溢出
简单说说冯·诺依曼结构与哈佛结构的区别?嵌入式 · easy
要点:前者指令与数据共用一套存储器与总线;后者分开存储、独立总线、可并行访问。
冯·诺依曼:程序与数据同存于一个存储器,共用总线,结构简单、灵活(程序可以像数据一样被加载), 但取指与访存会争用总线(冯·诺依曼瓶颈)。哈佛:指令存储器与数据存储器物理分离、总线独立, 可同时取指与取数,速度快,常用于 MCU(如 STM32 的 Flash 存指令、SRAM 存数据)与 DSP。 嵌入式面试可补充:现代 CPU 通过 L1 指令/数据 Cache 分离在冯·诺依曼体系上模拟哈佛优点。
追问:- 追问:STM32 属于哪种结构?(片上 Flash 与 SRAM 分离,属哈佛结构变体)
- 追问:什么是冯·诺依曼瓶颈?
评分要点:- 说出存储是否分离
- 说出总线是否独立与并行访问
- 举出应用场景加分
一条指令在 CPU 里是怎么执行的?嵌入式 · easy
要点:经典五步:取指→译码→执行→访存→写回;PC 记录下一条指令地址。
取指(按 PC 从内存/缓存取指令,PC 自增)→ 译码(控制器解析操作码与操作数)→ 执行(运算器完成计算)→ 访存(需要时读写内存)→ 写回(结果写回寄存器)。可补充现代 CPU 的流水线:五步重叠执行,提高吞吐。
追问:- 追问:PC 里存的是什么?
- 追问:流水线为什么会提高速度?
评分要点:- 五步顺序正确
- 说明 PC 存地址
- 提及流水线加分
为什么程序要分"取指"和"执行"两个阶段?嵌入式 · easy
要点:因为"存储程序"设计:指令先存在于存储器中,CPU 必须先取出才能知道要做什么。
冯·诺依曼体系里指令以二进制存放在存储器,CPU 内部只有少量寄存器,不能一次装下整个程序, 因此必须一条一条取出再执行。这也是 PC 寄存器存在的意义。可以延伸到:这正是现代 CPU 流水线、分支预测、指令缓存优化的起点。
评分要点:- 关联"存储程序"思想
- 说明逐条取出的必要性
git add 和 git commit 有什么区别?为什么要有暂存区?高频C/C++ · easy
要点:add 把改动登记进暂存区(下一次提交的候选清单),commit 把暂存区内容固化为一次带哈希的快照提交;暂存区让"提交什么"由开发者精细控制。
git add 把工作区文件改动的当前内容快照登记进暂存区(index);git commit 把暂存区的 内容固化为一次提交对象并移动分支指针。暂存区的价值:① 一次提交可以只包含部分改动, 把一次编辑拆成多个语义清晰的提交(改了两个 bug 就拆两次提交);② 提交前有检查点, git diff --cached 可以复核"即将提交什么";③ 配合 git diff 构成 "工作区→暂存区→版本库"的对比体系。注意 add 登记的是那一刻的内容, 之后继续修改同一文件需要重新 add。
追问:- 追问:git commit -am 是怎么回事?它等价于哪些操作?
评分要点:- add/commit 职责区分
- 暂存区的部分提交与提交前复核两个价值
- add 后继续改需重新 add
本地仓库刚 commit 了一次错误提交,如何回退?不同需求分别用什么命令?高频C/C++ · medium
要点:按需求分级:撤销提交但保留改动用 reset --soft;连暂存也撤销用 reset --mixed;提交和改动都不要用 reset --hard;只丢弃未提交的工作区改动用 restore/checkout --。
场景一:只想撤销最近一次提交、改动保留 → git reset --soft HEAD~1(分支指针回退一步, 改动留在暂存区与工作区,重新 commit 即可)。场景二:撤销提交且回到"未 add"状态 → git reset --mixed HEAD~1(默认模式,改动留在工作区)。场景三:提交连同改动都不要 → git reset --hard HEAD~1(不可恢复,最危险)。如果只是改了文件还没 add,直接 git restore 文件(旧写法 git checkout -- 文件)丢弃工作区改动。回答时务必强调: ① reset --hard 不可恢复;② 已推送的提交不要用 reset 改历史,要用 revert 生成反向提交 (revert 在 0.3.4 展开)。
追问:- 追问:git reset 和 git revert 的本质区别是什么?
- 追问:--soft/--mixed/--hard 三者分别动了哪三个区域?
评分要点:- 三级 reset 模式与影响区域
- restore/checkout -- 的适用场景
- 已推送提交用 revert 的意识
git log 有哪些常用参数?怎么查"某次提交改了哪些文件、文件改了什么"?C/C++ · easy
要点:常用:--oneline(一行摘要)、-p/--patch(补丁)、--stat(文件级统计)、--graph(分支图)、-n 数量限制、--author 过滤;查单次提交用 git show <哈希>。
最常用的组合:git log --oneline 快速浏览;git log -p 查看每次提交的完整补丁; git log --stat 查看每次提交改动的文件与增删行数;git log --graph --oneline --all 查看分支合并结构;git log -5 只看最近 5 条;git log --author=名字 过滤作者。 针对某一次提交:git show <短哈希> 一次性给出提交信息、文件列表与补丁。 延伸:git reflog 记录 HEAD 的移动轨迹,误操作(如 reset --hard 过头)时可据此找回 提交哈希;git blame 文件可逐行追溯每行的最后一次修改(1.4.x 阶段展开)。
追问:- 追问:git log 和 git reflog 的区别?
评分要点:- oneline/p/stat/graph 四参数
- git show 查单次提交
- reflog 意识
git merge 的快进合并(Fast-forward)和三路合并有什么区别?分别在什么情况下发生?高频C/C++ · medium
要点:快进:分叉后目标分支没有新提交,指针直接前进,不产生新提交;三路:两边都有新提交,git 以共同祖先为基准三方比较生成合并提交;若三方修改冲突则报 CONFLICT 留人工解决。
快进合并:从分叉点之后目标分支(如 main)没有任何新提交,合并时 git 只需把 main 指针 前进到被合并分支(如 dev)的最新提交,历史保持一条直线,不产生新提交对象。 三路合并:两边在分叉后都有新提交,git 取"共同祖先(base)+ main + dev"三份内容比较: 只有一边改动的部分自动采用改动方;两边改了同一文件的同一区域时无法自动合并, 生成冲突标记(<<<<<<< HEAD / ======= / >>>>>>> dev)留在文件里,由人工决定最终内容, 解决后 git add + git commit 生成一个有"两个父提交"的合并提交。 面试加分点:能说出 git merge --no-ff 可以强制禁用快进(生成合并提交保留分支历史), 以及"冲突是正常协作流程,不是事故"。
追问:- 追问:怎么强制产生合并提交而不是快进?(--no-ff,及其使用场景)
评分要点:- 两种合并的发生条件
- 三路合并的 base 概念
- 冲突标记与合并提交两个父提交
关联章节:0.3.3 分支与合并
merge 出现冲突后,完整的解决流程是什么?有哪些常见坑?高频C/C++ · medium
要点:打开冲突文件,理解 <<<<<<< ======= >>>>>>> 三段标记的含义 → 手动编辑保留最终内容并删除全部标记 → git add 标记已解决 → git commit 完成合并;坑:残留标记、解决不彻底、丢弃对方有效改动。
完整流程:① git status 找到冲突文件(Unmerged paths);② 打开文件,<<<<<<< HEAD 到 ======= 之间是当前分支内容,======= 到 >>>>>>> dev 之间是对方分支内容;③ 手动编辑成 想要的最终内容(可能两边各取一部分),删除全部三组标记;④ git add 冲突文件告诉 git "已解决";⑤ git commit 完成合并(合并提交有两个父提交)。常见坑:提交前残留 <<<<<<< 标记(可用 git grep '<<<<<<<' 自查);解决时只删标记不改内容导致逻辑错误; 把对方有效改动整体删掉;冲突解决到一半又去 merge 别的分支。工具提示: git merge --abort 可以在解决前放弃本次合并回到干净状态。
追问:- 追问:git merge --abort 什么时候用?
- 追问:冲突解决错了还能找回吗?(reflog/再次合并)
评分要点:- 三段标记含义
- add 表示"已解决"的语义
- 残留标记与 --abort 两个坑/工具
关联章节:0.3.3 分支与合并
git rebase 和 git merge 的区别是什么?为什么说 rebase 有危险、哪些场景不能 rebase?高频C/C++ · medium
要点:merge 保留分叉历史并生成合并提交;rebase 把本分支的提交搬到目标分支最新提交之后重放,历史成直线。危险在于改写提交(哈希变化);已推送的公共分支不能 rebase。
区别:merge 是"合并两条线",保留分叉与合并结构,历史呈树状;rebase 是"把本分支的 提交取下来、放到目标分支最新提交之后依次重放",得到一条直线历史,提交哈希全部改变 (父提交变了,哈希必然变)。危险根源就在"改写历史":如果被 rebase 的分支已经 push 给他人,别人仓库里的旧提交与你的新哈希对不上,强行同步会制造重复提交甚至需要 force push,破坏他人工作。安全用法:只对"自己本地、尚未推送"的功能分支做 rebase 整理历史;公共分支(main/dev)一律用 merge。加分:能说出 interactive rebase (git rebase -i)可以压缩/重排提交消息——同样只在本地分支做。
追问:- 追问:为什么 rebase 后提交的哈希会变?
- 追问:git pull --rebase 与 git pull 的区别?
评分要点:- 历史形状差异(树状 vs 直线)
- 哈希改变与改写历史的危险
- 公共分支禁 rebase 的边界
关联章节:0.3.3 分支与合并
好的 commit message 有什么标准?团队为什么要统一提交规范?高频C/C++ · easy
要点:标准:type 前缀 + 一句话说明(feat/fix/docs/refactor 等),第一行简短说清改了什么,需要时正文补充为什么改。统一规范让历史可检索、可回溯、可自动生成发布说明。
一条好的提交信息回答两个问题:改了什么、为什么改。常见约定(Conventional Commits 风格): feat(新功能)、fix(修复)、docs(文档)、refactor(重构不改行为)、test、style、chore; 第一行保持在 50 字以内,必要时用空行分隔正文补充动机与影响面。团队统一规范的价值: ① git log 即文档——新成员看历史就能理解演进;② 检索与统计——按前缀过滤 git log --grep=fix 快速定位缺陷修复;③ 自动化——按类型自动生成 CHANGELOG、 判断版本号(fix 升 patch、feat 升 minor)、触发对应流水线;④ code review 效率—— 提交粒度与语义清晰,评审者不用猜。反例是"update""改了一下",这类信息对回溯毫无价值。
追问:- 追问:一次提交应该包含多少改动?(单一职责:一个提交只做一件事)
- 追问:refactor 与 fix 怎么区分?(行为不变是重构;行为变化是修复或新功能)
评分要点:- type 前缀与一句话说明
- 历史可回溯/可检索/可自动化
- 提交粒度单一职责
.gitignore 的语法要点有哪些?实际使用中最常见的坑是什么?高频C/C++ · medium
要点:要点:* 通配、以 / 结尾匹配目录、! 否定、/ 开头锚定根目录。最常见坑:已跟踪文件不受 .gitignore 影响,需要 git rm --cached。
语法要点:① *.o 这类通配匹配任意层级的同名文件;② build/ 结尾斜杠只匹配目录 (build 不带斜杠则文件目录都匹配);③ ! 前缀表示否定(重新包含),但父目录已被整体 忽略时子文件无法用 ! 找回,需先放开父目录;④ / 开头把模式锚定到 .gitignore 所在目录。 常见坑:文件已经被 git 跟踪后,再把它写进 .gitignore 不会生效——.gitignore 只作用于 未跟踪文件;要先 git rm --cached <文件> 把它移出跟踪(保留磁盘文件)并提交。 另一个高频问题是把编译产物(.o/.exe/build)长期留在仓库里,导致每次 diff 全是 产物噪音、仓库膨胀;正确做法是建仓第一版就提交 .gitignore。
追问:- 追问:全局 .gitignore 怎么配置?(core.excludesFile,如 ~/.gitignore_global)
- 追问:如何查看某个文件为什么被忽略?(git check-ignore -v 文件)
评分要点:- 通配/目录/否定/锚定四点
- 已跟踪文件不受影响的坑
- 产物入库的工程危害
git tag 与分支有什么区别?发布版本时一般怎么做?C/C++ · medium
要点:分支是可移动的指针,随新提交自动前进;tag 是固定在某个提交上的标签,不会移动。发布流程:在要发布的提交上打 tag(如 v1.0.0),必要时推送 tag 到远程。
从实现看两者都是指向提交的引用(refs/heads/* 与 refs/tags/*),但语义完全不同: 分支是"活"的——在其上继续提交时指针自动向前移动;tag 是"死"的——永远指向打标时的 那次提交,用于标记里程碑(发布版本、验收基线)。tag 分轻量(仅一个指针)与附注 (git tag -a,额外记录打标人、时间、说明与 GPG 签名,发布推荐附注标签)。 发布流程:main 上代码稳定后 git tag -a v1.0.0 -m "发布 1.0.0",再 git push origin v1.0.0 推送到远程;之后无论代码怎么演进,都能 git checkout v1.0.0 精确回到发布状态。 反例是把 tag 当分支用(在 tag 上继续提交),会破坏"里程碑不可变"的团队共识。
追问:- 追问:tag 打错了怎么办?(git tag -d 本地删除;已推送的需 git push origin :refs/tags/名字)
- 追问:语义化版本号 v1.2.3 三段分别代表什么?(major.minor.patch)
评分要点:- 可移动与固定的本质区别
- 轻量/附注两种 tag
- 发布与回滚场景
项目里为什么要用版本控制工具?它解决了什么问题?高频C/C++ · easy
要点:记录每次修改的历史,支持回退、对比、协作与发布,避免"改坏了回不去"和多人互相覆盖。
核心是"可追溯、可回退、可协作":每次提交都有快照和编号,任何历史版本都能找回与对比 (谁、何时、改了什么);多人并行开发时按分支隔离、合并冲突显式化,而不是互相覆盖文件; 配合远程仓库还能异地备份与代码评审。对比"网盘备份":网盘只能存"某一时刻的整份文件", 无法回答"第 3 天改了什么、为什么改",也没有分支/合并/评审这些工程语义。
追问:- 追问:没有版本控制的团队一般会遇到什么典型事故?
评分要点:- 可追溯/可回退/可协作三点
- 与网盘备份的本质区别
关联章节:0.3.1 Git 核心概念与安装
工作区、暂存区、版本库有什么区别?git add 和 git commit 各自做了什么?高频C/C++ · medium
要点:工作区是磁盘上的目录;暂存区是下一次提交的候选清单;版本库保存全部历史提交。add 把改动放入暂存区,commit 把暂存区内容固化为一版快照。
工作区(working tree)是正在编辑的目录;暂存区(index/staging area)记录"下一次提交将包含哪些改动"; 版本库(repository,.git 目录)保存所有历史提交对象与引用。git add 把文件改动登记进暂存区 (可以先 add 一部分),git commit 把暂存区的内容固化为一次带哈希编号的快照提交。 正因为有暂存区,才能做到"只提交改动的一部分、拆成多个语义清晰的提交"。
追问:- 追问:git commit 之后暂存区会变空吗?(不会,提交的就是暂存区内容本身)
- 追问:如何跳过暂存区直接提交已跟踪文件的改动?(git commit -am)
评分要点:- 三区定义准确
- add/commit 职责区分
- 提到"部分提交"能力
关联章节:0.3.1 Git 核心概念与安装
Git 是分布式版本控制,这对实际开发意味着什么?和 SVN 相比有什么优势?高频C/C++ · medium
要点:每个克隆都是完整仓库:离线可提交、可看全部历史;操作快、不怕中央服务器故障;协作靠推拉而不是唯一中央库。
分布式意味着每个开发者的克隆都包含完整历史,断网时依然可以提交、建分支、看日志, 联网后再 push 同步;日常操作(log/diff/commit)不依赖服务器,速度快;中央服务器故障 不阻塞本地开发,任何完整克隆都能恢复仓库。SVN 等集中式工具提交必须联网,历史在服务器上。 代价:模型多一个"推/拉"环节,概念稍多(这正是 0.3.1 要学的四区模型)。
追问:- 追问:分布式是否意味着不需要服务器?(不,远程仓库承担备份与协作枢纽角色)
评分要点:- 离线可提交/完整历史
- 速度与容灾
- 与集中式的区别
关联章节:0.3.1 Git 核心概念与安装
git fetch 与 git pull 有什么区别?分别适合什么场景?高频C/C++ · easy
要点:fetch 只把远程新提交下载到本地并更新远程跟踪指针,不修改工作区;pull = fetch + merge,下载后立即合并到当前分支。
fetch 是"只取不并":它把远程的新提交对象下载回来,更新 origin/main 等远程跟踪引用, 工作区与当前分支完全不动,因此任何时候执行都安全,适合"先看看别人改了什么再决定"。 pull 是 fetch 与 merge 的连招:下载后立即把远程改动合并进当前分支;若本地有未提交改动 或双方修改了同一位置,可能产生冲突,需要处理。工程上的常见纪律:morning pull 用 fetch 先观察、确认无误再 merge;或者直接用 pull 但保持工作区干净。git pull --rebase 是另一种 策略(变基代替合并),初学先掌握默认合并即可。
追问:- 追问:git pull 与 git pull origin main 的区别?(后者显式指定远程与分支)
- 追问:fetch 之后怎样查看远程新增了什么?(git log origin/main、git diff main origin/main)
评分要点:- fetch 只下载不合并
- pull = fetch + merge
- 各自适用场景
关联章节:0.3.4 远程仓库 Gitee
git push 被拒绝(! [rejected] ... non-fast-forward)时怎么排查处理?高频C/C++ · medium
要点:说明远程有本地没有的新提交。先 git pull(或 fetch + merge)把远程改动合并进来,解决可能的冲突后重新 git push。
排查分两类。第一类"non-fast-forward":远程分支上有本地缺失的提交(别人推过或网页上改过), 直接 push 会覆盖对方历史,git 拒绝执行。正确流程:git pull origin main 合并远程改动, 有冲突就解决冲突、git add 后 git commit,再 git push。禁止用 git push -f 强行覆盖—— 会抹掉队友的提交,除非个人分支且团队明确约定。第二类"认证失败":HTTPS 检查用户名与 访问令牌(不是登录密码),SSH 检查本机密钥与平台公钥是否配对(ssh -T 测试)。此外 确认 git remote -v 地址正确、当前分支有上游跟踪(git push -u origin main 建立)。
追问:- 追问:解决冲突后还需要重新 add 和 commit 吗?(需要,冲突解决产生一次合并提交)
- 追问:什么情况下 push -f 才是可接受的?(个人 feature 分支、团队明确约定、远程无人依赖)
评分要点:- 识别 non-fast-forward 含义
- 先 pull 再 push 的正确流程
- 认证失败排查点
- 强调 -f 的危险
关联章节:0.3.4 远程仓库 Gitee
项目为什么要使用远程仓库?git remote 与 origin 是什么?C/C++ · easy
要点:远程仓库承担异地备份、多人协作、代码评审与发布枢纽的角色;remote 是远程地址在本地配置里的登记名,origin 是默认别名。
远程仓库解决三个问题:① 异地备份——本地磁盘损坏仍可从远程完整恢复(分布式模型中 每个克隆都含全部历史);② 多人协作——每个人把本地提交 push 到同一远程、pull 对方的 改动,冲突显式化而不是互相覆盖文件;③ 评审与发布——基于远程做 pull request/代码评审、 打 tag 发布版本。git remote 命令管理这些远程的登记信息(git remote -v 查看), 每条登记是一个"名字 + 地址"对;origin 只是 git clone 或约定俗成使用的默认名字, 不是关键字,可以改名为 gitee、github 等。理解"名字只是别名"就不会被多个远程搞晕。
追问:- 追问:一个本地仓库可以同时有多个远程吗?(可以,如 git remote add upstream ...)
评分要点:- 备份/协作/评审三点
- remote 与 origin 的别名语义
关联章节:0.3.4 远程仓库 Gitee
从写下 hello.c 到屏幕输出 Hello World,中间发生了什么?高频C/C++ · medium
要点:预处理→编译→汇编→链接得到可执行文件,运行时加载进内存执行,printf 经标准库与系统调用输出。
四阶段:预处理(展开头文件/宏)→ 编译(C 翻译成汇编)→ 汇编(汇编成目标文件 .o)→ 链接(目标文件与标准库合并成可执行文件)。运行时:shell 创建进程、加载器把程序映射进内存、 从 main 开始执行,printf 最终经标准库缓冲与 write 系统调用把字符送到终端。 嵌入式延伸:交叉编译产物需拷贝到目标板运行(阶段 7)。
追问:- 追问:printf 一定直接调用 write 吗?(有用户态缓冲)
评分要点:- 四阶段齐全
- 运行加载概念
main 函数的返回值有什么作用?C/C++ · easy
要点:向调用者(通常是 shell)报告退出状态:0 成功、非 0 失败,脚本据此做判断。
shell 通过 $? 获取退出码;脚本与 CI 大量依赖它(make/测试脚本按退出码决定成败)。 规范:0=成功,非 0=失败(可用不同码区分错误类型)。补充:return 0 在 main 中可省略 (C99 起隐式返回 0),但显式写更清晰。
追问:- 追问:exit(0) 与 return 0 在 main 里等价吗?(等价;exit 可出现在任意函数)
评分要点:- 0/非 0 语义
- 脚本依赖场景
Windows 上编译的 exe 为什么不能直接在 Linux 运行?高频C/C++ · easy
要点:可执行文件格式与系统调用接口不同(PE vs ELF、Win32 API vs POSIX),且依赖各自的动态库。
Windows 可执行文件是 PE 格式,调用 Win32 API,链接 msvcrt/ucrt 等;Linux 是 ELF 格式, 走 POSIX 系统调用与 glibc。同一份 .c 源码需要分别在目标平台编译(或用交叉编译器生成 目标平台二进制)。这也解释了为什么学习环境强调"在哪个平台运行就在哪个平台编译"。
追问:- 追问:什么是交叉编译?(阶段 7 详细展开)
评分要点:- 格式差异
- 接口/库差异
- 重新编译/交叉编译
转行学习嵌入式,你打算按什么路线学?嵌入式 · easy
要点:按"零基础→C→数据结构→Linux 系统编程→网络→嵌入式 Linux→项目→求职"推进,配合每日练习与项目验收。
这类问题面试官考察的是学习规划能力与对岗位技能栈的认知。参考答法:先打 C 语言与工具链基础 (能独立编译调试多文件工程),再学数据结构与 Linux 系统编程(文件 IO/进程线程/IPC), 然后网络编程(TCP/UDP/epoll),最后嵌入式 Linux 四件套(交叉编译/U-Boot/设备树/驱动入门), 每阶段配一个可验收的小项目,最后做简历级综合项目。强调"以输出项目为标准"比"看视频时长"更有说服力。
追问:- 追问:你目前处于哪个阶段?手上有什么项目?
评分要点:- 路线主线正确
- 强调项目输出
- 诚实说明当前进度
关联章节:0.1.5 学习方法与课程路线总览
你怎么判断自己"学会"了一个知识点?嵌入式 · easy
要点:能复述概念、独立写出代码、通过测验、并完成一个用上它的实践任务。
参考答法:能给别人讲清楚(费曼法)→ 不看资料写出正确代码(编译通过、处理边界)→ 通过客观测验 → 在真实小任务里用上。单纯"看过"不算会;本课程用阅读/练习/测验/任务 四维记录来逼近这个标准。这样的回答展示学习方法的成熟度。
追问:- 追问:遇到看不懂的源码怎么办?
评分要点:- 多维度判据
- 提及输出/验证
关联章节:0.1.5 学习方法与课程路线总览
学过的知识容易忘,你的复习策略是什么?嵌入式 · easy
要点:间隔复习 + 以练代背 + 错题重做 + 面试题自测。
参考答法:①间隔复习(新学内容 1/3/7 天后重测);②以输出代输入(把笔记整理成可运行的 小例子,而不是收藏文档);③错题与失败测验定期重做;④用面试题做"检索式练习"——先默答 再对答案,比重复阅读有效。可补充:本站的测验重测与进度页错题/待复习入口就是按这个思路设计。
评分要点:- 具体可执行的策略
- 提到检索式练习加分
关联章节:0.1.5 学习方法与课程路线总览
程序运行时内存分为哪几个区域?分别放什么?高频C/C++ · medium
要点:代码区(指令)、常量区(字符串字面量等)、全局/静态区(.data/.bss)、堆区(动态分配)、栈区(局部变量/调用帧)。
经典五区:代码区(text,只读);常量区(rodata);全局/静态区(已初始化 .data、未初始化 .bss); 堆区(malloc/new,向高地址增长);栈区(局部变量、参数、返回地址,向低地址增长)。 补充:Linux 下可用 size 命令查看各段大小;未初始化全局变量在 .bss,不占文件体积。
追问:- 追问:static 局部变量放哪?(全局/静态区)
- 追问:为什么 .bss 不占可执行文件体积?
评分要点:- 五区齐全
- 每区举例
- 增长方向加分
关联章节:0.1.4 存储器层次与内存布局概念
栈和堆有什么区别?高频C/C++ · medium
要点:栈自动管理、速度快、空间小、连续;堆手动分配、速度慢、空间大、易碎片化。
栈:函数调用自动压栈/弹栈,分配快(移动栈指针),空间通常受限(Linux 默认 8MB 量级, MCU 上可能只有几 KB),生命周期随函数。堆:malloc/new 按需分配,需要手动释放, 管理开销大、易产生碎片与泄漏,但空间大。选型:小对象/临时数据用栈,大块/跨函数生命周期用堆。
追问:- 追问:栈溢出会发生什么?(崩溃/段错误)
- 追问:MCU 上为什么常禁止深递归?
评分要点:- 管理方式
- 速度/空间/碎片
- 生命周期
关联章节:0.1.4 存储器层次与内存布局概念
嵌入式系统中 Flash 和 RAM 各有什么用途?高频嵌入式 · easy
要点:Flash 非易失,存程序与常量;RAM 易失,存运行时变量、栈与堆。
Flash(NOR 为主):掉电保持,容量大,存放固件代码、只读数据;支持片内执行(XIP)。 RAM(SRAM/DRAM):速度快、掉电丢失,存放全局变量、栈、堆与运行期数据。 因此 MCU 选型看 Flash/RAM 配比;程序规模看 Flash,运行期数据量看 RAM——"程序装不下" 和"运行内存不够"是两个不同的资源问题。
追问:- 追问:const 变量一定放在 Flash 吗?(编译期常量可能,运行期 const 不一定)
评分要点:- 非易失/易失区分
- 各自存放内容
- 选型视角加分
关联章节:0.1.4 存储器层次与内存布局概念
为什么计算机使用二进制?嵌入式 · easy
要点:物理实现简单可靠:高低电平天然对应 0/1,抗干扰强、电路易于实现与校验。
二进制只有两个状态,用高低电平(开/关、导通/截止)即可表示,元件容差大、抗噪声能力强; 逻辑运算(与或非)与二进制天然对应;多值逻辑(如三进制)在物理实现与可靠性上成本高得多。 补充:十六进制是二进制的"速记法",4 个二进制位正好一个十六进制位,便于人阅读与书写。
追问:- 追问:为什么用十六进制而不是十进制做速记?
评分要点:- 物理可靠性
- 与逻辑运算的对应
- 提及十六进制是二进制的缩写加分
关联章节:0.1.2 进制与进制转换
0xFF 等于多少?这类值在嵌入式里有什么用?高频嵌入式 · easy
要点:255。常用于位掩码(取低 8 位)、寄存器配置、GPIO 端口全置位等。
0xFF = 1111 1111 = 255。嵌入式里常作位掩码:value & 0xFF 取低 8 位;GPIO 端口写 0xFF 表示 8 个引脚全部置高;UART 帧等协议里 0xFF 常作填充/帧头。记住 0x0F/0xF0/0xFF/0xFFFF 这类"满位"值与 0x80/0x8000 这类"最高位"值是面试与工程的日常。
追问:- 追问:value & 0xFF 与 value % 256 等价吗?(对非负 value 等价)
评分要点:- 算出 255
- 举出掩码/寄存器用法
关联章节:0.1.2 进制与进制转换
八进制在哪些场景还会遇到?嵌入式 · easy
要点:文件权限(如 chmod 755)、printf 的 %o 输出、少数老协议;主流场景已被十六进制取代。
Linux 文件权限 rwx 三组恰好三位二进制对应一个八进制位(755 = 111 101 101),是八进制最 常见的现代应用;C 语言中 0 开头的字面量是八进制(如 0755),书写时要小心:010 ≠ 10。 其余场景(地址、寄存器、调试器)基本都用十六进制。
追问:- 追问:C 语言里 010 等于十进制多少?(8)
评分要点:- 文件权限场景
- 指出 0 前缀字面量陷阱
关联章节:0.1.2 进制与进制转换
说一下 SSH 免密登录的原理。高频Linux 系统编程 · medium
要点:客户端持私钥,服务器 authorized_keys 存公钥;连接时服务器用公钥挑战,客户端用私钥签名应答。
ssh-keygen 生成公私钥对:私钥留客户端(保密),公钥放进服务器的 ~/.ssh/authorized_keys。 登录时服务器生成随机挑战,客户端用私钥签名,服务器用公钥验签——全程不传输私钥。 排查要点:authorized_keys 权限 600、目录权限 700,否则 sshd 会拒绝。
追问:- 追问:为什么 authorized_keys 权限太宽会失效?
评分要点:- 公私钥分工
- 挑战-应答机制
- 权限细节加分
systemctl enable 和 start 有什么区别?Linux 系统编程 · easy
要点:enable 设置开机自启,start 立即启动;enable --now 两者一次完成。
systemctl start 只对本次运行生效(重启后失效);systemctl enable 写入开机自启链接 (重启后自动启动),但不影响当前状态;enable --now = enable + start。 对应排查场景:服务"重启后没了"→ 忘了 enable。
追问:- 追问:怎么查看服务是否开机自启?(systemctl is-enabled)
评分要点:- 立即启动 vs 开机自启
- enable --now 用法
虚拟机的桥接和 NAT 模式有什么区别?Linux 系统编程 · easy
要点:桥接=虚拟机直接接入局域网(独立 IP);NAT=经宿主机共享出口,宿主机可访问虚拟机。
桥接(bridged):虚拟机像一台独立电脑接入路由器,获得局域网 IP,局域网内其他设备可直接访问; NAT:虚拟机经宿主机做网络地址转换上网,宿主机能访问虚拟机,但局域网其他设备默认不能。 选型:需要外部设备直接访问虚拟机(如板卡 tftp 烧录)选桥接;日常学习与 SSH 用 NAT 即可。
追问:- 追问:NAT 模式下怎么让局域网访问虚拟机?(端口转发)
评分要点:- 两种模式网络位置
- 应用场景
你平时怎么在 Windows 上开发 Linux 程序?Linux 系统编程 · easy
要点:VS Code + Remote-SSH 连 Linux 虚拟机/服务器,代码与编译都在远端。
工作流:本地 VS Code 只做编辑器界面,Remote-SSH 在远端启动 VS Code Server, 终端、编译、调试全部在 Linux 上执行。好处:环境与生产一致、避免 Windows/Linux 差异问题 (换行、路径、权限)。补充:也可用 WSL2、独立虚拟机或云服务器做远端,流程相同。
追问:- 追问:WSL2 和虚拟机选哪个?(体验与性能权衡,视需求)
评分要点:- 说明远端执行模型
- 提环境一致性
关联章节:0.2.3 VS Code 远程开发
Remote-SSH 连不上,你的排查顺序是什么?Linux 系统编程 · medium
要点:手动 ssh 能通?→ 查 sshd/网络;不通 → 查插件与 VS Code 配置。
先隔离问题层次:①手动 `ssh 用户@IP` 是否成功——失败说明是 SSH/网络层(sshd 未启动、 IP 错误、防火墙),回 0.2.2 排查;②手动能通但插件不通——插件版本、远端平台选择、 VS Code Server 安装失败(可看输出日志);③都不行再升级 VS Code 与插件到当前稳定版。
追问:- 追问:VS Code Server 装在远端哪里?(~/.vscode-server)
评分要点:- 分层排查顺序
- 先手动 ssh
关联章节:0.2.3 VS Code 远程开发
远程开发的代码和编译产物分别在哪里?Linux 系统编程 · easy
要点:都在远端:代码在远端磁盘,编译与运行在远端执行,本地只有界面渲染。
Remote-SSH 模型下本地不保存代码副本(编辑的是远端文件),编译产物也在远端。 因此远端机器需要有编译器(gcc)、磁盘与网络资源。要回传产物可用 scp/rsync, 或直接在远端 Git 提交后本地拉取——推荐后者(代码进版本库,机器只是环境)。
追问:- 追问:怎么把远端文件拉回本地?(scp/rsync/git)
评分要点:- 代码与产物在远端
- 回传方案
关联章节:0.2.3 VS Code 远程开发
为什么装了 gcc 但终端提示"不是内部或外部命令"?C/C++ · easy
要点:可执行文件所在目录不在 PATH 中,或配置后未重开终端。
终端按 PATH 环境变量中的目录顺序查找命令;gcc.exe 在 MinGW 的 bin 目录,若该目录不在 PATH, 就必须输入完整路径才能执行。解决:把 bin 目录加入 PATH 后重开终端(旧终端不刷新环境变量)。 排查顺序:where gcc 看是否找得到 → 找不到就查 PATH → 配好 PATH 重开终端再试。
追问:- 追问:PATH 的查找顺序有什么影响?(前面目录的同名程序会遮蔽后面的)
评分要点:- PATH 机制
- 重开终端
- 排查顺序
MinGW 和 MSVC 编译出来的程序有什么区别?C/C++ · medium
要点:MinGW 是 GCC 的 Windows 移植,遵循 GNU 工具链习惯;MSVC 是微软编译器,两者 ABI 与运行时不同。
MinGW-w64:GCC 系,命令行习惯与 Linux 一致(-Wall 等选项通用),生成原生 Windows 程序, 默认链接 msvcrt/ucrt;MSVC:微软工具链,优化与扩展不同,生成的库与 MinGW 不保证互相链接 (ABI 差异)。学习 Linux/嵌入式开发推荐 MinGW(与 Linux 上的 gcc 同源,教学一致性好)。
追问:- 追问:为什么本课程 Windows 环境选 MinGW 而不是 MSVC?
评分要点:- 同源性与选项一致
- ABI/运行时差异
描述一下你日常的 C 语言开发环境。高频C/C++ · easy
要点:Windows 用 MinGW-w64 + VS Code;Linux 用系统 gcc + VS Code Remote-SSH;多文件工程用 Makefile/CMake。
考察环境熟练度:编辑器(VS Code)、编译器(gcc)、构建工具(make/cmake)、调试器(gdb)、 版本管理(git)。能说清"编辑-编译-调试-提交"四个环节各用什么工具、一条命令如何跑通, 就是合格回答。加分项:提到远程开发与跨平台一致性。
追问:- 追问:多文件工程怎么编译?
评分要点:- 四环节工具齐全
- 一条命令跑通示例
read() 的返回值分别代表什么?高频Linux 系统编程 · medium
要点:>0 为实际读到的字节数;0 表示到达文件末尾(EOF);-1 表示出错,需查 errno。
read(fd, buf, n) 三种返回:①返回 >0:实际读入 buf 的字节数,可能小于 n(管道/网络尤其常见),应继续读; ②返回 0:EOF,正常结束,不是错误;③返回 -1:出错,errno 给出原因——其中 EINTR(被信号打断)通常可重试, EAGAIN 表示非阻塞模式下暂无数据。常见错误:把 0 当出错、把 -1 当 EOF。
追问:- 追问:为什么一次 read 不一定读到 n 个字节?
- 追问:EINTR 场景如何处理?
评分要点:- 三种返回值各说清语义
- 区分 EOF 与错误
lseek() 的作用是什么?对 write 生效吗?高频Linux 系统编程 · medium
要点:lseek 修改文件当前偏移量,对后续的 read 和 write 都生效;O_APPEND 打开时 write 仍追加到末尾。
偏移量是文件对象的属性,不区分读写方向——"lseek 只对 read 生效"是原讲义的错误说法(本课程已勘误, 见 err-7.1-1)。两个要点:①lseek 可以越过文件末尾(SEEK_SET 超过 EOF),再写入会形成"空洞文件", 空洞区域读出来是全 0(ls -l 显示逻辑大小、du 显示实际占用,Linux 稀疏文件行为); ②以 O_APPEND 打开的文件,write 忽略偏移量、始终写在末尾。
追问:- 追问:什么是空洞文件?ls 与 du 为什么不一致?
评分要点:- 说清偏移量对 read/write 都生效
- 说出 O_APPEND 例外
- 能提空洞文件加分
errno 的正确使用方式是什么?高频Linux 系统编程 · easy
要点:先判断函数返回值,失败时才读 errno;成功调用不会清零 errno;用 perror/strerror 输出。
三个规则:①errno 只在"某次调用失败"后才有意义——正确姿势是检查返回值(如 open 返回 -1),失败再读 errno; ②任何成功调用都不会把 errno 清零,所以"先调用再判断 errno 是否为 0"是错误写法; ③errno 应尽快读取(后续函数调用可能覆盖),并用 perror 或 strerror(errno) 转成可读文本。 另外:常量名必须写对,例如权限错误是 EACCES(两个 C 一个 S),不存在 EACCESS(本课程勘误 err-7.1-3)。
追问:- 追问:EINTR 与 EAGAIN 分别怎么处理?
评分要点:- 返回值判成败、失败才看 errno
- 说出"成功不清零"
- perror/strerror 用法