news 2026/10/20 17:20:25

C++一维数组从入门到避坑:声明、初始化、遍历与排序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++一维数组从入门到避坑:声明、初始化、遍历与排序

连载走到第10篇,前面的变量、判断、循环都属于“单打独斗”——变量管一个数,循环管反复操作,可一旦遇到“一批”数据,这两个工具立刻就不够用了。班里60个人的成绩、一个月30天的气温、购物车里几十件商品的价格,你总不能声明60个变量,再写60遍判断找最高分。这个时候,一维数组就是 C++ 里最常用来装“同类型数据集合”的容器:一个名字,一串连续的格子,配合循环就能成批量地存、取、算。这篇适合刚学完循环、正准备写批量数据处理逻辑的零基础读者,我会从数组的长相讲到它最容易踩的坑,看完跟着敲一遍,批处理这一关基本就过了。

1. 一维数组到底是什么:先给内存画一幅地图

1.1 为什么需要数组:用“小卖部流水账”当引子

想象你管着一家小卖部,想记录今天30种商品的销售额。最原始的办法是声明30个变量:

int item0; int item1; int item2; // ... 一直写到 item29

手写30行声明已经够累了,更崩溃的是后面:你想找出“今天卖得最好的是哪个商品”,就只能把这些变量挨个比较。比较30个变量,你得嵌套多少层 if?代码会膨胀到比商品清单还长,还特别容易抄错变量名。

数组解决问题的思路非常直接:把30个同类型的数打包进同一个名字,用编号去定位其中的某一个。声明写int sales[30];,一行就能描述整批数据。更重要的是,数组的下标可以用变量表示,配合循环可以把30个元素全部访问一遍。数据的批量输入、批量输出、求和、找极值、排序,全都从这个“数组名+下标”的组合上长出来。

所以你不妨把数组理解成“为了让循环派上用场而存在的数据结构”。没有数组,循环只能反复处理一个变量;有了数组,循环就变成了流水线上的机械臂,挨个处理同一条传送带上的零件。

1.2 内存里的“格子排排坐”:连续存储为何查询快

C++ 的数组在内存里是一段连续的存储区域。声明int scores[10];,如果当前环境下 int 占4字节,编译器就会一次性分配40字节的连续内存。这40字节像宿舍楼同一层10个连着的小房间,房间号从0编到9,数组名就是这栋楼的名字。

访问scores[3]的时候,编译器做的事情其实很朴素:拿数组起始地址,加上3 * sizeof(int),就直接跳到了第4个房间的门口。整个过程不需要像查档案一样从头翻,两次算术运算就定位结束。这就是常说的“随机访问”——访问第7个元素和访问第2个元素,时间几乎一样,因为位置是算出来的,不是找出来的。

“连续”这个词值得多记几遍,因为后续学链表的时候,对比会非常明显。数组查询快,依赖的是连续存储;但它的弱点也埋在这里:想在数组中间插入或删除一个元素,后面的所有元素都得往前或往后挪位置,代价就比较大了。零基础阶段,先把“数组是连续内存、按下标定位”这一层想明白,后面理解指针、动态分配都会顺很多。

2. 声明、初始化与读写:三个动作带你快速上手

2.1 三种声明方式与“不初始化=垃圾值”的陷阱

声明一维数组的标准格式是:类型 数组名[元素个数];。元素个数必须是编译期能确定的值,通常是字面量或者 const 常量。常见的几种:

int scores[60]; // 60个int,适合存全班成绩 double prices[30]; // 30个double,适合存价格 char name[20]; // 20个char,后面讲字符串会用到

这一节最大的坑,是声明了却不初始化。int scores[60];写完之后,数组里存的是什么?答案是不确定值,也就是所谓“垃圾值”——这块内存之前可能被别人用过,残留数据谁也说不准。你直接把它输出,会看到一串莫名其妙的大数甚至负数。

很多新手第一次输出遇到这种“乱码”,第一反应是代码写错了,其实问题出在没给数组一个确定的初始状态。我的建议是:养成声明时就初始化的习惯。哪怕你想的是“待会循环里逐个输入”,声明时先给个= {0};也不亏,至少不会带着垃圾数据到处跑。

2.2 初始化的几种写法:自动补0的细节别小看

一维数组的初始化主要就这几种:

int a[5] = {1, 2, 3, 4, 5}; // 全部元素显式给出 int b[10] = {1, 2}; // 前两个是1、2,其余自动补0 int c[] = {5, 6, 7}; // 让编译器数一数,长度就是3 int d[5] = {}; // 5个元素全是0 int e[5] = {0}; // 第一个是0,剩下四个也补0

初学最容易忽略的是第二种和最后一种。int b[10] = {1, 2};你以为 b[2] 是垃圾值,其实不是,规则是:初始化列表给出的值少于数组大小时,剩余元素会做“值初始化”,对 int 来说就是 0。

这个规则非常实用。想要快速把整个数组清零,不需要写循环,一句int arr[100] = {0};就完成了。反过来也要注意:如果你声明后没有任何初始化列表,比如只写int arr[100];,那里面仍然可能是垃圾值。有木有{0}这一行,结果天差地别。

2.3 下标从0开始:第1个误区与两种读写法

int scores[60];的合法下标是0到59,不存在scores[60]。下标从0开始这件事,坑过无数新手,这里提供一个好记的解释:下标表示“离起点隔了几个格子”。第一个格子离起点0格,所以是0号;第五个格子离起点4格,所以是4号。你只要记住“有5个数,最后一个下标是4”,写循环的时候就不容易越界了。

在读写方面,arr[i]和普通变量的地位一样。它可以放在赋值号左边当“储物格”写入,也可以放在右边把值取出来用:

arr[0] = 88; cout << arr[0] << endl;

但真正发挥威力的是配合循环:

for (int i = 0; i < n; ++i) { cin >> arr[i]; }

这段代码从键盘读 n 个数依次存进数组。输入的时候数字之间用空格或回车分隔都行,cin 会自动跳过空白。如果你从1开始计数,比如想处理“第k个”元素,下标就要写成 k-1。很多同学喜欢“第1个数放 arr[1]”这种写法,虽然能避开从0开始的别扭,但会浪费 arr[0] 这个格子,而且后面学指针、容器时容易乱。统一从0开始,是C++的世界规则。

3. 遍历、求和、找最大值与冒泡排序:先跑通这批经典操作

3.1 遍历输出与范围for:懒人写法也要知道限制

遍历就是让下标从0走到 n-1,把每个格子都过一遍。最经典的是输出整个数组:

for (int i = 0; i < n; ++i) { cout << arr[i] << (i == n - 1 ? '\n' : ' '); }

这里用了个小技巧:最后一个数后面输出换行,前面都用空格分隔,输出格式干净,链接时也不会有多余空格。

C++11 之后还有一种更省心的写法,基于范围的 for 循环:

for (int x : arr) { cout << x << ' '; }

你不用管下标,循环会自动把 arr 里的每个元素依次赋给 x。但要注意,这个 x 是拷贝出来的副本,你在循环里修改 x,不会改动数组本身。如果想通过 x 修改数组里的元素,必须写成引用:

for (int &x : arr) { x *= 2; // 这样才会真的放大数组里的每个值 }

另外,范围for拿不到当前元素的下标。如果输出时需要显示“第几个”,或者有根据下标修改元素的需求,老老实实回到传统 for 更顺手。

3.2 累加求平均与打擂台求极值:循环里的老搭档

求和是最常见的批量统计。一个循环搞定:

int sum = 0; for (int i = 0; i < n; ++i) { sum += arr[i]; }

求平均就是总成绩除以人数。这里提醒一个类型问题:如果数据量很大,比如几百万个 int 累加,用 int 装 sum 可能会溢出,数字突然变成负数。稳妥的做法是用long long sum = 0;。平时练习题里的小数据用 int 没问题,但要有这个意识。

求最大值用的是“打擂台”思路:

int maxVal = arr[0]; // 先把第一个数当成临时的擂主 for (int i = 1; i < n; ++i) { if (arr[i] > maxVal) { maxVal = arr[i]; } }

先用 arr[0] 做初值,再从 i=1 开始比较,这样避免了给 maxVal 随便设一个可能比所有数都小的初值。循环结束后,maxVal 就是数组里的最大值。求最小值把>换成<就行。

把输入、求和、求最大值合在一起,就是一个很典型的完整小题:

const int MAXN = 10005; int arr[MAXN]; int n; cin >> n; for (int i = 0; i < n; ++i) { cin >> arr[i]; } int sum = 0; int maxVal = arr[0]; for (int i = 0; i < n; ++i) { sum += arr[i]; if (arr[i] > maxVal) { maxVal = arr[i]; } } cout << "平均分:" << 1.0 * sum / n << endl; cout << "最高分:" << maxVal << endl;

用1.0 * sum / n是为了把整数除法转成浮点除法,否则平均分会直接丢掉小数部分,88和89分就看不出来了。

3.3 冒泡排序:第一次接触“交换两数”

排序是数组应用的一个大头。冒泡排序最适合入门,因为它足够直观:一趟一趟扫描,把相邻的乱序元素交换过来,最大的数就像气泡一样慢慢“浮”到末尾。

for (int i = 0; i < n - 1; ++i) { for (int 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; } } }

外层循环控制“总共走几趟”,最多 n-1 趟一定能排好。内层循环负责从头开始比较相邻两个数,但每一趟结束后,末尾都会多一个已经就位的最大值,所以比较范围可以不断缩小,写成n - 1 - i。

这里有一个新手几乎必犯的错误,就是交换两个数时忘了用临时变量。你想当然地写:

arr[j] = arr[j + 1]; arr[j + 1] = arr[j];

结果两个位置都变成了原来 arr[j+1] 的值,数据就丢了。因为第一次赋值已经覆盖了 arr[j],再用 arr[j] 去覆盖 arr[j+1],拿到的就是改过的值。临时变量 temp 相当于一个“保险柜”,先把要覆盖的值存起来,再放心大胆地改。

冒泡排序的时间复杂度是 O(n²),数据量几百上千没问题,到了几万就会明显变慢。零基础阶段先把过程跑通,以后学到快排、归并或者标准库的 sort 时,你会回来感谢今天建立的感觉。

4. 数组传参给函数:这里有一道绕不开的坎

4.1 sizeof 陷阱:为什么函数里“数组变小了”

很多新手会写出这样一个函数:

void showArray(int arr[]) { for (int i = 0; i < ???; ++i) { cout << arr[i] << ' '; } }

问题来了:循环结束条件里的长度写什么?你没法在函数体内知道数组有多大,因为 C++ 里数组作为参数传到函数时,长度信息不会跟着传过去。更准确地说,形参int arr[]在函数内部会被当作int*指针处理,它只拿到了数组首元素的地址。

如果你在函数里写cout << sizeof(arr),得到的往往是指针的大小,比如64位系统上是8字节,而不是整个数组的字节数。这就是经典的“sizeof 陷阱”。

解决办法非常直接:把长度作为另一个参数传进去。

void showArray(int arr[], int n) { for (int i = 0; i < n; ++i) { cout << arr[i] << ' '; } cout << endl; } // 调用 showArray(scores, 60);

以后只要看到数组参数,第一反应就是“还要带上长度”。这是C风格数组的标准用法,也是后续学容器之前必须接受的规则。

4.2 传地址意味着“改得动”:const 保护只读参数

数组传参传的是首地址,这一点和普通变量传值完全不同。普通 int 传进函数只是拷贝一份,你在函数里怎么改都不影响外面;数组则不一样,函数里写arr[i] = something;会直接改变主函数里的数组。

很多新手第一次写“数组排序函数”,惊讶地发现主函数里的数组也变了,怀疑是不是哪里传错了。这其实不是 bug,而是数组传参的特性:函数拿到的是原数组的入口,改的就是原数组。这在批量处理中反而是优势,因为你不需要把数组复制来复制去,直接操作原始数据。

但如果你的函数只想读数组、不想改数组,就最好加上 const 保护:

void printArray(const int arr[], int n) { for (int i = 0; i < n; ++i) { cout << arr[i] << ' '; } }

const 相当于给参数加了一道锁:函数内部只能读,一旦写了arr[0] = 0;,编译器立刻报错。这个限制看着小,实际很管用,能帮你挡住不少“函数里手滑改了数据”的意外。

理解“改得动”和“只读”这两个方向,传数组这件事就算看明白了。

4.3 为什么不能直接返回一个数组:绕开新手雷区

有些同学想写int[] makeArray(),这在C++里行不通,函数不能直接返回数组类型。新手阶段,推荐的做法是让函数修改调用者传入的数组,数据自然就“带回来”了:

void fillArray(int arr[], int n) { for (int i = 0; i < n; ++i) { arr[i] = i * i; } }

调用方在 main 里准备好数组:

int values[10]; fillArray(values, 10); for (int i = 0; i < 10; ++i) { cout << values[i] << ' '; }

数值通过函数写进了 values 里面,主函数再访问时就已经是填好的结果了。至于“在函数内部创建新数组并返回”的写法,涉及内存生命周期的问题,容易踩悬空指针的坑,零基础阶段先别碰。后面用到标准库的 std::vector,这个问题会迎刃而解,现在先攒着这个疑问就好。

5. 别让程序“悄悄坏掉”:越界、垃圾值与调试技巧

5.1 越界访问为何很难发现:认识未定义行为

int arr[5]; arr[5] = 100;这句话在语法上完全合法,编译器也大多不报错,但它写入的位置在数组之外。C++ 把这种行为定义为“未定义行为”,也就是说,标准不保证程序接下来会发生什么。

有时候它碰巧没出问题,因为那段内存恰好没被占用;更多时候,它会覆盖相邻变量的地盘,把别人的值改得面目全非,然后程序在一个完全不同的地方暴雷;还有可能到即将结束时才崩溃,让你根本想不到问题出在这一句。最坑的地方是:它不一定立刻报错,所以排查特别难。

我的建议是:每写一个涉及下标的循环,先在心里把边界过一遍。访问最后一个元素的时候,下标是 n-1,循环条件一般是i < n,千万不要顺手写成i <= n。比赛和练习里大量死活找不到的 bug,最后揪出来都是越界访问,真的不夸张。

5.2 数组大小到底能不能是变量:标准与扩展的拉扯

不少初学者看别人写过:

int n; cin >> n; int arr[n];

这段代码在某些编译器上确实能跑通,于是很多人以为数组大小可以用变量。严格来说,这是变长数组(VLA)扩展,不是标准C++保证的行为。同一段代码换一台机器、换一个编译器版本,很可能直接编译失败,或者运行结果变得诡异。

零基础阶段建议直接绕开这个坑。想要稳定的做法,要么声明一个足够大的固定数组,只使用前面 n 个元素:

const int MAXN = 10005; int arr[MAXN]; // 最多能装10005个int cin >> n;

要么继续往前走,学 std::vector,用它来应对动态长度需求。与其赌“我这台机器能过”,不如一开始就写标准、可移植的代码。养成这个习惯,以后换环境才不会摔跟头。

5.3 所有元素输出“不对”:初始化与越界的连锁反应

分享一个很常见的现场。有同学写:

int arr[10]; for (int i = 1; i <= 10; ++i) { arr[i] = i * i; } for (int i = 0; i < 10; ++i) { cout << arr[i] << ' '; }

表面上看循环是从1到10,似乎只是“少了一个数”,实际却是一连串错位:i=1 时给 arr[1] 赋值,arr[0] 始终是没初始化的垃圾值;i=10 时又越界写到了 arr[10],那一段内存根本不是数组的地盘。输出时从0开始打印,第一个数就是垃圾,数组末尾又缺了数据,整个结果怎么看怎么不对。

遇到所有元素都不对的情况,第一反应就是检查循环的下标起点和终点。想要数组从0号开始放数,循环就从0开始;结束条件就写i < 10,不要为了“凑10次”写成i <= 10。这种错位往往不影响编译,跑起来也不报错,但结果就是不对,排查时盯着下标看就对了。

5.4 常见问题速查表:现象、原因、处理办法

现象可能原因处理办法
输出一串莫名其妙的巨大数字声明后没初始化,数组里是垃圾值声明时写int arr[10] = {0};,或先循环逐个赋值
程序运行中其他变量被乱改,或崩溃下标越界,写到了数组外检查所有 for 边界,最大下标必须是 n-1
函数里 sizeof(arr) 结果是8数组参数退化成指针不要在函数里用 sizeof,把长度当作参数传进去
函数里改了数组,外面跟着变正常现象,传的是首地址如果不想让函数修改,参数加 const
用变量当数组大小编译不过变长数组不是标准C++用 const 常量定义大小,或改学 std::vector
平均分总是整数,小数丢了int 除以 int 结果是整数先乘 1.0 再除,比如1.0 * sum / n

调试一维数组还有一个土办法很有效:不要盯着整个大数组看,先用一个长度为5的小数组复现问题,多打印几次中间结果,比如在循环里输出当前下标和对应值。如果怀疑越界,就把循环变量 i 打印出来。对比“输入阶段对不对”“运算阶段对不对”两个环节,问题能快速缩小到具体某一行。这种分段定位的思路,比对着屏幕空想要高效得多。

最后说点我自己的体会。教过一位零基础的同学,数组这一节他卡了三天,问题全出在下标上:要么循环从1开始导致第一个元素被跳过,要么结束条件多算一次导致越界。后来他按一个笨办法练:每次写数组循环前,先在纸上列出 n=3 时 i 的全部取值,再把 i 代入每个下标算一遍。这个动作看起来慢,却让他的准确率一下子上来了。数组这块真的没有捷径,把遍历、求和、找最值、冒泡排序这四个动作亲手敲到闭着眼睛都能写,后面的二维数组和字符串就会顺得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 17:20:24

JT809联调工具V1.2.5实战:从登录到车辆动态信息交换的避坑指南

简介&#xff1a;本资源为JT809联调工具V1.2.5&#xff0c;面向福建省部标809联调场景下的下级平台开发与测试人员&#xff0c;用于验证上行数据报文解析的正确性。工具可直观查看省平台接收到的数据解析情况&#xff0c;便于联调人员比对解析结果与下级平台上报数据是否一致&a…

作者头像 李华
网站建设 2026/10/20 17:20:24

基于STM32G474的400W微型逆变器设计与并网控制实现

想折腾一块能真正并网发电的小功率逆变器&#xff0c;又不想在驱动、保护、控制算法上走太多弯路的话&#xff0c;把目光放在“STM32G474 400W微型逆变器”这套组合上&#xff0c;是个非常务实的切入点。这套方案覆盖了从光伏组件直流输入到电网工频交流输出的完整链路&#x…

作者头像 李华
网站建设 2026/10/20 17:19:20

NLP别名发现实战:字符n-gram与多通道相似度检索的工程落地

做NLP越久越会发现&#xff0c;很多任务看似简单&#xff0c;真正落地的时候全是细节。今年年中某个业务方向忽然提了个需求&#xff1a;把“同一实体的不同叫法”自动找出来。当时我盯着需求文档想了很久&#xff0c;这不就是别名发现模型吗&#xff1f;于是就有了这篇文章里说…

作者头像 李华
网站建设 2026/10/20 17:19:12

行式存储与列式存储:从原理到选型的性能差异解析

1. 当你的查询慢到怀疑人生&#xff1a;先搞清楚数据是怎么躺的 做数据相关工作的朋友&#xff0c;几乎都遇到过这种场景&#xff1a;一张几亿行的宽表&#xff0c; SELECT * 飙得飞快&#xff0c;但一执行 SELECT AVG(某列) WHERE 某日期范围 就慢到像在等宇宙重启。你优化…

作者头像 李华
网站建设 2026/10/20 17:19:06

AI故障预测系统告警策略设计:告别告警轰炸,让预警真正被响应

做 AI 故障预测系统&#xff0c;我前后在几个不同规模的业务场景里落过地。最尴尬的不是模型精度不够&#xff0c;而是预警发出去之后&#xff0c;值班群里的反应从“赶紧看一下”慢慢变成“又是它啊&#xff0c;先放着吧”。我亲眼见过一个内部系统&#xff0c;一天往值班群里…

作者头像 李华
网站建设 2026/10/20 17:18:37

Harness Engineering深度解析:Agent工具与框架的六维对比

1. 先搞清楚Harness Engineering到底在管什么做了三年多Agent落地&#xff0c;我现在判断一个方案是否成熟&#xff0c;不看模型指标榜单&#xff0c;先看包围它的那层工程设施。这层设施业内有很多叫法&#xff0c;但目前听得最多的是Harness Engineering——你可以把它理解成…

作者头像 李华