资讯动态

C语言数组与字符串进阶练习:从排序到越界避坑全解析

发布时间:2026/10/21 16:29:27 来源:尧图企业网站定制
最近带了一批初学者做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*42个偏移。如果把这个关系记反了转置、矩阵乘法这类操作很容易写出越界代码。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²)处理少量数据时差别不大但代码结构和对数组元素的访问模式不同。冒泡排序频繁做相邻交换选择排序则是在一轮扫描之后才交换一次。实现选择排序时容易踩的坑是记录最小值下标之后忘了更新它导致交换时用了旧的下标。复杂度分析对基础练习者也很有意义。我给练习者算过数据量n1000时最坏情况下冒泡排序要做约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到2j从i1到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这些细节才是基本功真正扎实的标志。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑