上一篇我们学会了循环程序终于可以不知疲倦地重复干活了。但这时候一个尴尬的问题浮出水面如果我要存储一个班 50 个学生的成绩难道要定义score1, score2, score3 ... score50一共 50 个变量吗真要这样写别说打键盘的手指会抗议光是给变量起名字就能把人逼疯。幸好C 语言的创造者们早就想到了这一点——他们给了我们数组。数组能让你用一个名字管理一整排数据。它是几乎所有高级数据结构链表、栈、队列、哈希表……的根基。今天我们就先拿下它最基础的形式一维数组。一、数组是什么一组连续排列的抽屉如果变量是单个抽屉数组就是一整面连续排列的抽屉墙。所有抽屉共享一个名字数组名每个抽屉通过编号下标来区分。比如一个能装 5 个整数的数组在内存里长这样下标: [0] [1] [2] [3] [4] 内容: ? ? ? ? ?最重要的特征数组在内存中是连续存放的。如果第一个抽屉在地址A每个int占 4 字节那么第二个就在A4第三个在A8依此类推。这个“连续性”是数组高效的根本原因也是我们以后理解指针和数组关系的钥匙。二、声明与初始化数组1. 声明数组类型 数组名[大小];比如intscores[50];// 50 个 int下标 0~49floattemperatures[7];// 7 个 float存一周的温度charname[20];// 20 个 char存一个名字大小必须是一个常量表达式编译时就能确定的值。在 C89 里大小必须是整型常量C99 引入了变长数组VLA但初学阶段请先用常量大小避免不必要的麻烦。2. 初始化数组在声明的同时给数组赋予初值完全初始化列出所有元素intarr[5]{10,20,30,40,50};部分初始化前几个有值剩下的自动为 0intarr[5]{1,2};// 实际上 {1, 2, 0, 0, 0}省略大小初始化编译器根据初始化列表自动确定大小intarr[]{7,8,9};// 编译器推断大小为 3如果你不初始化局部数组里的值是垃圾值内存残留的随机数据所以务必要养成初始化的习惯。C99 的指定初始化器Designated Initializers也很方便intarr[5]{[0]5,[4]9};// {5, 0, 0, 0, 9}三、访问数组元素通过下标运算符[]来读写元素。下标从0开始到大小-1结束。intarr[5]{10,20,30,40,50};printf(%d\n,arr[0]);// 10printf(%d\n,arr[2]);// 30arr[3]100;// 把第4个元素改成100下标可以是整型变量或表达式这让我们能用循环来批量操作数组发挥它真正的威力。数组遍历循环 下标#includestdio.hintmain(void){intarr[5]{2,4,6,8,10};for(inti0;i5;i){printf(arr[%d] %d\n,i,arr[i]);}return0;}输出arr[0] 2 arr[1] 4 arr[2] 6 arr[3] 8 arr[4] 10结合上一篇文章的循环知识你就能一次性处理整列数据而不是一个个变量去敲。四、数组与内存看清它的本质我们反复说“数组在内存中连续”这到底意味着什么用一个小程序验证一下#includestdio.hintmain(void){intarr[5]{0};printf(%p\n,(void*)arr[0]);printf(%p\n,(void*)arr[1]);printf(%p\n,(void*)arr[2]);return0;}%p打印地址指针。你会发现三个地址依次相差 4sizeof(int)通常是 4。第一个元素地址最低往后依次增高。这个事实非常重要知道首地址 下标就能立刻算出任意元素的地址第 i 个元素地址 首地址 i * sizeof(类型)。这也是为什么数组下标从 0 开始——因为第一个元素就存在首地址本身偏移量为 0。也正是因为连续越界访问不会立即报错它可能悄无声息地踩到其他变量的空间。五、经典案例冒泡排序数组加上循环就能做很多有意义的事。最经典的入门算法之一就是冒泡排序。原理像气泡往上浮每一轮比较相邻的两个元素如果顺序不对就交换它们经过多轮扫描后最大的数被“推”到最后然后对剩下的再重复。#includestdio.hvoidbubble_sort(intarr[],intn){inttemp;for(inti0;in-1;i){// 一共 n-1 轮for(intj0;jn-1-i;j){// 每一轮比较前面未排序的部分if(arr[j]arr[j1]){// 交换两个相邻元素temparr[j];arr[j]arr[j1];arr[j1]temp;}}}}intmain(void){intscores[]{85,92,78,65,88,90};intnsizeof(scores)/sizeof(scores[0]);// 计算数组元素个数printf(排序前: );for(inti0;in;i){printf(%d ,scores[i]);}printf(\n);bubble_sort(scores,n);printf(排序后: );for(inti0;in;i){printf(%d ,scores[i]);}printf(\n);return0;}输出排序前: 85 92 78 65 88 90 排序后: 65 78 85 88 90 92sizeof(scores) / sizeof(scores[0])是计算数组元素个数的常用技巧——总字节数除以单个元素的字节数。但注意这个技巧只能在定义数组的同一个作用域内使用如果数组作为函数参数传入它已经退化成了指针sizeof就不对了这个坑后面讲指针时会详细填上。六、常见错误与陷阱1. 数组越界最恶名昭著intarr[5]{0};arr[5]10;// 错误下标最大为 4C 语言不检查数组下标是否越界。访问arr[5]不会报编译错误运行时可能覆盖相邻变量、破坏返回地址、或者悄无声息地继续运行但结果诡异。这是无数 bug 和安全漏洞的根源。永远确保你的下标在0到大小-1之间。2. 把数组名当成变量赋值intarr1[5]{1,2,3,4,5};intarr2[5];arr2arr1;// 错误数组名不能被赋值数组名代表数组首地址一个常量指针不能作为左值。要复制数组内容需要循环逐个元素赋值或者用memcpy函数。3. 初始化时大小越界intarr[3]{1,2,3,4};// 错误初始值比数组大编译器会报错。4. 用变量做数组大小非 VLA 的情况intn10;intarr[n];// C89 不允许C99 允许但可能有风险如果不确定编译器对 VLA 的支持程度初学者最好用#define定义常量大小。5.sizeof陷阱voidprint_array(intarr[]){intnsizeof(arr)/sizeof(arr[0]);// 错误arr 已退化为指针}在函数参数中arr实际上是指针sizeof(arr)返回指针大小4 或 8 字节而不是整个数组的大小。必须额外传递数组长度。七、小结数组是 C 语言中第一个真正意义上的“数据结构”。它让你能用 O(1) 的时间随机访问任何元素代价是内存连续分配、大小固定。一维数组是基础二维数组、字符数组字符串都将建立在这个概念之上。现在你已经能处理一组数据了。但是如果数据是二维的表格怎么办比如一个矩阵或者一个班级的多门成绩下一篇我们就来看看多维数组把一维的“排”扩展成二维的“矩阵”甚至更高维度的“立方体”。课后小练习定义一个包含 10 个整数的数组用循环让它存储 1 到 10 的平方值i*i然后再用循环倒序输出从第 10 个到第 1 个。让用户输入 5 个整数存入数组然后找出其中的最大值和最小值并输出。用自己的代码实现一个数组的逆置reverse原数组[1,2,3,4,5]变成[5,4,3,2,1]不允许使用第二个数组。小挑战在冒泡排序的基础上增加一个优化如果某一轮比较没有发生任何交换说明数组已经有序可以提前结束。改写bubble_sort函数来实现它。我们下期见获取本系列示例代码请访问 GitCode 仓库。