说起来挺不好意思的,我接触C语言的时间其实不算短了,但一直处于“看得懂代码、写不出程序”的尴尬阶段。每次下定决心要系统学一遍,打开菜鸟教程看完几章就开始犯困,指针还没弄明白就草草收场。这次不一样——我把学习过程中踩过的坑、反复琢磨过的例子、网上搜到的零碎知识点,全部整理成了一篇自己能看懂的笔记。这篇内容就是根据我整理记录的过程沉淀出来的,也算是对自己学习的一次阶段性总结。
C语言这门语言,说难很难,说简单也简单。难的是它给你极大的自由,指针、内存管理、文件操作,每一个都能把新手折腾得够呛;简单的是它的语法规则并不多,核心知识点就那些,关键是怎么把它们串起来用。这篇笔记会一直更新下去,我把自己从零开始学C语言的过程、观察到的特征、验证过的代码全部记录下来。如果你也跟我一样,正在被谭浩强老师的教材、PTA平台的练习题、还有各种编译报错折磨,可以对照着看看,也许能少踩几个坑。
我整理笔记的时候有个习惯:遇到任何一个知识点,都会去查好几份资料,把不同作者的解释放一起对比,然后自己动手写代码验证。所以这篇笔记怎么读、按什么顺序读,完全看你的需要。如果你是完全的新手,建议按章节顺序走;如果你已经学过一段时间,可以直接跳到报错排查和指针进阶部分,那边有你最需要的东西。
1. 学习环境准备:VSCode配置和虚拟机踩坑实录
1.1 环境选择的第一步:CPU和内存分配
学C语言不需要多好的电脑,但需要合适的编译环境。这里说的“环境”包括两部分:一是代码编辑器,二是在电脑上运行的编译器。很多新手分不清这两个概念,以为下载了Dev-C++就等于装好了编译器。实际上,Dev-C++那个软件自带了MinGW编译器,所以你不需要额外安装;而VSCode这类编辑器本身只是一个文本编辑工具,需要你自己配置编译器才能运行C代码。
我的建议是:如果你是非计算机专业的学生,只是要应付课程作业,直接装Dev-C++或者Code::Blocks就行,省心快捷;但如果你是计算机相关专业,或者打算长期学下去,早点用上VSCode或更专业的工具会更合适。为什么?因为VSCode的调试功能好用,而且以后你学Python、写网页、做嵌入式、接触ROS2,都会用得到它。这也是我最终选择VSCode的原因——从C语言开始,就把它当终身的开发环境来配置。
顺便提一个很多人问的问题:“大学生学C语言,哪个编译器最好?”其实没有最好,只有最适合。如果你的课程用的是类似浙江大学那套C语言基础编程题目,老师一般推荐VS或者Dev-C++,因为操作简单;但你想要的是长远发展,那就用VSCode配编译器。我目前用的是VSCode + GCC,在Windows和Ubuntu虚拟机里都配过一遍,下面把两次配置的过程都记下来。
1.2 VSCode安装和插件配置的具体步骤
先讲Windows上的配置。第一步是下载安装VSCode,这个直接官网下载就行,语言选中文或者英文都可以,后面用插件切换语言。第二步是安装编译器,推荐MinGW-w64,下载解压之后,把bin目录完整路径加到系统环境变量Path里。这里要注意一个新手很容易犯的错:环境变量配置完之后,需要重启终端或重新打开VSCode才能生效,不然一直提示“gcc不是内部或外部命令”。
确认编译器装好后,在终端输入gcc --version,能正常显示版本号就说明成功。然后在VSCode里安装两个插件:C/C++(官方出的那个)、Code Runner(用来快速运行代码小片段)。安装完插件,再创建一个文件夹作为你的学习目录,用VSCode打开这个文件夹,新建一个hello.c文件,输入一段简单的代码测试:
#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }按下F5或者点击运行按钮,如果一切正常,底部就会显示运行结果。如果你之前配置过但一直报错“无法打开源文件”,先检查两项:一是编译器路径是否正确,二是VSCode是否信任了这个文件夹。新版VSCode对不受信任的文件夹有限制功能,会禁用插件,所以打开外部文件夹时记得选择“信任”。
1.3 虚拟机里配置Ubuntu环境的不同点
再说Ubuntu虚拟机的配置。如果你需要练习makefile、gdb等,建议在Ubuntu或者Linux里学。热搜词里出现“虚拟机(ubuntu)配置c语言环境”,说明大家都在搞这个了。在Ubuntu里配置比Windows反而简单,一条命令就能装好GCC:
sudo apt update sudo apt install gcc装完验证一下:gcc --version。Ubuntu的默认编译器就是GCC,不需要额外配环境变量。在Ubuntu环境下,写C语言代码、用vim或VSCode远程连接开发,操作体验和Windows不太一样。主要的区别在于文件路径、换行符(CRLF和LF的差异)以及makefile的写法。在Windows里写的代码,拿到Linux里编译有时会报错,就是因为换行符不一致。这时候用sed -i 's/\r$//' filename.c或者直接在VSCode右下角切换行尾序列就能解决。
虚拟机还有一个小坑:如果你用的是虚拟机自带的共享文件夹,或者把Windows下的文件夹直接映射到Ubuntu里,文件权限可能出问题,导致gcc无法编译。建议在虚拟机内部创建独立目录,比如~/c_projects,然后把代码放那里。
1.4 makefile和菜鸟教程的搭配使用
热搜词里有个“makefile菜鸟教程”,这个知识点光看菜鸟教程是不够的。makefile解决的问题很简单:当你有一个项目包含多个.c文件时,如果每次编译都把所有文件重新编译,效率很低。makefile可以只编译修改过的文件,然后链接成可执行文件。
我自己的习惯是:基础阶段写小程序时,一个文件直接用gcc编译就够了:
gcc -o hello hello.c等到后面做课程设计、写管理系统项目时,再把代码拆分成多个模块,用makefile管理编译流程。关于makefile,我的经验是初学者不要一开始就死磕它,先把语法学明白,知道它其实就是一个带依赖关系和命令行指令的脚本文件,再用到项目里。等你写了一个超过3个文件的C程序,再回头学makefile,理解程度完全不一样。
2. 数据变量和基础语法:菜鸟教程里容易看漏的细节
2.1 数据变量的定义与分类
C语言中数据变量这个概念,看着简单,但对很多编程刚入门的人来说,容易把“变量定义”和“变量声明”混淆。因为课程教材里有时候没把这个区别讲透,结果在写多文件项目的时候经常报重复定义的错误。通俗地说:
- 定义:给变量分配内存空间。
- 声明:告诉编译器“存在这样一个变量”,但不分配内存。
举个例子:
int a = 10; // 定义 extern int a; // 声明(不分配内存)在单个.cpp或.c文件里,这个区别不明显;但在多文件项目中,如果你在头文件里写int a = 10;,然后被多个.c文件包含,链接阶段就会报错。所以正确的做法是在头文件中写extern int a;,在某个源文件中实际定义并初始化它。
变量分类还有按作用域和生命周期划分的方式:全局变量、静态变量、局部变量。这里面的区别可以在菜鸟教程里看到,但让我茅塞顿开的是理解了static这个关键字。static修饰局部变量时,它的生命周期从栈上的局部变量变成了和程序一样长,但作用域还是只在函数内部;修饰全局变量时,作用域被限制在当前源文件内,外部文件无法访问。
我在学到这里的时候,突然理解了为什么有些代码从C语言角度看起来能用但容易出问题:全局变量泛滥会导致项目难以维护,所以后面课程设计时我尽量用参数传递和结构体来组织数据,很少再用全局变量。
2.2 scanf函数的输入机制
热搜词里有一条特别有意思:“c语言里‘scanf’一定要输入abc吗 而不是可以h1 m1”。这是新手一定会遇到的问题。scanf的输入方式不是固定随意的,它的格式完全由格式控制字符串决定。比如:
scanf("%d%d", &a, &b);这里两个%d之间没有分隔符。那么你输入时可以用空格、Tab、换行把两个数字隔开,但如果你输入的是“1,2”,编译器就会把逗号错误地当作字符处理,导致第二个变量读取失败。反过来,如果你写:
scanf("%d,%d", &a, &b);那么输入时必须带着逗号:1,2,否则同样出错。
这个知识点在菜鸟教程上只是提了一句“按格式输入”,但碰到具体的PTA题目时,很多同学会卡住——题目明明要求输入两个整数,却总是读不到第二个数,其实就是没有理解scanf的格式匹配规则。记住一点:scanf中的普通字符(非格式化字符),必须在输入时原样输入。如果题目输入格式是“1 2”,就写scanf("%d %d", &a, &b);;如果输入是“1,2”,就写scanf("%d,%d", &a, &b);。这里面“%d”是为了提取整数,也用于跳过输入流中的空白字符。
还有一个实用的调试技巧:如果scanf读不到数据,可以用getchar()或查询返回值来判断。scanf返回成功读入的参数个数,如果返回0,说明输入格式不匹配,检查你的格式控制字符串吧。
2.3 数据类型范围和limits.h头文件的使用
C语言里的基本数据类型:int、float、double、char,这些大家都学过。但它们能表示的数据范围,很多人没有注意过。比如int在不同平台上占用的字节数不同——在常见的32位或64位系统中int通常是4字节,但理论上并规定不能小于16位。这个bug在大学时代坑过不少人。
所以我在学“计算5x5鞍点问题”这类题目时,特别注意了数据类型的范围。鞍点问题是这样的:在一个5x5的矩阵中,找到这样的位置——它是所在行的最大值、所在列的最小值——并输出它的位置和值。如果直接用int类型保存数值,需要确保题目给的数在int范围内;如果数值很大,就要改用long long或者unsigned类型。
下面是解决鞍点问题的一个实现,这段代码在网上搜过,也自己调过,基本能跑通:
#include <stdio.h> #include <limits.h> int main() { int matrix[5][5]; int row_max[5], col_min[5]; for (int i = 0; i < 5; i++) { row_max[i] = INT_MIN; for (int j = 0; j < 5; j++) { scanf("%d", &matrix[i][j]); if (matrix[i][j] > row_max[i]) row_max[i] = matrix[i][j]; } } for (int j = 0; j < 5; j++) { col_min[j] = INT_MAX; for (int i = 0; i < 5; i++) { if (matrix[i][j] < col_min[j]) col_min[j] = matrix[i][j]; } } int found = 0; for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { if (matrix[i][j] == row_max[i] && matrix[i][j] == col_min[j]) { printf("%d %d %d\n", i, j, matrix[i][j]); found = 1; } } } if (!found) printf("not found\n"); return 0; }这里用到limits.h里的INT_MAX和INT_MIN来初始化最大值、最小值,而不用猜数据范围,这是做这类题的常用技巧。刚开始学的时候,很多同学的写法是直接把row_max初始化为矩阵第一个元素,然后循环比较,这样的代码出错的概率高,因为边界条件容易写错。用INT_MIN和INT_MAX是更保险的写法。
2.4 字符串函数和常见操作
C语言字符串处理也是让新手比较头疼的领域。热搜词里“字符串逆序c语言pta”正好是PTA上的一道经典题。C语言的字符串本质上是以空字符('\0')结尾的字符数组。所以字符串的长度比char数组的长度至少小1。
常用的字符串函数,比如strlen、strcpy、strcmp、strcat,都在string.h头文件里。这里有几个很容易踩的坑:
strlen返回的长度不含'\0',如果你用来分配空间,要记得+1。strcpy不会自动检查目标数组空间是否够大,如果目标空间不足会缓冲区溢出。strcmp比较的是字符的ASCII码值,而不是字符串的长度。
字符串逆序这道题,能体现出指针和数组的关系。下面是PTA字符串逆序常见的一个解法:
#include <stdio.h> #include <string.h> int main() { char str[81]; gets(str); // 注意PTA平台上gets有时不可用,建议用fgets fgets(str, 80, stdin); str[strcspn(str, "\n")] = 0; // 去掉换行符 int len = strlen(str); for (int i = len - 1; i >= 0; i--) { printf("%c", str[i]); } return 0; }注意,现在PTA上一些题目不支持gets,因为它不安全,推荐用fgets。如果用fgets,字符串末尾会带上换行符,需要手动去掉,上面代码里用strcspn(str, "\n")找到换行符位置然后置零。这一类细节很多教程没有指出,导致代码在自己的编辑器里跑得好好的,一到平台就判错。
2.5 九九乘法表和冒泡排序的实际练习
作为一个C语言新手,九九乘法表和冒泡排序几乎是必做的练习。九九乘法表主要练习循环的嵌套和格式控制:
#include <stdio.h> int main() { for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { printf("%d*%d=%-4d", j, i, i * j); } printf("\n"); } return 0; }这里的%-4d是左对齐并占用4个宽度,让输出看起来整齐。如果写成%4d,则是右对齐,表会往右边偏。这种细节,自己有输出经验了才发现。
冒泡排序则是理解数组和交换算法的基础。冒泡排序原理很简单:相邻两个元素比较,如果顺序不对就交换,每轮从头到尾走一遍,把最大值冒泡到最后。下面是常见写法:
#include <stdio.h> void bubble_sort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int nums[] = {5, 1, 4, 2, 8}; int n = sizeof(nums) / sizeof(nums[0]); bubble_sort(nums, n); for (int i = 0; i < n; i++) printf("%d ", nums[i]); return 0; }冒泡排序的细节在于:内层循环的j范围会随着i增大而减小,因为每一轮结束,最大的元素已经被放到末尾了,没必要再比较。很多同学第一次写冒泡排序会把内层循环写成j < n-1,这样不会错,但多做了很多无用比较。我后期开始关注算法效率之后,会计算一下时间复杂度:冒泡是O(n²),这个复杂度在数据量小时无所谓,但上了10000连个元素就会开始明显变慢。所以这类排序,学思路可以,实际项目里用得不多,但它是理解更复杂排序算法的基础。
3. 指针和内存管理:跨越C语言的门槛
3.1 指针的本质:地址的抽象
很多初学者被C语言劝退,很大程度上是因为指针那章。我一开始也很迷惑:指针到底是个什么东西?后来我自己把指针理解为“存放地址的一种变量,就像房间的门牌号”。你只要知道门牌号,就能找到这个房间;同理,你只要拿到某个变量的地址,就能通过解引用访问它的值。
指针的声明用int *p,表示p是一个指向int类型变量的指针;赋值用p = &a,把a的地址交给p;访问值用*p。这个逻辑里,&是取地址运算符,*在声明里是指针标志,在使用时是解引用标志,两者容易混淆。我自己也搞混了很久,后来记住一个口诀:“声明星号着类型,使用星号取内容”。
指针和数组的关系是自然的:数组名本身就是一个常量指针,指向数组第一个元素。因此,arr[i]实际上等价于*(arr + i)。理解这一点,后面的字符串操作和函数传参就顺了。
3.2 指针在项目里的实际应用
指针不只是概念,在项目里是真正能提高效率的工具。比如写一个网吧计费管理小程序(热搜词里有“c语言网吧计费管理小项目”),要管理多个客户的上网信息,用结构体数组可以存数据,但当你需要按用户名查找用户、删除某个用户时,用指针操作链表会比数组高效很多。我当时做课程设计时写了一个简易学生管理系统,就用了结构体数组和指针,学完指针后才发现,如果用链表实现插入、删除、查找,效率和灵活度完全不是一个档次。
指针还有一个高频用法:函数参数传指针,可以修改外部变量。C语言函数传参默认是值传递,函数内部修改形参,并不会影响实参。想让函数内部修改外部变量,就要传地址(指针)。比如经典的交换函数:
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; }调用时写swap(&x, &y);。这个用法随处可见,PTA练习题里大量出现。如果你在写代码时发现函数内部改了值,但主函数里的变量还是原值,基本就是忘了传地址。
3.3 动态内存管理:malloc和free的正确姿势
热搜词里“c语言内存管理”是很多人的痛点。C语言的动态内存分配主要靠malloc、calloc、realloc和free。malloc分配的内存位于堆区,不自动初始化,所以分配之后要么手动赋值,要么用memset清零;calloc会初始化为0;realloc用来调整已经分配的内存大小。
这里有个必须养成的习惯:malloc之后一定要检查返回值是否为NULL,尤其是申请大块内存时。如果申请失败,程序就得不到有效的内存,后面访问就会出现问题。正确写法应该是:
int *arr = (int *)malloc(n * sizeof(int)); if (arr == NULL) { printf("Memory allocation failed\n"); exit(1); } // 使用完释放 free(arr);另外,free之后最好把指针置为NULL,因为free只是释放了内存,指针的地址值不会改变,但这段内存已经不可用。如果后续再次free同一个指针,就会出现double free的运行时错误;但如果free后马上把指针置NULL,第二次free(NULL)是安全的。这个习惯能帮你避免很多莫名其妙的崩溃。
我在这块踩过一个真实的坑:写一个数据处理的函数时,用malloc分配了内存,但在某个分支里提前return,忘了free,导致内存泄漏。程序跑了很久之后,内存占用越来越高。排查了半天才发现,在函数的每个出口都要记得释放内存。这是一个很好的教训:凡是malloc的内存,必须保证程序的每一条执行路径都会走到free。
3.4 原子操作和更底层话题的延伸
学到一定阶段,会看到一些比较底层的概念。热搜词里有“c语言原子操作”,这是在多线程编程中用来保证线程安全的概念。C11标准提供了atomic_int这类原子类型,操作它们不会被线程调度打断。不过作为C语言初学者,这个知识点可能用不太上,我建议先把基础打牢,把内存管理、指针、文件操作这些思路理顺,再去看多线程和原子操作的话题会更好。
3.5 cython是不是比单纯的C语言更高效?
热搜词里“cython 是不是比单纯的c语言更高效?”这个问题很有意思。cython是Python语言的一个超集,允许在Python代码中声明C语言类型,然后编译成C扩展模块。它的优势是让Python代码在关键路径上更接近C语言的执行效率,但cython代码本身不是纯粹的C语言。所以严格来说,cython比纯Python快,但不会比手写的C语言更快——因为cython生成的C代码可能会多出一层包装。如果你是C语言新手,暂时不用纠结这个问题,等以后接触Python、需要性能加速时再了解cython的生成流程即可。
4. 调试和常见报错:从崩溃到精通的关键训练
4.1 编译报错的常见原因和处理思路
C语言编译器报错信息对新手很不友好,满屏的英文让人头疼。但编译报错其实是很有价值的提示,只要学会读取,大部分问题能自己解决。常见的报错类型:
- 语法错误:比如少写了分号、括号不匹配。这类报错会提示在第几行,直接去那附近找。
- 未声明的标识符:说明这个变量或用到的函数没有定义。可能是忘记声明,也可能是include的头文件不对。
- 未定义引用:链接错误,一般是多文件项目中某个函数实现了但没参与编译,或者函数名拼写不一致。
- 段错误(Segmentation fault):运行时崩溃,最典型的原因是指针没有初始化就使用,或者数组越界。
我在调试“无法打开源文件”这类问题的时候,总结的经验是:先看错误是编译阶段还是链接阶段。报错名前有fatal error一般是编译问题;undefined reference一般是链接问题。编译问题看代码语法,链接问题看工程配置和文件有没有被编译进项目,两个方向判断对了,排查就快多了。
4.2 VSCode里的断点调试技巧
以前调试C代码,我习惯在代码里到处加printf打印中间变量。这个方法在小程序里凑合能用,但项目庞大了,每次都重新编译、编辑代码、删除调试语句,效率太低。后来学会使用VSCode断点调试,发现调试效率质的提升。
用VSCode调试C语言,需要先配置好launch.json,让调试器使用gdb。配置好之后,在行号旁边单击就能设置断点,程序运行到这个位置时会暂停,你可以逐个观察变量的变化。这一步比printf打印更快定位问题,因为你不用改代码,只需要在怀疑的地方打一个断点,然后单步执行。
GDB是命令行版的调试器,在Ubuntu下还可以用VSCode Remote SSH连上服务器调试,或者用gdb直接调试命令行程序。GDB常用命令不多:break设置断点、run运行、next单步执行、print打印变量值、continue继续执行、quit退出。不要被GDB的界面的复杂度吓到,你只需要记住这几个命令就能解决大部分调试问题。当年我看“翁恺C语言练习题”和浙江大学C语言编程题时,最难的就是怎么处理那些隐藏的边界错误。后来发现用GDB单步跟踪一遍,问题基本无所遁形。
4.3 fscanf和fprintf在文件处理中的陷阱
文件操作是C语言的新手分水岭。热搜词里“c语言fscanf和fprintf函数”就是这一节的内容。fscanf和fprintf是用于文件读写的格式化函数,和scanf/printf功能一样,只是需要指定文件流:
FILE *fp = fopen("data.txt", "r"); if (fp == NULL) { printf("Failed to open file\n"); return 1; } int a; while (fscanf(fp, "%d", &a) != EOF) { // 处理读取到的数据 } fclose(fp);fscanf返回值为EOF表示读到了文件结束,这个判断条件很常用。如果你在循环里读取数据,判断条件写成!feof(fp),那很有可能多读一次,因为EOF标志是读操作之后才被设置的。这是一个非常经典的新手错误。我自己也踩过:用while (!feof(fp))循环读取时,最后一行数据总是被重复输出。后来改成先fscanf判断返回值,问题就解决了。
5. 实战题目和进阶思考:从PTA到课程设计
5.1 PAT乙级1037题“在霍格沃茨找零钱”
热搜词里“PAT(乙级)1037在霍格沃茨找零钱(C语言)”这道题,是好多准备计算机考试的人会遇到的题。题目背景是《哈利·波特》里的货币系统:1个加隆等于17个西可,1个西可等于29个纳特。给你应付的钱和实付的钱,要求计算应找的零钱。这道题的核心思路是把复杂的货币换算成最小单位纳特,统一计算后再换算回来。
我的思路是:先把输入的钱转换成纳特,做差,再转换回加隆、西可、纳特。这里有一个细节:如果实付的钱小于应付的钱,要找负数。这一步很多同学会在符号处理上卡住。代码如下:
#include <stdio.h> int main() { int g1, s1, k1, g2, s2, k2; scanf("%d.%d.%d %d.%d.%d", &g1, &s1, &k1, &g2, &s2, &k2); int total1 = g1 * 17 * 29 + s1 * 29 + k1; int total2 = g2 * 17 * 29 + s2 * 29 + k2; int diff = total2 - total1; if (diff < 0) { printf("-"); diff = -diff; } int g = diff / (17 * 29); int s = (diff % (17 * 29)) / 29; int k = diff % 29; printf("%d.%d.%d\n", g, s, k); return 0; }这道题看起来简单,但能考出你对整数除法和取余的理解。把复合货币单位拆成最小单位再计算,是处理这类问题的通用思路,类似的还有时间换算(时分秒)、金额换算(元角分)。
5.2 其他常用算法和C语言进阶方向
热搜词里还有“c语言select解析”。select不是C语言语法,而是操作系统提供的一个I/O多路复用系统调用,在Linux网络编程中经常用到。如果你学习完基础C语言之后,想往网络编程方向发展,会接触到select、poll和epoll这些机制。这个方向不是入门阶段的内容,先把前面的指针、内存管理、文件操作这些基础打牢再说。
还有“c语言课程设计”、“虚拟存储器管理c语言”这些热搜词,说明很多人学完C语言之后,要做操作系统相关的课程设计。用C语言模拟虚拟存储器管理,一般会用到指针和结构体数组,还要理解页式存储、LRU页面置换算法等概念。这个属于综合项目的难度,但区分度恰恰是在基础知识是否扎实。
我自己做课程设计时的经验是:先把项目拆成模块,每个模块用单独的函数完成,尽量少用全局变量,数据用结构体组织。即使最后完成度不高,整个实现的思路是清晰的,也方便排查问题。写代码能力很大程度是由“你能把大问题拆成小问题的能力”决定的。
5.3 关于学习路径的反思和计划
我学C语言的最大感受是:不能只看教程,不动手写代码。菜鸟教程上的每个示例,我几乎都亲手敲过一遍,哪怕只是加了一个printf,你也会发现和看的感受完全不同。比如printf格式化输出的各种转换说明,在教程上扫一眼根本记不住,但是当你实际要输出一个右对齐的表格时,你才会真正理解%-4d和%4d的区别。
还有一件事很值得做:拿PTA或浙大的练习题来检验自己。我自己最近在刷“浙江大学C语言基础编程题目”,题目难度有梯度,每个题目都有隐藏的边界条件。刷题的过程会逼着你考虑更全面的情况,比如输入可能包含空行、在数组越界处不能访问等。如果只是跟着教程走,很难意识到这些细节。
后续的学习计划,我打算分这么几个阶段:
- 第一遍先把菜鸟教程的基础语法过完,重点是数组、函数、结构体和文件操作。
- 第二遍结合PTA和PAT题目刷题,用刷题检验知识点的掌握程度。
- 第三遍做一个综合性小项目,比如学生管理系统、网吧计费管理软件之类,把学到的知识点串起来。
- 最后如果有余力,再往数据结构和算法方向延伸一下,比如用C语言实现链表、栈、队列。
另外,很多人推荐的谭浩强《C程序设计》第六版,我自己也翻过。书里的例题确实经典,但代码风格偏老旧,比如大量使用void main而非int main。在实际工程中,int main才是规范写法。如果你是初学者,跟着教材学没问题,但要留意书上代码风格和新标准的区别,比如教材里常用的gets()在新版本里已经不推荐,应该用fgets()替代。
在N皇后的热潮背后,是算法和数据结构的功底,所以C语言只是起点,但不能停在起点。我希望通过持续更新这篇笔记,记录自己从菜鸟到能独立写项目的全过程。每次学到新的内容,就更新一篇学习记录。这篇笔记今天先更新到这里,后续会持续补充。
我在学习中发现一个小技巧,分享给我的读者:如果你在练习时总是遇到同样的报错,可以把报错信息原文复制到搜索框里搜索,多看一眼别人的解决方案,再对比自己的代码。这些解决方案不仅会告诉你哪里错了,有时还会顺带给你一些好用的思路。但别整段复制答案——最后你发现,动手写一遍并且弄懂每一行的含义,比抄十遍都记住得牢固。