news 2026/10/15 13:12:29

C语言笔试真题复盘:从sizeof到线程通信的核心考点解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言笔试真题复盘:从sizeof到线程通信的核心考点解析

简介:这是一份面向C语言求职者与在校学生的经典笔试试题汇总文档,聚焦面试高频考点与易错细节,适合准备技术笔试、巩固语言基础的中初级开发者使用。压缩包内共1个doc文件,约122KB,内容以问答形式组织,涵盖sizeof与strlen的区别、指针与数组长度计算、栈与堆的内存分配差异、大小端存储判断、结构体指针偏移运算、数组地址偏移算法,以及const、static、volatile等关键字的语义与线程间通信机制等核心主题。文档通过具体代码片段与逐条解析,帮助读者厘清编译期与运行期行为差异,理解内存布局与跨平台数据交换要点,并规避栈溢出、内存泄漏和指针误用等常见问题。目前已有319人学习,适合作为笔试冲刺前的查漏补缺材料,也可用于日常复习C语言底层知识。

1. 一份被低估的 C 语言笔试真题集:从 sizeof 到线程通信的完整复盘

翻招聘群里的聊天记录,十个吐槽 C 语言笔试的,八个卡在sizeof和strlen上。这份《C语言笔试试题汇集(超经典).doc》我前后拆过三遍,它不是那种按章节铺知识点的教材,而是一份按面试官出题逻辑组织的题库——从sizeof指针和数组的长度、stack和heap的区别,一路推到大小端存储、结构体指针偏移、const/static/volatile三个关键字的完整用法,最后落到进程与线程、线程间通信机制。适合两类人:一是正在准备嵌入式或系统方向笔试的求职者,二是工作两三年但发现基础概念有模糊地带、想系统补一遍的开发者。它最大的价值在于每道题都给了答案和推导过程,不是只丢一个结论让你背。

2. sizeof 与 strlen 的边界:数组退化、指针大小和编译期计算

2.1 为什么 sizeof(str1) 是 6 而 strlen(str1) 是 5

先看这份资料里最经典的一组对比。在 32 位系统下:

char str1[] = "Hello"; // 自动追加 '\0' char str2[5] = {'H','e','l','l','o'}; // 无 '\0' char str3[6] = {'H','e','l','l','o','\0'}; char *p1 = "Hello"; char *p2[] = {"hello", "world"}; int n = 10; int *q = &n;

逐个拆解sizeof和strlen的结果:

表达式sizeof 结果strlen 结果原因
sizeof(str1)65数组大小含\0,strlen 不含
sizeof(str2)5未知无\0,strlen 越界读取
sizeof(str3)65显式写了\0,结果同 str1
sizeof(p1)4—指针大小,32 位系统固定 4 字节
sizeof(p2)8—两个指针的数组,2×4=8
sizeof(n)4—int 类型大小
sizeof(q)4—指针大小

这里最容易翻车的是str2。strlen(str2)的结果是未定义的——它从str2的首地址开始逐字节扫描,直到遇见第一个\0才停。如果内存里恰好有个 0,结果可能是个很小的数;如果没有,程序直接崩。我见过有人在笔试里写strlen(str2) = 5,面试官当场让他解释为什么,答不上来就挂了。

2.2 sizeof 是操作符,strlen 是函数——这个区别决定了什么

资料里列了九条区别,但真正影响写代码的只有三条:

第一条:sizeof 在编译期求值,strlen 在运行期求值。这意味着sizeof可以用来定义数组维度:

char str[20] = "0123456789"; int a = strlen(str); // a = 10,运行时计算 int b = sizeof(str); // b = 20,编译期确定

第二条:数组做 sizeof 参数不退化,传给 strlen 就退化为指针。这是函数传参时最隐蔽的坑:

void func(char arr[]) { // 这里 arr 已经退化为 char*,sizeof(arr) = 4 // 不是数组的实际大小 int len = sizeof(arr); // 4,不是数组长度 }

如果函数内部需要知道数组大小,必须额外传一个长度参数,或者用memcpy把数据拷出来再操作。资料里给的做法是fun(unsigned char *p1, int len),进入函数后用memcpy(buf, p1, len)拷贝,长度由调用方传入。

第三条:sizeof 可以用类型做参数,strlen 只能用char*。sizeof(int)合法,strlen(int)编译不过。sizeof后面跟类型必须加括号,跟变量名可以不加——因为它是操作符不是函数。

注意:sizeof返回类型是size_t,在头文件中 typedef 为unsigned int。打印时用%zu而不是%d,否则在 64 位系统上可能输出异常。

3. 内存布局与地址运算:从 stack/heap 到结构体指针偏移

3.1 程序内存五区与 stack/heap 的真实差异

资料里把内存分成五个区:栈区、堆区、全局区(静态区)、文字常量区、程序代码区。用一个例子程序就能看清每个变量的归属:

int a = 0; // 全局初始化区 char *p1; // 全局未初始化区 int main() { int b; // 栈 char s[] = "abc"; // 栈 char *p2; // 栈 char *p3 = "123456"; // p3 在栈上,"123456\0" 在常量区 static int c = 0; // 全局(静态)初始化区 p1 = (char *)malloc(10); // 堆区,10 字节 p2 = (char *)malloc(20); // 堆区,20 字节 strcpy(p1, "123456"); // "123456\0" 放常量区 }

stack 和 heap 的核心差异不在「一个自动一个手动」这种表面描述,而在分配效率和碎片特性:

  • 栈:由编译器自动分配释放,存放函数参数值、局部变量。操作方式类似数据结构里的栈,后进先出。空间有限,通常几 MB,超出就是 stack overflow。
  • 堆:由程序员手动分配释放(malloc/free),不释放则程序结束时由 OS 回收。分配时系统遍历空闲内存链表,找第一个空间大于申请量的节点,删除后分配给程序。堆是向高地址扩展的不连续内存区域,大小受限于系统有效虚拟内存。

资料里特别提醒:堆分配和数据结构里的「堆」是两回事,只是分配方式类似链表操作。这个区分在面试里经常被追问。

3.2 结构体指针偏移:p+0x200 到底加了多少字节

这是资料里最容易算错的一道题:

struct BBB { long num; // 4 字节(32 位系统) int index; // 4 字节 char *name; // 4 字节 } *p; p = 0x1000000;

sizeof(BBB) = 12。三个表达式的计算结果:

表达式计算过程结果
p + 0x2000x1000000 + 0x200 × 120x1001800
(unsigned long)p + 0x2000x1000000 + 0x2000x1000200
(char *)p + 0x2000x1000000 + 0x200 × 10x1000200

关键区别在于指针的步长。p是struct BBB *类型,p + n实际偏移n × sizeof(BBB)字节。强制转成unsigned long后,加法变成纯数值运算,加多少就是多少。转成char *后步长为 1 字节,结果和数值加法一致。

数组地址偏移也是同一套逻辑。资料里给了五条规则,核心记住二维数组的展开方式:

int a[2][3] = {1,2,3,4,5,6}; // *(*(a+1)+1) = 5 // a+1 指向第二行,*(a+1) 取第二行首地址,+1 移到第二个元素

对于字符指针数组char *p[] = {"ABC","DEF"},*(*(p+1)+1)取的是'E'——先定位到第二个字符串,再取第二个字符。

3.3 大小端存储的判定与代码验证

0x12345678在内存中的存放顺序,大端和小端完全相反:

地址偏移大端模式小端模式
0x000012 (OP0)78 (OP3)
0x000134 (OP1)56 (OP2)
0x000256 (OP2)34 (OP1)
0x000378 (OP3)12 (OP0)

大端把高位字节放在低地址,小端把低位字节放在低地址。x86 系列是小端,部分嵌入式处理器用大端。判定方法用联合体最直接:

int check_endian() { union { int i; char c[4]; } u; u.i = 0x12345678; // 小端返回 1,大端返回 0 return (u.c[0] == 0x78); }

联合体所有成员共享同一块内存,给i赋值后读c[0]就能看到最低地址上的字节。小端模式下c[0]是0x78,大端模式下是0x12。

4. 关键字与函数实现:const、static、volatile 和手写 strcpy/memcpy

4.1 const 的四种写法与 volatile 的三个陷阱

const的位置决定了谁不能被修改:

const int a; // a 是常整型数 int const a; // 同上 const int *a; // a 指向常整型数,值不可改,指针可改 int * const a; // a 是常指针,指针不可改,值可改 int const * a const; // 值和指针都不可改

面试里常问「为什么要用 const」,资料给了三个理由:向读代码的人传达参数用途、给优化器附加信息产生更紧凑代码、让编译器保护不希望被改变的参数。我自己的习惯是,函数参数只要不在内部修改就加const,这是最省事的防御性写法。

volatile的核心是告诉编译器「这个变量可能被意想不到地改变,每次用都必须重新从内存读」。三个典型场景:并行设备的硬件寄存器、中断服务子程序访问的非自动变量、多线程共享变量。

资料里有个经典的volatile陷阱题:

int square(volatile int *ptr) { return *ptr * *ptr; }

这段代码看起来是求平方,但编译器可能生成:

int square(volatile int *ptr) { int a, b; a = *ptr; // 第一次读 b = *ptr; // 第二次读,值可能已经变了 return a * b; }

如果*ptr在两次读之间被中断修改,a和b不同,结果就不是平方。正确写法是先读到局部变量再计算:

long square(volatile int *ptr) { int a = *ptr; return a * a; }

static的三个作用也值得记牢:函数体内静态变量在调用过程中维持值不变;模块内静态变量可被模块内函数访问但不能被模块外访问;模块内静态函数只能被本模块调用。第三点很多人答不上来,但它恰恰是 C 语言实现模块化封装的基础。

4.2 手写 strcpy 和 memcpy:边界处理与返回值设计

资料里给了两个函数的实现,我补上完整的边界检查:

#include <assert.h> char* strcpy(char* strDest, const char* strSrc) { assert(strDest != NULL && strSrc != NULL); char* strTmpD = strDest; // 保存首地址用于返回 while ((*strTmpD++ = *strSrc++) != '\0') { // 逐字符拷贝,遇到 '\0' 停止 } return strDest; // 返回首地址,支持链式调用 } void* memcpy(void* pDest, const void* pSrc, unsigned int size) { assert(pDest != NULL && pSrc != NULL); char* pTmpD = (char*)pDest; const char* pTmpS = (const char*)pSrc; while (size-- > 0) { *pTmpD++ = *pTmpS++; } return pDest; }

两个函数的区别:strcpy只能拷贝字符串,遇到\0停止;memcpy是内存原样拷贝,不管内容是什么,指定多少字节就拷多少。strcpy返回char*是为了支持strlen(strcpy(dst, src))这种嵌套调用。

注意:memcpy不处理源和目标内存重叠的情况。如果pDest和pSrc有重叠区域,需要用memmove或者从高地址向低地址拷贝。

4.3 宏定义中的括号陷阱与 do-while(0) 惯用法

资料里列了 20 条宏定义技巧,最容易出错的是括号问题:

// 错误写法 #define ADD(a, b) a + b // ADD(1, 2) * 3 展开为 1 + 2 * 3 = 7,不是 9 // 正确写法 #define ADD(a, b) ((a) + (b))

多语句宏必须用do { } while(0)包裹:

#define DO(a, b) do { \ a + b; \ a++; \ } while(0)

如果不用do-while(0),在if-else中使用会出问题:

if (cond) DO(a, b); // 展开后 else 会找不到匹配的 if else ...

其他实用宏包括:ARR_SIZE(a)返回数组元素个数、MAX(x,y)和MIN(x,y)求极值、FPOS(type, field)获取结构体成员偏移量。FPOS的实现是((dword) &((type *)0)->field),把 0 地址强制转成结构体指针再取成员地址,偏移量就是成员地址本身。

5. 进程、线程与通信机制:笔试最后一道大题的拆解方式

5.1 进程和线程的本质区别

资料里对进程和线程的描述很长,但核心就四句话:

  • 进程是资源分配的基本单位,线程是 CPU 调度的基本单位。
  • 每个进程有独立地址空间,同一进程的线程共享地址空间。
  • 进程崩溃不影响其他进程,线程崩溃导致整个进程挂掉。
  • 创建和撤销进程的开销远大于线程。

一个程序至少有一个进程,一个进程至少有一个线程。线程自己不拥有系统资源,只有程序计数器、一组寄存器和栈,但可以访问隶属于进程的资源。

5.2 进程间通信的六种方式与线程间同步的三种机制

进程间通信方式:

方式特点适用场景
管道(pipe)亲缘关系进程间通信父子进程简单数据传递
有名管道(named pipe)允许无亲缘关系进程通信任意进程间通信
信号(signal)软件层模拟中断事件通知
消息队列(message queue)消息链表,克服信号量有限缺点结构化消息传递
共享内存(shared memory)多进程访问同一块内存大数据量高效通信
信号量(semaphore)同步和互斥手段配合共享内存使用
套接字(socket)网络间不同机器进程通信分布式系统

线程间同步机制:

  • 信号量:二进制信号量、互斥信号量、整数型信号量、记录型信号量。互斥信号量必须同一任务申请和释放,可递归申请;二进制信号量允许一个任务申请、另一个任务释放。
  • 消息:消息队列、消息邮箱。
  • 事件:event 机制。

资料里特别区分了互斥信号量和二进制信号量:互斥信号量是二进制信号量的子集,但互斥信号量有优先级继承机制,能防止优先级反转。这个细节在嵌入式笔试里经常考。

5.3 可重入函数与递归函数的判断标准

可重入函数是指多个任务可以同时调用而不破坏数据的函数。判断标准:不使用全局变量或静态变量、不调用不可重入函数、不返回指向静态数据的指针。递归函数要特别注意栈溢出风险,每次递归调用都会在栈上分配新的局部变量,递归深度受栈大小限制。

6. 用这套题集做笔试复盘:我的三遍刷题法和一个验证脚本

第一遍按顺序做,不看答案,把每道题的推导过程写在纸上。重点不是做对,而是暴露自己的思维盲区。我第一遍做sizeof(str2)时写了 5,翻答案才发现strlen结果是未定义的——这个错误让我重新理解了字符串结束符的底层含义。

第二遍只做错题,并且用编译器验证。比如大小端判定,我写了个小脚本在本地跑:

#include <stdio.h> int main() { // 验证大小端 union { int i; char c[4]; } u; u.i = 0x12345678; printf("c[0]=0x%02x, c[3]=0x%02x\n", u.c[0], u.c[3]); // 小端输出:c[0]=0x78, c[3]=0x12 // 验证 sizeof 和 strlen char str1[] = "Hello"; char str2[5] = {'H','e','l','l','o'}; printf("sizeof(str1)=%zu, strlen(str1)=%zu\n", sizeof(str1), strlen(str1)); // sizeof(str1)=6, strlen(str1)=5 // 验证结构体指针偏移 struct BBB { long num; int index; char *name; } *p = (struct BBB *)0x1000000; printf("p+0x200 = 0x%lx\n", (unsigned long)(p + 0x200)); // 0x1001800(32 位系统) return 0; }

编译命令:

gcc -m32 -o test_endian test_endian.c ./test_endian

-m32强制 32 位编译,确保指针大小和结构体对齐与题目一致。如果系统没有 32 位库,去掉-m32后指针大小变成 8 字节,sizeof(p2)会变成 16 而不是 8,结构体偏移也会变。

第三遍把题目按知识点归类,找出反复出错的概念。我的错题集中在三个地方:数组退化的边界条件、结构体对齐对偏移量的影响、volatile在多线程场景下的正确用法。针对每个薄弱点,我写了一个最小验证程序,跑通后再回头看题目,基本就不会再错了。

从那以后我每次遇到指针偏移和sizeof相关的题,都强制自己在纸上画出内存布局图再算——这个习惯帮我避开了至少三次笔试里的低级错误。希望帮到你。

本文还有配套的精品资源,点击获取

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

Spring Boot 3.3.1中文文档解析与最佳实践

1. Spring Boot 3.3.1 官方文档中文版的价值与定位作为Java生态中最主流的应用开发框架&#xff0c;Spring Boot每次版本更新都会带来大量新特性和改进。3.3.1版本作为当前最新的稳定分支&#xff0c;其官方文档是开发者最权威的参考资料。但英文原版文档对许多国内开发者存在语…

作者头像 李华
网站建设 2026/10/15 13:11:56

IEC 61508-1:2010功能安全解读:安全生命周期与SIL分配

简介&#xff1a;IEC 61508-1:2010《电气/电子/可编程电子安全相关系统的功能安全 第1部分&#xff1a;一般要求》是功能安全领域的基础性标准&#xff0c;面向从事E/E/PE安全相关系统设计、开发、验证与维护的工程师、项目经理及安全评审人员&#xff0c;也为产品与应用部门标…

作者头像 李华
网站建设 2026/10/15 13:11:50

伪造技术亲缘:身份认证安全测试实战指南

1. 先从“数字父亲”说起&#xff1a;认证体系里的信任链条做软件测试做到一定年头&#xff0c;你会发现最让人上头的不是功能测试&#xff0c;不是性能调优&#xff0c;而是安全测试里那些“反常识”的攻防博弈。今天聊的“伪造技术亲缘”&#xff0c;说白了就是一门让系统稀里…

作者头像 李华
网站建设 2026/10/15 13:11:47

RMAN归档日志恢复失败的元数据根源与修复七步法

1. 项目概述&#xff1a;这不是一次普通的RMAN报错&#xff0c;而是物理Standby搭建中最具迷惑性的“假失败”现场物理Standby数据库的搭建&#xff0c;在某高校DBA团队和某金融系统运维组里&#xff0c;向来被称作“三道坎”里的第一道——表面看是复制&#xff0c;实则是一场…

作者头像 李华
网站建设 2026/10/15 13:10:47

PyTorch卷积网络实战:MNIST全流程调试与工程化避坑指南

1. 为什么MNIST仍是卷积网络入门的“黄金标尺”很多人看到“PyTorch MNIST 卷积网络”这个组合&#xff0c;第一反应是&#xff1a;这都2024年了&#xff0c;还讲这个&#xff1f;太老套了吧。我第一次带某高校实验室的A同学做项目时&#xff0c;他也这么问。结果我们用同一份…

作者头像 李华