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 5static 局部变量只初始化一次,函数调用结束后值保留。所以每次调用 counter()count 都在上次的基础上加1。

这个特性在嵌入式里很常用。比如记录函数被调用了多少次,或者保存一个状态机的当前状态。

static 的三种用法:

位置含义
函数内局部变量生命周期贯穿整个程序,只初始化一次
文件内全局变量/函数作用域限制在本文件,其他文件不可见
函数内静态变量同上,但只在函数内可访问

说白了,static 就是 C 语言里实现“封装”的一种手段。在嵌入式裸机编程中,我经常用 static 来隐藏模块内部的全局变量,防止别的文件乱改。

知识体系总览

下面这张图,把这几道题背后的核心知识点串了起来。你可以把它当作一个快速复习的索引。

C语言面试高频易错点 sizeof vs strlen 编译时 vs 运行时 ++ 运算符 未定义行为 const 限定符 类型修饰与转换 宏定义陷阱 文本替换与括号 static 关键字 生命周期与作用域 避坑核心原则 1. 区分运算符与函数(sizeof vs strlen) 2. 避免同一表达式多次修改同一变量 3. 宏定义加括号,const 转换要小心

写在最后

这几道题,其实考的不是你会不会写代码,而是你对 C 语言底层机制的理解有多深。面试官想看的,不是你背答案,而是你面对不确定的问题时,能不能分析出“为什么不确定”。

我面试别人时,如果候选人能说出“这是未定义行为,具体取决于编译器实现”,我反而会给他加分。因为这说明他踩过坑,或者认真读过标准。

好了,这期就到这里。记住:写代码时多问自己一句“编译器会怎么理解这句话”,很多坑就能提前避开。


公众号:蓝海资料掘金营,微信deep3321