资讯详情

C语言初学数组题:从下标、越界到指针动态数组的完整练习路线

📅 2026/10/3 4:09:48 | 华诺云谱 👁 阅读
C语言初学数组题:从下标、越界到指针动态数组的完整练习路线
数组是C语言里最常见的考点也是许多初学者第一次感到“代码不是照着敲就能跑通”的地方。为什么数组下标从0开始为什么越界后不直接报错反而程序跑到后面才莫名其妙崩掉为什么字符串逆序输出这么简单的一道题能让人卡半小时这篇文章要聊的“适合C初学数组题”就是把这些问题打包成一套能照着跑的练习先过一维数组的初始化、遍历、边界控制再上二维数组的鞍点、成绩统计最后把排序去重、指针数组、动态数组和常见报错一并讲清楚。刚学完分支和循环、准备啃数组的同学可以把它当成一张学习路线图备考计算机二级或者刷学校机试的也能拿来做查漏补缺。数组题说难也难说简单也简单。难在它第一次把“内存”“类型”“下标”这些概念揉在一起一不留神就写出越界代码简单在你只要真正理解了“数组是一块连续内存”这个本质绝大多数题最终都能归到“遍历判断”四个字上。这文章里没有高深技巧只有贴着初学者水平走的题目和踩坑记录代码都用标准C编写GCC编译验证过可以直接放心复现。1. 题材怎么选数组题到底在练什么1.1 一道数组题的标准拆法数组题看起来五花八门其实拆开就四件事存储、遍历、判断、输出。我拿最简单也最常考的“输入n个数逆序输出”举例。你可以把数组想象成一排连续编号的储物格每个格子里放一个int编号从0开始。代码写出来是这样#include stdio.h #define MAXN 100 int main(void) { int n, i; int arr[MAXN]; scanf(%d, n); for (i 0; i n; i) { scanf(%d, arr[i]); } for (i n - 1; i 0; i--) { printf(%d, arr[i]); if (i 0) { printf( ); } } return 0; }这段代码背后有三个新手最容易忽略的点。第一是#define MAXN 100先把数组上限写死初学者暂时不用折腾变长数组和动态内存可以把注意力放在逻辑上第二是逆序输出时i从n-1开始而不是从n开始因为最后一个有效下标就是n-1从n开始一上来就越界第三是输出空格的时机很多人习惯每输出一个数就加一个空格结果末尾多了一个空格在OJ上会报Presentation Error。逆序输出这道题应该背下来它同时练了循环、下标、边界三个基本功。1.2 给初学者的一条刷题路线数组部分的学习顺序我建议按“一维基础题 → 字符串处理 → 二维数组题 → 排序查找题 → 数组与指针结合题 → 动态数组”这样推进。不要一上来就碰指针数组很多教材把指针和数组放在同一章容易让人误以为“数组就是指针”实际上它们只是在函数传参时存在等价关系数组本身是一块连续内存指针只是一个保存地址的变量。顺序一旦打乱很容易卡在中途失去信心。选题目时优先选边界条件明确的题比如“输入若干个数求最大值及所在下标”“把数组元素循环右移一位”这些题能把遍历、判断、交换练透。题源方面我比较推荐翁恺老师的C语言课配套练习题以及PTA平台的C语言基础题集。这两个题源共同的优点是题目短、输入输出格式写得很清楚、样例覆盖广适合先自己画思路再上机验证。我自己的习惯是每道题写两种做法比如逆序输出可以下标逆着走也可以双指针交换后顺序输出这样练出来的思路更灵活。1.3 题目范围与工具要求这篇文章里出现的题目全部只需要标准C库就能跑不依赖任何第三方库。运行环境我用的是GCC/ClangWindows上可以装MinGW-w64再用VS Code或者Dev-C写代码都行macOS/Linux直接在终端里用gcc编译就是。C语言标准建议用C11或更新写代码时尽量让编译器开启警告比如GCC的-Wall -Wextra。对初学者来说编译时多看警告比运行时调试更省力因为很多错误在警告里已经暗示了。环境能支持单文件编译和最基本的调试就够用别在编辑器主题、代码补全上折腾半天那不是“适合C初学数组题”的重点。2. 一维数组入门题初始化、下标、边界控制2.1 数组初始化与下标为什么从0开始先看最简单的初始化。定义数组后里面的值是“不确定”的所以新手第一件事就是学会“清零”#include stdio.h int main(void) { int a[10] {0}; // 所有元素初始化为0 int b[5] {1, 2, 3}; // b[3]、b[4]自动补0 for (int i 0; i 10; i) { printf(a[%d] %d\n, i, a[i]); } return 0; }很多人只记住了“大括号初始化”却不知道部分初始化时剩余元素会自动补0。int b[5] {1,2,3}这种写法程序员如果只显式给了3个值后两个也会是0这一点在统计、累加的题里非常有用。如果定义后不写大括号比如int a[10];那数组里装的是栈上的残留数据很可能出现“每次运行结果都不一样”的诡异现象。排查方法很简单定义时顺手加上{0}。下标从0开始也不是编译器随便定的而是因为C语言规定a[i]等价于*(a i)也就是“从数组首地址向后偏移i个元素”。第一个元素的偏移量是0所以下标从0开始。理解了这一点后面学指针数组、指针算术时都会顺畅很多。另外要记住C语言不支持数组整体赋值int c[5]; c b;编译不过想复制只能逐元素循环或用memcpy。2.2 越界不报错不等于可以越界C语言为了提高运行效率默认不检查数组下标是否越界这是它和Java、Python最大的区别。不检查带来的后果是越界那一刻程序可能还是一副没事的样子但已经悄悄改坏了旁边变量的值或者破坏了栈上的返回地址直到运行到很后面才崩溃。#include stdio.h int main(void) { int a[3] {1, 2, 3}; int b 10; a[3] 99; // 越界写入编译不报错 printf(b %d\n, b); // 在某些编译器布局下b 会被改成 99 return 0; }这个例子依赖编译器内存布局不同平台结果可能完全不一样不要用这种写法去“验证”什么。它存在的意义是提醒你边界问题只能靠人肉盯。最常见的越界场景来自for循环的边界写错比如数组长度是n结果习惯性写成了i n最后一次循环恰好把数据写到arr[n]这个“不存在”的位置。排查越界时推荐用一个简单办法在循环里临时加一句printf(i %d\n, i);把所有下标都打出来一眼就能看出哪里多走了一步。2.3 字符串逆序输出一整类题的入口字符数组本质也是数组只是它用来存字符并且约定以\0结尾。字符串逆序输出几乎是所有OJ平台都会出现的题它考的不是什么高深算法就是“取长度”和“反向遍历”两个基本功#include stdio.h #include string.h int main(void) { char s[101]; scanf(%100s, s); // 最多读100个字符防止越界 int len strlen(s); for (int i len - 1; i 0; i--) { putchar(s[i]); } putchar(\n); return 0; }这里有一个细节特别值得说char s[101]能容纳的字符串长度其实是100个字符加一个\0所以scanf里写了%100s而不是%101s否则一旦输入正好100个字符就容易丢掉结束符。如果只是逆序输出直接反向遍历就行如果要求“把字符串本身反转”可以双指针原地交换void reverse(char s[]) { int left 0, right strlen(s) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } }这一整类题的入口在于所有字符串处理都逃不开“先找长度、再逐个字符操作”这两步。后续学比较两个字符串、统计单词数、去掉空格甚至“数组分割并显示包含某一字符”这类题目其实都是在这个框架上做变形。核心还是遍历字符数组、按分隔符切分、逐段判断本质没有跳出“长度遍历”的圈子。3. 二维数组与经典算法题3.1 二维数组的存储本质逻辑表格物理线性二维数组在逻辑上是“行和列”但在内存里其实是一段连续的一维空间。比如int a[3][4]C语言按行优先存储先放第0行的4个int再放第1行、第2行。这个本质决定了大部分二维数组题的做法。#include stdio.h int main(void) { int a[3][4] {0}; for (int i 0; i 3; i) { for (int j 0; j 4; j) { a[i][j] i * 10 j; } } for (int i 0; i 3; i) { for (int j 0; j 4; j) { printf(%4d, a[i][j]); } putchar(\n); } return 0; }用两层循环遍历时习惯上外层控制行i内层控制列j这样访问顺序和内存存储顺序一致性能也最好。如果你想知道数组是不是真的连续可以用int *p a[0][0];之后用p[5]这种一维下标访问会发现它和a[1][1]是同一个位置。少数题目会利用这种连续性比如用memset(a, 0, sizeof(a))一次性清空整个二维数组前提就是二维数组的内存必须连续。3.2 鞍点问题一道题练穿多重循环和状态判断“计算5×5矩阵的鞍点”是C语言数组题里很有代表性的综合题PTA和其他练习册里都有。题目说的是如果某个元素在它所在的行上最大同时在它所在的列上最小它就是一个鞍点。这道题把二维数组遍历、比较、布尔状态标记三者全部考到了。#include stdio.h #define N 5 int main(void) { int a[N][N]; int i, j, k; int found 0; for (i 0; i N; i) { for (j 0; j N; j) { scanf(%d, a[i][j]); } } for (i 0; i N !found; i) { for (j 0; j N; j) { int isRowMax 1, isColMin 1; for (k 0; k N; k) { if (a[i][k] a[i][j]) isRowMax 0; if (a[k][j] a[i][j]) isColMin 0; } if (isRowMax isColMin) { printf(鞍点: a[%d][%d] %d\n, i, j, a[i][j]); found 1; break; } } } if (!found) { printf(不存在鞍点\n); } return 0; }我见过不少人的第一反应是“先找每行最大值再判断它是不是列最小”这个思路本身没问题但写起来容易漏一行里可能存在多个相同最大值只看第一个会漏掉。上面的写法是暴力判断每个元素对任意a[i][j]在同一行里找有没有比它大的在同一列里找有没有比它小的两个条件都满足就是鞍点。5×5规模很小三重循环最坏也才125次比较初学阶段完全可行。做题时还要看清题目对相等情况的定义有的题说最大值唯一有的允许并列如果你只是“遇到大于才否定”那就默认允许并列这在大多数OJ上都能通过。3.3 二维数组实战成绩统计与九九乘法表二维数组最贴合的应用就是表格数据比如“5名学生、3门课程的成绩表求每个学生的平均分和每门课的平均分”。这个题考的是二重循环的两种方向按行走和按列走。#include stdio.h #define ROWS 5 #define COLS 3 int main(void) { int scores[ROWS][COLS]; for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { scanf(%d, scores[i][j]); } } double stuAvg[ROWS], subjAvg[COLS]; for (int i 0; i ROWS; i) { int sum 0; for (int j 0; j COLS; j) { sum scores[i][j]; } stuAvg[i] sum / (double)COLS; } for (int j 0; j COLS; j) { int sum 0; for (int i 0; i ROWS; i) { sum scores[i][j]; } subjAvg[j] sum / (double)ROWS; } for (int i 0; i ROWS; i) { printf(第%d名学生平均分: %.2f\n, i 1, stuAvg[i]); } for (int j 0; j COLS; j) { printf(第%d门课平均分: %.2f\n, j 1, subjAvg[j]); } return 0; }注意sum / (double)COLS如果不转成double整数除法会把平均分的小数部分直接丢掉。九九乘法表其实也是二维数组的经典练习可以先把结果存到int table[9][9]里再按行打印虽然题目本身不用数组也能打印但用数组存一遍再输出能让你更清楚第i行第j列的下标映射关系。这类“表格类”数组题的通用套路就是先确定行和列的含义再用二重循环填数据最后按需求方向做统计。4. 数组进阶题指针传参、排序与动态内存4.1 冒泡排序与数组去重最经典的综合基础题学完一维数组和二维数组之后就该练排序了。冒泡排序是C数组题中最经典的综合基础题它把遍历、比较、交换三个动作全串在一起。基本思路是每一轮把相邻元素两两比较大的往后挪这样一轮下来最大值就“冒”到了末尾。void bubbleSort(int a[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { int tmp a[j]; a[j] a[j 1]; a[j 1] tmp; } } } }内层循环为什么是n - 1 - i而不是n - 1因为每一轮结束后数组末尾已经排好了i个元素它们不需要再参与下一轮比较。如果少了- i排序结果不会错但会做很多无意义比较初学阶段虽然看不出来养成这个好习惯以后写更复杂的算法会受益匪浅。数组去重也是经常出现的题常见套路有两种。第一种是双重循环遇到重复元素就把后续元素整体前移一位直观但有点啰嗦第二种是先用冒泡排序排好序再遍历一次只保留和前一个不同的元素。后者把问题转化成“判断相邻元素是否不同”代码短也好查错。这两道题一起练能把“边界”“交换”“状态标记”这三个核心动作一次补齐。4.2 指针数组与数组指针一次分清到了这一节题目的难度就上来了很多初学者在这里开始迷糊。先说结论指针数组是一个数组里面装的是指针数组指针是一个指针它指向一个数组。#include stdio.h int main(void) { char *names[3] {C Language, Array, Pointer}; int (*p)[3]; // 指向“含3个int的数组”的指针 int a[3] {1, 2, 3}; p a; for (int i 0; i 3; i) { puts(names[i]); } printf(%d %d %d\n, (*p)[0], (*p)[1], (*p)[2]); return 0; }char *names[3]怎么读从变量名往右看先看到[3]说明它首先是一个数组元素类型是char *。int (*p)[3]因为有括号所以p先是指针指向的对象是含3个int的数组。指针数组最常用的场景就是存放字符串因为字符串本身是字符数组在C里习惯用char *指向它的首字符多个字符串就放在一个char *数组里。要提醒的是直接初始化的字符串字面量在部分平台是只读的比如names[0][0] X可能直接崩溃如果题目要求修改字符串最好改成二维字符数组char names[3][20]或者用动态分配复制一份。4.3 数组传参会退化函数里别用sizeof数组名在绝大多数表达式里会“退化”成指向首元素的指针函数传参时尤其明显。也就是说函数形参写成int arr[]和int *arr完全等价。这个特性导致一个经典错误在函数内部用sizeof(arr)求数组长度得到的是8字节指针大小而不是数组真实大小。#include stdio.h void badSize(int arr[]) { printf(在函数里sizeof(arr) %zu\n, sizeof(arr)); // 64位机器上通常是8 } int main(void) { int a[10]; printf(在主函数里sizeof(a) %zu\n, sizeof(a)); // 通常是40 badSize(a); return 0; }正确的做法是把长度作为参数传进去。二维数组传参也遵循同样逻辑void print(int a[][4], int rows)的列数不能省略因为编译器要用列数计算a[i][j]的实际偏移行数则可以根据需要作为第二个参数传进来。很多刚开始刷题的同学在编写函数处理数组时发现结果不对就要先怀疑是不是把数组长度用错了。4.4 用malloc让数组长度由输入决定练习到后面你会发现“固定大小的数组”有时候不够用比如题目要求读入n个整数但n可能是10也可能是100000。这时可以动态分配数组让长度由输入决定#include stdio.h #include stdlib.h int main(void) { int n, *arr; scanf(%d, n); arr (int *)malloc(n * sizeof(int)); if (arr NULL) { return 1; } for (int i 0; i n; i) { arr[i] i * i; printf(%d , arr[i]); } printf(\n); free(arr); return 0; }malloc返回的是void*在C语言里可以直接赋给任意指针写成(int *)是为了兼容C和部分老编译器最重要的是检查返回值是否为NULL忘记检查在内存耗尽时会直接对空指针写入程序当场崩掉。用完后一定记得free但free之后不要再碰这块内存这是动态数组最容易踩的坑。二维动态数组稍微麻烦一点需要先分配行指针数组再给每行分配列空间int **matrix malloc(rows * sizeof(int *)); for (int i 0; i rows; i) { matrix[i] malloc(cols * sizeof(int)); } // 使用... for (int i 0; i rows; i) { free(matrix[i]); } free(matrix);释放顺序和分配顺序相反先释放每一行再释放行指针数组这个顺序写反同样会造成内存错误。5. 环境配置与排查报错实录5.1 初学数组最容易遇到的高频错误速查表先给一张速查表基本覆盖常用痛点现象常见原因排查思路输出每次结果都不一样数组没初始化读到栈残留数据定义时写{0}程序运行到一半崩溃数组下标越界写坏了未知内存检查for边界是否出现n字符串输出一堆乱码后崩溃字符数组缺少\0结束符定义数组时多留一个字节scanf读完一个数后“卡住”输入缓冲区残留换行在格式串前加空格或用getchar清掉编译报错“assignment to expression with array type”对数组名整体赋值改用strcpy或逐元素复制某个变量的值莫名变掉附近数组越界写入打印循环下标缩小范围定位printf打印出奇怪字符或负数格式串写错比如字符串用了%d对照printf格式说明逐一检查OJ上样例一模一样却判错多输出了空格/换行或数组开小了检查输出格式和数组容量这张表是我带新手时总结出来的数组题出错基本不会超出这几个范围。排查的时候我习惯先分两类一类是编译错误编译器会告诉你在哪一行另一类是运行结果不对这类最需要耐心把输入规模改小、逐步打印中间量通常很快就能定位。5.2 我踩过的几个经典坑第一个坑是scanf(%s, s)读不进带空格的字符串。数组题里的字符串处理经常要求读一整行但%s遇到空格就会停下来于是明明输入了“hello world”程序只读到“hello”。处理办法是换成fgets(s, sizeof(s), stdin)它会连换行符一起读进来用完记得把末尾的\n去掉if (s[strlen(s) - 1] \n) s[strlen(s) - 1] \0;。第二个坑是初始化误判。很多初学者写了int a[3]; a[0] 0;就以为三个元素都是0其实只有第一个是0后面两个还是随机值。正确的想法是只有定义时的大括号初始化才有“自动补0”的效果定义之后逐个赋值每个元素都要显式处理。第三个坑是越界错误在OJ上不容易复现。本地小数据能跑提交到OJ上就崩或者答案变来变去多半是越界访问或数组开太小。我的建议是数组大小在允许范围里尽量多留一点比如题目说n不超过100就定义int a[105]或int a[110]多出的几个位置根本不影响逻辑却能把很多边界错误挡在外面。5.3 VS Code跑C数组题的极简配置既然标题里提到了C语言入门就顺手把VS Code写C的配置说清楚。我自己在Windows上用的方案是先装MinGW-w64安装时可以直接放在D:\mingw64这种非系统目录一是路径里没有空格和中文二是不用往C盘塞东西C盘也不容易爆。装完把D:\mingw64\bin加进系统PATH然后在命令行敲gcc --version验证。VS Code里装一个名为“C/C”的扩展作者是Microsoft然后建一个.vscode/tasks.json{ version: 2.0.0, tasks: [ { label: C Build, type: cppbuild, command: gcc, args: [ -g, -stdc11, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe ], group: build } ] }再建一个.vscode/launch.json用来调试{ version: 0.2.0, configurations: [ { name: C Debug, type: cppdbg, request: launch, program: ${fileDirname}\\${fileBasenameNoExtension}.exe, miDebuggerPath: D:/mingw64/bin/gdb.exe, cwd: ${fileDirname} } ] }这样写完代码按F5就能编译调试。如果不想折腾直接用Dev-C或者Code::Blocks也完全可以初学阶段没必要在编辑器上花大把时间。VS Code配置最大的好处是调试时能看到数组每个下标对应的值这一步对理解越界问题帮助很大值得花十分钟把它配好。最后再分享一个我自己练数组题的小习惯每写一道题先在纸上画出数组和下标的关系再用printf把循环里每个关键下标临时打出来。这个“笨办法”帮我解决过不少很难查的bug。初学数组最重要的不是背语法而是把每个下标盯清楚下标对了题目基本就通了一半。练完这些基础题你再看指针数组、排序查找、动态数组那些内容会发现它们都只是数组这根主线上长出来的枝叶。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑