ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言面试题深度解析:static、const、sizeof与指针内存考点

C语言面试题深度解析:static、const、sizeof与指针内存考点 1. C语言面试题到底在考什么1.1 从一道“简单题”说起先看一道我面过不下五十个候选人的题#include stdio.h void func(void) { char str[] hello; char *p hello; printf(%zu %zu\n, sizeof(str), sizeof(p)); }能一口报出答案的人不少但能说清楚“为什么”的人十个里大概只有三个。这道题表面在考sizeof实际在考三件事数组名在sizeof下的特殊性、字符串字面量的存储位置、以及指针和数组的本质区别。面试官想知道的不是你背没背过答案而是你有没有真正理解C语言的内存模型。这就是C语言面试题的核心特征——题目短小考点密集追问空间极大。一道三行的代码可以顺着问到内存布局、编译链接、未定义行为、编译器优化甚至延伸到嵌入式场景下的栈空间分配。所以准备C语言面试死记硬背是最低效的路径真正有效的是把每个知识点背后的“为什么”打通。1.2 高频考点的分布规律我把近几年接触到的C语言面试题做了一个粗略统计考点分布大致是这样的考点类别出现频率典型关键词关键字语义极高static、const、volatile、extern内存与指针极高指针运算、数组退化、内存对齐sizeof与类型高sizeof、strlen、类型转换预处理与编译中高宏定义、头文件、条件编译位运算中掩码、移位、大小端数据结构手写中链表、字符串操作、排序未定义行为中越界、野指针、序列点这张表不是让你按频率去押题而是帮你建立一个认知C语言面试的考点是收敛的。它不像前端面试那样每年都有新框架、新工具C语言的核心考点二十年没怎么变过。变的只是提问的角度和深度。1.3 不同岗位的考察侧重同样是考C语言岗位不同侧重点差异很大。我整理了一个对照表岗位方向考察重点典型追问嵌入式开发位操作、volatile、内存布局、栈空间“单片机C语言没有堆栈吗”这类问题系统/内核开发指针、内存管理、并发、ABI虚拟存储器管理、页表相关应用层开发字符串处理、数据结构、算法链表操作、字符串逆序音视频/图形内存对齐、类型转换、性能结构体对齐、SIMD相关通用后端基础语法、常见陷阱static/const语义、sizeof所以你在准备的时候先搞清楚目标岗位的方向再决定把精力往哪里倾斜。嵌入式岗位不会深究虚拟存储器后端岗位也不太会问你寄存器操作。2. static、const、sizeof三大金刚逐个拆解2.1 static关键字的三种面貌static是C语言面试里出现频率最高的关键字之一没有悬念。它的核心难点在于同一个关键字在不同位置有完全不同的语义。第一种修饰局部变量void counter(void) { static int count 0; count; printf(%d\n, count); }每次调用countercount都保留上一次的值。原因在于static局部变量存储在静态存储区通常是数据段而不是栈上。它的生命周期贯穿整个程序运行期但作用域仍然局限在函数内部。这里有个常见的追问static局部变量什么时候初始化答案是程序启动时、main函数执行之前而且只初始化一次。如果你写static int count 0;编译器实际上把它放在了.data段已初始化或.bss段未初始化或初始化为0。第二种修饰全局变量和函数// file1.c static int internal_var 42; static void internal_func(void) { /* ... */ }这时候static的含义变成了内部链接internal linkage。也就是说internal_var和internal_func只在当前编译单元可见其他.c文件即使写了extern声明也链接不到。这个特性在实际工程中非常有用——它实现了编译单元级别的封装。C语言没有private关键字static就是最接近的东西。我在做嵌入式项目时几乎所有的驱动内部函数都会加static避免符号污染和命名冲突。第三种修饰函数参数C99起void process(int arr[static 10]) { // 编译器可以假设arr至少有10个元素 }这个用法比较冷门但面试时偶尔会被问到。static在这里是给编译器的优化提示表示数组至少有这么多个元素。实际项目中用得不多但知道它能体现你对标准的熟悉程度。注意static修饰局部变量时改变的是生命周期修饰全局变量/函数时改变的是链接属性。这两个语义完全不同面试时一定要分清楚说。2.2 const的“只读”到底是什么意思const是另一个高频考点而且是最容易被误解的关键字。很多人把它简单理解为“常量”但它的准确含义是只读read-only不是常量表达式constant expression。const int n 10; int arr[n]; // C99之前这是错误的因为n不是常量表达式在C语言中const变量仍然是变量只是你不能通过它来修改值。它和#define的区别在于#define是预处理阶段的文本替换const是编译阶段的类型检查。const和指针的组合是面试的重灾区四种写法必须分清楚写法含义能否修改指针能否修改指向的值const int *p指向常量的指针可以不可以int const *p同上可以不可以int * const p常量指针不可以可以const int * const p两者都不可变不可以不可以记忆技巧从右往左读。const int *p读作“p是一个指针指向const int”int * const p读作“p是一个const指针指向int”。const在函数参数中的作用也很重要void print_string(const char *str) { // 保证函数内部不会修改str指向的内容 }这不仅是给自己看的约束也是给调用者的承诺。在团队协作中凡是只读的指针参数都应该加const这是一个好习惯。实操心得const修饰的变量在C语言中默认是外部链接的和C不同如果写在头文件里被多个源文件包含会导致重复定义。正确做法是配合static使用或者只在.c文件中定义。2.3 sizeof的坑比你想象的多sizeof是C语言面试的“送命题”——看起来简单但几乎每道题都有坑。坑一sizeof是运算符不是函数int a 10; printf(%zu\n, sizeof(a)); // 4假设32位int printf(%zu\n, sizeof a); // 4可以不加括号 printf(%zu\n, sizeof(int)); // 4类型必须加括号sizeof在编译期求值除了变长数组所以sizeof(a)不会导致a自增。这一点经常被用来出题。坑二数组名在sizeof下的特殊性int arr[10]; int *p arr; printf(%zu\n, sizeof(arr)); // 40 printf(%zu\n, sizeof(p)); // 864位系统数组名在大多数表达式中会“退化”为指向首元素的指针但在sizeof、、以及字符串字面量初始化数组时不会退化。这是数组和指针最本质的区别之一。坑三sizeof和strlen的区别char str[] hello; printf(%zu\n, sizeof(str)); // 6包含\0 printf(%zu\n, strlen(str)); // 5不包含\0sizeof是编译期计算的内存占用strlen是运行期遍历到\0为止。两者完全不同但经常被放在一起考。坑四结构体的sizeof和对齐struct A { char c; int i; char d; }; printf(%zu\n, sizeof(struct A)); // 12不是6这是因为内存对齐。int需要4字节对齐所以c后面填充3字节d后面再填充3字节。对齐规则是每个成员的偏移量必须是其自身大小的整数倍结构体总大小必须是最大成员大小的整数倍。成员大小偏移量填充c103字节i440d183字节总计12字节如果调整成员顺序为int i; char c; char d;大小就变成8字节。这就是为什么在嵌入式开发中结构体成员的排列顺序会影响内存占用。提示sizeof返回的是size_t类型用%zu打印。用%d在64位系统上可能出问题这是一个容易被忽略的细节。3. 指针与内存面试的真正分水岭3.1 指针运算的本质指针运算是C语言的精髓也是面试中区分“会用C”和“懂C”的分水岭。int arr[5] {1, 2, 3, 4, 5}; int *p arr; printf(%d\n, *(p 2)); // 3 printf(%p\n, (void*)p); // 假设是0x1000 printf(%p\n, (void*)(p 2)); // 0x1008不是0x1002p 2不是地址加2而是加2 * sizeof(int)。指针的加减运算以指向类型的大小为单位。这个规则看起来简单但在多级指针和类型转换的场景下就容易出错。int arr[3][4]; int (*p)[4] arr; printf(%p\n, (void*)(p 1)); // 跳过一整行16字节二维数组的指针运算更容易混淆。arr退化为指向第一行的指针类型是int(*)[4]所以p 1跳过4个int。3.2 野指针、悬空指针和内存泄漏这三兄弟是C语言面试的常客也是实际项目中最容易出bug的地方。野指针未初始化的指针。int *p; // 野指针指向随机地址 *p 10; // 未定义行为悬空指针指向已释放内存的指针。int *p malloc(sizeof(int)); free(p); *p 10; // 悬空指针未定义行为 p NULL; // 正确做法释放后置空内存泄漏分配了内存但忘记释放。void leak(void) { int *p malloc(100 * sizeof(int)); // 忘记free(p) return; }排查内存泄漏的常用手段ValgrindLinux、AddressSanitizer跨平台、以及自己封装malloc/free做计数。在嵌入式环境中没有这些工具通常靠代码审查和静态分析工具。实操心得我个人的习惯是free之后立刻把指针置为NULL。这样即使后面误用了也是解引用空指针通常会崩溃容易定位而不是操作一块已经不属于你的内存可能悄无声息地破坏数据。3.3 内存布局从栈到堆一个典型的C程序内存布局是这样的区域存储内容生长方向生命周期代码段(.text)机器指令-程序运行期只读数据段(.rodata)字符串字面量、const全局-程序运行期已初始化数据段(.data)初始化的全局/静态变量-程序运行期未初始化数据段(.bss)未初始化的全局/静态变量-程序运行期堆(heap)malloc分配的内存向上增长手动管理栈(stack)局部变量、函数参数向下增长函数调用期面试中经常问的一个问题是“单片机C语言没有堆栈吗”这个问题其实问的是嵌入式环境下栈和堆的配置。单片机当然有栈只是栈的大小通常在启动文件或链接脚本中固定配置而且很多嵌入式项目会刻意避免使用堆因为动态内存分配在资源受限环境下容易导致碎片化。// 典型的嵌入式启动文件中的栈配置 Stack_Size EQU 0x00000400 // 1KB栈空间在资源紧张的MCU上栈溢出是一个常见问题。递归调用、大型局部数组、深层函数嵌套都可能导致栈溢出。我踩过的坑是在STM32上定义了一个char buffer[2048]的局部数组结果直接hard fault因为默认栈只有1KB。4. 预处理、编译与那些“看不见”的坑4.1 宏定义的陷阱宏定义是C语言面试中另一个高频考点因为它的“文本替换”本质导致了很多反直觉的行为。#define SQUARE(x) x * x int a SQUARE(2 3); // 展开为 2 3 * 2 3 11不是25正确写法是给参数和整体都加括号#define SQUARE(x) ((x) * (x))但即使这样也有副作用问题#define MAX(a, b) ((a) (b) ? (a) : (b)) int i 5; int m MAX(i, 3); // i被执行了两次所以现代C语言中能用static inline函数的地方尽量不用宏。宏的优势在于类型无关、可用于条件编译、可以生成代码片段。#和##运算符也是常考点#define STR(x) #x #define CONCAT(a, b) a##b printf(%s\n, STR(hello)); // hello int CONCAT(var, 1) 10; // 等价于 int var1 10;4.2 头文件与链接头文件相关的问题在面试中也很常见尤其是static和const在头文件中的行为。// header.h const int MAX_SIZE 100; // 每个包含该头文件的.c都会有一个副本 static int count 0; // 同上每个编译单元独立正确做法是在头文件中用extern声明在.c文件中定义// header.h extern const int MAX_SIZE; // source.c const int MAX_SIZE 100;或者对于简单的常量直接用#define或enum。头文件保护是另一个基础但重要的话题#ifndef HEADER_H #define HEADER_H // 内容 #endif现代编译器也支持#pragma once但它不是标准的一部分。在跨平台项目中我通常还是用传统的#ifndef方式。4.3 未定义行为面试官最爱追问的深水区未定义行为Undefined BehaviorUB是C语言面试中区分度最高的考点之一。面试官通过UB问题来判断你是否真正理解C语言的内存模型和编译器行为。常见的UB包括数组越界访问解引用空指针或野指针有符号整数溢出序列点之间的多次修改如i i使用已释放的内存类型双关type punning违反严格别名规则int i 0; i i; // UBi的值不确定int arr[5]; arr[5] 10; // UB越界写入面试中如果被问到UB不要只说“这是未定义行为”要能解释为什么标准要把它定义为UB——因为不同的硬件平台和编译器优化策略可能导致不同的结果标准无法统一规定。提示在实际项目中开启编译器的警告选项-Wall -Wextra -Werror和运行时检测工具AddressSanitizer、UBSan可以帮你捕获大部分UB问题。5. 手写代码题链表、字符串与排序5.1 单链表操作链表是C语言面试中最常见的手写代码题没有之一。核心操作包括创建、插入、删除、反转、查找中间节点、判断是否有环。反转链表是出现频率最高的题目struct Node { int data; struct Node *next; }; struct Node* reverse(struct Node *head) { struct Node *prev NULL; struct Node *curr head; while (curr ! NULL) { struct Node *next curr-next; curr-next prev; prev curr; curr next; } return prev; }这段代码的关键在于三个指针的配合prev记录已反转部分的头curr是当前处理的节点next暂存下一个节点。写的时候一定要画图否则很容易指针丢失。判断链表有环用快慢指针int has_cycle(struct Node *head) { struct Node *slow head; struct Node *fast head; while (fast ! NULL fast-next ! NULL) { slow slow-next; fast fast-next-next; if (slow fast) return 1; } return 0; }快指针每次走两步慢指针每次走一步。如果有环快指针一定会在环内追上慢指针。这个算法的时间复杂度是O(n)空间复杂度是O(1)。5.2 字符串处理字符串逆序是另一个高频题void reverse_string(char *str) { if (str NULL) return; int len strlen(str); for (int i 0; i len / 2; i) { char tmp str[i]; str[i] str[len - 1 - i]; str[len - 1 - i] tmp; } }注意边界条件空指针、空字符串、单字符字符串。面试时先把这些边界条件说清楚再写代码会加分不少。字符串拷贝也是常考的重点在于处理重叠内存void *my_memcpy(void *dst, const void *src, size_t n) { char *d dst; const char *s src; while (n--) *d *s; return dst; } void *my_memmove(void *dst, const void *src, size_t n) { char *d dst; const char *s src; if (d s) { while (n--) *d *s; } else { d n; s n; while (n--) *--d *--s; } return dst; }memcpy不处理重叠memmove处理重叠。面试时如果被要求实现memcpy主动提一句重叠问题会显得你考虑周全。5.3 排序算法冒泡排序是C语言入门必学也是面试中偶尔会考的基础题void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped 1; } } if (!swapped) break; // 优化已经有序 } }加上swapped标志是一个小优化最好情况下时间复杂度从O(n²)降到O(n)。面试时写出这个优化能体现你有实际工程思维。快速排序的手写版本也经常被要求int partition(int arr[], int low, int high) { int pivot arr[high]; int i low - 1; for (int j low; j high; j) { if (arr[j] pivot) { i; int tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } } int tmp arr[i 1]; arr[i 1] arr[high]; arr[high] tmp; return i 1; } void quick_sort(int arr[], int low, int high) { if (low high) { int pi partition(arr, low, high); quick_sort(arr, low, pi - 1); quick_sort(arr, pi 1, high); } }6. 常见问题速查与避坑指南6.1 面试高频问题速查表问题核心考点一句话答案static局部变量的生命周期存储位置静态存储区程序运行期const和define的区别编译阶段const有类型检查define是文本替换sizeof数组和指针数组退化数组名在sizeof下不退化指针加法的单位类型大小以指向类型的大小为单位结构体对齐规则内存对齐偏移量是成员大小整数倍野指针和悬空指针内存管理未初始化 vs 已释放宏定义的副作用文本替换参数可能被多次求值未定义行为的例子语言标准越界、空指针解引用、有符号溢出6.2 我踩过的坑坑一sizeof返回size_t用%d打印在64位系统上会出错。这个坑我在早期项目中踩过在32位系统上测试没问题换到64位就打印出奇怪的数字。正确做法是用%zu。坑二const变量在头文件中定义导致重复符号。在C语言中const全局变量默认是外部链接的如果头文件被多个.c包含链接时会报重复定义。解决方案是加static或者用extern声明。坑三static局部变量在多线程环境下的初始化问题。C11之前static局部变量的初始化不是线程安全的。虽然大多数编译器会生成线程安全的初始化代码但标准并不保证。在多线程环境下最好用显式的一次性初始化机制。坑四malloc(0)的返回值。标准规定malloc(0)的行为是实现定义的可能返回NULL也可能返回一个可以free的指针。不要依赖任何特定行为。坑五char是否有符号。C标准没有规定char默认是有符号还是无符号这是实现定义的。在x86上通常是有符号的在ARM上通常是无符号的。如果需要一个明确有符号的8位整数用int8_t需要无符号的用uint8_t。6.3 准备策略建议最后说几点准备C语言面试的策略。第一不要只刷题要理解底层原理。每道题背后都对应一个知识点把知识点打通了题目怎么变都不怕。第二动手写代码。看会了和写出来是两回事链表反转、字符串处理这些题一定要在纸上或编辑器里手写几遍。第三关注目标岗位的方向。嵌入式岗位多准备位操作和内存布局后端岗位多准备数据结构和算法。第四准备好追问。面试官问完static的作用后很可能追问“static局部变量在什么阶段初始化”“多线程下安全吗”这些延伸问题才是真正拉开差距的地方。C语言这门语言入门容易精通难。面试题只是表象真正要建立的是对内存、编译、链接这些底层机制的理解。这个理解一旦建立起来不光面试能用实际工作中排查bug、优化性能都会受益。
返回列表