本篇整理了 200 道 C 语言常见面试题,包含基础知识点、代码手写题和常见问题解析,答案详细,适合学习和面试复习。
目录
一、基础语法与数据类型
二、C 语言核心关键字
三、指针与地址
四、内存管理与五大分区
五、结构体、联合体、位域、柔性数组
六、数组与字符串高频题
七、预处理、编译、汇编、链接
八、函数、递归、回调、调用栈
九、位运算
十、嵌入式 C 专属面试题
十一、代码 Bug 分析与输出题
十二、面试必手撕代码题
十三、大厂进阶原理题
一、基础语法与数据类型
1. C 语言中有哪些基本数据类型?在 32/64 位下各占多少字节?
| char | 1 | 1 |
| short (int) | 2 | 2 |
| int | 4 | 4 |
| long (int) | 4 | 8 |
| long long (int) | 8 | 8 |
| float | 4 | 4 |
| double | 8 | 8 |
| 指针类型 | 4 | 8 |
2. signed 和 unsigned 类型的区别是什么?
- signed 最高位是符号位,可表示正数、负数、0。
- unsigned 没有符号位,全部位都表示数值,只能表示 ≥0 的数。
- 运算中 unsigned 溢出会按照模 2ⁿ循环,不会出现负数。
3. 变量声明和变量定义的区别?
- 声明:告诉编译器变量的名字和类型,不分配内存,如 extern int a;
- 定义:为变量分配实际内存空间,可以赋值,如 int a;
- 一个变量可以多次声明,但只能定义一次。
4. 局部变量、全局变量、静态变量的区别?
- 局部变量:存储在栈区,作用域仅限函数内部,生命周期随函数结束销毁,默认值为随机垃圾值。
- 全局变量:存储在全局 / 静态区,整个程序内均可访问,程序运行全程有效,默认初始值为 0。
- 静态局部变量:存放于全局静态区,程序生命周期常驻内存,仅第一次调用时初始化,作用域仍仅限当前函数。
- 静态全局变量:作用域限制在当前.c 文件,其他文件无法通过 extern 访问。
5. 什么是隐式类型转换?有什么风险?
- 定义:不同数据类型参与运算时,编译器自动将低范围 / 低精度类型转换为大范围 / 高精度类型。
- 风险:数值精度丢失、符号扩展出错、溢出问题隐藏、业务逻辑异常。
6. 什么是算术溢出?举一个典型例子。
- 含义:运算结果超出当前数据类型能存储的数值范围,最终结果不符合预期。
- 示例:char a = 127; a++; 执行后 a 的值变为 – 128。
7. 运算符优先级从高到低大致顺序?
括号 () > 单目运算符 (++ — ! ~ * &) > 算术运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 三目运算符 > 赋值运算符 > 逗号运算符
8. && 和 || 的短路特性是什么?有什么实际用途?
- && 短路规则:左侧表达式结果为假时,右侧表达式不会执行。
- || 短路规则:左侧表达式结果为真时,右侧表达式不会执行。
- 实际用途:规避空指针解引用崩溃,示例代码 if (p && p->data)。
9. ++i 和 i++ 的区别?
- ++i:前置自增,先对变量完成自增,再使用变量参与表达式运算。
- i++:后置自增,先使用变量原有值参与表达式运算,运算结束后再自增。
- 补充:单独一行仅写 ++i; /i++; 时,两者执行效果完全一致。
10. 三目运算符 a?b:c 等价语句是什么?
if(a) {
b;
} else {
c;
}
11. 逗号表达式的结果是什么?
- 执行逻辑:从左至右依次执行每一段表达式,最终整个逗号表达式的取值为最后一个表达式的结果。
- 示例:int a=(1,2,3); 最终变量 a 赋值为 3。
12. 什么是左值和右值?
- 左值:拥有独立内存地址,可以放在赋值符号 = 左侧,支持修改。
- 右值:临时数值,无固定内存地址,仅能放在赋值符号 = 右侧,无法修改。
13. C 语言中有哪些常量类型?
整型常量、浮点常量、字符常量、字符串常量、枚举常量、const 修饰只读常量。
14. 常用转义字符及其含义?
\\n 换行符、\\t 水平制表符、\\ 反斜杠、" "双引号、\\0 字符串结束符、\\r 回车符
15. 表达式与语句的区别?
- 表达式:具备运算结果、拥有确定数值,无分号也可独立存在。
- 语句:以分号作为结束标记,作用是执行一段操作,本身无返回数值。
16. #include < > 和 #include " " 的区别?
- #include < >:优先去系统库安装路径查找头文件,多用于标准库头文件。
- #include " ":优先在当前项目源码目录查找头文件,找不到再检索系统路径,多用于自定义头文件。
17. 头文件重复包含的危害及解决方法?
- 危害:多个源文件同时引入同一头文件,会触发变量、函数重复定义,编译直接报错。
- 解决方案 1:头文件守卫 #ifndef / #define / #endif
- 解决方案 2:编译器专用指令 #pragma once
18. 什么是强类型语言?C 语言属于强类型吗?
- 强类型语言定义:变量数据类型约束严格,不同类型数据禁止直接运算,不会自动转换。
- C 语言属性:弱类型语言,支持自动隐式类型转换,类型校验规则宽松。
二、C 语言核心关键字
19. static 关键字的三个作用?
- 修饰局部变量:延长生命周期至程序结束,仅初始化一次,函数多次调用会保留上次值。
- 修饰全局变量:作用域仅限当前.c 源文件,外部文件无法访问,避免全局命名冲突。
- 修饰普通函数:函数仅当前文件内可见,其他源文件不能调用该函数。
20. const 四种常用写法及含义?
- const int a; 变量 a 只读,无法修改数值
- const int *p; 指针指向的数据只读,指针 p 自身可以修改指向
- int *const p; 指针 p 本身固定不可变,指向的数据可以修改
- const int *const p; 指针和指向的数据均只读,全部不可修改
21. volatile 关键字的作用和使用场景?
- 核心作用:告知编译器该变量可能被硬件、中断、多线程异步修改,禁止编译器做读写优化,每次访问必须从原始内存读取,不使用寄存器缓存值。
- 使用场景:硬件寄存器、中断服务函数共享变量、多线程全局共享变量。
22. register 关键字的作用与限制?
- 作用:向编译器建议,将该变量存储至 CPU 寄存器,减少内存读写,提升访问速度。
- 限制:不能对 register 变量取地址 &;编译器可直接忽略该优化建议;不能用于全局变量。
23. extern 关键字的作用?
用于声明外部全局变量或外部函数,仅告知编译器符号存在,不会分配内存,实现跨源文件访问变量 / 函数。
24. typedef 和 #define 的本质区别?
- typedef:编译阶段处理,专门给数据类型创建别名,具备完整类型校验,安全性高。
- #define:预处理阶段纯文本替换,无类型检查,极易产生运算符优先级 Bug。
25. sizeof 是关键字还是函数?
sizeof 是 C 语言内置关键字,不是库函数,数值在编译阶段直接计算完成。
26. sizeof 和 strlen 的区别?
- sizeof:计算变量 / 数据类型占用的总内存字节,包含字符串末尾 '\\0',编译期求值。
- strlen:标准库函数,仅统计字符串有效可见字符长度,不含末尾 '\\0',运行时遍历内存计算。
27. auto 关键字的作用?
auto 用于标记局部变量为自动存储类型,是所有局部变量默认属性,日常开发几乎不会手动书写。
28. break 和 continue 的区别?
- break:直接跳出当前所在循环、switch 分支,终止循环整体执行。
- continue:仅跳过本次循环剩余代码,直接进入下一次循环条件判断,循环不会终止。
29. goto 语句的用法与优缺点?
- 用法:无条件跳转到代码中自定义标签位置。
- 优点:快速跳出多层嵌套循环、异常错误处理代码简洁高效。
- 缺点:无节制使用会打乱代码执行流程,可读性、可维护性大幅下降。
30. void 类型的用途?
标识函数无返回值;标识函数无入参;定义通用泛型指针 void*,可接收任意类型地址。
31. enum 枚举类型的作用?
批量定义一组有业务含义的整型常量,提升代码可读性;枚举常量默认从 0 开始依次自增。
32. C 语言中有 this 指针吗?为什么?
C 语言不存在 this 指针。this 是 C++ 面向对象语法特性,C 语言没有类、对象的语法概念。
三、指针与地址
33. 指针的本质是什么?
指针本质是内存地址;指针变量是专门用来存放内存地址数值的变量。
34. 空指针、野指针、悬空指针的区别?
- 空指针:赋值为 NULL(地址 0),属于合法指针,代码中可以判断规避风险。
- 野指针:指针未初始化,指向随机、不受管控的非法内存地址。
- 悬空指针:指针原本指向堆内存,内存已调用 free 释放,指针未重置,地址失效。
35. 如何避免野指针和悬空指针?
- 指针定义时直接初始化为 NULL;
- 调用 free 释放堆内存后,立刻将指针赋值为 NULL;
- 使用指针访问内容前,先判断 if (p != NULL)。
36. 指针数组 int p [10] 和数组指针 int (p)[10] 的区别?
- int p [10]:指针数组,数组包含 10 个 int类型指针变量。
- int (*p)[10]:数组指针,p 是单一指针,指向一个包含 10 个 int 元素的整型数组。
37. 指针函数 int fun () 和函数指针 int (fun)() 的区别?
- 指针函数:普通函数,函数返回值类型为指针。
- 函数指针:本身是指针变量,专门存储函数入口地址,多用于回调机制。
38. 二级指针的作用?
修改一级指针本身的指向;动态分配二维数组;将一级指针作为函数参数传递并修改。
39. 指针加减运算的规则?
- p ± n 实际内存偏移字节 = n × sizeof (指针指向的数据类型)
- 示例:int指针 + 1 偏移 4 字节;char指针 + 1 仅偏移 1 字节。
40. 数组名和指针的区别?
- 数组名是地址常量,不能执行自增、赋值修改;sizeof (数组名) 计算整个数组总内存。
- 普通指针是变量,可自由修改指向;sizeof (指针) 固定为 4/8 字节。
41. 数组作为函数参数为什么会退化为指针?
C 语言为节省栈开销、提升执行效率,不会拷贝完整数组,仅传递数组首元素地址。
42. void 泛型指针的作用?
通用无类型指针,可接收任意类型变量地址;使用时必须强制转换为对应数据类型。
43. 解引用空指针会发生什么?
触发段错误 (Segmentation fault),操作系统终止程序,程序直接崩溃。
44. 函数指针的典型用途?
回调函数、状态机实现、表驱动编程、命令注册、模块化插件框架。
45. 指针强制类型转换有什么风险?
内存对齐异常、内存越界访问、数据解析错乱、非法内存访问崩溃。
46. NULL 的本质是什么?
空指针常量,底层定义为 (void*) 0,代表无效、不可访问的内存地址。
47. 如何理解复杂声明 int (s[10])(int)?
s 是数组,数组包含 10 个元素,每个元素是函数指针;该函数接收 1 个 int 入参,返回 int 类型数值。
48. 为什么不能返回局部数组的地址?
局部数组分配在栈内存,函数执行完毕后栈空间自动销毁,返回的地址变为非法,访问直接崩溃。
49. 常量指针可以指向非常量变量吗?
可以,但无法通过该常量指针修改原始变量内部数值。
50. 什么是指针越界?
指针访问超出自身合法分配的内存范围,会篡改其他变量数据、破坏栈结构,引发程序异常。
51. 指针和数组访问效率谁更高?
现代编译器具备完善优化,两者运行效率几乎无差别。
52. 什么是泛型编程?C 语言如何实现?
- 泛型编程:编写与具体数据类型无关的通用业务代码。
- C 实现方案:void * 通用指针搭配函数指针完成泛型逻辑。
四、内存管理与五大分区
53. C 程序内存五大分区及存放内容?
- 栈区 (stack):局部变量、函数入参、函数返回地址,函数执行结束自动回收。
- 堆区 (heap):malloc/calloc/realloc 手动分配的动态内存,需手动 free 释放。
- 全局 / 静态区:全局变量、static 修饰静态变量,程序运行全程常驻。
- 常量区:字符串常量、const 修饰全局只读数据,内存只读不可写。
- 代码区:编译后的程序二进制指令,只读、可执行。
54. 堆内存与栈内存的详细区别?
| 管理方式 | 系统自动分配、自动释放 | 程序员手动 malloc 申请、free 释放 |
| 空间大小 | 容量很小,一般几 MB | 容量大,占用系统空闲内存 |
| 内存生长方向 | 从高地址向低地址生长 | 从低地址向高地址生长 |
| 访问速度 | 速度极快 | 速度较慢,需要内核系统调用 |
| 内存碎片 | 不会产生碎片 | 频繁分配释放极易产生内存碎片 |
55. malloc、calloc、realloc 三者区别?
- malloc:分配指定字节堆内存,内存不做初始化,内部是随机垃圾值。
- calloc:分配 n 块 size 大小内存,分配完成后自动全部清零。
- realloc:调整已分配堆内存块的大小,支持扩容、缩容。
56. free 函数的原理?
仅向系统标记这块内存为空闲可用,不会清空内存原有数据,也不会自动将传入指针置 NULL。
57. 为什么 free 后要把指针置为 NULL?
避免指针变为悬空指针,防止重复 free、非法访问失效内存导致程序崩溃。
58. 什么是内存泄漏?危害是什么?
- 定义:malloc 申请堆内存后未调用 free 释放,同时保存地址的指针丢失,内存永久无法回收。
- 危害:长期循环运行的程序会持续占用内存,最终耗尽系统内存,程序卡顿、崩溃。
59. 什么是内存碎片?如何减少?
- 内存碎片:大量零散、不连续的空闲小块内存,无法分配大块连续内存。
- 优化方案:内存池预分配、减少频繁 malloc/free、一次性申请大块内存。
60. 段错误常见原因?
空指针解引用、野指针访问非法地址、数组下标越界、修改常量区字符串、栈内存溢出、悬空指针访问。
61. 字符串常量存储在哪里?能否修改?
- 存储位置:只读常量区。
- 修改权限:禁止修改,强行写入会触发段错误程序崩溃。
62. memset 作用和注意事项?
- 作用:按单个字节批量填充一块内存。
- 注意:int 整型数组仅能 memset 填充 0;填充 1 会导致每个字节为 0x01,数值完全错误。
63. memcpy 和 memmove 的区别?
- memcpy:不判断内存区间重叠,源地址与目标地址重叠时会覆盖原始数据,拷贝出错。
- memmove:自动判断内存重叠,重叠场景反向拷贝,保证数据完整,兼容性更强。
64. malloc (0) 的返回值是什么?
返回合法非 NULL 指针,但该指针无可用读写内存,程序结束前必须调用 free 释放,否则内存泄漏。
65. 动态内存分配失败如何处理?
分配失败 malloc 返回 NULL,每次申请内存后必须判断指针有效性,否则解引用空指针直接崩溃。
66. 全局变量和 static 变量为什么默认是 0?
存储在全局初始化段,程序加载到内存时操作系统统一清零。
67. 局部变量为什么是随机值?
分配在栈区,操作系统不会初始化栈内存,变量值是上一次函数遗留的垃圾数据。
68. 什么是栈溢出?
栈内存容量耗尽,常见诱因:递归调用深度过大、函数内定义超大局部数组、多层中断嵌套。
69. alloca 和 malloc 的区别?
alloca:在栈上分配临时内存,函数执行完毕自动释放,不需要 free。
malloc:在堆上分配内存,必须手动调用 free 释放。
70. 堆内存逻辑上是连续的吗?
逻辑地址连续;物理内存不一定连续,操作系统通过虚拟内存映射管理。
71. 什么是写时复制(Copy On Write)?
- 操作系统内存优化机制:fork 创建子进程时不复制完整内存,父子进程共享同一块内存;
- 只有任意一方修改数据时,才单独分配内存拷贝副本,降低内存开销。
72. 如何判断指针是否合法?
仅能判断指针是否等于 NULL;无法校验野指针、悬空指针指向的地址是否可用。
五、结构体、联合体、位域、柔性数组
73. struct 和 union 的区别?
- struct 结构体:所有成员独立分配内存,总大小为对齐后所有成员内存之和。
- union 联合体:全部成员共用同一块内存,同一时刻仅一个成员有效,总大小等于最大成员占用字节。
74. 结构体内存对齐规则?
- 结构体第一个成员偏移地址固定为 0;
- 后续成员起始偏移必须是自身数据类型字节大小的整数倍;
- 结构体整体总字节,必须是内部最大基础类型字节的整数倍;
- 结构体嵌套时,内部子结构体对齐规则同步生效。
75. #pragma pack (n) 的作用?
- 手动设置内存对齐系数 n,强制所有成员按 n 字节对齐;
- 常用于网络协议解析、硬件寄存器存储,节省内存空间。
76. 位域是什么?用途?
- 位域:结构体中以 bit 位为单位分配成员内存,极致节省存储空间。
- 用途:单片机硬件寄存器、通信协议标志位、大量布尔标记存储。
77. 位域有哪些限制?
- 不能对位域成员取地址 &;
- 不能跨字节分配;
- 无法作为函数入参;
- 对齐规则特殊,兼容性差。
78. 什么是柔性数组?
结构体最后一个成员定义为无长度数组,示例:int data [];,无固定占用内存。
79. 柔性数组相比指针的优势?
- 结构体与数据内存连续,缓存命中率高、访问更快;
- 仅需要一次 free 释放,无二次内存泄漏;不额外占用指针 8 字节内存开销。
80. 如何计算结构体成员偏移量?
#define OFFSET(struct_type, member) ((size_t)&(((struct_type *)0)->member))
81. 结构体之间可以直接赋值吗?
同类型结构体支持直接赋值,编译器自动逐字节拷贝全部成员数据。
82. 结构体作为函数参数有什么问题?
传参时完整拷贝整个结构体,栈开销大、执行效率低;开发中优先传递结构体指针。
83. 空结构体大小是多少?
- GCC 编译器中空结构体占 0 字节;
- VS 编译器中空结构体占 1 字节占位。
84. 结构体可以包含函数吗?
C 语言结构体不能直接存放函数;可存储函数指针,模拟面向对象的成员方法。
六、数组与字符串高频题
85. char s = "abc" 和 char s [] = "abc" 的区别?
- char *s = "abc":指针 s 存字符串常量地址,字符串位于只读常量区,内容不可修改。
- char s [] = "abc":栈上字符数组,常量字符串拷贝至栈内存,数组内容支持修改。
86. 字符串为什么必须以 \\0 结尾?
- strlen、strcpy 等所有字符串库函数依靠 '\\0' 判断字符串终止位置;
- 无结束符会持续越界读取内存。
87. 实现 strlen(不使用库函数)
int my_strlen(const char *s) {
int len = 0;
while (*s++) len++;
return len;
}
88. 实现 strcpy
char *my_strcpy(char *dest, const char *src) {
char *d = dest;
while ((*d++ = *src++));
return dest;
}
89. 实现 strncpy
char *my_strncpy(char *dest, const char *src, int n) {
int i;
for (i = 0; i < n && src[i]; i++)
dest[i] = src[i];
for (; i < n; i++)
dest[i] = '\\0';
return dest;
}
90. 实现 strcmp
int my_strcmp(const char *a, const char *b) {
while (*a && *b && *a == *b) {
a++;
b++;
}
return *a – *b;
}
91. 实现 strcat
char *my_strcat(char *dest, const char *src) {
char *d = dest;
while (*d) d++;
while ((*d++ = *src++));
return dest;
}
92. 实现 memcpy
void *my_memcpy(void *dest, const void *src, int n) {
char *d = (char *)dest;
const char *s = (const char *)src;
while (n–)
*d++ = *s++;
return dest;
}
93. 实现 memmove
void *my_memmove(void *dest, const void *src, int n) {
char *d = (char *)dest;
const char *s = (const char *)src;
if (d < s) {
while (n–) *d++ = *s++;
} else {
d += n;
s += n;
while (n–) *–d = *–s;
}
return dest;
}
94. 字符串逆序实现
void reverse_str(char *s) {
if (!s) return;
char *l = s, *r = s + my_strlen(s) – 1;
while (l < r) {
char c = *l;
*l = *r;
*r = c;
l++; r–;
}
}
95. 单词逆序(I am C → C am I)
void reverse_words(char *s) {
int len = my_strlen(s);
reverse_str(s);
char *p = s;
for (int i = 0; i <= len; i++) {
if (s[i] == ' ' || s[i] == '\\0') {
s[i] = '\\0';
reverse_str(p);
p = s + i + 1;
}
}
}
96. 判断回文字符串
int is_palindrome(char *s) {
if (!s) return 1;
int l = 0, r = my_strlen(s) – 1;
while (l < r) {
if (s[l] != s[r]) return 0;
l++; r–;
}
return 1;
}
97. 二维数组在内存中如何存放?
采用行优先连续存储,同一行元素地址连续,存储完一整行再存储下一行。
98. 数组越界为什么不一定立刻崩溃?
越界访问的内存仍属于进程可读写虚拟内存,仅篡改其他变量数据,不触发操作系统内存保护,不会直接崩溃。
99. 数组不完全初始化会怎样?
显式赋值的元素填入对应数值;未初始化的数组元素系统自动填充 0。
100. 什么是字符串常量折叠?
编译器优化机制:代码中完全相同的字符串常量,在常量区仅存储一份,所有指针指向同一块地址。
101. sprintf 和 snprintf 区别?
- sprintf:无缓冲区长度限制,极易发生缓冲区溢出,存在安全漏洞。
- snprintf:传入缓冲区最大长度,截断超长字符串,规避溢出风险,开发推荐使用。
102. 如何安全拼接字符串?
使用 strncat 函数,传入缓冲区剩余可用长度,限制拷贝字符数量,防止缓冲区溢出。
七、预处理、编译、汇编、链接
103. C 语言完整编译四步骤?
- 预处理:源文件.c 生成.i 预处理文件;
- 编译:.i 文件编译生成汇编.s 文件;
- 汇编:.s 汇编文件转为二进制目标.o 文件;
- 链接:多个.o 目标文件 + 系统库链接,生成可执行程序。
104. 预处理阶段主要工作?
展开宏定义、递归引入头文件、执行条件编译指令、删除所有注释、替换系统预定义宏。
105. 条件编译的用途?
多平台代码适配、调试日志开关、功能模块裁剪、防止头文件重复包含。
106. 宏定义优点和缺点?
- 优点:无函数调用栈开销,运行效率极高;
- 缺点:无类型校验、运算符优先级陷阱、无法断点调试、重复展开造成代码膨胀。
107. 宏参数为什么必须加括号?
宏是纯文本替换,不加括号会因运算符优先级改变运算逻辑,示例标准写法:#define SQR (x) ((x)*(x))
108. inline 函数和宏的区别?
- inline 内联函数:编译阶段处理,完整类型检查,支持 GDB 断点调试,语法是标准函数;
- 宏定义:预处理文本替换,无任何类型校验,极易产生逻辑 Bug。
109. 静态链接和动态链接区别?
| 库处理方式 | 库代码完整拷贝进可执行文件 | 仅记录库依赖,运行时加载外部库 |
| 程序体积 | 体积大,包含全部库代码 | 体积小巧,仅保存依赖符号 |
| 运行依赖 | 独立运行,无需外部库文件 | 运行环境必须存在对应动态库 |
| 更新方式 | 更新库需要重新编译程序 | 替换动态库文件即可,无需重编 |
110. 为什么头文件不要定义全局变量和函数?
头文件被多个.c 源文件同时引入时,变量 / 函数会重复定义,链接器直接报错。
111. 常见预定义宏?
- FILE 当前源文件名字符串
- LINE 当前代码行号整型数字
- func 当前执行函数名称
- DATE 程序编译日期
- TIME 程序编译时间
112. 什么是符号解析?
链接阶段核心步骤:找到代码中所有变量、函数符号对应的定义位置,绑定符号地址。
113. 什么是重定位?
目标文件.o 内地址均为相对偏移,链接时修正所有地址偏移,转换为进程虚拟内存真实地址,程序才能正常寻址运行。
114. 宏可以递归吗?
不可以,预处理器内置屏蔽逻辑,递归宏不会无限展开,仅展开一次。
115. # 和 ## 在宏中的作用?
- # 字符串化运算符:将宏参数转换为字符串字面量;
- ## 符号连接运算符:拼接两个标识符,合并为新变量 / 函数名。
116. 什么是强符号和弱符号?
- 强符号:已初始化全局变量、普通函数;
- 弱符号:未初始化全局变量;
- 链接规则:多个文件同名符号,强符号覆盖弱符号,多个强符号直接报重定义错误。
八、函数、递归、回调、调用栈
117. 函数传值和传址区别?
- 传值:传入变量副本,函数内部修改副本,不会影响外部原始变量;
- 传址:传入变量内存地址,函数通过地址直接操作原始变量,外部数值同步变更。
118. 什么是递归?
函数直接或间接调用自身,通过逐层拆解问题,直至抵达终止条件。
119. 递归必备两个条件?
- 基线终止条件:递归停止出口,避免无限调用;
- 递归递推逻辑:每次调用向终止条件逐步逼近。
120. 递归的缺点?
・重复计算多,执行效率低;多层调用占用大量栈内存,极易栈溢出;递归深度存在硬性上限。
121. 什么是函数调用栈?
操作系统为线程分配的栈空间,专门存储所有未执行完毕函数的调用信息,实现函数调用、返回跳转。
122. 什么是栈帧?
- 每次调用函数,在调用栈上新开辟一段内存空间,称为栈帧;
- 存储函数入参、返回地址、局部变量、寄存器现场。
123. 什么是可变参数函数?
- 函数入参个数、类型不固定,典型示例 printf;
- C 语言通过 stdarg.h 头文件 va_list 系列接口实现。
124. 什么是可重入函数?
- 函数执行过程中可被中断打断,中断内再次调用该函数,执行结果不会错乱;
- 核心要求:不使用全局 /static 变量、不调用不可重入库函数、不操作共享无锁资源。
125. 什么是回调函数?
将函数指针作为入参传递给其他函数,满足指定条件时,由对方函数主动调用,多用于事件驱动、模块化解耦。
126. C 语言支持函数重载吗?为什么?
- C 语言不支持函数重载;
- 编译后函数符号仅保留函数名,丢弃参数类型信息,同名函数链接时无法区分。
127. 如何让代码在 main 之前执行?
GCC 编译器扩展语法:attribute((constructor)) 修饰函数,程序初始化阶段自动执行,早于 main 函数。
128. 函数指针数组用途?
命令注册表、状态机跳转表、菜单功能映射、批量回调注册。
129. 什么是函数调用约定?
规定函数入参入栈顺序、函数执行完毕由谁清理栈、寄存器使用规则,常见 cdecl、stdcall。
130. 库函数和系统调用区别?
- 库函数:标准 C / 第三方库封装,运行在用户态,部分库函数底层封装系统调用;
- 系统调用:操作系统内核提供接口,触发 CPU 权限切换,进入内核态操作硬件资源。
九、位运算
131. 常用位运算有哪些?
& 按位与、| 按位或、^ 按位异或、~ 按位取反、<<左移、>> 右移
132. 置位、清位、翻转某一位?
// 将变量a第n位置1
a |= (1 << n);
// 将变量a第n位清0
a &= ~(1 << n);
// 将变量a第n位翻转
a ^= (1 << n);
133. 位运算判断奇偶?
运算表达式 x & 1;结果等于 1 为奇数,等于 0 为偶数。
134. 位运算交换变量(无临时变量)
a ^= b;
b ^= a;
a ^= b;
135. 统计二进制中 1 的个数
int count_bit1(int x) {
int c = 0;
while (x) {
c++;
x &= x – 1;
}
return c;
}
136. 算术移位与逻辑移位区别?
- 算术右移:针对有符号数,右侧空位填充符号位;
- 逻辑右移:针对无符号数,右侧空位统一填充 0。
137. 左移等价乘 2,右移等价除 2?
- 无符号正数、有符号正数完全成立;
- 负数算术右移结果不等于除法,存在差异。
138. 什么是大小端模式?
- 小端模式:低字节数据存储在低内存地址,PC x86 架构默认;
- 大端模式:高字节数据存储在低内存地址,网络传输、单片机常用网络字节序。
139. 代码判断大小端
int is_little_endian(void) {
int a = 1;
return *(char *)&a;
}
140. 位运算判断是否为 2 的幂?
判断表达式:x != 0 && (x & (x – 1)) == 0
141. 位运算求绝对值?
int abs_bit(int x) {
int mask = x >> 31;
return (x ^ mask) – mask;
}
142. 高低字节交换
unsigned short swap16(unsigned short val) {
return (val << 8) | (val >> 8);
}
unsigned int swap32(unsigned int val) {
return ((val & 0xff000000) >> 24) |
((val & 0x00ff0000) >> 8) |
((val & 0x0000ff00) << 8) |
((val & 0x000000ff) << 24);
}
十、嵌入式 C 专属面试题
143. 中断服务函数注意事项?
- 函数代码尽量简短,禁止耗时循环;
- 禁止调用 printf、浮点运算等耗时函数;
- 与主循环共享的全局变量添加 volatile 修饰;
- 不能调用不可重入库函数。
144. 为什么中断变量要用 volatile?
防止编译器做读写优化,强制每次访问都从原始内存读取,避免寄存器缓存旧数值导致逻辑错误。
145. 什么是内存映射 I/O?
// 直接操作硬件寄存器地址
*(volatile unsigned int *)0x40020000 = 1;
146. ROM、RAM、Flash 分别存放什么?
- Flash/ROM:程序二进制代码、字符串常量、全局初始化常量;断电数据不丢失。
- RAM:栈、堆、全局变量、运行临时数据;断电数据清空。
147. 栈溢出常见原因?
递归调用层级过深、函数内部定义超大局部数组、多层中断嵌套、无边界校验循环。
148. 什么是临界区?如何保护?
- 临界区:多中断 / 多线程共享资源的代码段,执行过程不允许被打断;
- 保护方案:关闭总中断、互斥信号量、互斥锁。
149. 嵌入式为什么少用动态内存?
- 频繁分配释放产生内存碎片,长期运行内存耗尽;
- 分配耗时不确定,破坏系统实时性;
- 容易内存泄漏、野指针崩溃。
150. 什么是看门狗?
- 硬件独立定时器,程序正常运行时定时喂狗清零计数器;
- 程序卡死、跑飞未喂狗时,硬件自动复位单片机。
151. 什么是裸机编程?
无操作系统,直接操作硬件寄存器、中断,单循环 while (1) 驱动业务逻辑,多用于单片机产品。
152. 如何用 C 操作 GPIO?
通过内存映射寄存器地址,配合位运算置位、清零寄存器对应 bit,控制引脚高低电平。
十一、代码 Bug 分析与输出题
153. #define SQR (x) x*x,SQR (3+1) 结果?
运行结果 7;展开式 3+13+1,优先级出错;标准安全写法 #define SQR (x) ((x)(x))
154. i++ + ++i 为什么结果不确定?
C 标准未规定同一表达式多个自增的求值顺序,属于未定义行为,不同编译器运算结果不一致。
155. 多次 free 同一指针会怎样?
堆内存管理结构损坏,程序直接崩溃;free (NULL) 属于安全操作,不会报错。
156. malloc 不判空直接使用?
内存分配失败返回 NULL,直接解引用空指针触发段错误,程序崩溃。
157. 全局 / 局部变量未初始化值?
全局、static 静态变量默认初始值 0;函数内局部变量是栈残留随机垃圾值。
158. 浮点数为什么不能用 == 比较?
float/double 存储存在精度丢失,十进制小数无法二进制精确存储;推荐判断差值绝对值小于极小值 fabs (a-b) < 1e-6。
159. 修改字符串常量为什么段错误?
字符串常量存储在只读常量内存段,操作系统禁止写入操作,触发内存保护崩溃。
160. 数组越界后果?
篡改其他变量数值、破坏栈帧返回地址、程序逻辑错乱、随机崩溃、安全缓冲区溢出漏洞。
161. 递归无结束条件会怎样?
无限递归持续占用栈内存,栈空间耗尽触发栈溢出,程序崩溃。
162. 野指针一定崩溃吗?
不一定;若随机指向进程合法可读写内存,仅篡改数据,不会直接崩溃,但逻辑完全错乱。
163. 悬空指针访问一定崩溃吗?
不一定;释放后的堆内存若未被重新分配覆盖,短暂访问可能读取旧数据,无崩溃现象。
164. free 野指针后果?
堆管理链表破坏,堆内存彻底损坏,程序立即崩溃。
165. 全局变量太多的问题?
持续占用 RAM 内存、全局命名冲突、多线程 / 多中断无保护下线程不安全、代码耦合高,维护困难。
十二、面试必手撕代码题
166. 单链表反转
struct Node {
int data;
struct Node *next;
};
struct Node* reverseList(struct Node *head) {
struct Node *prev = NULL, *cur = head, *next;
while (cur) {
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
167. 二分查找
int binarySearch(int a[], int n, int x) {
int l = 0, r = n – 1;
while (l <= r) {
int mid = l + (r – l) / 2;
if (a[mid] == x) return mid;
else if (a[mid] < x) l = mid + 1;
else r = mid – 1;
}
return -1;
}
168. 快速排序
void quickSort(int a[], int l, int r) {
if (l >= r) return;
int i = l, j = r, pivot = a[l];
while (i < j) {
while (i < j && a[j] >= pivot) j–;
a[i] = a[j];
while (i < j && a[i] <= pivot) i++;
a[j] = a[i];
}
a[i] = pivot;
quickSort(a, l, i – 1);
quickSort(a, i + 1, r);
}
169. 冒泡排序
void bubbleSort(int a[], int n) {
for (int i = 0; i < n – 1; i++) {
for (int j = 0; j < n – 1 – i; j++) {
if (a[j] > a[j + 1]) {
int t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
}
170. 实现 atoi
int my_atoi(const char *str) {
int num = 0, sign = 1;
while (*str == ' ') str++;
if (*str == '+' || *str == '-') {
sign = (*str == '-') ? -1 : 1;
str++;
}
while (*str >= '0' && *str <= '9') {
num = num * 10 + (*str – '0');
str++;
}
return num * sign;
}
171. 实现 itoa
char *my_itoa(int num, char *str) {
char *start = str;
int sign = num;
if (num < 0) num = -num;
do {
*str++ = num % 10 + '0';
} while ((num /= 10) > 0);
if (sign < 0) *str++ = '-';
*str = '\\0';
char *l = start, *r = str – 1;
while (l < r) {
char c = *l;
*l = *r;
*r = c;
l++; r–;
}
return start;
}
172. 字符串空格替换为 %20
void replaceSpace(char *str, int len) {
int space = 0;
for (int i = 0; i < len; i++) {
if (str[i] == ' ') space++;
}
int new_len = len + 2 * space;
for (int i = len – 1; i >= 0; i–) {
if (str[i] == ' ') {
str[new_len–] = '0';
str[new_len–] = '2';
str[new_len–] = '%';
} else {
str[new_len–] = str[i];
}
}
}
173. 链表判断环
int hasCycle(struct Node *head) {
struct Node *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return 1;
}
return 0;
}
174. 合并两个有序链表
struct Node *mergeTwoLists(struct Node *l1, struct Node *l2) {
struct Node dummy, *p = &dummy;
dummy.next = NULL;
while (l1 && l2) {
if (l1->data < l2->data) {
p->next = l1;
l1 = l1->next;
} else {
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
p->next = l1 ? l1 : l2;
return dummy.next;
}
175. 简单内存池
#define BLOCK_SIZE 64
#define POOL_NUM 100
typedef struct {
char data[BLOCK_SIZE];
int used;
} MemBlock;
MemBlock pool[POOL_NUM];
void pool_init() {
for (int i = 0; i < POOL_NUM; i++)
pool[i].used = 0;
}
void *pool_alloc() {
for (int i = 0; i < POOL_NUM; i++) {
if (!pool[i].used) {
pool[i].used = 1;
return pool[i].data;
}
}
return NULL;
}
void pool_free(void *ptr) {
MemBlock *p = (MemBlock *)((char *)ptr – offsetof(MemBlock, data));
p->used = 0;
}
176. 大小端转换
unsigned short bswap16(unsigned short v) {
return (v << 8) | (v >> 8);
}
unsigned int bswap32(unsigned int v) {
return ((v >> 24) & 0xff) |
((v >> 8) & 0xff00) |
((v << 8) & 0xff0000) |
((v << 24) & 0xff000000);
}
177. 宏实现 MAX/MIN
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
178. 查找重复数字(1~n)
int findDuplicate(int nums[], int n) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
179. 回调通用排序
typedef int (*cmp_t)(const void *, const void *);
void sort_cb(int arr[], int n, cmp_t cmp) {
for (int i = 0; i < n – 1; i++) {
for (int j = 0; j < n – 1 – i; j++) {
if (cmp(&arr[j], &arr[j+1]) > 0) {
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
}
int cmp_asc(const void *a, const void *b) {
return *(int *)a – *(int *)b;
}
180. 循环队列
#define Q_SIZE 10
typedef struct {
int data[Q_SIZE];
int front, rear;
} Queue;
void q_init(Queue *q) {
q->front = q->rear = 0;
}
int q_full(Queue *q) {
return (q->rear + 1) % Q_SIZE == q->front;
}
int q_empty(Queue *q) {
return q->front == q->rear;
}
int q_push(Queue *q, int v) {
if (q_full(q)) return -1;
q->data[q->rear] = v;
q->rear = (q->rear + 1) % Q_SIZE;
return 0;
}
int q_pop(Queue *q, int *v) {
if (q_empty(q)) return -1;
*v = q->data[q->front];
q->front = (q->front + 1) % Q_SIZE;
return 0;
}
十三、大厂进阶原理题
181. 什么是缓冲区溢出?
- 向固定大小缓冲区写入超长数据,超出容量覆盖栈、堆内存;
- 恶意利用可篡改程序执行逻辑、注入恶意代码,属于高危安全漏洞。
182. C 语言如何模拟面向对象?
- 封装:结构体存储属性,搭配函数指针存储操作方法;
- 继承:结构体内部嵌套父结构体实现属性复用;
- 多态:统一函数指针表,不同结构体挂载不同实现函数。
183. 程序从加载到运行全过程?
- 操作系统内核读取 ELF 可执行文件;
- 虚拟内存映射、重定位修正地址;
- 全局 / 静态变量初始化;
- 执行系统启动文件;
- 调用 main 函数进入业务逻辑。
184. 如何定位内存泄漏?
- 方案 1:封装自定义 malloc/free,记录每次分配文件、行号、大小,程序退出打印未释放内存;
- 方案 2:Linux 工具 valgrind 运行程序检测泄漏;
- 方案 3:编译器 ASAN 地址消毒工具,运行时精准定位泄漏行。
185. C 语言性能优化手段?
- 减少内存拷贝,优先使用指针传递;
- 栈内存替代堆内存;
- 位运算替代乘除模;
- 循环展开减少跳转;
- 高频函数添加 inline;
- 内存池消除碎片;
- 减少全局变量缓存命中。
186. 什么是系统调用?
- 用户态程序向操作系统内核发起资源操作的标准接口;
- 执行系统调用 CPU 切换内核态,硬件、内存、进程操作均依赖系统调用。
187. 什么是 DMA?为什么用 volatile?
- DMA:直接内存访问硬件,无需 CPU 搬运数据,硬件自主读写内存;
- 硬件异步修改内存数值,编译器无法感知,必须 volatile 禁止缓存优化,保证读取最新数据。
188. 什么是线程安全?
- 多线程同时调用同一段代码、操作共享资源,不会出现数据错乱、死锁、崩溃;
- 实现手段:互斥锁、原子操作、临界区关中断。
189. 什么是可重入代码?
- 函数执行中途被中断、多线程抢占调用,多次执行结果完全一致;
- 禁止使用全局、static 共享变量,不调用不可重入函数。
190. 什么是内存映射文件?
将磁盘文件直接映射到进程虚拟地址空间,操作内存等价操作文件,省去 read/write 系统调用拷贝,大文件读写效率极高。
191. 什么是野指针?产生原因?
- 野指针:指向随机非法内存地址的指针;
- 产生原因:定义指针未初始化、数组越界赋值指针、强制转换非法地址赋值指针。
192. 什么是悬空指针?
堆内存调用 free 释放后,保存该内存地址的指针未重置为 NULL,指针仍指向失效内存。
193. 静态变量为什么能保留值?
静态变量存储在全局静态数据段,生命周期等于整个程序运行周期,不会随函数栈帧销毁,仅首次执行初始化一次。
194. 函数调用栈原理?
- 调用函数:入参压栈→返回地址压栈→开辟栈帧分配局部变量;
- 执行完毕弹出栈帧,读取返回地址跳转回上层函数。
195. 什么是字节对齐?为什么要对齐?
- 字节对齐:编译器按固定字节间隔排布结构体、变量内存;
- 对齐目的:适配 CPU 硬件读写宽度,单次读取完整数据,避免多次拆分读取,提升 CPU 访问速度;部分硬件架构非对齐访问直接崩溃。
196. 全局变量和局部变量能否同名?
- 可以;
- 局部变量作用域优先级更高,会屏蔽同名全局变量;如需访问全局,可通过 extern 或结构体限定。
197. const 常量可以修改吗?
- 栈上局部 const 变量:可通过指针强制转换篡改数值;
- 全局 const 字符串常量:存储只读段,强行修改触发段错误崩溃。
198. 什么是段错误?如何调试?
- 段错误:程序访问操作系统保护的非法内存地址触发的崩溃;
- 调试方案:dmesg 查看崩溃地址、生成 core 转储文件搭配 GDB 调试、ASAN 地址检测、分步注释定位出错代码。
199. C 语言编译后为什么找不到变量名?
变量名仅用于源码开发阶段;编译汇编链接后,所有变量、函数名全部替换为内存偏移 / 虚拟地址,可执行文件仅保留指令与地址,丢弃符号(未加 – g 调试信息时)。
200. 为什么 main 函数可以没有 return?
- C99 及之后标准规定:main 函数末尾省略 return 语句时,程序正常退出默认返回 return 0;
- 其他普通函数无 return 会返回随机栈垃圾值,属于未定义行为。
以上就是200 道 C 语言面试题,内容覆盖基础到进阶,多看多练,对编程和面试都会有帮助。
制作不易,如果对你有用,希望能点赞收藏支持一下。

