在 C 语言求职笔试与面试中,核心考点集中在内存管理、指针操作、关键字用法、结构体设计等领域。本文基于《经典笔试面试题目.pdf》,将 62 道题目按 “面试问答类”“笔试编程类” 分类拆解,每道题均附详细解析与避坑指南,帮你系统掌握考点,轻松应对考核。
一、面试高频问答类(理解与记忆考点)
1. 野指针的危害(海康威视面试)
野指针定义:指向非法内存地址的指针,常见场景包括指针未初始化、指向已释放的内存、指针越界访问。
核心危害:
- 访问非法内存会直接触发程序崩溃(如 Linux 下的 “Segmentation Fault”);
- 若野指针指向合法但非预期的内存(如其他变量地址),会篡改该内存数据,导致逻辑混乱;
- 嵌入式场景中,野指针可能误操作硬件寄存器,引发设备异常。
避坑建议:指针初始化时设为NULL
,内存释放后及时置NULL
,使用前检查指针有效性。
2. 简述 i++ 和 ++i(模拟面试 + 延伸)
两者均为自增运算符,核心差异在于 **“自增时机” 与 “返回值”**,具体对比如下:
运算符 | 执行逻辑 | 示例(初始 i=3) | 结果 |
---|---|---|---|
i++ | 先返回 i 的原始值,再执行 i=i+1 | j = i++; | j=3,i=4 |
++i | 先执行 i=i+1,再返回自增后的值 | j = ++i; | j=4,i=4 |
延伸考点:
- 循环中(如
for(i=0;i<5;i++)
),若不使用自增返回值,两者效率无差异; - 表达式中(如
a = i++ + ++i
),结果依赖编译器(未定义行为),面试中需避免此类写法。
3. sizeof 和 strlen 区别(模拟面试)
sizeof
与strlen
是 C 语言中常用的 “长度计算工具”,但本质与用法完全不同,对比如下:
对比维度 | sizeof | strlen |
---|---|---|
本质属性 | 运算符(编译期计算结果) | 库函数(运行期计算结果) |
计算范围 | 变量 / 类型占用的总字节数(含字符串结束符\0 ) | 字符串中有效字符数(不含\0 ,需以\0 结尾) |
支持参数 | 变量、数组、数据类型(如sizeof(int) ) | 仅字符串指针或字符数组(非字符串会导致越界) |
示例(char str[]="abc" ) | sizeof(str)=4 ('a'+'b'+'c'+'\0') | strlen(str)=3 (仅统计 'a'/'b'/'c') |
4. new/malloc、delete/free 的区别(模拟面试延伸)
malloc/free
是 C 语言内存管理工具,new/delete
是 C++ 运算符,核心区别如下:
对比维度 | malloc/free | new/delete |
---|---|---|
所属语言 | C 语言标准库函数 | C++ 语言运算符 |
类型检查 | 无(返回void* ,需强制转换类型) | 有(自动匹配变量类型,无需转换) |
构造 / 析构 | 仅分配 / 释放内存,不调用对象的构造 / 析构函数 | 分配内存时调用构造函数,释放时调用析构函数(对象场景必需) |
内存失败处理 | 返回NULL ,需手动判断 | 抛出bad_alloc 异常,需用try-catch 捕获 |
数组支持 | 需手动计算总字节数(如malloc(10*sizeof(int)) ) | 支持数组语法(new int[10] ,需用delete[] 释放,避免内存泄漏) |
5. 内存泄漏和堆内碎块(海康威视面试)
-
内存泄漏:
动态分配的堆内存(如malloc
/new
申请的内存)使用后未释放,导致内存被永久占用。程序运行时间越长,可用内存越少,最终可能因内存耗尽崩溃。
示例:char* p = (char*)malloc(100);
未调用free(p);
,p
指向的 100 字节内存永久泄漏。 -
堆内碎块:
频繁分配 / 释放不同大小的堆内存后,堆中产生大量 “空闲小内存块”(碎片)。这些碎片总容量足够,但单个碎片无法满足大内存分配需求,导致内存利用率下降。
解决方案:使用内存池(预先分配大块内存,按需分割),或尽量使用固定大小的内存分配。
6. volatile 关键字(移远通信面试)
核心作用:告诉编译器 “变量的值可能被意外修改(如硬件中断、多线程操作)”,禁止编译器对该变量进行优化(如将变量缓存到寄存器),确保每次访问都从内存读取最新值。
典型应用场景:
- 硬件寄存器地址变量:
volatile unsigned int *uart_reg = (unsigned int*)0x1234;
(寄存器值可能被硬件自动修改); - 多线程共享变量:
volatile int flag = 0;
(线程 A 修改flag
后,线程 B 需实时读取最新值,避免读取寄存器缓存); - 中断服务函数中修改的变量:
volatile int count = 0;
(主函数读取count
时,需获取中断修改后的最新值)。
7. static 关键字(纳思达面试、移远通信面试)
static
关键字的核心作用是 **“限制作用域”** 和 **“延长生命周期”**,修饰不同对象时效果不同:
修饰对象 | 具体作用 | 示例 |
---|---|---|
局部变量 | 生命周期延长至整个程序运行期(仅初始化 1 次),作用域仍局限于函数内部 | void func(){ static int a=0; a++; } (多次调用func ,a 会累加) |
全局变量 | 作用域限制在当前.c 文件(其他文件无法通过extern 引用),避免全局变量重名冲突 | static int g_val = 10; (仅当前文件可访问g_val ) |
函数 | 作用域限制在当前.c 文件(其他文件无法调用),隐藏内部实现细节 | static void inner_func(){}; (仅当前文件可调用inner_func ) |
8. const 关键字(纳思达面试)
const
的核心是定义 “只读变量”,防止意外修改,提升代码安全性,常见用法有 4 种:
- 修饰普通变量:
const int a = 10;
(a
的值不可修改,必须初始化,否则编译报错); - 修饰指针(重点考点):
const int *p;
:指针p
指向的内容不可改(如*p = 20;
报错),但p
本身可改(如p = &b;
合法);int *const p = &a;
:指针p
本身不可改(如p = &b;
报错),但指向的内容可改(如*p = 20;
合法);const int *const p = &a;
:指针p
和指向的内容均不可改;
- 修饰函数参数:
void func(const int x);
(防止函数内部修改x
,保护传入参数); - 修饰函数返回值:
const int func();
(返回值不可作为左值修改,如func() = 20;
报错)。
9. 一个参数既可以被 const 修饰也能被 volatile 修饰吗?
可以。const
与volatile
的作用不冲突,前者限制 “程序代码不能修改参数”,后者限制 “编译器不能优化参数”,适用于 “参数值由外部(非程序代码)修改,程序仅读取” 的场景。
典型示例:硬件状态寄存器(值由硬件修改,程序仅读取,不可修改):
void read_sensor(const volatile unsigned int *status_reg);
const
:程序不能修改status_reg
指向的寄存器值;volatile
:编译器不能优化status_reg
,确保每次读取都是最新的硬件状态。
10. 内联函数 inline(模拟面试)
核心作用:将函数代码 “嵌入” 到调用处,避免函数调用的开销(如栈帧创建、参数传递、返回地址保存),提升程序执行效率,适用于短小、频繁调用的函数(如工具函数、数学计算函数)。
与宏定义的区别(高频对比考点):
对比维度 | 内联函数(inline) | 宏定义(#define) |
---|---|---|
类型检查 | 有(遵循函数类型规则,参数 / 返回值均需匹配) | 无(仅文本替换,可能因类型不匹配导致错误) |
调试支持 | 支持(可设置断点,查看变量值) | 不支持(替换后代码合并,无法定位宏内部问题) |
副作用风险 | 低(参数仅计算 1 次) | 高(参数可能多次计算,如#define ADD(a,b) a+b ,ADD(1,2*3) 结果为 7) |
注意:inline
是 “编译器建议”,若函数体过大(如含循环 / 递归),编译器会忽略inline
,按普通函数处理。
11. C 语言中 EOF 的值是多少?(扬智科技面试)
EOF
(End of File,文件结束符)是 C 语言标准库中的宏定义,在<stdio.h>
中定义为-1
。
核心原因:字符类型char
默认是signed char
(范围 - 128~127),-1
的二进制为0xFF
,不会与普通字符(0~127)冲突,可明确标识 “文件读取结束”。
典型用法:判断文件读取是否结束:
int ch;
FILE *fp = fopen("test.txt", "r");
while ((ch = fgetc(fp)) != EOF) { // 读取到EOF时退出循环
putchar(ch);
}
12. C 语言与 C++ 中 NULL 的值是多少?(通则康威简答延伸)
NULL
是 “空指针常量”,但在 C 语言与 C++ 中的定义不同,核心差异如下:
语言 | NULL 定义 | 注意事项 |
---|---|---|
C 语言 | #define NULL (void*)0 | 本质是void* 类型,可隐式转换为其他指针类型(如int* p = NULL; 合法) |
C++ | #define NULL 0 | C++ 不允许void* 隐式转换为其他指针类型,若定义为(void*)0 会编译报错;C++11 后推荐用nullptr (专门的空指针常量,避免与0 混淆) |
13. 递归次数过多如何优化
递归次数过多会导致栈溢出(每次递归调用都会创建栈帧,栈空间有限,通常为几 MB),优化方案有 4 种:
- 迭代改写:用
for
/while
循环替代递归,彻底避免栈帧叠加。例如,斐波那契数列的递归实现改迭代:// 迭代实现斐波那契(第n项) int fib(int n) { if (n <= 2) return 1; int a = 1, b = 1, c; for (int i = 3; i <= n; i++) { c = a + b; a = b; b = c; } return c; }
- 尾递归优化:将递归调用放在函数最后一步(尾递归),编译器可将其优化为循环(如 GCC 支持尾递归优化)。例如,阶乘的尾递归实现:
int fact_tail(int n, int res) { if (n == 0) return res; return fact_tail(n - 1, n * res); // 尾递归调用 } int fact(int n) { return fact_tail(n, 1); }
- 扩大栈空间:通过编译器参数临时扩大栈大小(如 GCC 的
-Wl,--stack=1024000
,将栈设为 1MB),仅适用于临时测试,不推荐生产环境。 - 记忆化搜索:对重复计算的递归(如斐波那契、动态规划问题),用数组 / 哈希表缓存中间结果,减少递归次数。
14. 堆和栈的区别 (纳思达面试)
堆和栈是程序内存的两个核心区域,管理方式与用途差异极大,对比如下:
对比维度 | 栈(Stack) | 堆(Heap) |
---|---|---|
分配方式 | 编译器自动分配 / 释放(函数调用时分配栈帧,函数返回时释放) | 程序员手动分配 / 释放(malloc /free 、new /delete ) |
生长方向 | 从高地址向低地址生长 | 从低地址向高地址生长 |
大小限制 | 通常较小(由操作系统决定,如 Windows 默认 1MB,Linux 默认 8MB) | 较大(接近系统可用物理内存,受虚拟内存限制) |
分配效率 | 高(栈帧操作是 CPU 指令级操作,无碎片) | 低(需查找空闲内存块,可能产生碎片,分配耗时较长) |
存储内容 | 函数参数、局部变量、返回地址、寄存器现场 | 动态分配的变量(如大型数组、结构体实例) |
安全性 | 高(自动释放,不易泄漏) | 低(需手动释放,易出现内存泄漏、野指针) |
15. scanf 的原理 (模拟面试)
scanf
是 C 语言标准输入函数,用于从输入流(默认是键盘)读取数据,核心原理分 3 步:
- 解析格式字符串:按
%d
(整数)、%s
(字符串)、%c
(字符)等格式符,确定要读取的数据类型; - 跳过空白字符:默认跳过输入流中的空白字符(空格、回车、制表符
\t
),除非格式符是%c
(需读取空白字符); - 写入目标内存:将解析后的数据存入对应指针指向的内存,返回成功读取的数据项数(若读取失败或到达 EOF,返回
EOF
)。
常见坑点:
- 读取字符串时,
scanf("%s", str)
会自动在末尾加\0
,但不检查str
的数组大小,需确保str
足够大,避免内存越界; - 读取
%c
时,会读取空白字符(如前一个%d
后的回车),需在%c
前加空格(如scanf(" %c", &ch)
)跳过空白。
16. printf 的原理 (模拟面试延伸)
printf
是 C 语言标准输出函数,用于向输出流(默认是屏幕)打印数据,核心原理分 3 步:
- 格式化数据:按格式字符串(如
%d
、%s
、%.2f
)将数据(整数、字符串、浮点数等)转换为字符流; - 写入输出缓冲区:默认使用 “行缓冲”,即字符流先存入缓冲区,当遇到
\n
、缓冲区满或调用fflush(stdout)
时,才将缓冲区内容刷新到屏幕; - 返回结果:返回成功打印的字符数(若失败,返回负数)。
常见坑点:
- 无
\n
时,数据可能停留在缓冲区,需调用fflush(stdout)
强制刷新(如嵌入式开发中打印调试信息); - 格式符与数据类型不匹配(如
printf("%d", 3.14)
),会导致未定义行为(输出乱码)。
17. 内核链表 kernellist (模拟面试)
内核链表是 Linux 内核中常用的双向循环链表,与普通链表的核心区别是 **“链表节点不包含数据,数据包含链表节点”**,灵活性极高,支持任意数据结构挂接。
后续,将会继续补充。