99、面试题精选:经典的 C 语言面试题解析
走到第99题,咱们已经快把C语言的坑踩遍了。今天这期,我不打算讲新知识,而是挑几道真正在面试场上考倒过人的题目,跟你一起拆解拆解。
说实话,我这些年面试过不少人,也被人面试过。有些题,看着简单,一写就错。为什么?因为C语言的细节太多,编译器又太“老实”——你让它干啥它就干啥,哪怕你写的是错的。
好,咱们直接上题。
题目一:诡异的 sizeof 与 strlen
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
char *p = str;
printf("sizeof(str) = %zu\n", sizeof(str));
printf("strlen(str) = %zu\n", strlen(str));
printf("sizeof(p) = %zu\n", sizeof(p));
printf("strlen(p) = %zu\n", strlen(p));
return 0;
}
问:输出是什么?
很多人第一反应:sizeof(str) 是5,strlen(str) 也是5。错!
sizeof(str) 算的是整个数组的大小,包括结尾的 '\0'。所以结果是6,不是5。而 strlen 只数到 '\0' 为止,所以是5。
再看 sizeof(p),p 是个指针。在64位系统上,指针占8字节。所以输出是8。而 strlen(p) 还是5,因为它顺着指针找到字符串,再数到 '\0'。
核心区别:
sizeof是运算符,编译时求值,算的是内存占用strlen是函数,运行时求值,算的是字符个数(不含'\0')
我个人习惯:只要看到数组名出现在 sizeof 里,就提醒自己——这是整个数组的大小,不是指针。这个坑我踩过不止一次。
题目二:++ 运算符的副作用
#include <stdio.h>
int main() {
int i = 1;
int j = (i++) + (i++) + (i++);
printf("i = %d, j = %d\n", i, j);
return 0;
}
问:i 和 j 的值是多少?
嗯,这道题其实是个陷阱。标准C语言里,这种写法是未定义行为。为什么?因为你在同一个表达式中多次修改同一个变量,而且修改的顺序没有明确规定。
不同的编译器,甚至同一个编译器的不同优化级别,结果都可能不一样。我见过有人拿这个题去考别人,结果自己先被不同平台的输出搞懵了。
警告:永远不要写这种代码!面试时如果遇到,直接说“这是未定义行为,实际项目中应该避免”。这比猜一个数字更能体现你的专业水平。
我曾经在代码审查里看到过类似的写法,当时就让人改了。你想想看,如果这段代码换了个编译器,或者升级了版本,行为变了,排查起来得多痛苦?
题目三:const 到底修饰谁?
#include <stdio.h>
int main() {
const int a = 10;
int *p = (int *)&a;
*p = 20;
printf("a = %d\n", a);
return 0;
}
问:能编译通过吗?输出是什么?
能编译,但有警告。因为 const int * 转 int * 是丢弃了 const 限定符。至于输出……嗯,不一定。
理论上,a 是 const,你强行改它,行为是未定义的。实际上,很多编译器会把 const 变量放到只读段,改了也没用——你改的是内存里的值,但编译器可能已经把 a 优化成常量了,直接替换成10。
所以,你打印 a 可能还是10,但 *p 已经是20了。同一个地址,两个值?对,这就是未定义行为的诡异之处。
记住:
const int *p—— p 指向的内容不可改int * const p—— p 本身不可改const int * const p—— 两者都不可改
读法技巧:从右往左读。比如 const int *p,先读 *p,再读 const int,意思是“p 是一个指针,指向一个 const int”。
题目四:宏定义的陷阱
#include <stdio.h>
#define SQUARE(x) x * x
int main() {
int a = 5;
int b = SQUARE(a + 1);
printf("b = %d\n", b);
return 0;
}
问:b 是多少?
很多人脱口而出:36。因为 5+1=6,6的平方是36。
错!宏是文本替换,不是函数调用。SQUARE(a + 1) 展开后是 a + 1 * a + 1,也就是 5 + 1 * 5 + 1,结果是11。
怎么改?加括号:
#define SQUARE(x) ((x) * (x))
这样展开就是 ((a + 1) * (a + 1)),结果才是36。
我建议:宏定义里,参数和整个表达式都要加括号。这是血的教训。我曾经因为少加一个括号,在电机控制算法里算错了PID参数,电机抖得像跳舞。
题目五:static 的隐藏含义
#include <stdio.h>
int counter() {
static int count = 0;
return ++count;
}
int main() {
for (int i = 0; i < 5; i++) {
printf("%d ", counter());
}
return 0;
}
问:输出是什么?
输出是 1 2 3 4 5。static 局部变量只初始化一次,函数调用结束后值保留。所以每次调用 counter(),count 都在上次的基础上加1。
这个特性在嵌入式里很常用。比如记录函数被调用了多少次,或者保存一个状态机的当前状态。
static 的三种用法:
| 位置 | 含义 |
|---|---|
| 函数内局部变量 | 生命周期贯穿整个程序,只初始化一次 |
| 文件内全局变量/函数 | 作用域限制在本文件,其他文件不可见 |
| 函数内静态变量 | 同上,但只在函数内可访问 |
说白了,static 就是 C 语言里实现“封装”的一种手段。在嵌入式裸机编程中,我经常用 static 来隐藏模块内部的全局变量,防止别的文件乱改。
知识体系总览
下面这张图,把这几道题背后的核心知识点串了起来。你可以把它当作一个快速复习的索引。
写在最后
这几道题,其实考的不是你会不会写代码,而是你对 C 语言底层机制的理解有多深。面试官想看的,不是你背答案,而是你面对不确定的问题时,能不能分析出“为什么不确定”。
我面试别人时,如果候选人能说出“这是未定义行为,具体取决于编译器实现”,我反而会给他加分。因为这说明他踩过坑,或者认真读过标准。
好了,这期就到这里。记住:写代码时多问自己一句“编译器会怎么理解这句话”,很多坑就能提前避开。
公众号:蓝海资料掘金营,微信deep3321