C++函数与数组字符串核心解析:从参数传送到递归排序

发布时间:2026/10/4 7:21:24
C++函数与数组字符串核心解析:从参数传送到递归排序 这段时间帮几个学弟学妹看CMOOC的课后作业发现大家集中卡在了第五讲和第六讲。一问才知道基本上都是被函数参数传递、数组当函数参数、字符串初始化这些绕得头晕再加上有些题给的是“参考答案”但没解释为什么抄了代码还是不会举一反三。这篇文章就把这两讲的重点拆开揉碎讲清楚作业题的实际代码我也挑典型的重写了一遍每一步都配了注释和易错点说明适合正在跟MOOC进度、或者复习准备考试的同学对照着看。先说清楚这两讲到底在讲什么。第五讲核心是函数包括函数的定义与调用、参数传递、函数重载、递归这些第六讲核心是数组与字符串包括一维数组、二维数组、字符数组与字符串的基本操作以及跟函数结合时的各种坑。这两讲在整门课里处于“从语法走向结构”的转折点——前面你写的代码基本是顺序结构最多加个循环到函数这块开始要求你会把一个完整程序拆成若干功能模块这其实就是工程化思维的雏形。而数组和字符串又是最常用的数据组织形式。可以说这两讲学不扎实指针、结构体、链表这些后面全都会受影响。1. 内容整体设计与思路拆解1.1 为什么第五讲是“分水岭”很多同学在学到函数之前写代码就是一段main打完收工变量从上到下一路声明逻辑长了以后自己都看不出层次感。函数这一讲之所以关键是因为它强制你把“做什么”和“怎么做”分开调用方只需要关心函数提供了什么能力不关心内部怎么实现被调函数只需要关心输入是什么、输出是什么不关心调用方是谁。这个“职责分离”的思路到了后面做类、做模块、做项目都绕不开。同济MOOC第五讲的课后题通常会有几类典型题编写函数求两个数中的最大值、最小公倍数、最大公约数这类纯计算题使用函数实现数组元素的排序、查找函数重载的辨析题让你判断调用的是哪一个函数版本递归函数比如用递归求阶乘、斐波那契数列。我见过很多同学做这些题时函数是写出来了但完全没有“接口设计”的意识把所有逻辑都塞进一个函数里参数一大堆返回值也不统一。这样做作业能过但后面做综合实验就吃力了。所以我在下面给的参考答案里刻意把“这个函数负责什么、参数是什么、返回什么”都标注出来。1.2 第六讲的本质连续内存的操作第六讲数组本质上是教你怎么在同一段连续内存里高效存取一批相同类型的数据。数字数组就是一批数排成一列字符串就是一批字符排成一列并以\0结尾作为结束标记。这个理解到位了后面的下标运算、指针迁移、数组传参就都好说了。字符串这块刚开始容易懵因为C里字符串的操作方式比Python、Java都“原始”得多——它没有一个“天生”的字符串类型虽然现在有std::string但MOOC前几讲还在用字符数组老办法。字符数组必须手动保证结束符\0存在否则一调用strlen、strcpy这种函数就会越界乱读。这一点我会在后面用专门的段落展开。第六讲的课后题常见类型数组逆序、数组元素求和、统计数组中某数的出现次数冒泡排序和选择排序的实现二分查找在有序数组中的实现字符串反转、统计字符串中某个字符的出现次数二维数组的转置、求最大值。这些题几乎把后续程序竞赛、课程设计里最基础的算法操作都覆盖了。所以第六讲不是常规的“背语法”章节而是你算法体感起步的地方。2. 函数部分的核心细节与实操要点2.1 参数传递传值、传引用与传数组的区别我批改作业时看到最多的错误就是“为什么我函数里改了值回到main里变量没变”——这是典型的传值问题。C函数参数默认是值传递也就是函数内部拿到的是实参的一个拷贝你在函数里对这个拷贝做什么都不会影响外部原来的变量。如果想在函数里修改外部变量有两条路传引用int a或者传指针这在后面章节才学所以MOOC早期一般只讲引用。举一个最典型的例子交换两个数的函数// 错误示范传值交换外部变量不会变 void swap_wrong(int a, int b) { int tmp a; a b; b tmp; } // 正确示范传引用交换 void swap_right(int a, int b) { int tmp a; a b; b tmp; }传引用本质上传的是变量的别名函数内部操作a其实就是操作外部那个变量。写参数时看到类型后面带就要反应出“这里可以影响外部变量”。这个理解到位了很多坑都能避开。数组传参就更特别了。数组名作为实参传给函数时C会自动把它当作指向数组首元素的指针来传递。这意味着函数内部操作数组元素会直接影响外部数组不需要带引用符号。例如void fill_array(int arr[], int n) { for (int i 0; i n; i) { arr[i] i * i; } }这里虽然写的是int arr[]但编译器会把它当int* arr处理。所以arr[i]本质上是通过指针访问的改的就是原来的数组。既然数组传参是“共享数据”那排序、修改元素这些操作就不需要返回值了直接在原数组上动手就行。2.2 函数重载的判定规则别只盯着返回值函数重载是第五讲里最容易丢分的概念。判定的核心规则是函数名相同参数列表不同——包括参数的类型、个数、顺序。返回值类型不同不能作为重载的依据。很多同学写重载的时候只改返回值int fun(int a) { return a; } double fun(int a) { return a * 1.0; } // 错误与上一个函数参数列表完全相同编译器会直接报重复定义哪怕返回类型不同也不行。因为函数调用的时候编译器是靠实参的类型和个数来决定调用哪一个版本它根本没法靠“你期望返回什么类型”来确定版本。还有一个常考的细节参数类型转换导致的重载歧义。比如有void fun(int)和void fun(double)两个版本你调用fun(3.14f)时编译器会因为浮点数既可以转成int也可以转成double而拿不定主意。实际中为了避免这种问题重载版本的类型尽量区分得清楚一点也就是数值类型和精度差异尽量别同时作为区分维度。2.3 递归先写终止条件再写递归关系第五讲的递归题目学生最常见的错误是递归没有终止条件或者终止条件的位置不对导致无限递归直到栈溢出。递归函数其实就两个要素递归公式把一个规模大的问题拆成规模小一点的同类型问题和基准情形规模足够小可以直接返回结果不再递归。我来用递归求斐波那契数列举个例子int fib(int n) { // 基准情形n 1 时直接返回 if (n 1) { return n; } // 递归公式 return fib(n - 1) fib(n - 2); }看着很简单但你让它算fib(40)试试会非常慢因为同样的子问题被算了无数次。这就是递归的另一个要点递归不是万能的它逻辑清晰但是性能代价高实际做性能敏感的程序时能用循环尽量用循环或者把递归改成带备忘录的写法。不过MOOC阶段能正确写出递归就已经足够了性能和优化后面课程会再讲。在作业里我还经常看到一个错误有人把fib(0)和fib(1)的返回值写反导致所有结果错位。写递归时一定要先把最小的几个输入亲手算一遍验证基准情形没有问题再去推大一点的输入。3. 数组、字符串与基础算法实操从课后题到万能模板3.1 数组的初始化与传参常见误区第六讲一开始就讲数组初始化。很多同学的问题是不知道什么时候数组元素被自动初始化为0什么时候是随机值。int a[5] {0};——第一个元素明确为0其余元素默认置0int a[5];——这是局部数组元素值不确定一定要先赋值再使用int a[] {1, 2, 3};——省略长度编译器自动算出长度3char s[] hello;——字符串数组长度为6因为末尾自动加了\0。数组作为函数参数时最值得注意的一点是在函数内部用sizeof(arr) / sizeof(arr[0])算数组长度是失效的。因为数组传参时退化成指针sizeof(arr)在函数内得到的是一个指针的大小64位系统下通常是8字节而不是整个数组的大小。所以一定要额外传一个参数表示数组长度比如int sumArray(int arr[], int n)。我改作业见过不下十次有人栽在这个坑上函数内部循环边界算错结果要么漏处理末尾元素要么越界访问了内存里的垃圾值。3.2 冒泡排序和二分查找的“标准答案”思路排序和查找是第六讲的作业的常客。我强烈建议你不要直接背代码而是把每一步都跟“数组下标的变化”对应起来。冒泡排序的核心思想是每一轮把相邻元素两两比较把最大的元素逐步“冒”到末尾。所以外层循环控制“需要执行几轮”最多n-1轮内层循环控制“这一轮比较到哪个下标为止”已经归位的末尾元素不需要再参与比较。#include iostream using namespace std; // 冒泡排序将数组a升序排列 void bubbleSort(int a[], int n) { for (int i 0; i n - 1; i) { // 轮数 bool swapped false; // 优化用本轮是否发生交换 for (int j 0; j n - 1 - i; j) { // 比较范围逐渐缩小 if (a[j] a[j 1]) { int t a[j]; a[j] a[j 1]; a[j 1] t; swapped true; } } if (!swapped) break; // 本轮没有交换说明已经有序提前结束 } } int main() { int a[] {5, 2, 9, 1, 6}; int n 5; bubbleSort(a, n); for (int i 0; i n; i) { cout a[i] ; } return 0; }这里加了一个swapped标志位数组原本就有序的情况下第一轮扫描发现没有任何交换程序可以提前跳出省掉不必要的循环。这是面试里常见的优化思路在MOOC作业里属于加分项。二分查找的前提是数组已经有序思路是每次取中间元素跟目标值比较目标值小就往左半边找大就往右半边找区间长度每次减半。#include iostream using namespace std; // 二分查找在有序数组a中查找target // 返回找到的下标未找到返回 -1 int binarySearch(int a[], int n, int target) { int left 0, right n - 1; while (left right) { int mid left (right - left) / 2; // 防止溢出 if (a[mid] target) { return mid; } else if (a[mid] target) { left mid 1; } else { right mid - 1; } } return -1; } int main() { int a[] {1, 3, 5, 7, 9}; int n 5; cout binarySearch(a, n, 7) endl; // 输出3 cout binarySearch(a, n, 4) endl; // 输出-1 return 0; }注意mid的写法用left (right - left) / 2而不是(left right) / 2。这是老生常谈的防溢出写法虽然作业里测试数据一般不会大到让left right溢出但这个好习惯值得从入门就养成。3.3 字符串数组初始化与常用函数实操第六讲里字符串的题目核心就一句话字符数组必须以\0结尾系统提供的字符串处理函数都依赖这个结束符来判定位数。我给一个最典型的反转字符串的例子#include iostream #include cstring using namespace std; // 反转字符串原地操作 void reverseStr(char s[]) { int len strlen(s); int left 0, right len - 1; while (left right) { char t s[left]; s[left] s[right]; s[right] t; left; right--; } } int main() { char str[] hello; reverseStr(str); cout str endl; // 输出 olleh return 0; }这段代码能跑通前提就是str末尾有\0strlen(s)才能正确得到长度5。如果你自己构造字符数组时忘了加\0那strlen会一直向后找直到在内存中碰巧遇到一个\0得到的结果完全不可控。这个坑太经典了我愿意再说一遍字符数组当字符串用必须手动保证以\0结尾尤其是在一个字符一个字符往数组里填的时候。再补充一个字符串初始化对比表声明方式数组长度内容说明char s1[] hi;3{h,i,\0}最常用自动补结束符char s2[] {h,i};2{h,i}没有结束符不是合法字符串char s3[10] hi;10{h,i,\0,...}剩余位置为\0安全string s4 hi;由实现管理hiC标准库字符串类型功能强大std::string在实际开发中肯定更好用但MOOC阶段考试和作业很多还是用字符数组所以两种方式都得会还得知道它们之间的转换。3.4 第5、6讲课后作业参考答案可直接运行版下面这几段是我根据MOOC常见题型整理出来的完整可运行代码每一段都包含题目、代码和注释。你做作业的时候不要直接抄而是“先看题目自己在编辑器中写一遍卡住了再看这里的实现思路”效果会好得多。题目A编写函数接收一个整数n返回n是否质数。质数的判断思路一个大于1的自然数如果除了1和它本身之外没有其他因数就是质数。我们只需检查从2到sqrt(n)范围内是否存在能整除n的数即可。为什么到sqrt(n)就够因为如果n有因子a那么必有对应的因子b使得a * b na和b不可能都大于sqrt(n)所以只要在小因子范围内检查就行了。#include iostream #include cmath using namespace std; // 判断质数是返回true不是返回false bool isPrime(int n) { if (n 1) return false; if (n 2) return true; // 2是唯一一个是偶数的质数 if (n % 2 0) return false; // 偶数直接排除 for (int i 3; i sqrt(n); i 2) { if (n % i 0) { return false; } } return true; } int main() { int n; cin n; if (isPrime(n)) { cout n is prime endl; } else { cout n is not prime endl; } return 0; }这里用i 2跳过偶数是判断质数的一个常见优化把循环次数减半。代码里先处理n 2的特殊情况再排除偶数思路顺下来很干净。题目B编写函数接收一个int数组和数组长度返回数组元素的最大值。这道题考察数组遍历和归约操作。我的标准写法#include iostream using namespace std; // 求数组最大值 int getMax(int arr[], int n) { int maxVal arr[0]; // 先假设第一个元素是最大值 for (int i 1; i n; i) { if (arr[i] maxVal) { maxVal arr[i]; } } return maxVal; } int main() { int a[] {3, 7, 2, 9, 5}; int n sizeof(a) / sizeof(a[0]); // 在main函数里计算数组长度是ok的 cout getMax(a, n) endl; // 输出9 return 0; }注意main函数里用sizeof(a) / sizeof(a[0])是可行的因为在main函数中数组名还没有退化成指针sizeof(a)是整段数组的字节大小。如果你把这个计算放到getMax函数内部就会出问题。题目C编写一个函数fun(int n)用递归求1到n的和。这个题是递归入门必做思路是sum(n) n sum(n-1)基准情形是sum(1) 1。#include iostream using namespace std; int sum(int n) { if (n 1) { return 1; } return n sum(n - 1); } int main() { cout sum(10) endl; // 输出55 return 0; }写递归时一定要先想清楚基准情形覆盖所有边界输入。如果这道题允许n传入0那么基准情形就要改成if (n 1) return n;才不会死循环。题目D用字符数组实现统计字符串中字母a出现的次数。这个题是字符串遍历的基础。只需要从头到尾扫描字符数组遇到大于0的字符就判断是否为目标字符直到\0出现就停止。#include iostream #include cstring using namespace std; int countChar(const char s[], char c) { int count 0; int n strlen(s); for (int i 0; i n; i) { if (s[i] c) { count; } } return count; } int main() { char s[] banana; cout countChar(s, a) endl; // 输出3 return 0; }注意参数写成了const char s[]这代表函数承诺不会修改数组内容。在只读场景下加const是好习惯能防止自己手滑在函数里写出修改原数组的代码。4. 常见问题与报错排查实录4.1 环境配置问题Visual C Redistributable报错很多同学做MOOC作业用的是Visual Studio在别的电脑上运行自己写的程序时经常遇到一个报错error: Microsoft Visual C 14.0 or greater is required或者是运行程序时提示缺少VCRUNTIME140.dll之类的动态库。先说结论这类报错并不是你的代码写得不对而是目标电脑缺少C运行库。用Visual Studio编译出的程序依赖系统里有匹配版本的Visual C Redistributable可再发行组件包。解决办法有三条方案一在目标电脑上安装对应版本的Visual C Redistributable可以从微软官网下载vc_redist.x64.exe或x86取决于你的程序是64位还是32位方案二把程序编译模式改成“静态链接”/MT这样运行库会被直接编译进exe文件里目标电脑不需要额外安装运行库但exe文件体积会变大方案三如果只在提交作业的场景下直接把整个Visual Studio工程或者源码交给老师/同学让对方在自己的环境下编译运行。我推荐作业提交时优先用方案三因为老师那边环境齐全重新编译一次最省事。如果是要发给没装开发环境的朋友看效果就选方案二省得对方还要折腾运行库。4.2 VS Code配置C/C运行环境的关键点另外现在不少同学不用Visual Studio而是用VS Code写C配置环境的时候也容易卡住。这里我提两个关键点照着做基本能跑起来。第一VS Code本身只是个编辑器不包含编译器。你需要自己安装编译器。Windows上通常装MinGW-w64一个Windows下的GCC移植版装好后把bin目录里面有g.exe添加到系统的PATH环境变量里。然后在VS Code里安装“C/C”扩展它会帮你调用编译器。第二运行程序有两种方式。一种是用扩展自带的“运行”按钮直接跑它会自动调用编译器并生成可执行文件另一种是自己在终端里手动编译先cd到源码目录再执行g main.cpp -o main ./main手动编译的好处是你能看到完整的报错信息方便定位代码问题。VS Code的集成终端偶尔会把编译器路径搞错报g: 无法将“g”项识别为 cmdlet、函数、脚本文件或可运行程序的名称这种基本就是PATH没配好重开一次VS Code让环境变量生效就行。4.3 五六讲代码常见编译错误速查表我把批改作业时最常见的编译和逻辑错误整理成了表格每一条都是真实踩坑记录错误类型典型报错或现象原因解决方案数组下标越界程序运行崩掉或输出异常值循环边界写错访问了a[n]检查循环条件是否写了 n数组最后一个合法下标是n-1忘了加#include‘cout’ was not declared in this scope没包含iostream包含头文件使用std::命名空间字符数组长度不足程序运行崩掉或字符串被截断数组长度没给结束符留位置char s[5] hello改为char s[6]递归没有基准栈溢出程序崩溃递归调用永远不停止检查递归函数最开头是否写了if条件返回数组传参后用sizeof循环次数错误函数内sizeof(arr)得到的是指针大小额外传入数组长度参数重载仅仅返回值不同编译报重复定义重载不依据返回值区分修改参数列表忘记初始化变量运行结果随机局部变量默认值不确定使用前赋初值这个表建议存下来写作业之前先扫一眼很多低级错误能在编译之前就被拦下来。除了这些我还想特别强调一下运行结果不对的时候别急着改代码先在关键位置用cout把中间值打印出来看循环变量走到了哪一步、数组中间状态是什么这样定位问题会比盯着代码脑补快很多。这也是学编程早期最值得养成的调试习惯。5. 延伸应用把函数数组用到小游戏里最后讲一个我觉得特别适合用来复习这两讲的小项目猜数字游戏。它能把随机数、循环、函数封装、数组记录历史输入全串起来。很多人学到数组和函数时觉得“这东西能干嘛”做完这个小游戏就会有直观感受。先看核心代码#include iostream #include cstdlib #include ctime using namespace std; // 生成一个[min, max]范围内的随机整数 int randomInt(int min, int max) { return min rand() % (max - min 1); } // 判断用户猜的数与答案的关系 // 返回1表示猜大了-1表示猜小了0表示猜对 int compareGuess(int guess, int answer) { if (guess answer) return 1; if (guess answer) return -1; return 0; } int main() { srand(time(0)); // 用当前时间作为随机数种子 int target randomInt(1, 100); // 生成1-100的随机数 int guess; int history[20]; // 记录用户的所有猜测 int count 0; cout 我已经想好了一个1到100之间的数字你来猜猜看。 endl; while (true) { cout 输入你的猜测: ; cin guess; history[count] guess; // 记录到数组 count; int result compareGuess(guess, target); if (result 0) { cout 猜对了答案就是 target endl; break; } else if (result 0) { cout 猜大了再试一次。 endl; } else { cout 猜小了再试一次。 endl; } if (count 20) { cout 次数已用尽答案是 target endl; break; } } cout 你一共猜了 count 次历史记录: ; for (int i 0; i count; i) { cout history[i] ; } cout endl; return 0; }这个程序用到的知识点正好覆盖第五、六讲randomInt和compareGuess是两个纯函数各自职责单一后续想加功能直接改函数内部就行history数组记录每次猜测数组下标就是猜测次数配合循环输出历史记录srand(time(0))是随机数初始化的关键忘了写的话每次运行生成的随机序列都相同游戏就没意思了数组history[20]固定的容量20一旦超过就可能越界所以循环里要加上count 20的保护逻辑。你可以在这个基础上扩展把20次上限改成用户可选难度或者增加“最接近数字”功能来提示用户“上一次太热还是太冷”。这些扩展都是对函数和数组的再次练习做完之后五六讲的知识基本就吃透了。根据我自己帮人改作业的经验五六讲学得扎实不扎实就看你能不能独立完成一个“函数拆分数组处理”的小程序。如果一个猜数字游戏你能从头写到尾还能把这个过程的每个函数参数、返回值、数组边界都讲明白那这两讲就过关了。如果还是卡在某些地方建议回去看课本上关于“形参实参”和“数组下标从0开始”的部分再动手把上面代码逐行敲一遍。编程这东西看懂了跟写出来之间差的其实就是那几十次报错和调试的距离。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询