尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C++一维数组全攻略:初始化、遍历、排序与避坑指南

发布时间:2026/9/26 5:42:23

资讯中心
01
ARTICLE

C++一维数组全攻略:初始化、遍历、排序与避坑指南

C++一维数组全攻略:初始化、遍历、排序与避坑指南
刚开始学C的时候多数人接触的第一个数据结构就是一维数组。可这家伙看起来简简单单实际上暗坑不少——数组名到底是不是指针为什么函数里算不出数组长度为什么明明给了5个位置我写a[5]也没报错这些问题我在带新人的时候几乎每批都会遇到。这篇文章就把一维数组从定义、初始化、遍历到求和、最值、逆序、查找、统计、排序这些常见操作一次性讲透顺带把几个高频的坑也摆出来。适合刚学完C基础语法、想系统搞定数组的同学也适合打算复习、想补细节漏洞的选手。1. 先从内存视角看一维数组它到底是个什么玩意1.1 数组的本质连续内存加同类型元素你可以把一维数组想象成一排紧挨着的储物柜。柜子一个接一个编号从0开始你在哪一号柜子就用下标去找。C的数组就是一个这样的连续内存区域里面每个元素类型相同整块区域占用的总字节数 元素个数 × 单个元素字节数。举个例子int a[5];这里每个int通常是4字节那么这块区域总共就是20字节。编译器在编译期间就能确定这20字节的位置和大小所以数组访问特别快要拿a[3]它不需要像链表那样从头一个个找直接根据首地址 3 × 4字节算出位置一步到位。这也是很多教程强调数组支持随机访问的原因。理解连续内存这一点后面指针遍历、数组传参退化成指针这些现象就都好解释了。1.2 定义数组的语法以及那个常量表达式要求定义一维数组的标准语法是类型 数组名[元素个数];比如int scores[60]; // 60个int下标从0到59 double weights[10]; // 10个double char name[20]; // 20个char这里有个C初学者最容易困惑的点元素个数必须是编译期就能确定的常量表达式普通变量不行。int n; cin n; int arr[n]; // 换VS或clang直接报错GCC可能能编过但不是标准C为什么标准不允许因为C的静态数组要编译器在编译阶段就知道它占多少内存、怎么安排栈帧如果长度靠运行时输入才能确定编译器就没法在编译期安排好。那很多人在GCC编译器下写int arr[n]也能跑是因为GCC把C语言里的变长数组VLA扩展带到了C里但这玩意不属于C标准换一个编译器就可能编不过跨平台项目里千万别依赖它。真要运行期确定长度用std::vectorint arr(n);才是正道或者用new[]动态分配。数组本身定位就是固定大小、编译期确定这个特性很关键。定义完数组后每个元素的值是未初始化的内置类型int、double这些在局部数组里是垃圾值所以定义完通常要初始化。2. 一维数组初始化{} 与 的区别、自动推断、sizeof的陷阱2.1 几种常见初始化写法以及它们的行为写数组初始化最稳的方式就是大括号初始化列表。下面几种都是合法写法int a[5] {1, 2, 3, 4, 5}; // 全部显式初始化 int b[5] {1, 2}; // 后面的3个元素自动补0 int c[5] {}; // 5个元素全部为0 int d[] {1, 2, 3}; // 编译器根据大括号数量推断长度为3 int e[5]{1, 2, 3}; // C11起可以省略等号这里有几个初学者容易搞错的行为我展开说说。第一种int b[5] {1, 2};这种情况剩下的位置不是保留随机值而是被值初始化成0。也就是说数组元素只会是0、1、2这三种值。这是标准规定和很多人直觉里初始化了前两个后面谁知道是啥完全相反。第二种int d[] {1, 2, 3};数组长度3这是自动推断。一旦用了这种写法数组长度就由初始化列表里的元素个数决定了你不能再指望它变成其他长度。第三种C11开始推荐的大括号初始化int e[5]{1, 2, 3};和int e[5] {1, 2, 3};几乎等价区别在于大括号初始化是列表初始化遇到窄化转换比如把double塞给int数组会直接报错等于帮你提前拦截了一批潜在问题。2.2 用 sizeof 算数组长度为什么推荐什么时候失效想要数组里有多少个元素一个经典写法是int arr[] {10, 20, 30, 40, 50}; size_t len sizeof(arr) / sizeof(arr[0]); // 结果是5原理很简单sizeof(arr)是整个数组占的字节数20sizeof(arr[0])是单个元素占的字节数4两者一除就是元素个数。但是在函数参数里这个写法会失效。原因是数组作为实参传递时会退化成指针函数里拿到的arr其实是个int*这时候sizeof(arr)得到的是指针本身的大小64位机器上是8除以sizeof(int)4就变成了2完全不是数组长度。这个问题面试爱问实际编写代码时也特别容易踩后面第6章会专门展开。C17之后标准库提供了更方便的std::size()在iterator头文件里#include iterator int arr[] {1, 2, 3}; size_t len std::size(arr); // 依然要求arr是真正的数组不是指针如果你用的是std::array那直接调arr.size()就更省事了。不过教学阶段我建议还是先把sizeof的原理吃透知道它什么时候能用、什么时候不能用比单纯记住一个API重要得多。3. 数组遍历的三种主流写法到底该选哪一种3.1 经典 for 循环加下标最直观也最灵活遍历数组最经典的方式就是下标循环int arr[] {5, 3, 8, 1, 9}; size_t len sizeof(arr) / sizeof(arr[0]); for (size_t i 0; i len; i) { cout arr[i] ; }为什么下标从0开始因为arr[i]的本质是首地址往后偏移 i 个元素的位置第一个元素不需要偏移所以是0。很多初学者会在这纠结其实想通偏移量就释然了。这种写法的灵活性在于你完全控制下标可以逆序遍历、每隔一个取一个、知道当前处理到第几个元素。比如逆序输出for (int i len - 1; i 0; --i) { cout arr[i] ; }需要同时知道元素值和它下标场景比如找到第一个大于10的元素并输出它的位置就只能用下标循环。3.2 范围 forC11 时代最舒服的只读遍历C11开始遍历数组有了新姿势for (int x : arr) { cout x ; }它读起来和写起来都干净不需要手动算长度也不容易写越界。这里有个非常重要的细节范围for循环里的x是元素的拷贝你改x不会改到原数组。想修改数组元素必须用引用for (int x : arr) { x * 2; }这一句int极其容易漏。很多新手写完范围for想给数组每个元素翻倍结果原数组纹丝不动就是因为忘了加引用。同样遍历大型对象或不想拷贝时也要用const int来避免复制开销。范围for的局限也很明显它拿不到下标而且它要求遍历的对象必须是能确定首尾范围的真实数组或容器。如果你把数组传给函数函数里的参数已经退化成了指针那for (int x : arr)在函数里是编不过的。这一点到第6章再细说。3.3 指针遍历理解 arr[i] 和 *(arr i) 的关系C里数组和指针的关系千百年来都是重点。最核心的一条是数组名在大多数表达式中会隐式转换成指向首元素的指针。也就是说int arr[] {2, 4, 6, 8}; int* p arr; // 数组名退化成指针等价于 int* p arr[0];有了这个基础arr[i]本质就是*(arr i)。编译器碰到arr[i]时实际上是当成从首地址偏移 i 个元素后解引用来处理的。所以你可以用指针遍历for (int* p arr; p arr 4; p) { cout *p ; }这里p从头走到尾条件p arr 4里的arr 4指向数组最后一个元素的后面一个位置循环条件是安全的。但要注意数组名虽然经常变成指针它本身不是指针。最经典的证据就是sizeof(arr)返回数组总字节数而sizeof(指针)只返回指针大小。另外arr是非法的因为数组名不是可修改的左值你不能让它指向别处。指针可以数组名不行。至于性能下标访问、范围for、指针遍历经过现代编译器优化后生成的机器码几乎没差别。所以选哪种主要是看可读性和场景要下标用下标循环只遍历不改就用范围for研究底层或为学指针做铺垫就用指针。4. 四个高频操作求和、最值、逆序、查找再加一个频次统计4.1 求和与平均分从一次成绩统计说起假设你有一组学生成绩想算总分和平均分。最直接的做法是for循环累加#include iostream using namespace std; int main() { int scores[] {78, 92, 66, 85, 59, 73}; size_t len sizeof(scores) / sizeof(scores[0]); int sum 0; for (size_t i 0; i len; i) { sum scores[i]; } double avg static_castdouble(sum) / len; cout 总分: sum endl; cout 平均分: avg endl; return 0; }几个要点sum的初始值必须是0不然累加结果就是垃圾值计算平均分时用static_castdouble(sum) / len而不是sum / len否则整数除法会把小数部分直接丢掉。这是C里除法的一个经典细节两个int相除结果还是int商向下取整。4.2 找最大值和最小值擂台法以及初始值怎么定找最大值的过程很像打擂台先假设第一个人是擂主然后让后面的每个元素上来比谁大谁当擂主。int maxVal scores[0]; for (size_t i 1; i len; i) { if (scores[i] maxVal) { maxVal scores[i]; } }这里有个关键习惯maxVal直接初始化为scores[0]而不是0或INT_MIN。为什么如果你初始化为0而数组里全是负数比如{-5, -3, -8}那么最大值会被错误地算成0因为没有任何元素大于0。初始化为第一个元素天然避开了这种边界问题。标准库里有现成的std::max_element可以一行搞定但在学习阶段我强烈建议先手写个几十遍。道理很简单手写能逼你理解比较逻辑和状态更新这才是算法素养的底子。等理解了再用库函数效率才真正属于你。4.3 数组逆序双指针原地交换把一个数组反过来最常见的是双指针从两端往中间走边走边交换int arr[] {1, 2, 3, 4, 5}; size_t n sizeof(arr) / sizeof(arr[0]); int left 0, right n - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; --right; }交换两个元素必须经过临时变量这一点新手很容易写成arr[left] arr[right]; arr[right] arr[left];结果两个位置都变成了原来的arr[right]数据丢失。记住口诀交换三个动作一存、二写、三回填。当然你也能用std::swap(arr[left], arr[right])标准库帮你封装好了但底层依然是这三个动作。循环继续的条件是left right当数组长度为奇数时中间那个元素不用和自己交换偶数时左右指针会在中间相遇后停住整个数组正好完成逆序。4.4 线性查找找到第一个符合条件的元素就停在一维数组里找一个值线性查找是最朴素的思路从头扫到尾找到了就返回下标全找完还没有就返回 -1。int findIndex(int arr[], size_t len, int target) { for (size_t i 0; i len; i) { if (arr[i] target) { return i; } } return -1; }调用处就可以根据返回值判断等于-1就是没找到否则就是找到了并且下标是多少。如果想找所有出现的位置那就别急着 return而是把符合条件的下标存到另一个数组里int positions[100], posCount 0; for (size_t i 0; i len; i) { if (arr[i] target) { positions[posCount] i; } }这个套路在后续很多题目里都会出现一个数组存原始数据一个数组存结果。理解了收集-后处理这种模式后面做去重、筛选就轻松很多。4.5 频次统计桶计数思想的入门如果数组元素的值域有限比如成绩在0到100之间那可以用桶来统计每个分数出现了多少次int scores[] {78, 92, 66, 85, 59, 92, 78}; size_t n sizeof(scores) / sizeof(scores[0]); int cnt[101] {0}; // 下标0~100初始全0 for (size_t i 0; i n; i) { cnt[scores[i]]; } // cnt[78]就是78分出现的次数cnt[92]就是92分出现的次数桶计数的核心思想是用值当下标把出现次数存进去。这里必须保证cnt的容量比最大值大1否则cnt[100]就越界了。这个思路后面还会延伸到哈希表、计数排序一维数组阶段先把这个以空间换时间的模型建立起来。5. 手撕冒泡排序排序代码里藏着的数组传参陷阱5.1 冒泡排序原理和完整实现排序题在数组练习里属于必修课其中冒泡排序最适合拿来理解双重循环 相邻比较 交换。它的原理用一句话总结每一趟把相邻元素中较大的往后移动就像气泡往上冒一趟下来最大的数沉到末尾。void bubbleSort(int arr[], int len) { for (int i 0; i len - 1; i) { // 共 len-1 趟 for (int j 0; j len - 1 - i; j) { // 每趟比较范围逐渐缩小 if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }为什么内层循环的结束条件是len - 1 - i因为第0趟结束后最大的元素已经到了arr[len-1]下一趟就没必要再碰它第1趟结束后第二大的到了arr[len-2]。每一趟都能把一个当前最大值固定到末尾所以内层范围随着i增大逐步缩小。如果某一趟完全没有发生交换说明数组已经有序可以提前结束这叫冒泡排序的优化版bool swapped true; for (int i 0; i len - 1 swapped; i) { swapped false; for (int j 0; j len - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped true; } } }提前退出对已经有序或接近有序的输入能省下不少时间这种根据运行状态判断是否提前结束的思路也是工程里很常见的优化手段。5.2 函数传参时数组长度为什么必须单独传注意上面bubbleSort的函数签名我特意写了void bubbleSort(int arr[], int len)第二个参数len必不可少。原因还是数组退化为指针。当数组作为实参传进函数时编译器不会把整个数组复制一份过去而是传递首元素的地址。所以在函数内部int arr[]这个形参看起来像个数组实际上等价于int* arr。你用sizeof(arr) / sizeof(arr[0])去算长度得到的根本不是数组长度而是(指针字节数) / (int字节数)在64位系统上大概率是2直接导致排序只处理前面一点点元素。所以规范做法是在调用处算好长度再把长度一起传进去。int main() { int arr[] {5, 2, 9, 1, 7}; int len sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, len); // 正确 }如果想在函数内部安全处理数组又不想手动传长度更现代的做法是用std::array或std::vector它们自带长度信息。这个对比放到第6章一起说。5.3 用随机数造一组测试数据为了测试排序代码手写一组数据不够刺激最好让程序自己生成随机数据。C里生成随机数的传统写法是rand()#include cstdlib #include ctime #include iostream using namespace std; int main() { srand(static_castunsigned(time(nullptr))); // 用当前时间做随机种子 int arr[10]; for (int i 0; i 10; i) { arr[i] rand() % 100; // 生成0~99的随机数 } cout 排序前: ; for (int x : arr) cout x ; cout endl; bubbleSort(arr, 10); cout 排序后: ; for (int x : arr) cout x ; cout endl; return 0; }rand() % 100生成一个0到99之间的整数如果要把分数区间变成0到100就用rand() % 101。给srand加时间种子是因为rand()内部是伪随机序列不换种子的话每次运行结果都一样。C11之后还有更先进的random库比如mt19937不过作为一维数组学习的辅助工具用rand()顶住完全够用等以后深入再过渡。写程序时记得在排序前后分别打印一次数组眼见为实排没排对一目了然。这种打点输出也是以后排查程序问题最基本的手段。6. 一维数组那些一年后才踩明白的坑6.1 数组越界C 为什么不帮你检查int arr[5]的合法下标是0到4你写arr[5]、arr[-1]编译器多数情况下都不报错程序运行起来也可能看起来正常。但这是典型的未定义行为你实际上访问的是数组边界之外的内存那里可能存着别的变量、别的对象甚至不可读写的内存页。运气好程序没反应运气不好某个变量莫名其妙被改坏或者程序直接崩溃。我见过一个真实案例一个人定义了int a[5]紧跟着定义一个int count 0循环里误写了a[5] 1结果 count 被改成1程序行为变得诡异排查了半小时才发现是越界写入。怎么防第一写循环时多检查边界条件尤其是和的边界第二需要安全访问时可以考虑std::arrayint, 5 a;然后用a.at(5)at()会抛异常第三调试时可以开AddressSanitizer之类的工具帮你捕捉越界。总之别指望C自动帮你兜底它默认相信程序员。6.2 数组参数退化成指针长度信息是怎么丢的这一节值得反复强调因为它和排查问题直接相关。看这两段代码void func1(int a[]) { cout sizeof(a) / sizeof(a[0]); // 错以为能拿到数组长度 } void func2(int* a) { cout sizeof(a) / sizeof(a[0]); // 同样错但至少知道a是指针 }func1里的int a[]和func2里的int* a是完全等价的写法编译器不做任何区分。数组长度信息在函数调用那一刻就丢掉了函数只知道从这块地址开始往后读取int。所以在函数里想正确遍历数组要么靠调用方传入长度要么用引用方式传递数组语法比较绕不建议新手先学要么改用STL容器。理解这个退化机制你就明白为什么bubbleSort必须带len参数也明白为什么范围for在函数参数里会编译失败——因为它需要知道数组的边界而数组参数已经只剩指针了。6.3 返回函数内的局部数组这是悬垂指针陷阱函数里定义一个局部数组然后把这个数组名返回给调用方能不能行看起来能实际是灾难int* makeArray() { int arr[3] {1, 2, 3}; return arr; // 返回的是指向栈内存的指针 }当函数返回时arr所在的那块栈内存就被回收了这块内存之后可能被其他函数调用覆盖。调用方拿到的指针变成了悬垂指针解引用它读到的值是随机的、不稳定的。正确做法有几种在函数内部用new[]动态创建数组返回指针然后记得在外部delete[]或者直接返回std::vectorint让容器自己管理内存。对初学者来说我的建议是需要函数生成一组数据直接用 vector 返回安全省心。原生数组在函数间传递要么传指针加长度要么修改变量而不返回新数组。6.4 字符数组和字符串末尾那个 \0 不能忘char数组是一种特殊的一维数组因为C风格字符串由它承载。一个常见错误是char str[5] hello; // hello实际占6个字节因为末尾有\0数组长度5装不下6个字符编译器可能报错也可能只警告但运行时就可能越界。char str[] hello;才是稳妥写法它会自动把数组长度定为6。C风格字符串函数如strlen、strcpy都依赖末尾的\0来判定字符串结尾如果数组里没有\0这些函数就会一直向后读取直到某处碰巧遇到0字节行为完全不可预测。初学阶段字符串默认用std::string。但一旦你理解了一维数组就很容易理解std::string底层不过是一段连续内存加长度信息反过来看懂C风格字符串也能让你在调试、对接C库时更从容。6.5 更现代的替代方案std::array 和 std::vector原生数组的槽点很明显不知道自己的长度、容易退化成指针、越界不检查。C11之后大家更推荐这两个替代品特性原生数组std::arraystd::vector大小是否固定固定固定动态是否知道自己的长度不知道知道知道是否检查越界不检查.at()可检查.at()可检查能否直接返回不能可以可以与C接口互操作直接退化成指针.data()获取指针.data()获取指针内存位置栈局部栈堆std::arrayint, 5 a;长度在编译期固定行为像原生数组但带有STL的便利std::vectorint v;长度动态可变是工程中最常用的线性容器。那原生数组还要不要学当然要。它是理解内存布局、指针运算、函数传参退化的基础也是嵌入式、底层C接口等场景里躲不开的写法。只是在实际开发中非性能敏感的核心路径优先用vector就够了。7. 四道拿来练手的一维数组练习题光看不练等于白看。这四道题是我认为最有代表性的入门题覆盖了定义、遍历、越界、去重、移位等多个关键点每道题都给了思路提示和核心片段建议你关掉教程自己完整写一遍。7.1 斐波那契数列前20项存入数组再输出斐波那契数列的递推关系是前两项为1从第三项开始每一项等于前两项之和。int fib[20] {1, 1}; for (int i 2; i 20; i) { fib[i] fib[i - 1] fib[i - 2]; } for (int i 0; i 20; i) { cout fib[i] ; }这道题的主要练习点是用循环基于数组前面已计算好的值推导后面的值。注意fib[20]下标范围是0到19千万别把循环写成i 20那样fib[20]就越界了。7.2 成绩统计输出最高分、最低分、平均分输入若干个学生成绩存入数组然后分别统计最高分、最低分和平均分。可以用固定数组上限比如假设最多100个学生int scores[100]; int n; cin n; // 实际学生人数 for (int i 0; i n; i) cin scores[i]; int maxScore scores[0]; int minScore scores[0]; int sum 0; for (int i 0; i n; i) { if (scores[i] maxScore) maxScore scores[i]; if (scores[i] minScore) minScore scores[i]; sum scores[i]; } cout 最高分: maxScore endl; cout 最低分: minScore endl; cout 平均分: static_castdouble(sum) / n endl;这里的隐藏考点是输入人数n时必须先保证n不超过100否则再往scores[100]后面的位置写就是越界了。工程里经常说的输入校验就在这种小细节里体现。7.3 去重把数组中的重复元素处理掉给定一个数组比如{3, 1, 4, 1, 5, 9, 2, 6, 5, 3}要求只保留第一次出现的元素输出不重复的序列。思路是用两层循环外层逐个取元素内层检查这个值在前面是否已经出现过。可以先把结果存到另一个数组里int src[] {3, 1, 4, 1, 5, 9, 2, 6, 5, 3}; int dst[10]; int dstLen 0; for (int i 0; i 10; i) { bool duplicate false; for (int j 0; j dstLen; j) { if (dst[j] src[i]) { duplicate true; break; } } if (!duplicate) { dst[dstLen] src[i]; } }空数组dst的长度在可去重过程中动态增长这个dstLen就是结果数组实际有效长度的经典用法。这道题练的是两层循环配合标志位是后续很多过滤类操作的雏形。7.4 循环右移 k 位一个细节拉满的题目把一个数组循环右移 k 位比如{1, 2, 3, 4, 5}右移2位变成{4, 5, 1, 2, 3}。看着简单做起来有几个容易错的点。第一k可能大于数组长度所以先取k % n;。第二移动时要处理好覆盖问题不能直接一个个挪否则元素会丢。最直观的方法是开一个临时数组存结果int arr[] {1, 2, 3, 4, 5}; int n 5, k 2; k % n; int tmp[5]; for (int i 0; i n; i) { tmp[(i k) % n] arr[i]; } for (int i 0; i n; i) { arr[i] tmp[i]; }这里(i k) % n是循环移位的核心公式搞懂这个取模运算类似题就通了大半。如果想要原地移位、不开临时数组可以用三次反转法那是进阶版本先掌握了临时数组版本再说。这四道题一路做下来一维数组的定义、初始化、遍历、边界控制、双指针、标志位、取模运算这些基本功基本都能过一遍。如果你能不看答案把这几道题独立写对那这一章的内容算是真正吸收了。最后分享一个我自己的习惯写数组相关代码时把数组长度单独拎出来作为一个变量不要在循环里反复写魔法数字这样越界概率能低一大半。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。