本篇博客将正式进入数组的学习,到这里,这个章节其实像披着虎皮的猫,看似复杂,其实多为逻辑理解上的问题,只需要理解数组的概念和数组在实际开发中的运用就好了,至于二维以上的数组,我们只需要做大概的了解,当我们理解了一维和二维数组后,我们自然就会理解多维数组,希望这篇博客能帮助大家对数组有个更深入的了解,下面让我们开始今天的数组学习吧先问你个问题:现在要存一个班 20 个人的数学成绩,你打算怎么办?
方案 A:
intscore1,score2,score3,/* …… */,score20;写到score18的时候你大概已经忘了score12是谁的。期末统计平均分?你得写 20 行sum += scoreN;。这简直是灾难。
数组就是来救你的——一个名字,一排柜子:
intscore[20];// 20 个抽屉,编号 0~19名字起好了,剩下的交给下标。今天这篇就把这个"抽屉柜"彻底讲明白,顺带把几个让新同学头秃的坑(乱码、sizeof和strlen打架、二维数组的真相)一次填完。
第一部分:数组的基本修养
1.1 怎么创建
语法就一句话:
类型 数组名[常量值];intmath[20];// 20 个 intcharch[8];// 8 个 chardoublescore[10];// 10 个 double[]里的常量值就是柜子抽屉的数量,定死了以后不能改。
1.2 初始化:往柜子里塞东西
完全初始化——抽屉数可以不写,编译器自己数:
intarr[5]={1,2,3,4,5};intarr[]={1,2,3,4,5};// 效果完全一样,size 还是 5不完全初始化——这个是新手最爱忘的:
intarr2[6]={1};// 实际内容:{1, 0, 0, 0, 0, 0}没给值的元素,编译器会默默帮你补 0。就跟新房交付带了个空冰箱一样,虽然你没放东西,但它不是"随机残留物",是干净的 0。
翻车现场:
intarr3[3]={1,2,3,4};// 抽屉只有 3 个,你塞了 4 个编译器当场翻脸,但翻脸方式因编译器而异:
- MSVC(VS2022/2026):直接error(C2057: too many initializers),代码根本编译不过
- GCC:只给warning(excess elements in array initializer),照样给你编出一个能跑的程序
看到这儿有人要问了:不是"少的补 0"吗,多的怎么就拦?
少的补,多的拦。补 0 是编译器大方,初始化项比抽屉还多属于根本装不下,必须拦你。
⚠️顺手提醒:GCC 只给 warning 才是真正的坑——代码"能跑",但
arr3里实际只有{1,2,3},那个 4 被悄无声息地丢掉了,编译器只是提醒你一声。别把 warning 当空气。同一个错误,MSVC 直接编译失败反而更安全。
1.3 下标:从 0 开始,不是从 1
这是数组使用频率最高的一个坑。
intarr[10]={1,2,3,4,5,6,7,8,9,10};// 值: 1 2 3 4 5 6 7 8 9 10// 下标: 0 1 2 3 4 5 6 7 8 9printf("%d\n",arr[7]);// 输出 8,不是 7说人话就是:arr[i]里的i是"从门口数第几格",不是"第几个"。你家门牌号从 0 开始写,数组也一样。
访问用[],它叫下标引用操作符:
arr[7]=0;// 也能改1.4 遍历:让 for 循环替你数
for(i=0;i<10;i++)printf("%d ",arr[i]);一个数组元素的访问靠一个下标,遍历全部元素的办法就是把 0~n-1 这些下标全造出来。循环一学,这块就通了。
1.5 别再写死 10:sizeof帮你数元素个数
以后数组大小万一变了,满篇i < 10就全错了。教你一个能用到下辈子的写法:
intarr[10]={0};printf("%zu\n",sizeof(arr));// 40 整个数组占多少字节printf("%zu\n",sizeof(arr[0]));// 4 一个元素占多少字节printf("%zu\n",sizeof(arr)/sizeof(arr[0]));// 10 元素个数原理很简单:所有元素类型都一样,拿总字节数除以单个元素的字节数,不就是个数吗。
所以这行请背下来:
intsz=sizeof(arr)/sizeof(arr[0]);1.6 数组也是有类型的
把数组名去掉,剩下的就是它的类型:
intarr1[10];// 类型是 int[10]intarr2[12];// 类型是 int[12]charch[5];// 类型是 char[5]所以int a[10]和int b[12]压根不是一个类型,不能随便互相赋值。这个在函数传参时会炸雷,等指针那章我们再回来算账。
不信?sizeof(int[10])= 40,sizeof(char[5])= 5 —— 类型里连长度都写死了。
第二部分:新手重灾区——'\0'的恩怨情仇
2.1 两行代码,差一个字符
chararr1[]={'a','b','c'};chararr2[]="abc";你:这俩能有啥区别?
编译器:不,你不能。
来看内存里到底躺了什么(实测十六进制):
arr1: 61 62 63 → 'a' 'b' 'c' 就 3 个字节 arr2: 61 62 63 00 → 'a' 'b' 'c' '\0' 4 个字节!用双引号写字符串,编译器会偷偷在末尾塞一个'\0';用大括号一个一个写,它不管你。
'\0'是什么?ASCII 码 0,一个值为 0 的字符。它是 C 语言里字符串的结束标志——你可以理解成字符串队伍最后那个举着小旗的"哨兵"。
2.2 为什么用%s打印arr1会出乱码?
%s的工作流程:从数组第一个字节开始,一个一个往外吐,碰到'\0'才收工下班。
arr2有'\0'→ 打印"abc",干干净净。arr1没有'\0'→ printf 得一直找下去,找过 arr1 的边界,踩进别人家的内存→ 乱码。
来看实测:
arr1 用 %s 打印: [abc] arr1 内存: 61 62 63 00 arrB 内存: 61 62 63 61 62 63 00 07 00 00看出名堂了吗?arrB只有 3 个字节合法(61 62 63),但它后面紧挨着编译器放的arr1(又一份61 62 63),再往后才是00。
于是%s一路读过去,打印出了abcabc。
敲过这个坑的举手——是不是觉得"咦,好像也没乱码啊"?
那是运气,不是本事。换个编译器、换个栈布局、换个变量声明顺序,它可能读到的是别的垃圾,也可能直接段错误崩给你看。你今天的"没出事",只是内存布局恰好帮了你。
2.3strlen也得跟着遭殃
printf("%zu\n",strlen(arr1));// 3printf("%zu\n",strlen(arrB));// 我这儿是 6,你那儿是几?不知道,全看运气strlen的逻辑跟%s一模一样:从头数到'\0'为止。arrB里没有'\0',它就一直数、一直数,越界访问。
记住这句话:
char arr[] = {'a','b','c'}这种写法里装的是三个字符,不是字符串。想存字符串就用双引号,别自找麻烦。
2.4sizeof和strlen:相爱相杀两兄弟
这两个被搞混的频率高得离谱。说人话就是——它们回答的根本不是同一个问题。
sizeof | strlen | |
|---|---|---|
| 身份 | 操作符(关键字,编译器认识) | 库函数(要#include <string.h>) |
| 问的问题 | 这块内存占了多大地方? | 这个字符串有几个字符? |
| 单位 | 字节 | 个数 |
| 会不会越界 | 不会,它只看类型和数组长度 | 会,没有'\0'它能给你数到天荒地老 |
| 在意内存里存什么吗 | 完全不在意 | 死盯着'\0' |
实测对照:
chararr1[]="abc";// 占 4 字节chararrB[]={'a','b','c'};// 占 3 字节sizeof(arr1)=4strlen(arr1)=3sizeof(arrB)=3strlen(arrB)=?// 未定义行为为什么差 1?
sizeof问的是"占了多少地方",那个'\0'也实实在在占了 1 字节,得算进去;strlen问的是"有几个有效字符",'\0'是哨兵,哨兵自己不算自己。
所以那个除法算出来的是元素个数(4 ÷ 1 = 4),不是字符串长度。只有字符串才有资格用strlen。
2.5 一个小提醒:printf("%d", sizeof(x))会告警
sizeof的返回类型是size_t(无符号),用%d打印会有格式警告,老老实实用%zu。
第三部分:二维数组其实不二维
3.1 长得像矩阵,内存里是一条直线
intarr[3][5]={1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};// 3 行 5 列你可以把它想象成一张 Excel 表格,但内存里压根没有"行"这回事。
我打印了首尾两个元素的地址:
&arr[0][4] = 000000c96cfffb70 &arr[1][0] = 000000c96cfffb74差 4 字节。明明跨了一行,地址照样只往后挪了一个int的距离。
说人话就是:二维数组在内存里就是一条笔直的数轴,从头排到尾,一格挨一格。"行"只是你脑子里为了方便画出来的辅助线,编译器眼里只有一条直线。
这个点现在记不住没关系,学到指针那一章它会让你"啊——原来是这样"。
3.2 初始化:三种姿势
// 姿势1:不完全初始化,剩下的全补 0intarr1[3][5]={1,2};// 姿势2:完全初始化,按行顺序平铺intarr3[3][5]={1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};// 姿势3:按行分组,一行一个 {}(推荐,最清爽)intarr4[3][5]={{1,2},{3,4},{5,6}};// arr4[0] = {1,2,0,0,0}// arr4[1] = {3,4,0,0,0}// arr4[2] = {5,6,0,0,0}行数可以省略,列数不行:
intarr5[][5]={1,2,3};// OK,编译器数出 1 行为什么列数不能省?因为编译器得先知道一行有多长,才能算出第二行从哪个地址开始。不给列数,它连地址都算不出来。
顺带一提:用"姿势2"那种平铺写法时,GCC 会给你一条
warning: missing braces around initializer,意思是"你省了内层花括号"。姿势3 按行分组才是最保险的写法——编译器没意见,你读起来也清楚。
3.3 下标:行和列都从 0 开始
printf("%d\n",arr[2][4]);// 7第 3 行第 5 列,值是 7。注意"第几行"和"下标"之间又差 1。
3.4 遍历:双层 for
for(i=0;i<3;i++){// 外层管行for(j=0;j<5;j++){// 内层管列printf("%d ",arr[i][j]);}printf("\n");}第四部分:小试牛刀——二分查找
数组最爽的用法之一:在有序数组里找东西。
从 1~10 里找 7,笨办法一个个找,最多找 10 次;二分查找最多找 4 次。因为它每次直接砍掉一半。
#include<stdio.h>intmain(void){intarr[]={1,2,3,4,5,6,7,8,9,10};intsz=sizeof(arr)/sizeof(arr[0]);// 元素个数,别写死 10intleft=0,right=sz-1;intkey=7,mid=0,find=0;while(left<=right){mid=left+(right-left)/2;// 取中间那个if(arr[mid]>key)right=mid-1;// 太大,往左缩elseif(arr[mid]<key)left=mid+1;// 太小,往右缩else{find=1;break;}// 找到了,收工}if(find)printf("找到了,下标是 %d\n",mid);elseprintf("找不到\n");return0;}跑起来:
找到了,下标是 6(数组里找的 7,而arr[6]才是 7 —— 又是一次"第几个 ≠ 下标" 😄)
两个必须记住的细节:
- 为什么写
mid = left + (right - left) / 2而不是(left + right) / 2?因为 left 和 right 都很大时,left + right可能溢出。换成前者意思完全一样,但永远不溢出。这个细节面试官爱问。 - 二分查找的前提是数组必须有序!数组无序,二分就是个笑话。
课后加餐:随机点名器
顺便预告一个用二维字符数组存名单的例子(char students[20][10],每行存一个名字),配合srand(time(NULL))+rand() % 20取随机下标,就能做一个课堂点名器。二维数组存"一堆字符串"的玩法,等你把上面这套跑通了自己就能写出来——先自己想一想,再去敲。
结尾
数组这东西,说白了就一句话:
一块连续的内存 + 一个起点 + 一套从 0 开始的编号。
上面所有的坑——'\0'、sizeof、越界、二维数组的真相——全都是从这句话里长出来的。
给大一的你三个建议:
sizeof(arr) / sizeof(arr[0])背下来,能用一辈子。- 存字符串就用双引号,别手动凑
'\0',那是自找麻烦。 - 下标从 0 开始,写循环时先问自己一句:
i < 元素个数还是i <= 元素个数?
还有一句必须说的:下标越界这件事,编译器不会拦你。它只会在你运行的那一瞬间,悄悄把你的内存搅成一锅粥,或者干脆让程序崩给你看。
所以,多敲代码,多踩几个坑——在可控的小程序里踩,别拿生产环境当试验田。C 语言的坑,踩过一遍基本就再也不会踩第二次了。
祝各位,早日从"数组越界不报错"的幻觉里醒过来。😎