☰
C语言数组与字符串进阶练习:从排序到越界避坑全解析
2026/10/21 16:29:15 网站建设 项目流程

最近带了一批初学者做C语言基础练习,前两轮把变量、数据类型、分支和循环都过了一遍。到了第三轮,我照例把题目难度往上抬了一档,围绕数组、字符串和排序展开。这一练下来,暴露出来的问题比前两轮加起来都多,尤其是数组下标越界、字符串结尾符丢失这类经典毛病。这篇就把这一轮的练习内容、解析思路、踩坑记录完整写出来,给正在学C语言的朋友做个参考。

这轮练习一共四道题,难度是循序渐进的,从一维数组的逆序和极值统计,到排序算法的实现对比,再到二维矩阵转置,最后是字符串统计与反转。覆盖了数组定义、遍历、指针访问、字符串处理、经典排序等核心内容,都属于C语言基础练习里绕不开的关卡。

1. 练习设计的思路与选型

1.1 为什么第三练选择数组与字符串

每个系列的练习内容都要有明确的递进逻辑。第一轮我安排的是变量定义、格式化输入输出、算术运算,解决的是"怎么把一个数据存下来"的问题。第二轮是关系运算、逻辑运算、if-else、switch和三种循环结构,解决的是"怎么根据条件决定代码走哪条路"的问题。

到了第三轮,单变量的操作已经练够了,自然要进入批量数据的处理阶段。数组是C语言里最基础的复合数据类型,字符串本质上又是字符数组的特例,这两块掌握得好不好,直接决定后面学指针、函数传参、结构体时的顺畅程度。很多初学者学到指针就卡住,根子往往在数组这一关就没建立起"内存连续存储"的概念。

这一轮我故意没有引入结构体、文件操作这些内容,就是想让练习者把注意力集中在对数组本身的操控上。排序算法也只挑了冒泡和选择这两种最容易理解的,不碰快速排序和归并排序,避免算法复杂度把基础练习变成算法课。

1.2 四道题如何串起整块知识

题目设计不能东一榔头西一棒子,要从简单到复杂,让每一题都在上一题的基础上叠加新知识。

第一题是数组逆序加极值统计,只用一维数组和循环就能完成,重点考察数组的定义、下标访问、遍历累加这些基本功。第二题是排序算法的实现对比,在数组遍历的基础上引入了双重循环和元素交换,同时通过两种算法对比让练习者体会不同写法的效率差异。第三题把一维数组升级成二维数组,考察行和列的下标关系,转置这种操作特别容易出对称处理上的错误。第四题进入字符串处理,既要理解字符数组和字符串结尾符的关系,还要学会用指针或者下标方式遍历字符,顺便练了字符分类统计。

四道题做完,数组和字符串的核心操作基本都覆盖了。每道题的代码量都不大,但都藏着至少一个易错点,练习者如果只是照着书抄代码,很快就会被运行结果打脸。

2. 核心知识点:数组、字符串与排序的原理要点

2.1 数组与内存布局

数组在C语言里就代表一段连续的内存空间。定义int arr[10]的时候,编译器会分配10个int大小的连续内存,arr这个名字本身是这块内存的首地址,arr[3]则是从首地址偏移3个int位置后那个空间里的值。这套规则听着简单,但很多初学者理解不到位,后面学指针就会稀里糊涂。

我让练习者画过一张图:把数组的每个格子标上地址和下标,然后让他们在纸上走一遍逆序输出的过程。走完这张图,下标从0开始、越界访问的危害、为什么sizeof(arr)/sizeof(arr[0])能算出元素个数,这些问题都能一次搞清楚。

二维数组要特别注意行列顺序。C语言是行优先存储的,也就是先存完第一行的所有列,再存第二行。这意味着int a[3][4]在内存里实际是12个int连续排列,a[1][2]的地址是首地址加上1*4+2个偏移。如果把这个关系记反了,转置、矩阵乘法这类操作很容易写出越界代码。

2.2 字符串的本质与结尾符

C语言的字符串不是独立类型,而是以\0结尾的字符数组。"hello"这个字符串字面量在内存里实际占6个字节,最后一个字节存储\0。很多运行时的诡异问题,比如打印字符串后面跟着乱码、strlen算出的长度不对,都是因为没有正确理解这个结尾符。

处理字符串时还要留意字符数组的容量。char s[10]最多只能装9个有效字符,因为第10个位置要留给\0。初学者最容易犯的错误就是定义一个长度为10的数组,然后往里存了10个字符,结果没有地方放结尾符,后续调用printf("%s")就会一直向后读到内存里碰巧遇到的\0为止。

另外,gets函数已经被标准库标记为不安全,很多编译环境链接不了。所以练习里我统一用fgets读取一行字符串,它会自动在末尾追加\0,用完后手动把换行符替换成\0即可。

2.3 排序算法选择的思路

这一轮练习我安排了两道排序算法题,选型上很有讲究。冒泡排序的思路是相邻元素两两比较,把大的往后挪,每一轮确定一个当前未排序部分的最大值。代码写成双重循环,内层循环范围每轮缩窄一个。选择排序的思路则是每轮在未排序部分找到最小值,然后和当前位置交换。

两种算法的时间复杂度都是O(n²),处理少量数据时差别不大,但代码结构和对数组元素的访问模式不同。冒泡排序频繁做相邻交换,选择排序则是在一轮扫描之后才交换一次。实现选择排序时容易踩的坑是:记录最小值下标之后忘了更新它,导致交换时用了旧的下标。

复杂度分析对基础练习者也很有意义。我给练习者算过:数据量n=1000时,最坏情况下冒泡排序要做约50万次比较,这个数量级是可以接受的。但数据量到10万时,50亿次比较会让程序明显卡顿,所以后面才需要更快的排序算法。这个铺垫为后续学习快速排序打了基础。

3. 四道练习题完整解析与实操

3.1 第一题:数组逆序与极值统计

题面要求:输入10个整数,存入一维数组。先按逆序输出所有元素,再输出最大值、最小值以及它们第一次出现的下标。

先看逆序部分。最直观的做法是按下标从9到0反向遍历数组,直接输出。还有一种做法是把数组原地反转,也就是交换arr[0]和arr[9]、arr[1]和arr[8],以此类推。我要求练习者用第二种做法,因为原地反转更能检验对下标关系的理解程度。

#include <stdio.h> int main() { int arr[10]; int i, maxVal, minVal, maxPos, minPos; printf("请输入10个整数:\n"); for (i = 0; i < 10; i++) { scanf("%d", &arr[i]); } // 原地逆序 for (i = 0; i < 10 / 2; i++) { int temp = arr[i]; arr[i] = arr[9 - i]; arr[9 - i] = temp; } printf("逆序输出:"); for (i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); // 极值统计 maxVal = minVal = arr[0]; maxPos = minPos = 0; for (i = 1; i < 10; i++) { if (arr[i] > maxVal) { maxVal = arr[i]; maxPos = i; } if (arr[i] < minVal) { minVal = arr[i]; minPos = i; } } printf("最大值:%d,下标:%d\n", maxVal, maxPos); printf("最小值:%d,下标:%d\n", minVal, minPos); return 0; }

注意逆序循环的边界条件是i < 10 / 2,也就是i < 5。如果写成i < 10,元素就会被交换两次,整个数组又变回原来的顺序,这个问题在初学者中非常普遍。极值统计的初始化也很关键,我把最大值和最小值都初始化为arr[0],然后从下标1开始遍历比较,这样能保证多轮比较逻辑一致。

实际上我批改时发现有部分练习者把maxPos和minPos初始化为1,然后从下标0开始遍历,这虽然偶尔也能得到正确结果,但遇到最大值恰好在下标0时就会漏掉。统一从0初始化、从1遍历是最稳妥的写法。

3.2 第二题:冒泡排序与选择排序实现对比

题面要求:随机生成20个整数,分别用冒泡排序和选择排序将它们升序排列,每完成一轮排序后输出当前数组的状态。

这里我特意让练习者对比每轮输出的中间状态,目的是让他们直观理解两种算法在操作过程上的差异。

#include <stdio.h> #include <stdlib.h> #include <time.h> #define N 20 void bubbleSort(int arr[], int n) { int i, j; for (i = 0; i < n - 1; i++) { for (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; } } printf("冒泡第%d轮:", i + 1); for (j = 0; j < n; j++) { printf("%d ", arr[j]); } printf("\n"); } } void selectionSort(int arr[], int n) { int i, j; for (i = 0; i < n - 1; i++) { int minIdx = i; for (j = i + 1; j < n; j++) { if (arr[j] < arr[minIdx]) { minIdx = j; } } if (minIdx != i) { int temp = arr[i]; arr[i] = arr[minIdx]; arr[minIdx] = temp; } printf("选择第%d轮:", i + 1); for (j = 0; j < n; j++) { printf("%d ", arr[j]); } printf("\n"); } } int main() { int arr1[N], arr2[N]; int i; srand((unsigned)time(NULL)); for (i = 0; i < N; i++) { arr1[i] = rand() % 100; arr2[i] = arr1[i]; } printf("原始数据:\n"); for (i = 0; i < N; i++) { printf("%d ", arr1[i]); } printf("\n\n"); bubbleSort(arr1, N); printf("\n"); selectionSort(arr2, N); return 0; }

冒泡排序的代码里,内层循环的上界是n-1-i,每轮结束后数组尾部的元素已经排好序,不需要再比较,这个优化如果漏了,算法也能跑,但会做很多无效比较。选择排序代码里最关键的是minIdx的更新,我见过不少练习者把j和minIdx混用,导致比较时用的下标不是当前最小值的位置,排序结果自然出错。

额外插一句:用rand() % 100生成的是0到99的随机整数。如果不调用srand设置随机种子,每次运行程序生成的序列都一样,对练习来说倒也不算坏事,至少便于复现问题,但后续学真正需要随机性的场景时要注意这点。

3.3 第三题:二维数组的转置

题面要求:输入一个3x3矩阵,输出它的转置矩阵(行变列、列变行)。

开一个3x3的二维数组,用两层循环读取数据,然后用三层思路处理转置。最简单的思路是新建一个同样大小的二维数组,把a[i][j]赋值给b[j][i],再输出b。

#include <stdio.h> int main() { int a[3][3], b[3][3]; int i, j; printf("请输入9个整数,构成3x3矩阵:\n"); for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { scanf("%d", &a[i][j]); } } for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { b[j][i] = a[i][j]; } } printf("转置矩阵:\n"); for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { printf("%d ", b[i][j]); } printf("\n"); } return 0; }

这里我要特别强调一个进阶问题:能不能不需要第二个数组,直接在原数组上完成转置?

如果矩阵是方阵,确实可以原地转置,但遍历时只能处理对角线以上或以下的元素。也就是说,i从0到2,j从i+1到2,交换a[i][j]和a[j][i]。如果两层循环都从0开始,每个元素会被交换两次,矩阵等于没变。这跟第一题的逆序交换是同一个道理,但换了场景之后,意识到这个问题的人就变少了。

原地转置时条件j = i + 1不是随便写的,它保证只交换一次上三角元素。我在练习的参考实现里同时给出了新数组法和原地交换法,让练习者对比两种写法的内存开销差异。

3.4 第四题:字符串统计与反转

题面要求:输入一行字符串,统计其中大写字母、小写字母、数字、空格以及其他字符的个数,然后将字符串原地反转并输出。

这题考察字符串输入处理、字符分类判断和字符串反转操作。

#include <stdio.h> #include <string.h> int main() { char str[200]; int upper = 0, lower = 0, digit = 0, space = 0, other = 0; int len, i; printf("请输入一行字符串:\n"); fgets(str, sizeof(str), stdin); // 去掉可能存在的换行符 len = strlen(str); if (len > 0 && str[len - 1] == '\n') { str[len - 1] = '\0'; len--; } for (i = 0; i < len; i++) { if (str[i] >= 'A' && str[i] <= 'Z') { upper++; } else if (str[i] >= 'a' && str[i] <= 'z') { lower++; } else if (str[i] >= '0' && str[i] <= '9') { digit++; } else if (str[i] == ' ') { space++; } else { other++; } } printf("大写字母:%d个\n", upper); printf("小写字母:%d个\n", lower); printf("数字:%d个\n", digit); printf("空格:%d个\n", space); printf("其他字符:%d个\n", other); // 原地反转 for (i = 0; i < len / 2; i++) { char temp = str[i]; str[i] = str[len - 1 - i]; str[len - 1 - i] = temp; } printf("反转后:%s\n", str); return 0; }

fgets读取字符串时会把换行符也读进数组,所以需要手动判断并移除。这个细节特别容易被忽略,我看到过很多次统计结果里"其他字符"多了一个的情况,几乎都是换行符没有被处理。

字符分类也可以用ctype.h里的isupper、islower、isdigit、isspace函数,但在基础练习阶段我要求练习者直接用字符编码范围判断,这样能加深对字符编码连续性的理解。大写字母的ASCII码从65到90,小写字母从97到122,数字字符从48到57,知道这些范围后,很多字符串处理代码都不用死记硬背库函数。

反转操作的核心逻辑和第一题数组逆序完全一样,str[i]和str[len-1-i]交换,循环次数是len/2。如果忘了除以2,字符串反转两次之后又回到原样,运行时还不会报错,这种"静默错误"比直接崩溃更让人头疼。

4. 常见问题排查与避坑实录

4.1 运行时报错的排查套路

这一轮练习里最常见的运行时错误就是数组越界访问。C语言在访问数组越界时通常不会立即报错,而是修改了别的内存位置,导致后续数据莫名其妙变化或者程序崩溃。遇到这种问题,先检查所有数组下标有没有超出声明大小,尤其是循环边界是<=还是<,这个符号写错就足以让程序行为变得诡异。

scanf搭配后续的字符串输入也有坑。如果先用scanf("%d", &n)读整数,再调用fgets读字符串,输入缓冲区里残留的换行符会被fgets直接吃掉,导致读到的字符串是空的。解决办法是在两者之间加一个getchar()把换行符消耗掉。这个问题我在练习前的引导文档里特别标红过,但还是有近三分之一的人踩中。

4.2 我在批作业时最常看到的几个错误

第一个是数组初始化问题。很多练习者声明int arr[10]后不初始化就直接遍历输出,拿到一坨垃圾数据还以为是编译器出了问题。实际上局部变量不会自动清零,除非显式赋初值或者声明为全局变量。我要求练习者在做题前先想想这个数组是否需要初始值,该赋0就赋0。

第二个是sizeof使用错误。在main里对数组用sizeof(arr)/sizeof(arr[0])是没问题的,但如果把这个数组传给函数,在函数内部再用同样方式计算长度,得到的结果永远是1。原因是数组作为函数参数时退化为指针,sizeof(arr)实际是sizeof(指针)。这个知识点我已经讲过,但在第四题里依然有练习者在自定义函数里试图计算传入数组的长度。

第三个是忘记处理字符串结尾符。定义char s[100]后用循环逐个给字符赋值,却没有在最后设置s[i] = '\0',然后直接printf("%s", s),输出就会带着后面的内存垃圾字符。正确做法是赋值结束后手动加上结尾符,或者直接用sprintf这类自动添加结尾符的函数。

4.3 给练习者的自查清单

完成每道题后,我建议按以下清单过一遍,能有效减少低级错误:

  • 所有数组下标是否都在合法范围内?边界条件用的是<还是<=?换成边界值代入测试一下。
  • 循环是否正确处理了对称情况?需要交换的操作是否只执行一次?
  • 字符串数组是否预留了结尾符的位置?输入读取后是否有残留的换行符需要清理?
  • 比较两个值是否用了==而不是误写成赋值=?
  • 函数形参是数组时,是否注意到它在函数内会退化为指针,不能用sizeof计算长度?
  • 逆序或反转类操作,循环次数是否除以了2?

这些检查点做完,大部分低级错误都能在提交前被拦下来。我见过太多练习者把时间浪费在调试一个"数组只交换一半"的问题上,实际上就是循环次数没有除以2。


最后再分享一点个人体会。数组和字符串这个阶段是C语言学习的分水岭,能顺利通过这个坎的人,后面学指针、函数、结构体都会顺畅很多。我自己带完这一轮练习的最大感受是:代码写完能跑只是第一步,更重要的是自己复盘一下代码里每个下标、每个边界条件是怎么来的。多想想"为什么这里要除以2""为什么这里要减1",这些细节才是基本功真正扎实的标志。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询