C语言面试每日一练:指针与内存的送分题陷阱 1. 从一道送分题说起为什么指针和内存是面试官最爱挖的坑C语言面试题每日一练这个系列我打算做成一个长期更新的栏目原因很简单市面上大部分C语言面试题解析要么太散要么只给答案不讲推导过程读者看完还是不会。我自己带过几轮校招和社招的技术面也做过被面的一方深知一个规律——面试官问指针和内存从来不是想听你背定义而是想看你能不能把一段代码在脑子里跑一遍。这篇是第一练我挑了一道看起来像送分题、实际上能筛掉一半候选人的题目。它涉及指针运算、数组退化、sizeof与strlen的区别、以及栈上变量的生命周期。这几个点几乎覆盖了C语言面试中80%的基础追问。如果你正在准备面试或者想重新把C语言的底层逻辑捋一遍这篇内容适合你如果你已经工作几年但一直靠经验写代码从没认真想过这些细节那更应该看看——很多线上事故的根因就藏在这些送分题里。先看题目。下面这段代码问输出结果是什么#include stdio.h #include string.h void func(char arr[]) { printf(func sizeof: %zu\n, sizeof(arr)); } int main(void) { char str[] hello; char *p str; int a[5] {1, 2, 3, 4, 5}; int *q a; printf(str sizeof: %zu\n, sizeof(str)); printf(str strlen: %zu\n, strlen(str)); printf(p sizeof: %zu\n, sizeof(p)); printf(a sizeof: %zu\n, sizeof(a)); printf(q sizeof: %zu\n, sizeof(q)); printf(a1: %p, a1: %p\n, (void*)(a1), (void*)(a1)); func(str); return 0; }这道题我面过不下二十个人能一次性全答对的不到三成。大部分人卡在两个地方sizeof(arr)在函数里为什么变了以及a1和a1到底差多少字节。下面我逐层拆开讲不只是给答案而是把背后的规则讲透让你下次遇到变体也能推出来。2. 逐行拆解sizeof、strlen与数组退化的真实行为2.1 sizeof是编译期运算符不是函数很多人下意识把sizeof当成函数调用这是第一个认知偏差。sizeof是编译期求值的运算符它返回的是类型或对象在内存中占用的字节数单位是size_t。正因为它在编译期就确定了所以它根本不关心运行时变量里存的是什么值。sizeof(str)里str是char[6]hello加结尾的\0所以结果是6。注意这里有个细节字符串字面量hello本身类型是char[6]用它初始化数组时数组大小就是6不是5。如果你写成char str[5] hello;在C里是合法的会丢掉\0但后续用strlen就会越界读这是个经典坑。sizeof(p)里p是char*在64位平台上指针是8字节32位平台是4字节。所以这个结果依赖平台面试时一定要主动说明在64位环境下是8。我见过候选人直接答4然后被追问你确定吗其实他平时在32位单片机上写代码答4也没错但没说明前提就显得不严谨。sizeof(a)里a是int[5]每个int通常4字节所以是20。sizeof(q)是指针8字节。这几个都还算基础。2.2 strlen是运行期函数靠找\0停strlen(str)返回的是从起始地址到第一个\0之前的字符数不含\0所以是5。它和sizeof的本质区别在于sizeof看类型strlen看内容。这也是为什么strlen不能用于没有\0结尾的字符数组——它会一直往后读直到撞上内存里某个恰好为0的字节结果不可预测甚至直接段错误。这里有个实操经验在嵌入式或者处理网络报文时经常拿到的是带长度字段的缓冲区而不是以\0结尾的字符串。这时候用strlen就是灾难。正确做法是用memcpy配合已知长度或者手动补\0。我在一个通信项目里就见过同事对着一块recv回来的数据调strlen结果程序随机崩溃排查了两天才定位到。2.3 数组作为函数参数会退化成指针这是本题的核心考点。func(char arr[])这个写法arr看起来是数组但在函数参数位置上数组类型会自动退化为指针类型等价于func(char *arr)。所以sizeof(arr)在函数内部得到的是指针大小8字节而不是数组大小。为什么会这样因为C语言设计时数组传参如果按值传递整个数组开销太大所以规定传的是首元素地址。这个规则叫数组到指针的退化array-to-pointer decay。它只在三个场景下不发生退化sizeof的操作数、取地址的操作数、以及用字符串字面量初始化数组时。所以func里打印的是8不是6。这个点面试官几乎必问因为它直接反映你是否理解C的传参机制。想拿到真实数组长度必须在调用方把长度一起传进来比如func(str, sizeof(str))或者用strlen前提是有\0。2.4 a1和a1指针算术的单位是元素不是字节这是最容易翻车的一问。a是int[5]在表达式里a退化为int*指向首元素。a1就是往后移动一个int也就是4字节指向a[1]。而a的类型是int(*)[5]是指向整个数组的指针。a1移动的是一整个数组也就是5 * 4 20字节直接跳到数组末尾之后。所以如果a的地址是0x1000那么a1是0x1004a1是0x1014。两者相差16字节0x1014 - 0x1004 0x10 16而不是很多人以为的差一个元素。这个知识点在面试里经常以打印两个指针差值的形式出现答错基本就暴露了对指针类型理解不深。我把完整输出整理成表格方便对照表达式类型结果64位说明sizeof(str)size_t6数组含\0strlen(str)size_t5不含\0sizeof(p)size_t8指针大小sizeof(a)size_t205个intsizeof(q)size_t8指针大小func内sizeof(arr)size_t8退化为指针a1int*首地址4移动一个inta1int(*)[5]首地址20移动整个数组3. 面试官真正想筛的是什么三个隐藏的追问方向3.1 追问一如果数组在结构体里还会退化吗会有人接着问struct { int a[5]; } s;那sizeof(s.a)是多少答案是20因为sizeof的操作数不退化。但如果把s.a传给函数照样退化成指针。这个追问的目的是确认你理解退化只发生在特定上下文而不是死记数组就是指针这种错误结论。数组和指针在C里是两种不同的类型只是在大多数表达式里数组会转换。这个区别在extern声明时尤其重要extern int a[];和extern int *a;是完全不同的东西前者告诉编译器这里有个数组大小未知后者说这里有个指针变量。链接时如果搞混行为未定义。3.2 追问二sizeof返回size_t用%d打印会怎样size_t是无符号整数类型在64位平台通常是unsigned long。如果你用%d打印格式不匹配行为未定义实际可能打印出奇怪的值。正确做法是用%zu。这个细节在面试里是加分项说明你写代码严谨。我在代码review里见过有人写printf(%d, sizeof(x))在32位机器上碰巧能跑对一上64位就出问题。这类平台相关的bug最难查因为本地复现不了。3.3 追问三strlen的返回值能直接相减吗strlen返回size_t两个size_t相减如果结果是负数会回绕成一个巨大的正数。比如strlen(a) - strlen(b)如果a比b短结果不是负数而是接近SIZE_MAX的值。正确写法是先转成int或ptrdiff_t再比较。这个坑在写字符串处理逻辑时非常常见尤其是判断哪个字符串更长的时候。// 错误示范 if (strlen(a) - strlen(b) 0) { /* 永远为真 */ } // 正确写法 if (strlen(a) strlen(b)) { /* 直接比较 */ }4. 从这道题延伸出的四个高频变体4.1 变体一二维数组的退化int a[3][4];a退化成int(*)[4]不是int**。a1移动16字节一行。sizeof(a)是48sizeof(a[0])是16sizeof(a[0][0])是4。面试官常问int**能不能接收二维数组答案是不能类型不兼容。要传二维数组函数签名得写void f(int a[][4])或void f(int (*a)[4])。4.2 变体二字符串字面量的存储位置char *p hello;里的hello存在只读数据段p指向它。试图p[0] H是未定义行为通常直接崩溃。而char str[] hello;是把内容拷贝到栈上可以修改。这个区别在面试里经常以下面代码有什么问题的形式出现。4.3 变体三函数返回局部数组char* getStr(void) { char buf[10] hi; return buf; // 危险 }buf在栈上函数返回后栈帧失效返回的指针是悬垂指针。这是C语言最经典的错误之一。正确做法是用static、malloc或者让调用方传缓冲区进来。我倾向于最后一种因为内存归属清晰不容易泄漏。4.4 变体四指针与整数的加减p n移动n * sizeof(*p)字节。如果p是void*GCC会当作char*处理移动n字节但这是编译器扩展标准C里void*的算术是未定义的。写可移植代码时先把void*转成char*或unsigned char*再做算术。5. 我踩过的坑和给面试者的实操建议5.1 别背答案学会在纸上画内存图我刚开始准备面试时也背过数组退化为指针这种结论但一遇到变体就懵。后来我养成了一个习惯拿到指针题先在纸上画内存布局标出每个变量的地址、类型、占多少字节。画完图a1和a1的差别一目了然。这个方法比背任何口诀都管用。具体怎么画横轴画地址每个格子代表一个字节把变量按类型填进去。数组就画连续的格子指针就画一个格子然后箭头指向它指向的地方。a的箭头指向整个数组块的起始a的箭头指向第一个元素。移动的时候看箭头指向的单位是什么就不会错。5.2 面试时主动说明平台假设指针大小、int大小、字节序这些都是平台相关的。回答时主动加一句在64位小端环境下会让面试官觉得你有工程意识。如果面试官追问32位呢你也能顺势展开展示知识广度。我面别人的时候最怕听到斩钉截铁的就是8然后问为什么答不上来。5.3 用编译器验证别靠猜sizeof的结果、指针差值这些完全可以用代码验证。写个小程序打印出来比在脑子里推演靠谱得多。我建议每个学C的人都养成习惯对任何不确定的行为写代码跑一遍。用gcc -Wall -Wextra打开所有警告很多问题编译器会直接告诉你。gcc -Wall -Wextra -o test test.c ./test如果开了-Wall还编译通过但运行结果诡异那大概率是未定义行为这时候可以上-fsanitizeaddress,undefined能抓出越界、悬垂指针、有符号溢出等问题。这个工具我在实际项目里救过好几次命。5.4 一个容易被忽略的点sizeof对表达式的处理sizeof的操作数如果是表达式这个表达式不会被求值。比如int i 0; sizeof(i);i最后还是0。因为sizeof只需要类型信息不需要值。这个特性偶尔会被用来做编译期检查比如sizeof(0 ? (void)0 : (void)0)这种技巧。面试里如果问到能答出来是加分项。但要注意如果操作数是变长数组VLAsizeof会在运行期求值因为VLA的大小编译期未知。这是C99引入的特性日常业务代码里很少用但面试官可能拿来区分背书的和真懂的。6. 把这道题变成你自己的知识网络一道题的价值不在于答案本身而在于它能牵出多少相关知识。这道题表面考sizeof和指针实际上串起了类型系统、内存布局、函数调用约定、未定义行为这几大块。我建议你按下面的方式做笔记把零散知识点连成网类型维度数组类型 vs 指针类型退化规则sizeof与的例外。内存维度栈、堆、静态区、只读区各存什么生命周期多长。运算维度指针算术的单位size_t的回绕有符号无符号混用。工具维度-Wall、-fsanitize、gdb的p sizeof(x)和ptype命令。下次遇到新题先判断它落在哪个维度再调用对应的知识。比如看到函数返回局部数组立刻归到内存维度-栈的生命周期看到两个指针相减归到运算维度-指针算术。这样你的知识就是可检索的而不是一堆散点。我在实际带新人的时候发现能通过这种维度归类方式学习的人成长速度明显快于死记硬背的人。因为面试题千变万化但底层机制就那么几套。把机制吃透题目怎么变都不怕。最后留一道思考题给你练手如果func的签名改成void func(char (*arr)[6])那sizeof(arr)是多少arr1移动多少字节想清楚这道题说明你对数组指针的理解已经到位了。下一练我会讲函数指针和回调那是另一个面试重灾区。