1. 函数参数到底是什么先放下语法想清楚它是为了解决什么问题很多刚开始学C语言的朋友卡在函数参数这一节的时候其实不是语法看不懂而是压根没搞明白“参数”这个东西存在的意义。语法层面无非就是括号里写几个变量名调用的时候把数据传进去这不难。难的是你想不通为什么要这么绕一圈直接写在一起不就行了我用一个生活场景来解释。你去餐厅吃饭不会直接冲进后厨抢过锅铲自己炒菜而是告诉服务员“我要一盘番茄炒蛋”。这里“番茄炒蛋”就是参数后厨就是函数炒菜的过程就是函数内部执行的操作。你不需要关心后厨怎么洗菜、怎么切菜、火候多大你只负责把需求告诉它它就给你端出结果。这个“把需求告诉它”的动作在C语言里就是传参。函数参数解决了三个核心问题代码复用有一段逻辑你要用十次每次只换输入数据没必要写十遍代码写一个函数调用十次每次传入不同的值就行。逻辑隔离把复杂的业务拆成一块一块的独立模块每个模块只通过参数和返回值与外部打交道。出了问题定位到具体函数不用把整个程序从头看到尾。协作分工你写好一个函数别人只用看函数原型就知道怎么调用不需要理解内部实现。这就是为什么头文件里通常只放声明不放实现。再往深一层说理解函数参数的关键在于搞清楚一个底层事实C语言函数参数传递的本质是值传递。你传进去的是变量的值不是变量本身。哪怕你在函数内部把参数改了外面的变量也纹丝不动。有个经典例子值得亲手敲一遍#include stdio.h void swap(int a, int b) { int temp a; a b; b temp; printf(swap函数内部a %d, b %d\n, a, b); } int main() { int x 10, y 20; swap(x, y); printf(main函数里x %d, y %d\n, x, y); return 0; }运行结果会让你印象深刻swap内部确实交换了但main里的x和y根本没变。这是初学者最容易踩的坑没有之一。至于怎么才能真的交换后面讲指针和地址传递的时候再展开现在先记住结论形参是实参的拷贝二者住在不同的内存格子里面。当你把这个底层模型吃透了后面学指针、学数组传参、学结构体传参都是一条路走下来的不会觉得知识点是散的。2. main函数参数不只是“int main()”这么简单搜main函数参数的人不少说明很多人注意到了这个细节但一直不求甚解。教科书上写int main()编译器也能过但事实上C语言标准允许main函数带参数完整的签名是这样的int main(int argc, char *argv[])argc全称是argument count意思是在命令行里传入了多少个参数argv全称是argument vector是一个字符串数组存储了每个参数的具体内容。argv[0]有特殊含义它保存的是程序自身的名字。这一点很多教程一笔带过但它非常有用。举个实际场景。假设你写了一个素数判断程序希望在命令行里直接指定要判断的范围比如prime 100 200这个命令的意思是判断100到200之间有哪些素数。在程序内部argc就等于3argv[0]是primeargv[1]是100argv[2]是200。你需要做的是先检查argc符不符合预期再用atoi这样的函数把字符串转成整数然后再进入业务逻辑。#include stdio.h #include stdlib.h int main(int argc, char *argv[]) { if (argc ! 3) { printf(用法%s 起始值 结束值\n, argv[0]); return 1; } int start atoi(argv[1]); int end atoi(argv[2]); printf(范围%d 到 %d\n, start, end); return 0; }这个技能在真实开发里太常用了。命令行工具的配置项、脚本传入的参数、开发阶段调试用的临时开关全靠main函数参数来驱动。你去看很多开源项目的代码入口函数的参数处理逻辑能占几十上百行一点都不夸张。我个人的建议是初学阶段不需要把main函数参数的语法抠得多深但一定要知道有这回事并且亲手跑一次带参数的版本。因为从“只知道int main()”到“理解argc和argv”是你对“程序与外部环境如何交互”这个问题的第一次真正理解这个观念转变比记住几个语法细节重要得多。测试的时候有个小技巧如果在Visual Studio这类IDE里运行你需要到“项目属性-调试-命令参数”里填参数如果在VS Code里可以在launch.json的args字段里配置如果直接在终端里编译运行那就简单了程序名后面直接跟参数就行。我个人更推荐初学者用终端练这个功能因为你能亲眼看到argv[0]到底是什么比IDE里藏起来的效果直观很多。3. 100-200之间素数求解从暴力循环到边界优化素数这个练习题出现频率高到几乎每个C语言教程都绕不过去但恰恰因为太常见大部分人都是机械地抄代码从来没有想过代码为什么这么写也没有想过还有哪些更好的写法。先明确素数的定义只能被1和它本身整除的大于1的整数。2、3、5、7、11这些都是4、6、8、9这些不是。要判断一个数n是不是素数最朴素的想法是什么从2一直试到n-1看看中间有没有能整除n的数。这个思路没问题逻辑是对的但效率不高。原因很清楚如果n本身很大比如9973那你要做九千多次除法运算太浪费了。这里需要引入一个关键的数学观察如果n有一个大于√n的因子那么n必定同时有一个小于√n的因子。因为因子是成对出现的一对因子里至少有一个不超过√n。所以判断素数只需要从2试到√n就够了。这就是判断素数代码里为什么经常出现i * i n或者sqrt(n)的原因。我见过很多初学者不理解这一行为什么正确只是教学先写什么就抄什么这不叫学会叫复读。还有一类优化值得掌握除了2之外所有偶数都不可能是素数。所以在遍历100-200这个区间的时候完全可以跳过所有偶数或者换个说法100到200之间根本就没有偶数素数直接奇数判断就行。基于这些分析100-200之间素数的求解可以拆成两层结构第一层外部遍历100到200。这里可以skip掉偶数只对奇数调用判断函数。这样调用次数直接从101次减到50次。第二层单个数字的素数判断。这里用i * i n做边界从3开始按奇数试除进一步把循环次数压缩一半。给一个可以运行的完整版本#include stdio.h int isPrime(int n) { if (n 1) return 0; if (n 2) return 1; if (n % 2 0) return 0; for (int i 3; i * i n; i 2) { if (n % i 0) { return 0; } } return 1; } int main() { int count 0; printf(100到200之间的素数\n); for (int n 101; n 200; n 2) { if (isPrime(n)) { printf(%d , n); count; } } printf(\n共 %d 个\n, count); return 0; }运行这段代码你就能看到100到200之间一共有21个素数。这里插一句代码里count变量是初学者容易忽略的细节——不要觉得它多余很多题目要求你输出个数也很多场景需要你统计结果数量这是基本功。你自己上机练习的时候建议做三个动作对比先用从2到n-1的写法感受一下“笨办法”的循环到底跑了多少次再用从2到√n的写法对比一下耗时和代码量最后用奇数跳步的写法感受一下数学优化带来的简洁感。这三个步骤走下来你对这个题目的理解才算是完整的不是背下来的是推导出来的。4. 这道练习题暗含的设计思维参数怎么传才合理很多人做“100-200之间素数”这道题关注点全在素数判断逻辑上很少有人会停下来思考一个问题那个判断素数的函数参数到底应该怎么设计才能让代码更清晰、更可维护这个思考练习其实才是这道题真正的进阶价值。我们来看看同一个功能参数设计不同会带来多大的差异。版本一没有参数。#include stdio.h void printPrime100To200() { int count 0; for (int n 101; n 200; n 2) { int isPrime 1; for (int i 3; i * i n; i 2) { if (n % i 0) { isPrime 0; break; } } if (isPrime) { printf(%d , n); count; } } printf(\n共 %d 个\n, count); } int main() { printPrime100To200(); return 0; }这个版本把判断逻辑和输出逻辑全揉在一个函数里看起来也能运行但问题是如果哪天需求变了要判断200到300之间的素数你就得改函数内部的循环范围。如果再哪天要求改成先统计个数再输出你还是得改这个函数。改来改去一个函数干的事越来越多越来越难读。版本二参数是范围起点和终点。#include stdio.h void printPrimeRange(int start, int end) { int count 0; if (start 2) start 2; if (start % 2 0) start; for (int n start; n end; n 2) { if (isPrime(n)) { printf(%d , n); count; } } printf(\n共 %d 个\n, count); }这个版本就好多了你想判断哪个范围就传入哪个范围函数内部完全不需要知道外部需求它只负责“从start到end范围内打印素数”这一件事。调用方想判断50到500就传(50, 500)想判断1到1000就传(1, 1000)。这就是参数带给你的第一个好处灵活性。版本三参数是被判断的那个数返回值是判断结果。int isPrime(int n) { if (n 1) return 0; if (n 2) return 1; if (n % 2 0) return 0; for (int i 3; i * i n; i 2) { if (n % i 0) return 0; } return 1; }这个版本把“判断”和“输出”完全分开isPrime函数只回答一个问题这个数是不是素数是就返回1不是就返回0。至于拿到这个结果后是打印、计数、还是做别的isPrime一概不管。这个函数以后可以在任何地方复用没人关心它是在哪个项目里写的。这才是函数参数和返回值设计最漂亮的地方接口清晰内部实现随便改只要接口不变调用方就不用动。从版本一到版本三你看到的不仅是代码越来越简洁更是一种设计思维的递进函数职责单一数据通过参数流入结果通过返回值流出函数之间不共享全局变量不互相干扰。这是工程化代码的第一课很多工作了几年的人回头重构代码做的其实就是把版本一改成版本三这件看似简单的事。5. 为什么素数练习能一路用到排序参数思维是通用的搜冒泡排序c语言的朋友不少我猜很多人学到排序的时候又开始头疼了。但如果上面的素数练习你已经亲手写过了也理解了函数参数的设计逻辑排序其实是一件顺理成章的事。以最简单的冒泡排序为例按我们刚才的思路来拆解输入是什么一个数组以及数组元素的个数。输出是什么排序后的数组形式上可以理解为“在函数内部修改原数组”。这个函数能不能复用当然可以以后你不管排什么数组只要元素类型是int都可以调同一个函数。冒泡排序的C语言实现大概是这样的void bubbleSort(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; } } } }和素数练习对照着看你会发现参数设计的规律是一样的把变化的东西作为参数传进来把稳定的逻辑写在函数里。素数练习里变化的是“要判断的数字”排序里变化的是“要排序的数组和它的长度”。函数就是一个带参数的模板参数往里面一填就有了针对具体场景的执行结果。这里有一个初学者容易忽略的细节数组作为函数参数的时候实际传递的不是整个数组的拷贝而是数组首元素的地址。所以在bubbleSort函数内部直接对arr数组元素做修改main函数里的数组会跟着变。这个行为和前面的值传递看起来不一样但其实并不矛盾。值传递的是“变量的值”而数组名本身是一个地址值把首地址传进去函数就能顺着地址去修改真正数组元素的内容。等学到指针章节这个现象会从“知道是这样”变成“理解为什么这样”到那时候你对C语言的认知就进入新阶段了。我建议你把素数判断和冒泡排序放在同一个程序里练一次。写一个main函数定义一个数组把100到200之间求出来的素数存进去然后用bubbleSort排序再打印出来。这个练习同时涉及函数参数、数组、循环、条件判断和模块化思维一环扣一环做完你会觉得前面学的知识点全部被串起来了。6. 常用代码编辑环境下的调试建议与常见编译问题我在帮初学者解决问题的过程中发现几个特别典型的问题几乎人人都遇到过。趁这个机会汇总一下你提前有个心理准备以后真遇到了就不至于卡半天不知道搜什么关键词。问题一函数声明缺失。你写了这样一个程序#include stdio.h int main() { int result isPrime(17); printf(%d\n, result); return 0; } int isPrime(int n) { // 具体实现 }如果在isPrime这个函数定义之前、main函数里就调用它了有些编译器会直接报一个警告或错误提示“implicit declaration of function isPrime”。这是因为C语言要求函数先声明后使用而你的定义写在后面了。解决办法有两个要么把isPrime的定义挪到main前面要么在文件开头加一个函数原型声明。#include stdio.h int isPrime(int n); // 函数原型声明告诉编译器有这个函数存在 int main() { int result isPrime(17); printf(%d\n, result); return 0; } int isPrime(int n) { // 具体实现 }我个人的习惯是在头文件里集中放函数原型声明源文件里放具体实现。这是一个很好的工程习惯多人协作的时候别人只看头文件就能知道你提供了哪些接口。问题二scanf缓冲区残留导致循环只执行一次。这是初学者写交互式程序的经典坑。比如你想循环读入多个数判断素数写了int n; while (1) { printf(请输入一个数字); scanf(%d, n); if (isPrime(n)) { printf(%d是素数\n, n); } else { printf(%d不是素数\n, n); } }看起来逻辑没问题但当你输入一个字母或者带换行符时scanf可能会把缓冲区的换行符残留下来导致下一次进入循环的时候scanf直接读取残留内容而跳过输入。表现就是程序像卡死了一样或者自动跳过输入。这个问题初学时不需要解决得多完美但至少要有一个意识scanf并不是万能的它最怕的就是用户输入的内容格式跟预期不符一旦匹配失败缓冲区就会留下脏数据直接影响后续的读取。想要练好自己的基本功后期可以去学fgets加sscanf的组合那套才是处理用户输入的正规军。问题三用sqrt判断素数时忘记包含math.h。如果你采用sqrt(n)作为循环边界要记得加#include math.h且在编译时链接数学库。如果用gcc命令是gcc prime.c -lm -o prime其中-lm就是链接libm数学库。很多人栽在这个地方提醒一下。问题四VS Code运行C语言程序的环境配置。很多人用VS Code写C语言配置过程倒是没什么问题但运行的时候经常遇到中文乱码。这个问题的根源通常是源文件编码和终端编码不一致一般把文件编码改UTF-8、终端代码页改成65001或者统一用GBK就能解决。我个人的建议是新手阶段直接先在VS Code里配好编译运行环境能跑通就行等熟悉了编译原理再考虑折腾别的。7. 最适合用来练手的一道综合题把素数、函数参数和数组一起打通前面说了很多单独的知识点最后给你留一个我认为特别适合新手练手的综合题。它的代码量不大但覆盖了函数参数、数组、循环、条件判断、函数设计这几个核心知识点做完之后你的C语言初阶阶段就算有了一个比较好的句号。题目如下写一个程序接收用户输入一个正整数n输出从2到n之间所有的素数并将这些素数存放在一个数组里然后对这个数组做冒泡排序最后在控制台输出排序后的结果。这道题强制要求你至少写三个函数判断一个数是不是素数的函数int isPrime(int n)冒泡排序函数void bubbleSort(int arr[], int len)可以自己定义主函数int main()做主流程编排。你写的时候想一想这几个问题isPrime的参数是什么返回值表示什么bubbleSort的参数是什么为什么数组传参之后在函数内部修改数组外部数组也变了main函数怎么调用这些函数你需要在main里控制循环把素数存入数组还是让isPrime只管判断就够了等你把这个程序写完了再回来看本文开头那个“swap函数为什么交换不了”的问题你会有新的理解。不是说你现在就能解决它而是你会意识到函数参数传递需要区分传递的是值还是地址这个知识缺了一环后面指针章节补上即可。所以不要怕暂时不理解带着这些问题去学下一章效果会好很多。我自己也是从这条路走过来的当时写这个综合题卡在“为什么数组传进函数后能被修改”这个问题上纠结了整整两个晚上。后来学到指针才明白了原来数组名本质就是地址传数组等于传地址不像传int变量那样是传值。这个顿悟的过程就是C语言学习最迷人也最磨人的地方。你现在写的每一行代码、踩的每一个坑都是为了后面某一个时刻的“原来如此”做铺垫。