
说起来挺不好意思的我接触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反而简单一条命令就能装好GCCsudo 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会初始化为0realloc用来调整已经分配的内存大小。这里有个必须养成的习惯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语言只是起点但不能停在起点。我希望通过持续更新这篇笔记记录自己从菜鸟到能独立写项目的全过程。每次学到新的内容就更新一篇学习记录。这篇笔记今天先更新到这里后续会持续补充。我在学习中发现一个小技巧分享给我的读者如果你在练习时总是遇到同样的报错可以把报错信息原文复制到搜索框里搜索多看一眼别人的解决方案再对比自己的代码。这些解决方案不仅会告诉你哪里错了有时还会顺带给你一些好用的思路。但别整段复制答案——最后你发现动手写一遍并且弄懂每一行的含义比抄十遍都记住得牢固。