欢迎光临
我们一直在努力

200道C语言面试题全解析:基础到进阶一网打尽

本篇整理了 200 道 C 语言常见面试题,包含基础知识点、代码手写题和常见问题解析,答案详细,适合学习和面试复习。

目录

一、基础语法与数据类型

二、C 语言核心关键字

三、指针与地址

四、内存管理与五大分区

五、结构体、联合体、位域、柔性数组

六、数组与字符串高频题

七、预处理、编译、汇编、链接

八、函数、递归、回调、调用栈

九、位运算

十、嵌入式 C 专属面试题

十一、代码 Bug 分析与输出题

十二、面试必手撕代码题

十三、大厂进阶原理题


一、基础语法与数据类型

1. C 语言中有哪些基本数据类型?在 32/64 位下各占多少字节?

数据类型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 语言面试题,内容覆盖基础到进阶,多看多练,对编程和面试都会有帮助。

制作不易,如果对你有用,希望能点赞收藏支持一下。

赞(0)
未经允许不得转载:171主机测评 » 200道C语言面试题全解析:基础到进阶一网打尽
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址