尧图精选

C语言自学第11天:从语法到实战的复盘与避坑指南

🕒 发布时间:2026/10/1 4:47:48 📁 来源:尧图网络
如果有人问我自学C语言最痛苦的节点在哪我的答案既不是第1天也不是第30天而是第11天。第1天有新鲜感第30天已经能写点小项目可第11天正好卡在语法差不多都认识、题目稍微绕一下就懵的尴尬位置。我当年就是从这里开始一边刷翁恺老师的练习题一边在vscode里折腾环境、抠指针后来回头看这天做的事情比之前十天加起来都有用。这篇东西就是我第11天学习C语言的复盘记录把基础查漏、经典练习、小项目和踩过的坑一次讲清楚给同样走到这一步的人做个参考。1. 第11天的真实坐标语法背完以后该干什么1.1 先判断自己是不是“假装会了”很多人在第11天会产生一种错觉printf会用了for循环也写了书上例子都能看懂于是觉得自己入门了。我当时的判断标准很简单——把书合上自己从零写一个“输入三个数按从大到小输出”的程序能不能一次编译通过不用查任何资料。如果做不到说明前面的基础只是在“看会”不是“写会”。这个阶段最值得花时间的地方有三个变量定义和作用域的分类、while与do-while的区别、数组和字符串的基本操作。变量定义这块很多人只知道int a却分不清局部变量、全局变量、静态变量之间的差异。我当时在一个文件里同时定义了全局变量和局部变量又加了static打印出来才发现生命周期和作用域完全是两回事。while和do-while的区别更直接一个先判断后执行一个先执行后判断。写菜单程序时do-while几乎一定是更好的选择因为它至少输出一次菜单而while可能什么都不做。我把这些容易混淆的点整理成了一份清单每天写题之前先花十分钟过一遍。这些点不需要死记但要能脱口而出因为后面写指针、结构体、链表的时候默认你是已经掌握这些基础的。第11天还去翻if-else怎么写不是丢人而是说明前面的步子迈得太赶了。1.2 用limits.h把“整数能装多大”彻底搞清楚热词里有一条是“使用stdio.h和limits.h用c语言解决计算5*5鞍点问题”虽然重点是鞍点但limits.h这个头文件被单独提出来说明很多人根本没注意到它。我在第11天做的一件小事就是写了几行代码打印出INT_MAX、INT_MIN和UINT_MAX然后亲手试了一次溢出。#include stdio.h #include limits.h int main(void) { printf(INT_MAX %d\n, INT_MAX); printf(INT_MIN %d\n, INT_MIN); printf(UINT_MAX %u\n, UINT_MAX); return 0; }输出结果之后我又写了INT_MAX 1直接看到了负数。那一刻我才真正理解什么叫“溢出”而不是只在书上读到“会溢出”三个字。处理鞍点问题时经常要拿元素比较大小如果数组里存的是用户输入的大数一旦用错了类型比较结果就会出错。limits.h里还定义了LONG_MAX、SHRT_MAX等常量排查这类问题比自己去猜范围靠谱得多。这背后有一个更要紧的认知#include stdio.h不是语法规则而是把标准库的函数声明引入到当前文件的机制。stdio.h管输入输出limits.h管数据范围它们只是C标准库的一部分。搞清楚这一点之后再看malloc、time、math这些库就不会觉得每个头文件都需要背下来而是按需引入。2. 三道经典练习比翻十篇教程都管用2.1 九九乘法表循环嵌套和输出格式的紧箍咒九九乘法表几乎是每个初学者都会写的题但能一次写好的人并不多。倒不是因为逻辑难而是输出格式的细节太容易翻车。我一开始写成了这样外层i从1到9内层j也从1到9结果打出来的是完整的矩形而不是左下三角形。#include stdio.h int main(void) { for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-2d , j, i, j * i); } putchar(\n); } return 0; }这里的核心是内层循环的结束条件要写成j i而不是j 9。%-2d是左对齐每个结果占两位宽度这样表格列才能对齐。如果用\t制表符也能凑合但当数字位数不同时对齐效果会很乱。另一个容易犯的错是把printf(%d*%d%-2d , j, i, j*i)写成i*j两种写法的数学结果一样但输出的文字顺序会变成“212”还是“122”题目要求里通常有明确说明写之前先看题干。做完九九乘法表之后我还顺手加了两个变体输出上三角、输出一个用户可以指定范围的版本。变体不需要多么复杂关键是把外层循环和控制条件改来改去能亲手感受到“循环嵌套的执行顺序”。很多人卡在双重循环上其实是没在纸上画过执行轨迹。我建议把i1, j1、i2, j1的每一步打印出来看两遍就通了。2.2 字符串逆序与按空格拆分指针和字符串函数开始联手第11天开始接触字符串题目时我踩的第一个大坑是用了gets。这个函数在很多编译环境里直接报警告或直接报错因为它不检查缓冲区边界容易把内存写穿。正确的做法是用fgets读取一行再结合strlen和指针操作处理字符串。字符串逆序的经典思路是双指针一个指向开头一个指向结尾也就是strlen(str) - 1交换两个字符然后分别向中间移动直到两个指针相遇。#include stdio.h #include string.h int main(void) { char str[101]; fgets(str, sizeof(str), stdin); str[strcspn(str, \n)] \0; // 去掉换行符 int left 0; int right strlen(str) - 1; while (left right) { char tmp str[left]; str[left] str[right]; str[right] tmp; left; right--; } printf(%s\n, str); return 0; }str[strcspn(str, \n)] \0这行看着高级其实是在找字符串里第一个换行符的位置把它替换成字符串结束符。如果不做这一步逆序后的结果会在末尾多出一个换行输出看起来多了一空行排查半天才发现是它的问题。“把一个字符串按照里面的空格分开”这道题同样绕。最简单的方式是用strtok但strtok会把原始字符串里的分隔符改成\0也就是说原始内容会被破坏。如果后续还想用原字符串就得先复制一份。我更推荐手写一个拆分逻辑遍历字符串跳过连续空格把非空格的字符逐个复制到目标数组中。这个过程看起来笨但能逼你把指针、数组下标和字符串结束符想明白。热词里有“字符串逆序c语言pta”说明PTA这类平台对字符串题目的细节要求很严多写几遍没坏处。2.3 5x5鞍点问题数组和“先想后写”的逻辑鞍点问题的出现频率很高但很多人第一次看到题目是懵的。最常考的定义是在一个矩阵中某元素在其所在行上最大同时在其所在列上最小这个元素就叫鞍点。5x5矩阵就是这个规模下的典型输入。我当时并没有立刻写代码而是先在纸上把一个5x5的矩阵画出来标出每行的最大值位置再看这些位置是否同时满足“列上最小”。这个过程非常重要因为数组题如果不在草稿纸上推演很容易把行列坐标搞反。#define ROWS 5 #define COLS 5 for (int i 0; i ROWS; i) { int max_col 0; for (int j 1; j COLS; j) { if (matrix[i][j] matrix[i][max_col]) { max_col j; } } int flag 1; for (int k 0; k ROWS; k) { if (matrix[k][max_col] matrix[i][max_col]) { flag 0; break; } } if (flag) { printf(鞍点: matrix[%d][%d] %d\n, i, max_col, matrix[i][max_col]); } }这里要注意的是“大于”和“小于”符号是否带等号。如果题目要求“最大”和“最小”通常要把相等的情况算进去否则会出现明明有多个相等元素程序却说找不到鞍点。热词里还提到了limits.h其实是在处理边界值可以把每行最大值初值设成INT_MIN把列最小值初值设成INT_MAX这样不管输入什么数字都不会出错。这道题对我的价值不在写法而在“先想后写”的习惯。以前我总是打开编辑器就敲遇到问题再回头改。后来养成了先在纸上写关键逻辑的习惯十次有八次能避免坐标搞反的问题。C语言的数组和指针就是靠这种笨功夫磨出来的。3. 从练习题跳到小项目打字游戏和网吧计费3.1 打字游戏随机数、隐藏光标、clock计时第11天我已经不太满足于几道练习题了开始想写一点“能玩”的东西。打字游戏是个非常好的选择因为它需要同时用到随机数、字符处理、计时、条件循环和界面控制。先说随机数。C语言的rand()返回一个[0, RAND_MAX]范围内的整数但如果不设置随机种子每次运行程序生成的序列都是一样的。所以我用了srand(time(NULL))来初始化种子。生成随机小写字母的经典写法是char ch a rand() % 26;。这里要注意rand()%26并不是完全均匀的分布但对于游戏来说完全够用不需要上mt19937这类复杂方案。如果同一秒内多次运行time(NULL)返回的值可能相同导致种子一样这是另一个坑可以配合clock()生成混合种子。隐藏光标是一个很多人不知道的操作但又是打字游戏体验好坏的关键。Windows下用SetConsoleCursorInfo和GetStdHandleLinux终端则可以用ANSI转义序列\033[?25l来隐藏光标显示光标是\033[?25h。我的打字游戏逻辑很简单随机生成一个字符显示在屏幕上玩家输入相同字符得分用clock()记录耗时clock()返回的是程序启动以来的CPU时钟数除以CLOCKS_PER_SEC得到秒数。#include stdio.h #include stdlib.h #include time.h #include conio.h // Windows 下使用 int main(void) { srand((unsigned)time(NULL)); int score 0; clock_t start clock(); while (score 10) { char target a rand() % 26; printf(%c, target); char input _getch(); if (input target) { score; } } clock_t end clock(); printf(\n用时 %.2f 秒\n, (double)(end - start) / CLOCKS_PER_SEC); return 0; }conio.h里的_getch()可以无缓冲读取按键不用按回车就生效。这个例子在Linux下编译不过这是跨平台差异导致的正常现象。打字游戏虽然简单但把之前学的零散知识全部串起来了做完它我对“学C语言能做什么”第一次有了实感。3.2 网吧计费系统文件读写和时间差计算打字游戏偏交互网吧计费小项目则偏数据管理。热词里有“c语言网吧计费管理小项目”可见不少课程设计和期末项目都会选这个题材。这个项目的核心难点不是管理菜单而是时间和文件的处理。计费系统需要一个数据结构保存用户名、上机开始时间、结束时间和费用。时间在C语言里常用time_t类型表示time(NULL)获取当前时间difftime(end, start)返回两个时间点之间的秒数。按小时计费就可以用秒数 / 3600.0计算小时数再乘以单价。数据库功能不需要引入外部文件库直接用fprintf写文本文件就够了。这里有一个非常关键的细节fscanf和fprintf的返回值必须检查。fprintf写文件时可能因为磁盘满或路径错误而失败fscanf读文件时如果没有读到足够的字段返回值会小于期望的字段数。我一开始只写fscanf(fp, %s %lf, name, fee);不检查返回值结果读到文件末尾时变量里残留旧值计费数据就错了。正确的写法是永远判断返回值是否等于期望的字段数。FILE *fp fopen(records.txt, r); if (fp NULL) { printf(文件打开失败\n); return 1; } User u; while (fscanf(fp, %s %lld %lf, u.name, u.start, u.fee) 3) { // 处理每条记录 } fclose(fp);这里用%lld是为了匹配long long类型。如果格式串写错fscanf可能只读入部分数据后面的字段被跳过去。排查这类问题最好先打印读到的内容而不是盯着逻辑看。网吧计费项目做完之后我对fopen、fclose、fscanf、fprintf这几个函数的理解直接从“认识”变成了“会用”。3.3 为什么第11天就应该碰小项目很多人觉得第11天还是新手写不了项目。我的看法恰好相反小项目是检验基础知识最好的“照妖镜”。打字游戏看起来简单但动手以后你才会发现随机数、循环、字符输入、计时这些知识点其实没有被真正串联起来。网吧计费又更进一步逼着你处理结构体、文件、时间函数。这些都是靠做题很难覆盖到的组合场景。写小项目最容易犯的错误是想一步到位。我建议先砍需求网吧计费先不做界面只做命令行菜单先把“上机、下机、结账”三条路径跑通再把“保存记录到文件”加上。功能每多一个难度不是线性增加而是指数增加。一个能跑的300行程序比一个写了一半的2000行项目有价值得多。等到第30天再回头把代码拆模块、加注释、优化结构也不迟。4. 环境和工具链vscode配置C语言环境的避坑细节4.1 先把“编译器”和“编辑器”分清第11天有大量时间浪费在环境上主要原因是没分清编辑器、编译器和调试器三个概念。vscode是编辑器负责你输入代码编译器是gcc负责把.c文件变成可执行文件调试器是gdb负责在运行过程中查看变量变化。很多人说“vscode跑不了C语言”其实是只安装了vscode没有安装编译器。在Windows上最常见的方案是安装MinGW-w64它包含了gcc、gdb和配套的头文件库。安装之后在终端里输入gcc -v能输出版本信息就说明编译器已经生效。如果提示“不是内部或外部命令”大概率是安装了编译器但没有把bin目录加到PATH环境变量里。配置完PATH后记得重新打开终端窗口新配置才生效。这里还有个容易被忽略的版本问题MinGW-w64有32位和64位之分最好选择64位版本否则后面用long long或处理大数组时会有各种莫名其妙的问题。我见过有人装了两个不同的MinGW版本终端里gcc指向的是旧版调试半天才发现是编译器版本不一致。4.2 用vscode跑C语言的完整配置环境配置的核心是让vscode知道“用什么命令去编译和运行”。最简单的方案是安装C/C扩展然后配置tasks.json和launch.json这两个文件分别负责编译和调试。新手如果不熟悉JSON很容易把文件路径写错。我常用的一个更省心的方法是安装Code Runner扩展写好代码后直接右键“Run Code”它会调用当前环境里的gcc main.c -o main ./main。但Code Runner默认直接运行编译参数不一定带-Wall所以有些警告看不到。正式学习我还是建议用F5调试模式编译参数里至少加上-Wall -g-Wall把所有常见警告显示出来-g让调试器能获取变量信息。{ tasks: [ { type: cppbuild, command: gcc, args: [-fdiagnostics-coloralways, -g, -Wall, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe], group: build } ] }${file}表示当前打开的文件${fileDirname}表示文件所在目录。如果文件路径里有中文或空格编译很容易出问题最省事的办法是给所有C语言学习项目建立一个纯英文目录比如C:\code\day11。这条经验是我在连续三次因为路径里有中文导致编译失败之后总结出来的信我别折腾。4.3 双平台思路Windows和Ubuntu下的区别热词里提到“虚拟机(ubuntu)配置c语言环境”说明很多人在用虚拟机跑Linux练C语言。Ubuntu下配置其实很简单安装build-essential就会带上gcc和make。命令行直接gcc main.c -o main然后./main运行。相比WindowsLinux的好处是不用操心MinGW的路径和版本系统自带的gcc通常就是最合适的。虚拟机和实体机的主要区别在于资源占用和文件共享。如果只是想练C语言在虚拟机里开一个终端用vim或vscode写代码都行。我个人的建议是先在自己日常系统上把vscode配好再抽时间在虚拟机里用纯命令行的方式编译运行一次。这个过程的目的是让你意识到环境只是工具代码逻辑才是核心。同一份代码在Windows和Linux下的行为会有细微差异比如conio.h在Linux下不存在fseek的二进制模式在不同平台也有区别。越早认识到跨平台差异后面做项目时越少踩坑。虚拟存储器管理这种偏系统层面的题目通常会在操作系统课程里出现用C语言模拟页表、缺页中断和替换算法。这类题目对指针和结构体的要求很高在虚拟机上配合valgrind排查内存问题是很常见的做法。第11天不需要碰这么深但提前知道“C语言还能写这些”是有价值的。5. 第11天最容易踩的坑5.1 scanf和缓冲区的爱恨情仇scanf大概是新手第一个崩溃点。最常见的表现是连续读字符时第二次scanf(%c)读到的不是输入的字符而是一个换行符。原因在于第一次输入后按下的回车仍然留在输入缓冲区里%c会把那个换行符读走。解决办法有几种一是用scanf( %c, ch)在格式串最前面加一个空格让scanf先跳过多余的空白字符二是在读字符前用getchar()清掉一个换行符。第二种方法看起来简单但如果在多个分支里都清了缓冲反而会多读一个字符我自己就被坑过。推荐优先使用格式串加空格的方式代码意图清晰也不容易越清越乱。更彻底的建议是碰到整行输入时用fgets再配合sscanf解析。虽然代码多一点但能规避大部分缓冲区问题。尤其在做PTA这类题目时输入格式经常有行末空格用scanf上套餐很容易“明明输入没问题程序就是不对”。5.2 fscanf/fprintf容易忽略的返回值上一节提过文件读写要检查返回值这里再展开说一下。fscanf的返回值是成功匹配并赋值的参数个数。读文件时如果格式串是%s %d期望返回值是2如果文件被掏空了返回EOF如果只有字符串没有数字返回1。很多程序错误地用while (!feof(fp))来判断文件是否读完但feof只有在尝试读取越过末尾之后才返回真所以往往会多读一次读到无效数据。正确写法是让读取操作本身作为循环条件while (fscanf(fp, %s %d, str, num) 2)。这样既检查了返回值又自然地处理了文件末尾不需要对feof做额外判断。写回文件时同样要注意fprintf返回实际写出的字符数如果等于负数就说明写入出错。把文件关闭之前最好检查一下fclose的返回值虽然99%的情况下不会出问题但养成检查的习惯总没有坏处。5.3 随机数到底怎么才算“随机”rand()和srand(time(NULL))的组合是入门必备但有一个细节程序如果在一秒内重复启动time(NULL)返回的是秒级时间戳两次启动可能拿到同一个种子生成的随机序列就完全一样。解决的办法是加入clock()作为辅助种子srand(time(NULL) ^ clock())这样大多数情况下能打破同秒重复的问题。如果你在写跑批测试需要可复现的实验结果反而要固定种子比如srand(42)。这个操作会让随机序列完全确定方便对比不同算法之间的差异。随机数的“随机”也要看用途游戏里用rand()没问题但加密或者抽奖这种对随机质量要求高的场景必须换成更专业的随机数生成方式。热词里问“cython 是不是比单纯的c语言更高效”其实是在问不同工具的效率边界我的理解是C语言在底层灵活性和运行速度上仍然有不可替代的位置直接操作内存和指针这是很多高级语言做不到的。5.4 常见问题速查表第11天的问题往往集中在几个固定区域我把自己踩过的坑整理成一张表。这张表不需要背遇到问题直接对照。现象常见原因解决办法控制台闪一下就没了程序运行结束窗口被系统关闭在main结尾加getchar()或者用调试模式运行printf输出中文乱码源文件编码和终端编码不一致统一使用UTF-8编码或在Windows下改用GBK编码保存gcc编译报“undefined reference”链接时缺少对应库用到数学函数时加-lm用到线程时加-lpthread程序崩溃段错误指针越界或访问了非法内存检查数组下标检查指针是否初始化用-g编译后调试死循环程序停不下来循环条件永远为真变量没更新检查循环内是否有增量/减量语句必要时用printf打印观察变量值莫名变化数组越界或缓冲区溢出检查所有写数组的操作优先怀疑下标偏移scanf读不到想要的输入缓冲区残留换行符格式串前加空格或用fgets配合sscanf文件读出来是乱码文本文件编码不一致统一UTF-8或在打开文件时用二进制模式先排查用了gets直接编译报警gets不安全标准已废弃改用fgets并处理末尾换行符这张表里有些问题是第30天还会碰到的比如段错误和文件读写。第11天的重心不是解决所有问题而是建立“出问题先看现象、再猜原因、最后验证”的排查流程。编程里绝大多数时间不是写代码而是排查为什么代码不按预期跑。第11天这个节点我最大的体会就是别急着往后面赶进度把动手练习的深度提上来。你不需要在第11天就搞懂指针所有细节也不需要记住每个标准库函数的参数但你必须有一段“想不通就写出来跑一跑”的经历。热词清单里那些题目和项目每一道都对应着一个基本能力点九九乘法表练循环字符串逆序练指针鞍点问题练二维数组打字游戏练随机数和交互网吧计费用文件和时间。把它们逐个在编译器里跑通比收藏一百个教程都有用。最后再分享一个我后来一直用的小习惯每次写完程序在文件头部注释里写上“今天犯的错”和“明天要注意的点”。第11天我写的是“scanf缓冲区”“fgets要清换行”“行最大值初始化用INT_MIN”。等到第30天回看这份笔记你会非常清楚地看见自己是怎么从一脸懵走到能独立写小项目的。C语言这条路没有捷径但每一步踩实了后面就会走得特别快。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →