第八章 数组
数组是程序设计中最常用的数据类型之一。数组表示在指定的内存地址处,连续存储具有相同数据类型的一组元素。每个数组元素可以视为一个单独的变量,使用数组名和数组下标来表示。例如int类型的数组元素a[2],表示在内存地址a处,存储从0开始计数的第2个数组元素。数组元素可以看作是一组单个变量定义的简化形式。
本章学习概要:
数组的概念
数组遍历访问
多维数组
8.1 数组
本节必须掌握的知识点:
数组的概念
示例二十九
代码分析
汇编解析
8.1.1 数组的概念
什么是数组?数组是一个变量;由数据类型相同的一组元素组成,并且连续地存储在内存中。
【注:数组里的元素一定具有相同的数据类型】
那么数组既然是一个变量,它和其它基本变量有什么不同呢?基本变量在内存中是一块空间,比如定义一个int型变量,那么它在内存中存在的大小空间是4字节。而数组就不同了,数组是一串连续的内存空间。在下面示例中我们将详细介绍到。
■数组的语法格式
数组的语法格式:
数据类型 名称[];
●解析:
我们已经了解了 char、short、int、float、double、const等数据类型,这些都可以组成不同类型的数组。名称就是变量名,中括号”[]”内写的数值大小代表数组的元素个数。
【注:”[]”数组元素个数在中括号内指定,且中括号内必须是整型常量】
■数组的命名
既然数组是变量,那么它的命名规则和基本变量相同,即“数字、字母、下划线的组合,但不能以数字开头”。
【注】编译器编译时,数字开头表示行号。
举例
char c[50]; 定义了一个char型数组,数组的元素个数为50;
short s[24]; 定义了一个short型数组,数组的元素个数为24;
int i[24]; 定义了一个int型数组,数组的元素个数为24;
int arr[50]; 数组名arr表示数组的起时地址。
int _arr[50]; 数组名_arr表示数组的起时地址。
8.1.2 示例二十九
■数组的用途
数组有什么作用哪?我们来看一个例子。
●第一步:分析需求,设计程序结构框架。
分析需求:依次输入5名同学的分数,并显示他们的平均分及总分。
设计程序结构框架:顺序结构,依次输入5名同学的分数,然后计算他们的总分和平均分。
●第二步:数据定义,定义恰当的数据结构;
定义5个int类型的变量a,b,c,d,e,分别代表5名同学。再定义一个int类型变量sum保存总分,并初始化为0;定义一个double类型变量ave保存平均分,并初始化为0。
●第三步:分析算法。
sum = a + b + c + d + e;
ave = sum / 5.0;
●第四步:编写伪代码,即用我们自己的语言来编写程序。
int main(void) {
定义int类型变量a,b,c,d,e,并初始化为0,分别代表5名同学;
定义int类型变量sum,并初始化为0,保存总分;
定义double类型变量ave,并初始化为0,保存平均分;
调用printf函数打印信息:("请输入5名同学的分数。\n");
调用printf函数打印信息:("a:");
调用scanf_s接收键盘输入变量a;
调用printf函数打印信息:("b:");
调用scanf_s接收键盘输入变量b;
调用printf函数打印信息:("c:");
调用scanf_s接收键盘输入变量c;
调用printf函数打印信息:("d:");
调用scanf_s接收键盘输入变量d;
调用printf函数打印信息:("e:");
调用scanf_s接收键盘输入变量e;
计算总分:sum = a + b + c + d + e;
计算平均分:ave = sum / 5.0;
调用printf函数输出结果:
("5名同学的总分为: %d\t平均分为: %.1f \n", sum, ave);
system("pause");
return 0;
}
●第五步:画流程图,使用Visio、Excel或者其他绘图工具绘制算法流程和逻辑关系图;(略)
●第六步:编写源程序,其实就是将我们的伪代码翻译成计算机语言;
/*
依次输入5名同学的分数,并显示他们的平均分及总分
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int a = 0; //a同学的分数;
int b = 0; //b同学的分数;
int c = 0; //c同学的分数;
int d = 0; //d同学的分数;
int e = 0; //e同学的分数;
double ave = 0; //平均分;
int sum = 0; //总分;
printf("请输入5名同学的分数。\n");
printf("a:");
scanf_s("%d", &a);
printf("b:");
scanf_s("%d", &b);
printf("c:");
scanf_s("%d", &c);
printf("d:");
scanf_s("%d", &d);
printf("e:");
scanf_s("%d", &e);
sum = a + b + c + d + e;
ave = sum / 5.0;
printf("5名同学的总分为: %d\t平均分为: %.1f \n", sum, ave);
system("pause");
return 0;
}
●输出结果:
请输入5名同学的分数。
a:99
b:88
c:77
d:66
e:55
5名同学的总分为: 385 平均分为: 77.0
请按任意键继续. . .
●第七步:调试程序,修复程序中可能出现的BUG;
(略)
●第八步:优化代码,尝试更好的设计方案,效率更高的算法,逻辑更为清晰简洁明了。
示例代码二十九看似解决了这个问题,但是假如要输出100名同学的成绩,那我们该如何解决呢?难道还用示例代码二十九所展示的那样创建100个变量名?那未免太累了吧。我们不防使用数组来解决这个棘手的问题,请看实验五十五。
实验五十五: 一维数组
在VS中新建项目8-1-2.c:
/*
依次输入5名同学的分数,并显示他们的平均分及总分
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int score[5]; //5名同学的成绩
int sum = 0; //总分
double ave = 0; //平均分
printf("请输入5名同学的成绩。\n");
for (int i = 0; i < 5; ++i)
{
printf("请输入第%d名同学分数:", i+1);
scanf_s("%d", &score[i]);
sum += score[i];
}
ave = sum / 5.0;
printf("5名同学的总分为:%d\t 平均分为 %.1f\n", sum, ave);
system("pause");
return 0;
}
●输出结果:
请输入5名同学的成绩。
请输入第1名同学分数:1
请输入第2名同学分数:2
请输入第3名同学分数:3
请输入第4名同学分数:4
请输入第5名同学分数:5
5名同学的总分为:15 平均分为 3.0
请按任意键继续. . .
8.1.3 代码分析
示例二十九中代表5名同学的变量定义改用数组的方式定义:
int score[5];//score地址处分配5个int类型的地址空间,用于保存5名同学的成绩
然后再分别定义一个int类型变量sum,且初始化为0,用于保存总分;定义一个double类型的变量ave,且初始化为0,用于保存平均分。
接下来使用for循环语句 (int i = 0; i < 5; ++i),循环变量i的取值分别为0,1,2,3,4。循环5次,提示接收用户键盘输入5名同学的分数,并保存在数组元素score[0],score[1],score[2],score[3],score[4]内,完成数组初始化。
提示
int score[5]的含义:在score地址处分配5个int类型的地址空间。
&score[i]的含义为:score+i*sizeof(int)偏移地址,即第i个数组元素的地址。
score[i]的含义为:score+i*sizeof(int)偏移地址存储的值,即第i个数组元素的值。
int score[5];表示数组的元素个数为5,它是从下标0开始访问的。“[]”中括号中的操作数称为下标。比如score[3],其下标为3,表示score[3]是数组的第3个元素。
也就是说int score[5];可以写成 score[0]、score[1]、score[2]、score[3]、score[4]共5个元素。
for(i=0;i<5;i++) //for循环遍历数组,什么叫遍历数组?就是循环访问数组中每个元素。
{
printf("分数为:%d\n",score[i]); //输出数组元素的值
sum += score[i]; // 相当于sum = score[i]+sum。
}
在完成数组初始化之后,sum += score[i];语句使用复合运算符“+=” 计算总分。for循环结束后,使用ave = sum / 5.0;语句计算平均分。
最后使用printf函数输出总分和平均分。
8.1.4 汇编解析
■汇编代码
;C标准库头文件和导入库
include vcIO.inc
.data
score sdword 5 dup(?)
sum sdword 0
ave real8 0.0
i sdword 0
divisor dword 5 ;除数
.const
szMsg1 db "请输入5名同学的成绩。",0dh,0ah,0
szMsg2 db "请输入第%d名同学分数:",0
szMsg3 db "%d",0
szMsg4 db "5名同学的总分为:%d",09h,"平均分为 %.1f",0dh,0ah,0
.code
start:
invoke printf,offset szMsg1
.while i < 5
mov ecx,i
inc ecx
invoke printf,offset szMsg2,ecx
mov ebx,i
; ;数组元素的地址score+4*i
shl ebx,2
lea ecx,score
add ecx,ebx
invoke scanf,offset szMsg3,ecx
mov eax,sum
;4*i
mov ebx,i
shl ebx,2
add eax,sdword ptr [score+ebx] ;取数组元素值[score+4*i]
mov sum,eax
inc sdword ptr i
.endw
;浮点运算ave = sum / 5.0;
fild divisor ;加载浮点栈
fild sum ;加载浮点栈
fdiv ;浮点除法指令
fst ave ;保存结果
invoke printf,offset szMsg4,sum,ave
;
invoke _getch
ret
end start
●输出结果:
请输入5名同学的成绩。
请输入第1名同学分数:1
请输入第2名同学分数:2
请输入第3名同学分数:3
请输入第4名同学分数:4
请输入第5名同学分数:5
5名同学的总分为:15 平均分为 0.3
上述汇编代码中,使用.while伪指令对应C语言中的for循环语句。此外,我们需要特别关注以下几点:
1.数组定义为:score sdword 5 dup(?),意思是在score地址处分配5个sdword类型的未初始化内存空间。
2.取数组元素的地址:地址表达式score+4*i计算第i个数组元素在内存中的偏移地址。score表示数组的起始地址,即第0个数组元素的地址。4为比例因子,表示每个数组元素的数据类型sdword占用4个字节的内存空间。变量i为数组元素的下标,使用数组下标*比例因子=数组元素的地址。
3.取数组元素的值[score+4*i] :方括号表示内存空间存储的值,方括号内为数组元素的地址。
4.浮点数的计算:(参见《X86汇编语言基础教程》第三十九章 浮点处理器和浮点指令编码)
fild divisor 表示将除数转换为浮点数,然后加载到浮点栈,浮点栈由8个80位浮点寄存器组成。
fild sum表示将和转换为浮点数,然后加载到浮点栈。
fdiv ;浮点除法指令,计算sum/5.0.
fst ave ;将浮点栈顶的值存储到变量ave中
5.我们会发现最后输出的结果是错误的。因为保存在变量ave中的浮点数为64位,而printf输出的浮点数的格式与变量ave存储的格式不同。如果在汇编语言中正确的输出浮点数,需要对浮点数的格式进行转换,请读者参考源代码中的floatio.asm源文件中的WriteFloat函数定义。
总结
C语言中的使用数组下标表示相应的数组元素时,隐含了比例因子。汇编语言中精确的计算数组元素的偏移地址(以字节为单位)。
C语言标准库中的输入和输出函数实现了数据的格式化输出,而汇编语言中需要自己实现数据格式的转换。
C语言中会严格检查数组占用的内存空间是否发生溢出,而汇编语言中不做相应的检查,即使出现错误,也不会报错。这相应提高了对程序员的要求。由于我们在《X86汇编语言基础教程》做过类似的实验,此处不再赘述,请读者自己编写汇编代码验证一下。
■反汇编代码
int score[5]; //5名同学的成绩
int sum = 0; //总分
01201090 mov dword ptr [sum],0 ;变量sum初始化为0
double ave = 0; //平均分
01201097 xorps xmm0,xmm0
0120109A movsd mmword ptr [ave],xmm0 ;变量ave初始化为0
printf("请输入5名同学的成绩。\n"); ;输出提示信息
0120109F push 1203000h
012010A4 call printf (01201150h)
012010A9 add esp,4
for (int i = 0; i < 5; ++i)
012010AC mov dword ptr [ebp-1Ch],0 ;表达式1:初始化循环变量i为0
012010B3 jmp main+3Eh (012010BEh)
012010B5 mov eax,dword ptr [ebp-1Ch] ;表达式3,i++
012010B8 add eax,1
012010BB mov dword ptr [ebp-1Ch],eax
012010BE cmp dword ptr [ebp-1Ch],5 ;表达式2
012010C2 jge main+7Ch (012010FCh) ;变量i>=5时跳转012010FCh地址处
{
printf("请输入第%d名同学分数:", i+1);
012010C4 mov ecx,dword ptr [ebp-1Ch]
012010C7 add ecx,1
012010CA push ecx
012010CB push 1203018h
012010D0 call printf (01201150h) ;输出提示信息
012010D5 add esp,8
scanf_s("%d", &score[i]);接收键盘输入,并存入数组
012010D8 mov edx,dword ptr [ebp-1Ch] ;取出循环变量i的值存入edx
012010DB lea eax,score[edx*4] ;将第i个数组元素的地址存入eax
012010DF push eax
012010E0 push 1203030h
012010E5 call scanf_s (01201190h) ;将键盘输入的值存入第i个数组元素地址处
012010EA add esp,8
sum += score[i];
012010ED mov ecx,dword ptr [ebp-1Ch] ;将循环变量i存入ecx
012010F0 mov edx,dword ptr [sum] ;将变量sum的值存入edx
012010F3 add edx,dword ptr score[ecx*4] ;将第i个数组元素的值累加到edx
012010F7 mov dword ptr [sum],edx ;将累加和存入变量sum
}
012010FA jmp main+35h (012010B5h) ;跳转到表达式3
ave = sum / 5.0;浮点运算,计算平均分
012010FC cvtsi2sd xmm0,dword ptr [sum]
ave = sum / 5.0;
01201101 divsd xmm0,mmword ptr [__real@4014000000000000 (01202100h)]
01201109 movsd mmword ptr [ave],xmm0
printf("5名同学的总分为:%d\t 平均分为 %.1f\n", sum, ave);
0120110E sub esp,8
01201111 movsd xmm0,mmword ptr [ave]
01201116 movsd mmword ptr [esp],xmm0
0120111B mov eax,dword ptr [sum]
0120111E push eax
0120111F push 1203034h
01201124 call printf (01201150h) ;输出总分和平均分
01201129 add esp,10h
system("pause");
0120112C push 1203058h
01201131 call dword ptr [__imp__system (01202060h)]
01201137 add esp,4
return 0;
上述反汇编代码中,我们可以清晰的看出,数组名就是数组在内存中的起始地址。数组元素的下标*数组数据类型对应的比例因子得到准确的数组元素地址。读写数组元素首先是计算出每个数组元素的偏移地址,然后再对其进行读写操作。
请读者仔细阅读代码注释,理解程序的执行流程。
实验五十六: 测试数组越界导致的内存溢出错误
在VS中新建项目8-1-3.c:
/*
数组溢出
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int score[5]; //5名同学的成绩
int sum = 0; //总分
double ave = 0; //平均分
printf("请输入5名同学的成绩。\n");
for (int i = 0; i < 5; ++i)
{
printf("请输入第%d名同学分数:", i + 1);
scanf_s("%d", &score[i]);
sum += score[i];
}
score[5] = 10;//数组下标5超出范围
ave = sum / 5.0;
printf("5名同学的总分为:%d\t 平均分为 %.1f\n", sum, ave);
system("pause");
return 0;
}
在C语言中,i表示数组元素的下标。假设数组定义为int a[i];数组下标i的取值范围为0~n-1。如果下标超出范围,则会导致数组溢出,在C语言中可以正常通过编译,但是程序执行时,会导致内存溢出错误。例如,在8-1-2.c代码中添加下面的语句:
score[5] = 10;//数组下标5超出范围
编译后运行,显示如下错误提示:
图8-1 内存溢出错误
实验五十七: 观察内存中的数组存储
第一步:在项目8-1-2.c的int sum = 0; //总分语句处下一个断点,并打开watch窗口、内存窗口。
第二步:在监视1窗口名称栏输入数组名score,获取score地址及处存储的值。如图8-2所示。【注】数组名本身就是数组的起始地址,无需添加‘&‘地址符。
图8-2 查询数组地址及存储的值
第三步:将数组score的地址输出内存窗口的地址栏并回车,如图8-3所示。
图8-3 查看数组内存存储
如上图所示,在数组score未初始化之前,其内存存储的值是之前内存中遗留下来的数据。
第四步:按F10单步执行,接收键盘输入5个数组元素的值,并存入数组。如图8-4所示。
图8-4 数组的初始化
由上同可知,从键盘输入了1,2,3,4,5五个整数值,并已存入数组内。5个数组元素的地址分别为:
score[0]对应的内存地址是0x00F3FB70;
score[1]对应的内存地址是0x00F3FB74;
score[2]对应的内存地址是0x00F3FB78;
score[3]对应的内存地址是0x00F3FB7C;
score[4]对应的内存地址是0x00F3FB80;
由于我们定义的数组是int类型的,所以一个int占4个字节,5个int占20个字节,且这20个字节都是连续的,如图8-4所示。同理可知,如果是short类型数组,那么每个元素就占2字节;char类型数组,那么每个元素就占1字节。
■数组的初始化
数组初始化有以下几种形式:
- 可以在定义数组的时候,指定每一个元素的初始值:
int arr[3]={1,2,3};
2.定义数组时全部初始值为0:
int arr[3] = {0}; // 相当于arr[3] = {0,0,0};
3.定义数组时可以不赋初始值:
int arr[3];
4.指定一部分初始值:
int arr[5]={1,2,3}; //只指定前三个元素的初始值arr[5] = {1,2,3,0,0};
5.int arr[] = {1,2,3,4}; //当我们没有对数组定义元素个数时,必须有初始值。如arr数组里共有4个元素,那么编译器会自动默认为4个元素,也就是数组长度是4。
●错误写法:
1.int arr[3] = {1,2,3,4}; //这种写法是越界的行为,什么是越界?当初始值的个数超过数组元素个数的时候,程序会发生错误。
2.int arr[3];
arr = {1,2,3}; //不能使用赋值语句进行初始化,因为数组名表示地址,而不是变量。
■获取数组大小
数组的大小由元素的类型和元素的个数共同决定的。
可以使用sizeof获取数组大小:
int arr[100];
int size = sizeof(arr);
实验五十八: 获取数组的大小
在VS中新建项目8-1-4.c:
/*
获取数组的大小
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr[100];
int size = sizeof(arr);
printf("arr数组的大小为%d\n",size);
system("pause");
return 0;
}
●输出结果:
arr数组的大小为400
请按任意键继续. . .
●解析:
int 是4字节,定义了元素个数为100,所以长度为100*4字节=400字节。
练习
1、以下定义的数组哪些是错误的,为什么?
(1)、int arr [10] = {1,2,,3,4};
(2)、char char [];
(3)、short arr1 [10] = {};
(3)、int arr1 [] = {0};
2、请分别写出下面定义数组的元素的个数及数组长度。
(1)、short arr [10] = {1,2,3,3,4};
(2)、char char []={1,2,3,3,4};
(3)、int arr1 [13] = {0
3、使用do while语句和while语句重写8-1-2.c。