ARTICLE DETAIL

资讯详情

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

C语言第十天:从scanf到gdb,踩过的坑与学习笔记

C语言第十天:从scanf到gdb,踩过的坑与学习笔记 很多人以为学C语言到第十天起码能写出个像样的程序了。但真到了这一天你会发现一个尴尬的事实语法都认识题目不会做代码能编译跑起来却总不对数组还没搞明白指针又来添乱。我回头看自己走过的这十天其实最大的收获不是背了多少语法而是摸清了C语言学习的节奏——每天看着热搜里那些“scanf的坑”“指针移动”“字符串逆序”“gdb调试”的话题感觉自己好像都见过可真动手又是另一回事。这篇文章就把我第十天时的状态和学习笔记完整盘一遍。没有花里胡哨的理论都是我自己踩过的坑、翻过的车以及最后怎么把这些问题一个个解决的。这十天的跨度应该正好覆盖了环境配置、基础语法、循环分支、数组、函数、指针初阶和几个经典练习题。对正在学C语言的大学生、准备刷题的初学者或者卡在“看得懂但写不出”阶段的同学这份总结应该能对得上号。1. 第十天应该站在哪先把这十天学过的能力点盘一遍很多人第十天最大的困惑不是学不会而是不知道自己到底学到哪儿了。我给自己做了一个简单的能力盘点如果你第十天也处在一个类似的位置可以参考对照。1.1 从环境配置到第一个完整程序的跨越前三天基本都在和环境搏斗。Windows下装编译器有人用Dev C有人用VSCode配MinGW还有人跑去装Visual Studio。这些选择没有绝对的对错但有一条经验很关键别把时间花在反复换编译器上。我用VSCode配MinGW折腾了两天才把环境跑通后来发现最难的其实不是装软件而是搞懂“编译”和“运行”之间的区别。等你理解了“源代码→编译器→可执行文件→运行”这条链路用哪个编辑器就都不慌了。到第四五天基本语法已经过了一遍printf和scanf的格式化占位符、int/float/char这些基础类型、if-else和switch分支、while和do-while循环嵌套、for循环的三段式结构。这个阶段最值得做的练习是“九九乘法表”它不复杂但能把嵌套循环的缩进逻辑练明白。我第一次写出来的版本是错的外层循环和内存循环的分工没想清楚输出的形状七扭八歪。后来自己画了一张执行过程图才真正理解了外层循环控制行、内层循环控制列这件事。1.2 数组、指针和函数第十天真正的分水岭第六到十天内容突然变重了。一维数组和二维数组的声明与遍历、数组名作为函数参数传递时的退化行为、字符串数组和字符指针的区别、函数定义与调用、作用域和生命周期——这些内容每天都会在热搜里出现每一块都能单独写好几篇文章。指针是这个阶段最让人头大的东西。我到现在还记得第一次看到int *p a时的迷惑为什么要多写一个星号*p和p到底有什么区别后来我用了一个比较朴素的理解方式变量存的是数据指针变量存的是地址星号是用来“解引用”的操作符p是门牌号*p是开门进去看到的东西。有了这层理解后面再看数组和指针的关系就顺多了。函数这块要特别注意形参和实参的区分以及值传递和地址传递的区别。很多初学者写交换函数失败就是因为只传了值进去函数内部交换的是副本外面根本不受影响。第十天如果能把“为什么交换函数要用指针传参”这个问题彻底想明白指针就算是入门了。2. scanf的坑和缓冲区机制为什么输入总不按你想的走热搜里有一条“c语言里scanf一定要输入abc吗”还有一堆关于scanf的抱怨我太理解这些提问的人了。因为我至少花了两个晚上被scanf折磨得怀疑人生。2.1 scanf从缓冲区读数据不是从键盘直接读scanf表面上是从键盘读东西实际上是从标准输入缓冲区取数据。你按下回车键之后字符先进缓冲区scanf再按照格式字符串从缓冲区里解读。这个过程隐藏着很多行为比如你输入12a用%d读它读到12就停了剩下的a还在缓冲区里等着你要是再用一个%c去读a就直接被读走了中间根本没有停下来等你的机会。我第一次遇到这个问题是在写一个带菜单的小程序时。用scanf读用户输入的菜单选项再读一个字符确认退出结果程序完全跳过了第二次输入。当时我还以为电脑坏了后来才知道是缓冲区里残留的回车符被%c读走了。解决办法也很简单在%c前加一个空格写成scanf( %c, ch)让scanf跳过空白字符或者在每次读完之后用getchar()把残留的回车符吃掉。2.2 类型不匹配时的“连锁反应”另一个高频坑是输入类型不匹配。用%d读整数结果用户不小心按了个字母scanf返回0表示失败但缓冲区的字符还在。如果你不做处理接下来每一次scanf都会读到同一个无效字符程序就像死循环一样一直在接收坏数据或者数值直接变成一个奇怪的极大值。我后来翻了limits.h头文件里的定义才理解为什么会冒出那些奇怪的大数——%d读失败时变量保留的是之前的值如果变量恰好被初始化成了堆栈里的垃圾值打印结果就会看起来像随机数。解决这类问题的标准做法是检查scanf的返回值或者干脆用fgets读整行再配合sscanf解析。对新手来说先掌握scanf的常见坑再往fgets过渡是比较稳妥的学习路径。2.3 scanf的格式符细节%d、%f、%s之间不能想当然%s读字符串时遇到空格会停止所以用它读“hello world”只能拿到“hello”。%f和%lf的区别是把float和double区分开但很多教材在printf里混用也不报错到了scanf里就出问题了——scanf需要你传入float或者double不匹配会导致内存解读错乱。这个细节我第十天才彻底搞明白之前一直认为“printf怎么用scanf就能怎么用”现实给了我一记响亮的耳光。如果你也在学这十天左右建议把scanf的几个常见误用场景记下来没加、%c被残留换行吃掉、返回值不检查、%s遇空格截断、用scanf读带空格的字符串失败。每一条都是我亲自踩过的坑。3. 指针和数组的“变形题”ab、指针移动、类型转换的底层逻辑第十天之后你会发现热搜里的题开始“变形”了。不再是“输出一个数字”这么简单而是变成了ab的含义、指针移动指定位置输出字符、数组变量的类型转换这种打量细节的题。表面上是在考语法本质是在考察你有没有建立“内存模型”的思维方式。3.1 ab到底发生了什么很多初学者看到ab就懵其实拆开看很简单。是自增运算符放在变量前面是“先自增再使用”所以ab翻译成自然语言就是把b加1然后把加1之后的结果赋给a。比如b原来是5执行完之后b变成6a也变成6。这里有几个容易混淆的点首先是ab和ab的区别前者先把b的旧值赋给a再把b加1最后b是6但a是5其次要注意单独的b本身就可以作为表达式它表达式的值是自增后的结果这个特性和它被用在赋值语句左边还是右边没有关系。更深一层的问题是为什么C语言要设计出前置和后置两种自增答案和内存访问次数有关。编译器在处理ab时通常要先保存b的旧值再自增这比前置自增多一步“保留现场”的操作。在追求极致性能的场景下前置自增往往更高效。第十天的你不需要立刻深入到底层汇编但知道这层原因会帮你理解为什么很多老程序员都说“优先用i而不是i”。3.2 数组名和指针移动步长才是关键考察点数组和指针的关系是C语言面试和练习题里最经典的考点。一个典型的迷惑题是定义int arr[5]那么arr和arr的值相同吗答案是在数值上相同但类型不同。arr的类型是int*arr1会跳过4个字节arr的类型是int (*)[5]arr1会跳过整个数组占用的20个字节。我第一次做这个题的时候两次都选了“相同”结果被正确答案狠狠教育了。这个知识点用在做“指针移动指定位置输出字符”这类题时特别有用。比如写一个函数把一个字符串从第n个字符开始输出你可以用char *p str n - 1;拿到指针然后顺着指针往后遍历。每次p表面上看只是地址的算术变化实际背后隐藏着“指针的类型决定步长”这个规则。如果指针指向的是结构体p跳过的就不是一个字节而是整个结构体的大小。3.3 数组变量的类型转换强转不能乱来数组名在某些表达式中会“退化”成指向首元素的指针但在sizeof里又保持数组的本质——这个分裂行为是初学阶段最容易出错的点。比如int arr[5]; sizeof(arr);得到的是20而sizeof(arr0)得到的是4在32位机器上因为arr0已经把数组名变成了指针表达式。类型转换也是热搜热门。C语言的强制类型转换写起来很简单(int)floatValue这种但转换时要清楚自己在干什么。把int*转成char*之后再对它做取内容操作读出来的就只剩第一个字节了。还有更隐蔽的把一个大的整数赋值给char变量会发生截断只保留低8位。很多初学者以为“强制类型转换就是把数据原样换个类型”实际上有些转换是位模式不变、解释方式变比如int转float有些是直接截断比如int转char。不理解这一点做题时就会很痛苦。我后来给自己总结了一个小口诀指针看类型类型定步长数组名会变心sizeof里显原形。这句话帮我解决了不少练习题。4. gdb实战用一个段错误把程序“体检”一遍热搜词里出现了“利用gdb工具调试c语言程序”这个太真实了——因为几乎每个C语言学习者第一次写的指针程序都会段错误。我那个段错误程序是这样的申请了一个字符串指针没分配空间就直接往里写内容结果一运行程序就崩了。当时我还没学会gdb只能满屏printf找问题效率极低。后来老老实实学了gdb才发现调试器的威力比想象中大得多。4.1 从崩溃现场开始编译时要加-g、运行时要设断点用gdb的前提是编译时必须加调试信息命令行是gcc -g 文件名.c -o 输出名。这一条我一开始总是忘导致gdb里看不到源码只能看到汇编级别的地址排查起来非常吃力。加上-g之后gdb才能把机器码和源代码行号对应起来list命令才能显示源码。启动gdb的方式有两种gdb 可执行文件进入交互界面或者gdb 可执行文件 -batch -ex 命令直接执行非交互命令。我建议初学者先用交互模式一条条命令来能更清楚地看到程序停在哪里。4.2 断点、单步和查看变量gdb最常用的四个命令我在调试中常用的命令有四个基本可以覆盖80%的调试需求我用表格整理一下命令作用我用得最顺的场景break 行号或break 函数名设置断点在怀疑出错的那一行停下来run运行程序让程序执行到断点处print 变量名打印变量/表达式的值检查指针是不是NULL、变量是不是预期值next或step单步执行next不进入函数内部step会进入排查递归和嵌套函数时用step还有一个特别好用的命令是x。x/20x 变量名能以十六进制查看从某个地址开始的20个字节内存内容。当你的程序出现奇怪的输出时用x直接看内存比打印每个变量快十倍。我记得排查数组越界时就是靠x命令发现数据被写到了不该写的位置然后顺藤摸瓜找到for循环的边界问题。4.3 实测一次段错误的完整排查链路拿“往未分配空间的指针里写字符串”这个问题举例。程序长这样#include stdio.h #include string.h int main() { char *p; strcpy(p, hello); printf(%s\n, p); return 0; }运行直接Segmentation fault。用gdb调试的过程是加-g编译后启动gdb输入run程序直接崩掉gdb会提示“Program received signal SIGSEGV, Segmentation fault”并定位到strcpy那一行。然后我输入print p结果显示p是一个未初始化的指针里面存的地址是个垃圾值。这个答案比我自己猜测“是不是strcpy用错了”要准确得多。后续我又练习过监视变量变化的场景比如在冒泡排序里想确认每一轮交换后数组的顺序可以用watch 数组名[下标]设置观察点每次该值变化时程序自动停下来。这个命令在处理复杂的循环逻辑时特别好用比自己一个print一个print埋点高效得多。gdb这块我强烈建议花一个下午专门练一次磨刀不误砍柴工。5. 从鞍点到字符串逆序经典练习题的正确打开方式第十天前后应该开始刷题了。热搜里那些“5*5鞍点问题”“字符串逆序c语言pta”“九九乘法表”“冒泡排序”“完数”基本就是这一周最典型的习题集。我的经验是题目不怕经典怕的是你没练透。每道题背后藏的考点比题目本身重要得多。5.1 用stdio.h和limits.h解5×5鞍点问题鞍点问题是我觉得最值得做的一道题。定义是在一个5行5列的矩阵中找到一个元素它在所在行里是最大的同时在所在列里是最小的也有教材把条件反过来。这个题表面上考察二维数组和循环嵌套实际上还考察了“初始化策略”和“标志变量”的用法。我第一次做的时候用了很笨的方法三重循环加一堆if代码乱成一团还经常漏条件。后来参考了网上常见的解法思路才清晰起来先对每一行找出最大值以及它所在的列号再检查这个位置是否是该列的最小值。恰好题目要求里提到了limits.h我也用上了INT_MAX和INT_MIN来初始化极值比较的基准值。比如找一行最大值时可以把基准初始值设为INT_MIN这样即使矩阵里有负数也不会因为初始化成0而误判。核心伪代码逻辑是这样的#include stdio.h #include limits.h int main() { int a[5][5]; // 输入矩阵 for (int i 0; i 5; i) for (int j 0; j 5; j) scanf(%d, a[i][j]); int found 0; for (int i 0; i 5; i) { int maxRow a[i][0], col 0; for (int j 1; j 5; j) { if (a[i][j] maxRow) { maxRow a[i][j]; col j; } } // 检查该列是否都大于等于这个值 int isMin 1; for (int k 0; k 5; k) { if (a[k][col] maxRow) { isMin 0; break; } } if (isMin) { printf(鞍点: %d, 位置(%d, %d)\n, maxRow, i, col); found 1; } } if (!found) printf(不存在\n); return 0; }这段代码里有两个细节值得琢磨第一同一行可能有多个最大值题目通常会要求找“第一个”或“最后一个”所以要明确更新条件是用还是第二找到了鞍点不代表结束因为整个矩阵可能存在多个鞍点所以found标志只是用来最终确认“有没有”不是因为找到一个就停止外层循环。这个认知是我第二次做这道题时才建立的一开始我只要找到一个就break直接漏掉了后面的结果。5.2 字符串逆序双指针原地交换的思路字符串逆序题我一开始写的代码非常幼稚再开一个新数组从后往前拷贝然后输出。这种写法没错但浪费了空间也不是题目真正想考的。PTA和OJ平台上的字符串逆序题标准解法通常是双指针原地交换定义两个指针一个指向字符串开头一个指向结尾循环交换它们指向的字符然后头指针后移、尾指针前移直到两者相遇。#include stdio.h #include string.h void reverse(char *s) { char *left s; char *right s strlen(s) - 1; while (left right) { char temp *left; *left *right; *right temp; left; right--; } } int main() { char str[100]; gets(str); // 注意实际OJ常要求用其他输入方式这里仅为示例 reverse(str); printf(%s\n, str); return 0; }这个代码里最容易被忽略的错误是strlen(s)返回值类型是size_t无符号类型如果直接在strlen(s) - 1前没有强制转换遇到空字符串时会得到非常大的数字导致right指针直接越界。我第一次没考虑这个测试用例一传空字符串就出乱码。从那以后我写任何涉及字符串长度减一的代码都会先检查字符串是否为空。5.3 冒泡排序和完数看似简单其实隐藏了边界和效率的问题冒泡排序是我认为“人人都说会但一写就翻车”的典型。很多人的第一版代码是两层循环加条件交换却忘了记录“本轮是否发生过交换”这个优化点。如果某一轮没有任何交换说明数组已经有序可以直接退出。这个优化能把最好情况的时间复杂度从O(n²)降到O(n)。虽然第十天的学习未必要求你掌握复杂度分析但顺手加上一个标志变量并不难。完数问题的英文是perfect number指的是一个数恰好等于它的真因子之和比如6123。这个题真正的考点有两个一是怎么判断因子用循环从1试到n/2就够了二是怎么把因子收集起来再求和如果顺序和原题要求不符就会判错。还有一个隐藏考点是大数的性能问题如果求很大范围内的完数每次都从1遍历到n/2会非常慢可以用“因子成对出现”的思路把枚举范围缩小到sqrt(n)。不过第十天能写出正确版本已经很好优化可以慢慢来。6. VSCode、Ubuntu和参考书学习工具链的取舍与避坑最后一个大话题是环境和资料。热搜里“vscode怎么运行c语言代码”“虚拟机(ubuntu)配置c语言环境”“大学生c语言学习最好的编译器”“谭浩强c语言第六版pdf”这些提问我全都搜过。说实话学C语言的前十天工具和资料的选择带来的时间差距比想象中还大。6.1 编译器选择Dev C适合开箱即用VSCode适合愿意折腾的人如果你用的是Windows系统我建议第1天先用Dev C或者Code::Blocks这种开箱即用的IDE。它们自带编译器不需要配置环境变量打开就能写能跑对零基础的人极其友好。等你理解了编译运行的基本流程再切换到VSCodeMinGW的组合。VSCode的好处是轻量、跨平台、可以后续配合代码补全和版本管理工具但配置过程确实麻烦一些需要安装MinGW-w64把bin目录加入PATH环境变量再配置tasks.json和launch.json来完成编译和调试。我见过太多新手第一天就直奔VSCode结果折腾了一整天还停留在配环境上连第一行printf都没输出。这不是VSCode不好而是学习阶段应该优先降低摩擦。环境配置这件事放到第十天再研究你会更有底气。6.2 Ubuntu虚拟机的价值学会gcc命令行和gdb思路会清晰很多在Linux环境下学C语言有一个独特的优势所有操作都在命令行里编译器就是gcc调试器就是gdb没有IDE帮你隐藏细节。你在Windows下点击“运行”按钮时IDE帮你完成了编译链接的工作但你可能完全不知道发生了什么。而在Ubuntu里一条gcc 文件名.c -o 可执行名就能让你看到“编译”这个动作本身。配置步骤倒不复杂装好Ubuntu虚拟机更新软件源输入sudo apt install build-essential装gcc和make工具链再用vim或nano写代码gcc编译./a.out运行。这个过程一开始感觉很原始但用了几天之后你会对C语言的编译模型有更直观的理解。尤其是当遇到“为什么我的改动没有生效”这种问题时你马上会意识到哦原来是因为我忘了重新编译。这个认知在IDE环境下反而不容易建立。6.3 参考书和题库的搭配不要把教材当唯一很多初学者手里都有一本谭浩强的《C语言程序设计》。这本书作为入门教材体系完整例子多对零基础学生来说可读性不错。但它也有明显的争议点代码风格偏老旧、结构体等概念的讲解节奏比较跳跃、部分习题难度分布不均。我的建议是把它当成工具书而不是唯一老师。遇到不懂的概念去翻书配合网上课程和题库一起学。课程方面翁恺的C语言课在大学生圈子里口碑很好他的讲课节奏适合第二遍复习。练习题库方面PTA是个很好的选择里面有大量基础题比如“字符串逆序”这种经典题就是从PTA题单里来的。PAT乙级的题目比PTA基础题稍微难一点适合十天之后开始挑战。我的经验是看完一节视频、做三道题比一口气看十节视频效率高得多——因为写代码出现报错后的记忆比单纯听讲牢固十倍。还有一个常被忽视的事实网上那些“C语言基础代码大全”“C语言库函数大全”之类的资源我建议只看权威的。代码大全类的内容质量参差不齐很多样本代码本身就是错的初学者照着敲反而会被带歪。与其搜“大全”不如养成查看官方文档或者查man手册的习惯Linux下用man printf就能看到printf的完整说明这才是真正可持续的学习方式。第十天不是一个可以炫耀进度的节点而是一个“旧知识该消化、新知识刚冒头”的临界点。我个人的体会是这个阶段的代码量比什么都重要每道错题、每次段错误、每个奇怪的输出都比十个教学视频更值钱。如果你也正好走到这里别急着往前赶回头把scanf的缓冲、指针的类型、gdb的断点这三件事再亲手试一遍你会发现接下来的路反而变宽了。
返回列表