一维数组的定义和引用

官官方信息技术老师·23 页·深入(追求细节与边界)·0 次浏览·38 天前
数组内存布局下标引用越界

一维数组的定义和引用

看完你能说清数组在内存里长什么样,并亲手算出任意元素的地址

按 空格/→ 演示下一步

问一句
1 / 23 页

全部页面点击任意一页,跳回舞台从这页播放

数组内存布局下标引用越界

一维数组的定义和引用

看完你能说清数组在内存里长什么样,并亲手算出任意元素的地址

1第 1 页 · 一维数组的定义和引用

为什么需要数组

期末考完,要存全班30个人的成绩。用变量一个个写:score1、score2……写到 score30,光是声明就崩溃了,改个名字更是噩梦。程序语言早就为这种「一批同类型数据」准备好了容器——数组。

30个变量的痛点
声明重复、名字互不关联,想循环遍历根本无从下手
数组的思路
同一个名字,靠编号区分位置,一份声明装下整批数据
编号即下标
score[0]到score[29],编号让数据能被循环批量处理
一排编了号的储物柜对应 →一维数组

储物柜的名字对应数组名,柜子编号对应下标

2第 2 页 · 为什么需要数组

数组的定义语法

上一页说了:要把一串同类型的数据打包管理,就得先有个「盒子」。C 语言里声明这个盒子有一套固定写法,每个位置各管一件事,写错位置编译器就不认。

类型名
写在最前面,说明每个格子里装什么类型的数据
数组名
这个盒子的名字,起名规则和普通变量一样
[常量表达式]
方括号里写格子数,必须是编译期确定的常量
下标从 0 开始
定义时的 5 是数量,引用时下标 0~4,别混
一排带编号的储物柜对应 →一维数组

柜子大小对应类型名、柜子标签对应数组名、格子总数写在方括号里

3第 3 页 · 数组的定义语法

定义数组的合法与非法写法

c

四组对比示例:哪些数组定义合法、哪些直接编译报错,一眼看清长度必须是常量。

代码高亮加载中…

长度必须是编译期确定的常量表达式:字面量、宏可以,const 变量在 C 里不算;长度省略、为 0、用普通变量定义都会报错。

4第 4 页 · 定义数组的合法与非法写法

数组在内存中的样子

从左往右是内存地址增长方向:每个格子存一个元素,格子上方数字是下标,注意从0开始。

图解渲染中…
b0:首元素地址就是数组名a指向的位置x1:定义a[4]只有下标0~3,访问下标4即越界
5第 5 页 · 数组在内存中的样子

数组的初始化

上一页看到,刚定义的数组里装的是内存里的垃圾值。那能不能在定义时就顺手把格子填好?可以——这就是初始化,C 语言给三种写法。

完全初始化
int a[3]={1,2,3}; 每个格子都有值,一一对应填入
部分初始化
int a[3]={1}; 剩下的格子自动补 0,不是垃圾值
省略长度
int a[]={1,2,3}; 编译器数一数初值个数,自动定长度为 3
酒店入住登记对应 →数组初始化

完全登记=满房名单,只登记一间=其余房间自动清零

6第 6 页 · 数组的初始化

初始化 vs 先定义后赋值

两种给值方式常被混为一谈:有人以为效果等价,其实时机、写法、限制都不同,边界用法最容易踩坑。

定义时初始化
  • int a[3]={1,2,3}; 定义当场给值
  • 未写满的元素自动补 0
  • 可省长度:int a[]={1,2,3};
定义后逐个赋值
  • int a[3]; a[0]=1; a[1]=2; 定义时是随机值
  • 不会自动补 0,遗漏的元素是垃圾值
  • 长度必须写死,不能靠赋值列表推算
需要整体清零或让编译器数长度时选初始化;值要运行时才确定时选循环赋值。
7第 7 页 · 初始化 vs 先定义后赋值

定义与初始化自测

点击作答

已有定义 int a[5] = {1, 2};,关于数组 a 的当前状态,下列说法正确的是?

8第 8 页 · 定义与初始化自测

数组元素的引用

上一页我们把数组比作一排储物柜,定义时只是分配好了格子。可柜子是用来放东西、取东西的——这一页就看怎么准确打开其中某一格,而且打开之后既能读也能写。

引用格式
数组名[下标],下标从 0 开始
本质是变量
a[2] 用法和普通 int 变量完全一样
可读可写
赋值给它即写,出现在表达式右边即读
越界未检查
引用不存在的下标,编译器不报错但行为未定义
酒店房间号对应 →数组元素引用

房间号对应下标,入住退房对应写入读取

a[i]≡一个普通变量,i∈[0, n−1]\texttt{a[i]} \equiv \text{一个普通变量},\quad i \in [0,\,n-1]
9第 9 页 · 数组元素的引用

循环遍历数组

c

用 for 循环把下标 0~4 依次算出来,逐个访问数组元素——这是遍历数组的固定套路。

代码高亮加载中…

循环变量 i 正是下标:从 0 开始、止于 4(< 5),a[i] 随 i 逐个落到每个元素上,一行 printf 完成全部访问。

10第 10 页 · 循环遍历数组

数组的输入与输出

c

用循环配合 scanf/printf 逐个读写元素,看 C 为什么不能一次输出整个数组。

代码高亮加载中…

scanf 和 printf 都只认单个元素,必须由循环驱动下标 i 逐个访问;整体读写在 C 里不存在,直接打印数组名只会得到首元素地址。

11第 11 页 · 数组的输入与输出

越界访问:最常见的坑

从写错边界到程序崩溃,逐步看越界是怎么发生的。

1
写下循环
想遍历数组,int a[5],随手写出 i<=5 的条件
2
i=5 时越界
合法下标只有 0~4,a[5] 访问到了数组外的内存
3
读到脏数据
越界读到的值不确定,程序可能暂不出错,埋下隐患
4
写成脏数据
若越界赋值,可能破坏相邻变量或返回地址,行为未定义
5
守住纪律
统一用 i<n 而非 i<=n,从 0 到 n-1,边界才安全
12第 12 页 · 越界访问:最常见的坑

输入输出自测

点击作答

int a[5]; 下面的输入输出写法,哪一项是正确的?

13第 13 页 · 输入输出自测

成绩排序问题

前面我们学会了把成绩存进数组、逐个输出。但老师真正要的是排名:把一组成绩从大到小排出来。这个「排」的动作,就是排序算法要解决的经典问题。

需求场景
一组成绩存入数组,需要从大到小输出
两两比较
排序的基本动作:比较相邻元素,大的往前换
交换元素
借助临时变量 temp,完成两个下标元素的互换
多轮扫描
一遍比较不够,需多轮循环才能完全有序
整理扑克牌对应 →排序过程

每次比较相邻两张牌的大小,对应数组元素的依次比较

14第 14 页 · 成绩排序问题

选择法的核心思想

选择排序的每一轮:扫一遍数组,把最大值换到它该在的位置。

1
假设首个最大
默认 arr[i] 是当前最大,记下它的下标 maxIdx
2
逐个比较
从 i+1 到末尾,谁比 arr[maxIdx] 大就更新 maxIdx
3
记录新下标
发现更大元素只换下标不换值,避免频繁搬动
4
一轮结束交换
把 maxIdx 位置与 i 位置交换,最大值归位
5
缩小范围
i 后移一位,剩余部分重复上述过程
15第 15 页 · 选择法的核心思想

选择法逐轮演示

从左往右看:每个格子是一轮结束后的数组状态,箭头上写这轮做了什么。

图解渲染中…
a1:下标0~3依次存 5 3 8 2b2:最小值恰在原地,交换等于没换
16第 16 页 · 选择法逐轮演示

选择法排序代码

c

把选择法思想落到代码:外层定位置,内层找最值,最后交换。

代码高亮加载中…

外层 i 锁定要填的位置,内层 j 扫描未排序区记录最小值下标 min,一轮结束后才交换一次——这正是选择法比冒泡交换次数少的关键。

17第 17 页 · 选择法排序代码

排序自测

点击作答

用选择法将 {5,3,8,1} 升序排序(每轮选最小值放到最前),第 1 轮结束后数组是?

18第 18 页 · 排序自测

Fibonacci数列是什么

上一页我们用循环和下标操作数组已经很熟了。现在来个经典问题:一对兔子每月生一对小兔,小兔长大后又接着生,各月兔子总数是多少?这个数列藏着一个简单的规律。

兔子问题
第一二个月各1对,之后每月数量可推算
递推规律
从第3项起,每项等于前两项之和
数列前几项
1, 1, 2, 3, 5, 8, 13, 21……
为何适合数组
每项依赖前面的项,用下标存取正好
递推式
f[i] = f[i-1] + f[i-2],i从2开始
接力赛记总里程对应 →Fibonacci递推

每一棒的成绩都在前面几棒成绩的基础上累加得来

f[0]=f[1]=1,f[i]=f[i−1]+f[i−2]  (i≥2)f[0]=f[1]=1,\quad f[i]=f[i-1]+f[i-2]\;(i\ge 2)
19第 19 页 · Fibonacci数列是什么

用数组存Fibonacci数列

c

用数组把Fibonacci递推公式直接翻译成代码,下标即递推关系。

代码高亮加载中…

f[i]=f[i-1]+f[i-2]一行就是Fibonacci定义本身:数组下标i把『第i项』变成可循环的变量,递推关系无需写死20遍。

20第 20 页 · 用数组存Fibonacci数列

递推过程图解

从左往右看:数组格子逐个填入,每格都由前两格相加得到,对照循环变量的推进。

图解渲染中…
a3b:求和即递推式f[i]=f[i-1]+f[i-2]b1:下标0、1已初始化,循环从2开始a4b:i自增,对应格子向右推进一格
21第 21 页 · 递推过程图解

数组应用小结

  • ✓数组把多个同类型变量打包,靠下标统一管理
  • ✓初始化只在定义时一次写全,之后只能逐个赋值
  • ✓下标从 0 开始,最大到长度减 1,越界不报错但后果难料
  • ✓遍历靠 for 循环,输入输出和排序都建立在它之上
  • ✓排序与 Fibonacci 的共性:用下标表达元素间的关系
延伸主题:二维数组与多维数组字符数组与字符串数组作为函数参数传递
22第 22 页 · 数组应用小结

课后思考

先自己想一会儿,再看下面的参考答案——想不出来也没关系,带着问题翻回去看看。

1如果 a[5] 的数组写成 a[5]=10,越界写会立刻报错吗?为什么编译器常常不吭声?

参考答案不一定报错。C 语言不做边界检查,a[5] 只是算地址就写进去,踩到谁的内存就破坏谁。编译器不吭声是为了效率——运行时逐次检查太慢,代价转嫁给了程序员。

2int a[5] = {1, 2}; 剩下三个元素是垃圾值吗?换成先定义后赋值的写法呢?

参考答案初始化时剩余元素自动补 0,不是垃圾值;但如果只定义 int a[5] 再逐个赋值,没赋到的就是内存里原来的垃圾。区别在于初始化列表触发了整体清零语义。

3选择排序和 Fibonacci 递推都是「用数组下标组织计算」,它们共同利用了数组哪个本质特性?

参考答案随机访问:下标就是地址,O(1) 直接读写任意元素。排序靠它比较交换任意位置,递推靠它按序回取前面的结果。链表两样都做不了这么快。

23第 23 页 · 课后思考
一维数组的定义和引用 · 知识图解