news 2026/9/25 7:09:38

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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); }

每次调用counter,count都保留上一次的值。原因在于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)); // 8(64位系统)

数组名在大多数表达式中会“退化”为指向首元素的指针,但在sizeof、&、以及字符串字面量初始化数组时不会退化。这是数组和指针最本质的区别之一。

坑三:sizeof和strlen的区别

char str[] = "hello"; printf("%zu\n", sizeof(str)); // 6,包含'\0' printf("%zu\n", strlen(str)); // 5,不包含'\0'

sizeof是编译期计算的内存占用,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字节
i440
d183字节
总计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,不是0x1002

p + 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; }

排查内存泄漏的常用手段:Valgrind(Linux)、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 Behavior,UB)是C语言面试中区分度最高的考点之一。面试官通过UB问题来判断你是否真正理解C语言的内存模型和编译器行为。

常见的UB包括:

  • 数组越界访问
  • 解引用空指针或野指针
  • 有符号整数溢出
  • 序列点之间的多次修改(如i = i++)
  • 使用已释放的内存
  • 类型双关(type punning)违反严格别名规则
int i = 0; i = i++; // UB,i的值不确定
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、优化性能都会受益。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 7:09:23

认识 React 360:用 React 构建跨平台 360° 与 VR 网页应用

前端3D渲染 【免费下载链接】react-360 Create amazing 360 and VR content using React 项目地址&#xff1a; https://gitcode.com/gh_mirrors/re/react-360 点击查看 免费下载 React 360 是一个基于 React 构建 3D 与 VR 用户界面的开源框架&#xff0c;让你用熟悉的组件、…

作者头像 李华
网站建设 2026/9/25 7:06:58

昇腾Atlas 300V AI推理加速卡上部署YOLO模型全流程指南

1. 先说结论&#xff1a;Atlas 300V 到底是不是“运算加速卡”很多人第一次看到“atlas 300v 24g 是运算加速卡吗”这个热搜词&#xff0c;我就知道大概率是刚接触昇腾生态的开发者。这个问题问得其实很微妙——因为“运算加速卡”这个词本身就有歧义。如果你拿GPU的思维去理解…

作者头像 李华
网站建设 2026/9/25 7:04:43

I2C地址扫描实战:1000KHz速率下用Excel生成设备地址矩阵

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华