很多刚开始学 C/C 的同学都有过类似经历变量和循环学得挺熟一到“连续存一堆数”就卡住了。比如要存 5 个成绩想都不想就定义 score1、score2、score3……真到了处理 100 个数据这种写法根本没法继续。数组出现核心就是为了解决“同样类型、成批出现的数据该怎么存、怎么取”的问题。一维数组是其中最基础、最抽象、在后续算法里出现频率最高的一种。这篇文章围绕一维数组的概念、创建、初始化、使用四个部分展开我用 C/C 语法做演示但核心思路对其他语言同样适用。适合刚学完变量和循环的初学者也适合想把这个基础彻底补齐的同学。别把数组当作死记硬背的语法它其实是一套非常有规律的“按编号放数据”的方法。1. 核心概念说清楚一维数组到底是什么1.1 先从“五个变量”的痛点聊起假设你要统计一个班级里 5 个学生的成绩然后计算总分。如果只用变量你大概会写成这样int s1, s2, s3, s4, s5; cin s1 s2 s3 s4 s5; int sum s1 s2 s3 s4 s5;看着好像也能用。但如果学生人数不是 5而是 500 呢你再写 500 个变量任何正常人都会崩溃。更麻烦的是你想用循环去做输入、输出、统计这些变量之间没有“规律”程序根本没法自动遍历它们。用一维数组代码变成这样int scores[500]; for (int i 0; i 500; i) { cin scores[i]; }看起来只是换了一种写法背后却是一种完全不同的思路把多个同类型数据看成一个整体再通过编号去访问其中的某个元素。这个“编号”就是数组下标也叫索引。数组把这批数据的“名字”统一了所以循环才有机会处理它们。1.2 内存视角连续的存储单元才是根本从内存角度看一维数组是连续存放的。你声明了int scores[5]就意味着编译器在内存里划出一段连续的 4×520 字节空间5 个整型元素一个挨着一个排列。你可以把这想象成一排储物柜。柜子编号从 0 开始0 号柜放第一个数据1 号柜放第二个数据以此类推。访问scores[3]不是“找名字叫 scores 的变量”而是“从这排柜子的起始位置往后数 3 个柜子打开它”。这里有个初学者不太适应的点为什么下标不从 1 开始因为数组名本身代表“这一排柜子的起始地址”scores[i]的访问逻辑其实是“起始地址 i × 每个元素占用的字节数”。第一个元素的偏移量是 0所以它落在下标 0 上。这不是刁难人而是为了配合底层地址计算。以后学到指针你会发现这个理解能帮你省掉很多弯路。1.3 “一维”到底是什么意思“一维”指的是数据只有一行只有一个方向的排列。你可以把自己想象成站在一排人面前从左往右数第几个人是谁。而二维数组就像电影院里的座位需要“第几排第几列”才能定位三维数组又加了“第几层”的概念。一维数组是所有数组中最基本的结构也是二维数组、三维数组的基础。有人认为二维数组是“神奇的新东西”其实它完全可以看成“由多个一维数组组成的数组”核心规律没有变。另外还要记住一个关键约束数组里的元素类型必须一致。int数组里只能放整数double数组里只能放浮点数。不要出现一个数组既能放名字、又能放成绩的情况。如果你想表达“一个人的名字和他的成绩”那是结构体或对象要处理的事情数组的定位是“同类型数据的批处理”。2. 创建数组从声明到内存分配2.1 最基础的声明语法创建一个一维数组的通用写法是类型 数组名[数组长度];例如int a[10]; // 能放 10 个 int double b[20]; // 能放 20 个 double char name[50]; // 能放 50 个 char数组长度是元素个数不是占用字节数。这个“长度”有几个限制最好一开始就记住长度必须是正整数不能是 0 或负数。在标准 C/C 中数组长度在编译期就要确定不能用一个运行中才变大的变量直接定义普通数组。局部数组默认不会被自动清空里面的值是内存中残留的随机数据。你可以把“创建数组”理解成“向系统申请一排储物柜”。至于柜子里面原来是空的还是有杂物系统不管它只负责告诉你这排柜子从哪开始、有多长。后续你必须自己初始化或者逐个放数据进去。2.2 长度为什么必须“固定”以及不确定大小时怎么办新手最容易犯的一个错误是写出这样的代码int n; cin n; int arr[n]; // 看起来很方便但这是标准 C 里不推荐的写法在 C 里n是运行时才读入的值不是编译期常量你直接拿它做普通数组大小很多编译器并不支持或者只是以“扩展功能”的方式支持这是不规范的。比较稳妥的做法是事先定义一个足够大的固定长度然后用另一个变量记录实际使用的元素个数。const int MAXN 100; // 或者 C 风格里用 #define MAXN 100 int arr[MAXN]; int n; cin n; // 输入实际要用的数量 if (n 1 || n MAXN) { return 0; // 避免越界 } for (int i 0; i n; i) { cin arr[i]; }这里有必要说清楚const int和#define的小差异。纯 C 代码里const int MAXN 100通常没问题但如果你在 C 语言环境里写某些老编译器不认可const int是“整数常量表达式”直接用#define MAXN 100至少能保证两边都能编译。我自己教新手时更倾向于先让大家用#define或者直接写成const int N 100后不要把N当作变量去改动它。2.3 局部数组太大导致栈溢出数组一旦创建空间就成了“不可变”的存在。初学时很多人觉得反正我开了一个 1000 的数组只用前面 5 个也没问题。确实可以但要注意开得过大也会有风险。局部数组是分配在栈上的普通程序栈空间一般只有几 MB。如果你在main函数里直接写int data[10000000];这个数组大约占 40 MB远超默认栈大小程序很可能运行到这一行就直接崩掉而且很多时候编译阶段根本不报错。这是初学者最容易懵的情况“明明编译通过一运行就闪退/崩溃。”如果你确实需要很大的数组或者大小要运行时确定标准做法是使用堆内存。C 语言里用mallocC 里可以用new[]或直接学std::vector。但对刚接触一维数组的同学我通常建议先把固定长度数组用熟大概知道栈空间的限制再往动态方向走。2.4 数组名不能整体赋值和“重新换一排柜子”创建一个数组后你不能直接把一个数组“整个倒”给另一个数组int a[5] {1, 2, 3, 4, 5}; int b[5]; b a; // 错误不能直接整体赋值为什么不行因为数组名在这里更像一个“首地址”的表示而不是一个可赋值的普通变量。你没办法通过一个把一排柜子里的所有内容瞬时搬过去。想复制就老老实实用循环for (int i 0; i 5; i) { b[i] a[i]; }这个限制以后会因为 C11 的std::array和std::vector得到改善但初学阶段要接受这种“笨拙”。对底层理解得越透彻后面用高级工具时就越顺手。2.5 创建时的“先检查”习惯在写数组代码时我有几条自己的习惯所有数组长度用常量表示不能直接喷一个int a[10]然后到处写魔法数字 10。如果读入的实际元素个数可能变化先把上限检查写好再开始写循环。大数组尽量放全局变量或使用堆/容器别全部堆在main的栈上。养成这些习惯后很多莫名其妙的运行时错误会少一大半。数组不是“排场越大越好”而是“够用、可维护、边界清晰”。3. 数组初始化五种写法与三个高频坑3.1 五种常见初始化写法初始化是初学数组最容易踩雷的地方。常见写法其实只有五类我们直接看代码// 1. 给定全部初始值 int a[5] {1, 2, 3, 4, 5}; // 2. 只给一部分初始值剩余自动补 0 int b[5] {1, 2}; // b 中的元素为 1, 2, 0, 0, 0 // 3. 全部清零 int c[5] {0}; // c 中的元素全为 0 // 4. 由初始值数量自动确定数组大小 int d[] {1, 2, 3}; // d 的长度自动变成 3 // 5. C11 以后用空花括号置零 int e[5] {}; // 等价于全部为 0这五种看起来都不复杂但你要真正理解背后发生了什么。第 2 种很值得注意你不写全编译器会替你“补零”。这个补零行为是规范规定的不是巧合。很多成绩统计场景一开始需要把数组全部置 0用第 3 种写法是最省事的。3.2 为什么{0}能把整个数组都清零有人会纠结int c[5] {0};明明只写了第一个元素是 0凭什么后面四个都变成了 0因为初始化器的规则是如果提供了部分初始值剩余的元素会被自动初始化为该类型的“默认值”。int 的默认值就是 0。这不是某一个编译器的功能而是 C/C 明确规定的结果。你甚至可以这样看int c[5] {1};表示 c[0] 是 1其余都是 0int c[5] {1, 2};表示前两个是 1、2其余为 0。如果数组是全局变量或静态变量即使你不写 {0}它们默认也是 0。但局部数组就没有这个待遇所以我的建议是局部数组如果你希望从 0 开始累积、计数一律明确初始化不要赌那种“好像以前用过这次还是垃圾值”的运气。3.3 字符数组的初始化要特别注意结尾字符数组是一个特殊的一维数组。C 风格字符串实际上就是“以\0结尾的字符数组”。所以char s1[] hi; // s1 实际长度是 3内容是 h, i, \0 char s2[5] hi; // s2 内容是 h, i, \0, 0, 0很多人会把数组长度直接写成字符的个数比如char s[2] {h, i};然后拿去输出结果出现后面一堆乱码。原因就是没有给\0留位置。C 风格字符串需要这个结束标志来判断“字符串到这里就结束了”。如果你只放两个字符长度又只有 2系统不知道哪里是结尾就会越过数组边界继续读内存读到什么就显示什么。所以处理字符串数组时心里要有一笔账要存 n 个可见字符字符数组大小至少要 n1。这是非常经典的边界问题。3.4 三个初学高频坑我都帮你踩过了初始化项太多int a[3] {1, 2, 3, 4};会直接编译报错。你要嘛扩大数组要嘛减少初始值。忘记初始化就用局部数组局部数组里的值是随机残留不固定。同一个程序可能这次运行输出正常下次就出大问题。调试时非常折磨人。在声明之后才用花括号“赋值”int a[5]; a {1, 2, 3, 4, 5};是不行的。初始化只能在声明处做。声明之后想改值要么用循环要么逐个赋值。3.5 如果你已经用 C可以提前认识 vector现在很多教学书会把 Cstd::vector提前放进来。如果你已经开始用vector可以把它想成“会自己长大的、更安全的动态数组”vectorint v1 {1, 2, 3}; vectorint v2(5, 0); // 5 个 0它能做的事和一维数组很像但还能动态添加删除元素边界上也会更安全。不过我还是要提醒不要因为会了vector就不练数组。数组帮你看清“内存连续、下标偏移、长度固定”这些底层思想这些是vector内部实现的基础。4. 数组的使用遍历、传参和两个必练小算法4.1 下标从 0 开始的访问规则数组最核心的使用方式就是“下标访问”。一个小数组int a[5]里合法下标是0、1、2、3、4不存在a[5]。遍历数组最常用的就是 for 循环int a[5]; for (int i 0; i 5; i) { cin a[i]; } for (int i 0; i 5; i) { cout a[i] ; }我见过不少同学在循环条件里写i 5。这会导致最后一次访问a[5]它已经在数组外面了。这个行为在 C/C 里属于“未定义行为”编译器可能放你过去但运行结果不可预料。你运气好读出来的只是一个垃圾值运气不好你的程序会在运行中途崩溃。想让自己安心写 for 循环时养成“左闭右开”的直觉i 长度而不是i 长度-1。虽然两者在数学上等价但前者能让你从视觉上感知到“我最多走到最后一个合法位置”。4.2 数组传入函数最大的坑是“退化”新手把数组传给函数时很容易这样写void printArray(int a[]) { for (int i 0; i ???; i) { cout a[i] ; } }到画长度时才发现自己不知道传进来有多长。有人想用“数组长度公式”int len sizeof(a) / sizeof(a[0]);但这个公式在函数内部会翻车。原因是数组作为函数参数时并不会把整个数组拷贝一份而是“退化”成指向首元素的指针。sizeof(a)得到的不是整个数组的大小而是这个指针的大小。在 64 位系统上指针通常占 8 字节你得到一个 4 字节的 int算出来长度就成了 2。这个“2”经常让初学者摸不着头脑。正确做法是数组长度单独作为参数传进来void printArray(int a[], int n) { for (int i 0; i n; i) { cout a[i] ; } }调用方式int scores[5] {1, 2, 3, 4, 5}; printArray(scores, 5);请把“数组传入函数 传递首地址 长度”这一步刻在脑子里。这也是很多初学者第一次接触“指针”概念的地方。4.3 经典练习成绩统计程序把基础知识串起来的最好方式是做一个完整的小程序。比如输入 n 个成绩求总和、平均分、最大值和最小值。参考步骤#include iostream using namespace std; const int MAXN 100; int main() { int n; cin n; if (n 1 || n MAXN) return 0; int scores[MAXN]; for (int i 0; i n; i) { cin scores[i]; } int sum 0; int maxScore scores[0]; int minScore scores[0]; for (int i 0; i n; i) { sum scores[i]; if (scores[i] maxScore) maxScore scores[i]; if (scores[i] minScore) minScore scores[i]; } double avg (double)sum / n; cout sum sum endl; cout avg avg endl; cout max maxScore endl; cout min minScore endl; return 0; }这里有几个细节值得解释求最大值时用maxScore scores[0]初始化而不是maxScore 0。因为如果所有成绩都是负数0会干扰结果。(double)sum / n是强制把整数转换成浮点数再除否则sum / n会得到整数把所有小数部分直接丢掉。输入循环和统计循环可以合在一起也可以分开写。新手建议分开逻辑更清楚调试时也更容易定位问题。4.4 经典练习数组逆序“逆序”看起来简单但能练到交换、边界和循环嵌套。先看一个很自然的思路从尾部往头部输出不做实际修改直接输出for (int i n - 1; i 0; i--) { cout a[i] ; }这里要注意如果i是无符号整数i 0会永远成立容易造成死循环或越界。常规做法就是把i定义为有符号的 int并且循环写清楚i 0。如果要把数组真正反转最简单的双指针交换法void reverseArray(int a[], int n) { int left 0; int right n - 1; while (left right) { int temp a[left]; a[left] a[right]; a[right] temp; left; right--; } }双指针的好处是只用一次遍历不需要开另外一个大数组。如果你先把这些经典小操作做熟后面学排序、查找时会轻松很多。4.5 冒泡排序不背模板只看相邻交换一维数组学到这里冒泡排序几乎是必练题。它的核心思想是每一轮都从头开始把相邻两个数比较如果顺序不对就交换每轮结束后最大的数会像气泡一样“浮”到最后面。for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { swap(a[j], a[j 1]); } } }内层循环为什么是j n - 1 - i因为每排完一轮最后 i1 个位置已经放好了最大值不需要再去比较。这不是要背下来的魔法而是“已经排好的区域不需要再动”的自然逻辑。4.6 使用数组时的几个小习惯不要用未初始化的局部变量当“最大值初始值”优先用a[0]。访问下标前先确认这个下标在0 到 n-1之间。遍历时尽量用“长度变量”不要反复写魔法数字。如果数组以后可能会变大先想清楚上限再决定数组开多大。这些习惯每一条都不难但放在一起能帮你少掉大量排查 bug 的时间。5. 常见报错与自查手册5.1 典型问题速查表现象常见原因处理办法编译报错数组初始化项过多花括号里的元素数超过数组长度检查数组长度或删减初始值运行后程序崩溃或闪退数组下标越界访问了a[n]等非法位置检查循环条件是不是i n输出结果非常大、像乱码局部数组未初始化就使用声明处初始化或用循环逐个赋值字符串输出尾部有乱码字符数组没有\0或者长度不够至少留一个位置给结束符函数里算数组长度得到 2 或 8数组参数退化成指针sizeof失效手动把长度作为参数传入数组赋值b a编译不过数组不能整体赋值用循环逐个赋值或用容器5.2 一次典型的排查过程我常见到有人写出类似代码后问我“为什么输出全变成垃圾了”。最典型的情况是int main() { int s[5]; // 没初始化 for (int i 1; i 5; i) { s[i] i * 10; } for (int i 1; i 5; i) { cout s[i] ; } }一眼看过去好像输出 10 20 30 40 50实际却可能崩掉。问题有两个一是下标从 1 开始s[5]已经越界二是s[0]从未赋值后面打印还会带出原内存垃圾。排查时我通常建议先把数组初始化和循环边界一起检查下标是不是从 0 开始循环条件是不是而不是如果两个都没问题再怀疑数据本身。排查数组问题时最朴素的“打印法”也最有效。比如for (int i 0; i n; i) { if (i 0 || i n - 1) { cout i i value a[i] endl; } }把首尾两个值打出来很大程度上能判断是不是越界问题。别急着上调试器先学会用输出定位。5.3 练完这五个题才算真正过关如果只是看懂文章效果有限。我建议你把下面的题目亲手做一遍输入 n 个数逆序输出。找出最大值和次大值。统计某个数在数组中出现的次数。去掉数组中的重复元素并输出剩余元素。把两个升序数组合并成一个升序数组。这几题覆盖了下标、遍历、边界、复制、交换和简单的数值统计练完后你对一维数组的掌握就会从“看懂了”变成“会用”。我不推荐直接去看“最快解法”而是先用最笨的方法跑通再琢磨能不能少用一次循环。数组这一章我到今天都记得一个场景自己第一次用循环遍历数组成功输出 100 个随机数时那种“原来电脑可以这么听话”的感觉。之后学指针、学动态内存、学各种容器底层都绕不开一维数组这套基本功。如果你现在正卡在这一步别慌把上面这些代码一条一条亲手敲进编辑器跑通、改错、再跑通一维数组这道坎就算迈过去了。