ARTICLE DETAIL

资讯详情

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

C语言学习笔记:从菜鸟教程到指针、调试与项目实战

C语言学习笔记:从菜鸟教程到指针、调试与项目实战 菜鸟教程C语言个人学习笔记持续更新这个项目光看名字就很直白它不是系统教程也不是别人笔记的搬运而是我跟着菜鸟教程啃C语言时边学边记的个人学习记录。最早决定写它是因为我发现自己看菜鸟教程时每节都短、例子都能跑但合上网页就只剩“好像懂了”四个字。把笔记记下来、整理成自己能复现的东西才能对抗这种遗忘。这个项目解决的就是一件事让C语言基础概念在脑子里真正落地。它适合刚入门C语言、准备学单片机或嵌入式、或者正在准备计算机相关考试的同学参考。1. 学习路线与内容框架设计1.1 为什么拿菜鸟教程当主线我见过不少人一上来就啃KR或者直接拿《C程序设计语言》配套题硬刚结果前两周全花在和标准库、编译环境较劲上。菜鸟教程的优点不是“权威”而是把知识点切得足够小按“是什么、怎么用、注意什么”的方式讲非常契合零基础。它的C语言部分目录覆盖了基本语法、指针、结构体、共用体、文件读写、内存管理这些主线内容每节都附可运行的小例子复制到本地改一改就能加深理解。但也要说清楚菜鸟教程的深度有限它不是万能的。我的做法是把它当“主干目录”遇到不理解的地方再去找其他资料交叉验证。比如指针部分教程讲得挺清楚但“指针运算和数组的关系”这种展开我在别处才看到更透彻的版本。所以用它的同时要在自己的笔记里标记哪些内容需要二次补充别把网页上的话当成唯一解释。这也决定了我的笔记不是教程的复述而是“教程没讲透但我自己查明白了”的补充记录。1.2 我的学习顺序与章节取舍我实际走的顺序大致是先搞定编译环境再按“变量与数据类型、运算符、流程控制、数组、函数、指针、结构体、文件、动态内存”这条线往下走。这样做的好处是每一章都只用前面出现过的知识不容易出现“这里用了后面才讲的东西”的劝退感。有两个取舍值得一提。第一把指针放到函数后面学因为数组做函数参数时会退化成指针先了解函数参数传递再学指针会顺畅很多。第二把预处理、位域、枚举这类“看起来像独立语法”的内容放到第二遍再看第一遍只需要知道#define能做宏替换、enum能声明枚举常量避免在入门阶段被细节拖慢速度。学习一段时间后你会意识到C语言的语法骨架就那几块真正的深度在内存模型和指针组合前期没必要追求每个语法点都吃透再前进。1.3 给笔记定的几条修订规则个人笔记最怕记完不看。我给自己的笔记下了几条硬规则每个知识点后面必须附一个能运行的代码片段或练习链接每周末回看一周前的旧笔记能不看代码复述出要点就标记为“已掌握”否则标记为“待复习”出错题目单独建一个“错题录”小节重写一遍正确代码才算过关。这套规则听起来简单坚持下来效果很明显比单纯把教程翻页快多了。“持续更新”对我不是装饰词。我会在笔记开头写一个目录带日期每次新增内容就更新目录旧内容如果被新理解推翻直接在原文位置用修订注记保留学习轨迹。这样回看时你能清楚地看到自己什么时候想错了、什么时候改对了这种过程记录比笔记内容本身更能反映成长。2. 核心语法模块的笔记与易错点2.1 变量、格式化输出与%d读入字符的怪事第一个要写的不是“什么是变量”而是“类型和格式符真的会咬人”。在C语言里scanf和printf靠格式字符串和参数类型互相配合一旦对不上轻则输出乱码重则直接崩溃。我在笔记里给了一个对照表整数用%d无符号用%u八进制用%o十六进制用%x字符用%c字符串用%sfloat和double在printf里都用%f或%lf但在scanf里float必须%f、double必须%lf。这个区别很多人栽过我把它写在笔记最显眼的位置。我经常看到一个问题“C语言变量用%d输入一个字符后的值会是什么”这是初学者最容易踩的坑。假设int a 100; scanf(%d, a);用户输入的是字符a此时scanf发现输入流里第一个字符不是数字匹配失败返回0并且a保持不变而字符仍然留在输入缓冲区里。注意这里根本没有赋值动作发生不是因为“字符被转换成了ASCII码”而是scanf压根没读它。想确认这个结论最直接的方式是把scanf的返回值打印出来printf(ret%d a%d\n, scanf(%d, a), a);返回值0就说明匹配失败a维持原值。这种情况的补救思路不是去猜而是先清空缓冲区再重新读取。我试过直接fflush(stdin)它在某些编译环境下能用但C标准并没有保证这个行为移植性很差。后文会专门讲我推荐的清空方法。2.2 字符串函数与输入安全fgets比gets靠谱在哪说到字符串老教材里gets满天飞但gets不检查缓冲区长度用户输入超过数组大小就直接越界属于典型“教程害人”。现代写法是用fgets它接受目标地址、缓冲区大小和数据流三个参数能保证最多读入size-1个字符剩下的空间留给结尾的\0。不过fgets会保留换行符在字符串里如果不处理后面拼接、比较都会出问题。我习惯在处理后立刻用buf[strcspn(buf, \n)] 0;把换行删掉这个strcspn的用法是常见的“去换行”技巧比手动遍历循环更简洁。常用字符串函数我单独整理过strcpy/strncpy负责复制strcat/strncat负责拼接strcmp比较内容strlen统计长度strchr查找字符strstr查找子串。用的时候留心以str开头的“不安全版”多半都要求目标缓冲区足够大所以能选带n的版本就选带n的版本。strncpy也有自己的坑它不会保证以\0结尾源字符串长度大于等于n时目标串就没有结尾符所以最好在复制后手动把最后一个字符置成\0。字符串逆序是经典练习。我的写法是双指针左指针从头走右指针从尾走不停交换字符直到两个指针相遇。这个题目考的就是对数组下标和循环边界的敏感度。很多人会在边界上差一个字符导致逆序后末尾多出一个奇怪字符——其实就是没算明白结尾\0的位置。把strlen算出来的长度减一当作右指针起点就能避开这个坑。2.3 数组、冒泡排序与二维数组的内存排布数组在C语言里是连续内存块的抽象。int a[10]表示连续10个int所占的空间下标从0开始越界不查。这个“不查”不是功能缺失而是C语言信任程序员因为数组操作编译后就是“基地址偏移量”的指针运算运行时不做边界检查。越界后修改到别的变量、甚至函数返回地址都是可能发生的事这也是很多不稳定bug的根源。冒泡排序是入门必写我写的时候加了个flag优化如果某一轮没有任何交换说明已经有序直接结束。这个优化在实际数据接近有序时能把耗时从典型的平方级明显降下来。对初学者来说这个flag还教会一个道理排序算法的细节是可以按场景优化的不是背模板就完了。很多题目不会直接要求背冒泡而是考察“理解后能不能改进”所以笔记里我把原始版和优化版都贴出来方便对比差异。二维数组a[3][4]在内存里其实是连续排布的12个单元编译时按行优先展开。有朋友问“锯齿数组”怎么在C里实现这是个好问题。C语言本身没有真正的锯齿数组因为数组的每一行长度必须固定但可以通过指针数组来模拟char *lines[3]里每个元素指向不同长度的字符串这种技巧在文本处理里很常用。做这件事时才会真正理解“数组名是首元素地址”这句话的重量它把数组和指针彻底焊在了一起。2.4 指针与结构体C语言的灵魂与骨架指针这一章我习惯用一个类比内存就像一排快递柜每个柜子有编号指针变量保存的就是柜子编号。a取出a所在柜子的编号*ptr则是打开编号为ptr的柜子取东西。这个类比能解释大部分基础问题也能解释为什么指针本身也要占空间——它保存的是编号不是货物本身所以指针大小只跟平台位数相关64位环境下通常是8字节跟它指向的数据大小无关。学到函数传参时有一个关键点数组名作为参数传递时不是传整个数组而是传首地址函数里用sizeof得到的是指针大小而不是数组大小。所以在函数里需要另一个参数明确告诉它数组有几个元素。这个特性让很多人第一次意识到“为什么函数里不能直接求数组长度”。我踩过一次把数组传进排序函数函数里用sizeof(arr)/sizeof(int)算长度结果只有2因为sizeof(arr)拿到的是8字节指针大小。后来我老老实实把长度作为参数传进去世界就清净了。结构体是把多个字段打包成一种新类型。struct student { char name[32]; int age; };定义变量后可以用.访问成员有一个结构体指针时要用-。typedef可以给结构体起短名字省得每次写struct前缀。结构体数组、结构体指针数组、函数返回结构体指针这些组合是链表等后续内容的直接基础。我的笔记里专门画了一张小表对比“结构体变量、结构体指针、指向成员的指针”三种写法的内存形态它们长得像含义差很远。指针和结构体组合的一个实用场景是“用结构体指针在函数之间传递大对象”。传结构体变量会把整个结构体复制一遍传指针只复制一个地址代价小得多。这个道理和“寄整箱书”还是“寄书架取书码”的区别一样理解了它后面学链表、树、队列都会顺很多。3. 实操工程从“看得懂”到“跑得起来”3.1 在VSCode里搭一套C语言编译调试环境很多新手卡在最开始教程讲了语法本地却不知道怎么写、怎么编译。我最推荐的组合是VSCode加MinGW-w64在Windows上不算重又能直接练习标准C。装上VSCode后把MinGW的bin目录加入PATH命令行里能跑gcc --version说明环境就位。VSCode里跑C语言要配置三个JSON文件tasks.json负责编译launch.json负责启动调试c_cpp_properties.json负责IntelliSense的头文件路径。tasks.json可以简单写成调用gcc -g编译当前文件-g参数会生成调试信息launch.json用gdb调试器program指向编译出的可执行文件。这些配置我写进了笔记模板每新建一个学习目录就复制一份。配好之后写代码、按F5断点调试比在网页上改代码直观太多。给一个最简tasks.json示例{ version: 2.0.0, tasks: [ { label: gcc-build, type: shell, command: gcc, args: [-g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe], group: { kind: build, isDefault: true } } ] }launch.json的核心段大致是{ name: C Launch, type: cppdbg, request: launch, program: ${fileDirname}/${fileBasenameNoExtension}.exe, cwd: ${fileDirname}, externalConsole: true, MIMode: gdb, miDebuggerPath: gdb.exe }第一次跑通时记得先确认launch.json里的program路径和tasks.json输出的exe文件名一致不然会半天只看到“无法启动”的提示。这个坑我花了一晚上才查到所以特意在笔记开头标注了“先检查文件名拼接再怀疑调试器配置”。另外如果externalConsole设成false有时候控制台输出一闪而过看不清结果新手阶段反而建议用true让窗口停在那边等你观察。3.2 用makefile把多文件学习工程理顺学习阶段写单个.c文件还能靠gcc硬编一旦笔记里开始有多个模块命令行编译就变得难维护。我第二遍学习时把所有练习改成“makefile多文件”的结构每个专题一个目录目录里放main.c、模块.c和模块.hmakefile统一管理。一个最简makefile示例CCgcc CFLAGS-Wall -g main: main.o utils.o $(CC) $(CFLAGS) -o main main.o utils.o main.o: main.c utils.h $(CC) $(CFLAGS) -c main.c utils.o: utils.c utils.h $(CC) $(CFLAGS) -c utils.c clean: rm -f main *.o别忘了makefile里每条规则必须以Tab开头否则会报missing separator。这个报错是新手的经典劝退点但原因很无厘头就是编辑器把Tab自动换成了空格。我在VSCode里设置了editor.insertSpacesfalse才避免每次都被这个问题卡住。建议所有刚学makefile的人第一步检查缩进而不是先去查语法。用makefile的另一个好处是“增量编译”只改动一个.c时make会根据文件时间戳决定重新编译哪些文件不用每次都全量重编。这对学习阶段的编译反馈速度提升很明显到后面做单片机或者嵌入式交叉编译时更是刚需。学到这里你会开始理解为什么真实项目里没人愿意手动敲gcc命令来编译一个包含几百个文件的工程。3.3 三个能立刻练手的小项目语法看十遍不如动手三小时。我选练手项目的标准是“小但全”要覆盖输入输出、循环、数组或字符串、文件操作三个正好形成递进。第一个是打印九九乘法表。只需要两层循环外层1到9内层1到iprintf(%d*%d%2d , i, j, i*j)控制格式。别看题目简单它能把嵌套循环、变量作用域和输出对齐问题一次练明白。我还专门试过改成“右下三角”“上三角”等变体每改一次都是对循环边界的新理解尤其是%2d这种宽度控制不实际跑一遍很难体会它有多好用。第二个是字符打字游戏。可以用随机字母从屏幕顶部往下落玩家在底部输入对应字母得分。这个项目会用到rand、循环和二维数组。如果还想控制光标位置Windows下需要接触控制台API比如SetConsoleCursorPosition想做更底层的Windows桌面程序还会碰到winsock.h这类系统头文件那是另一个话题但至少你会明白C语言不止能写黑窗口。这个项目对想走Windows程序开发的人来说是个不错的切入点。第三个是“文件版字符串逆序”。读一个文本文件把内容按行逆序后写到新文件。这个项目会逼你把fopen、fgets、fprintf、fclose和字符串处理全串起来还会遇到“为什么最后一行读出来多一个换行”这种经典问题。做完这三个类型、循环、数组、字符串、文件这五块基本功基本就稳了。我每次回看笔记都觉得这三个小项目比后来做的一些复杂题更有价值因为它们逼着我把零散语法串成了完整程序。4. 常见问题与排查技巧实录4.1 scanf换行符、输入缓冲与清空方法用scanf连续读数字和字符时最大的坑是“残留换行符”。第一次调用scanf(%d, n)读走数字按回车产生的\n还留在缓冲区紧接着scanf(%c, c)会把换行符读给c于是程序表现成“跳过输入”。很多教材的示例都没有提这个细节导致新手以为自己的代码逻辑有问题。解决有三条路在格式串里加空格比如scanf( %c, c)这样scanf会跳过空白字符在前面手动getchar()吃掉换行或者写一个清空函数循环读取字符直到换行。我个人最推荐第三种因为不管中间少了哪次输入都适用。清空函数大概是void clear_input(void) { int ch; while ((ch getchar()) ! \n ch ! EOF) {} }再次强调别盲目用fflush(stdin)它虽然在某些编译库下能用但C标准并没有保证这个行为移植性很差。在Linux、macOS或者不同编译器下表现可能完全不一样学C语言的一个重要习惯就是“尽量写标准行为内的代码”。4.2 指针悬挂与内存泄漏动态内存是自学最容易漏的一环。malloc申请完free释放过后指针仍然保存着那块地址成为“悬浮指针”。如果不把指针置成NULL后面再用这个指针就是访问已释放内存轻则读到脏数据重则崩溃。所以我写的规则是free(p); p NULL;两步必须连在一起。这个习惯不能等出问题再养必须在第一次写动态内存的时候就刻进肌肉记忆。内存泄漏则相反不断malloc却从不free。学习阶段进程很快退出泄漏了可能发现不了但在长跑服务或者单片机环境里就是灾难。排查思路是把每个malloc和它的free配对记下来如果某个函数里malloc的对象要返回给外面那接收方必须负责释放这个责任链要写清楚。学到后面接触虚拟存储器、操作系统进程地址空间这些概念时你会更明白“堆”和“栈”的差别malloc管理的是堆区函数调用栈用的是栈区两者不是一回事。4.3 “单片机C语言没有堆栈吗”到底是怎么回事这个疑问在相关讨论里出现率很高我看到不少新手被“单片机C语言没有堆栈吗为什么”这个问题卡住。准确说法是函数调用和局部变量使用的栈通常存在只是资源极其有限时浅得让人感觉“好像没有”。关键在于理解“栈”在不同语境下的两层意思。一是“函数调用栈”几乎所有C程序在运行时都有调用栈8位单片机的C编译器也会用栈保存返回地址和局部变量。二是“操作系统的任务栈”如果跑的是裸机程序、没有操作系统那就没有操作系统帮忙为每个任务分配独立栈。市面上有些教程为了简化直接说“单片机不用堆栈”时间长了就被误传成“C语言没有堆栈”。实际更接近真相的说法是8位单片机常把栈放在有限的片内RAM里可能只有几十到几百字节递归深了、局部变量数组太大栈溢出风险很高。所以很多单片机教程干脆说“少用递归、少开大数组”。这句话是对的但原因不是“没有栈”而是“栈太小”。搞清楚这一点你以后写嵌入式C代码时就会主动控制函数嵌套深度和局部变量大小而不是被“不能用递归”的教条框死。4.4 高频报错与处理速查表最后把编译和运行的高频报错整理成表每次出错先对着表查一遍多数能秒定位报错/现象可能原因常见解法undefined reference to main连接阶段找不到入口点确认是否遗漏main函数或文件名、函数名拼写错误Segmentation fault指针访问了非法内存查空指针、越界下标、释放后继续使用expected ; before }上一行漏分号或大括号不配对从出错位置往上逐行检查最近改动的地方conflicting types for xxx函数声明和使用不一致先声明原型再定义保持参数和返回类型一致multiple definition of xxx头文件里定义了全局变量或函数头文件只放声明定义放到对应的.c文件makefile: missing separator规则行用了空格而不是Tab改成Tab缩进重新保存这张表是我从自己的编译日志里抽出来的对新手特别友好。每次遇到报错先看最关键的那一行错误信息再往上一两行找文件和行号很多时候比直接去搜索引擎找答案更快。报错本身不可怕可怕的是不看报错、瞎改代码那样改一天也定位不到问题。拿菜鸟教程当主线、写个人学习笔记这件事我坚持到现在最大收获不是知识点本身而是形成了一套“看懂、敲一遍、写心得、回看复习”的闭环。如果你也在入门C语言我的建议很简单不要只收藏不敲不要只看不写笔记每学一个语法点就逼自己在本地编译出一个可运行的最小例子把报错原样抄进笔记并记下解法。等回看时你会感谢当初那个认真记录的自己。笔记里不少坑还要靠后面的内容持续补上这也正是“持续更新”的意义。
返回列表