本节必须掌握的知识点:
示例三十
代码分析
汇编解析
在上一节中介绍了数组相关的概念,而在本节中将介绍数组的使用。
8.2.1 示例三十
■访问数组
示例代码三十 |
●第一步:分析需求,设计程序结构框架。
分析需求:
1.定义一个 int 型数组,其中有 5 个元素组成,5 个元素分别为 1、2、3、4、5。 int arr[5]={1,2,3,4,5};获取 arr 数组中第 2 个元素(从0开始计数)并打印出来。
2.将 arr 数组中第 2 个元素的值修改为 6。然后把改变后的数组遍历一遍,查看数组的元素是否有变化。
设计程序结构框架:顺序结构+循环结构。先输出数组第 2 个元素的值,再修改数组第 2 个元素的值,最后通过一个for循环语句遍历并输出数组。
●第二步:数据定义,定义恰当的数据结构;
定义一个int类型的数组变量int arr[5] = { 1,2,3,4,5 };
●第三步:分析算法。
(略)
●第四步:编写伪代码,即用我们自己的语言来编写程序。
int main(void) {
定义一个int类型数组int arr[5] = { 1,2,3,4,5 };
调用printf函数输出第二个数组元素:("%d\n",arr[2]);
修改第二个数组元素的值:arr[2] = 6;
调用printf函数输出修改后的第二个数组元素:("%d\n",arr[2]);
for (int i = 0; i < 5; i++)
{
调用printf函数输出每个数组元素的值:("arr[%d] = %d\n", i, arr[i]);
}
system("pause");
return 0;
}
●第五步:画流程图,使用Visio、Excel或者其他绘图工具绘制算法流程和逻辑关系图;(略)
●第六步:编写源程序,其实就是将我们的伪代码翻译成计算机语言;
/*
修改并访问数组元素
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr[5] = { 1,2,3,4,5 }; //定义数组
printf("%d\n", arr[2]); //打印arr数组中第2个元素
arr[2] = 6; //修改数组元素
printf("arr[2] = %d\n", arr[2]); //输出修改后的
for (int i = 0; i < 5; i++)
{
printf("arr[%d] = %d\n", i, arr[i]); //遍历数组元素
}
system("pause");
return 0;
}
●输出结果:
arr[2] = 3
arr[2] = 6
arr[0] = 1
arr[1] = 2
arr[2] = 6
arr[3] = 4
arr[4] = 5
请按任意键继续. . .
8.2.2 代码分析
我们可以利用数组名加中括号下标的形式访问数组中的每一个元素。如果要访问第2个数组元素,它的下标为2。如果有n个数组元素,访问最后一个数组元素下标为n-1,切记计算机中从0开始计数,数组下标不能溢出。
如果修改数组元素的值,同样需要使用数组名加中括号下标的形式表示数组元素,比如将 arr 数组中第 2个元素 3 变成 6。arr[2]=6;
我们可以把数组元素看作是一个独立的变量,变量名是变量存储的地址标号,数组元素就是使用数组下标方式命名的变量名。由于数组元素具有相同的数据类型,且连续存储在内存中,数组下标就是数组元素相对于数组起始地址的偏移(隐含比例因子),所以可以通过改变数组下标的方式变量数组元素。
一维数组使用一个for循环结构遍历数组。循环变量i作为数组下标arr[i]。循环变量i取值范围0~4,代表第0个到第4个数组元素。
8.2.3 汇编解析
■汇编代码
;C标准库头文件和导入库
include vcIO.inc
.data
arr sdword 1,2,3,4,5
i sdword 0
.const
szMsg1 db "arr[2] = %d",0dh,0ah,0
szMsg2 db "arr[%d] = %d",0dh,0ah,0
.code
start:
mov eax,[arr+2*4] ;输出第2个数组元素
invoke printf,offset szMsg1,eax
mov sdword ptr [arr+2*4],6 ;修改第2个数组元素的值为6
mov eax,[arr+2*4]
invoke printf,offset szMsg1,eax
.while i < 5
;数组元素的地址arr+4*i
mov ebx,i
shl ebx,2
mov eax,sdword ptr [arr+ebx];取数组元素值[arr+4*i]
invoke printf,offset szMsg2,i,eax
inc sdword ptr i
.endw
;
invoke _getch
ret
end start
●输出结果:
arr[2] = 3
arr[2] = 6
arr[0] = 1
arr[1] = 2
arr[2] = 6
arr[3] = 4
arr[4] = 5
访问数组元素:
语句:mov eax,[arr+2*4] ;输出第2个数组元素
第二个数组元素地址的计算方法:数组首地址arr+数组元素的偏移;
偏移的计算方法为:数组下标*4,第2个数组元素的下标为2,每个数组元素占4个字节空间,所以第3个数组元素相对于数组起始地址的偏移为arr+4+4=arr+2*4。
循环遍历数组元素:[arr+4*i]
遍历数组元素就是计算每个数组元素的地址的过程。循环变量i作为数组下标,取值范围0~4。每个数组元素占用4个字节空间,4作为比例因子*i,再加上数组的起始地址就是每个数组元素的地址。
■反汇编代码
int arr[5] = { 1,2,3,4,5 }; //定义数组
008E1046 mov dword ptr [arr],1
008E104D mov dword ptr [ebp-14h],2
008E1054 mov dword ptr [ebp-10h],3
008E105B mov dword ptr [ebp-0Ch],4
008E1062 mov dword ptr [ebp-8],5
上述代码初始化数组arr,汇编代码将数组arr定义在数据段中,为全局变量。而C语言中,将数组arr定义为局部变量,在main函数中,因此数组存储在函数的堆栈中。数组首地址arr为第0个数组元素的地址arr+0=ebp-18h,以此类推,第1个数组元素的地址为ebp-14h,第2个数组元素地址为ebp-10h,第3个数组元素地址为ebp-0Ch,第4个数组元素地址为ebp-8,分别存入1,2,3,4,5。
printf("arr[2] = %d\n", arr[2]); //打印arr数组中第2个元素
008E1069 mov eax,4 ;比例因子,每个数组元素占4个字节内存
printf("arr[2] = %d\n", arr[2]); //打印arr数组中第2个元素
008E106E shl eax,1 ;4*2
008E1070 mov ecx,dword ptr arr[eax] ;将第2个数组元素的值存入ecx
008E1074 push ecx
008E1075 push 8E3000h
008E107A call printf (08E1100h) ;输出第二个数组元素的值
008E107F add esp,8
arr[2] = 6; //修改数组元素
008E1082 mov edx,4 ;比例因子,edx为编译器随机分配的寄存器
008E1087 shl edx,1 ;4*2
008E1089 mov dword ptr arr[edx],6 ;将6存入第二个数组元素中
printf("arr[2] = %d\n", arr[2]); //输出修改后的
008E1091 mov eax,4
008E1096 shl eax,1
008E1098 mov ecx,dword ptr arr[eax]
008E109C push ecx
008E109D push 8E3010h
008E10A2 call printf (08E1100h) ;输出第二个数组元素的值
008E10A7 add esp,8
for (int i = 0; i < 5; i++);遍历数组元素
008E10AA mov dword ptr [ebp-4],0 ;表达式1
008E10B1 jmp main+7Ch (08E10BCh)
008E10B3 mov edx,dword ptr [ebp-4] ;表达式3
008E10B6 add edx,1
008E10B9 mov dword ptr [ebp-4],edx
008E10BC cmp dword ptr [ebp-4],5
008E10C0 jge main+9Dh (08E10DDh) ;表达式2
{
printf("arr[%d] = %d\n", i, arr[i]); //遍历数组元素
008E10C2 mov eax,dword ptr [ebp-4] ;将循环变量i存入eax
008E10C5 mov ecx,dword ptr arr[eax*4] ;取数组元素arr[i*4]的值
008E10C9 push ecx
008E10CA mov edx,dword ptr [ebp-4]
008E10CD push edx
008E10CE push 8E3020h
008E10D3 call printf (08E1100h) ;输出数组元素的值
008E10D8 add esp,0Ch
}
008E10DB jmp main+73h (08E10B3h) ;跳转到表达式3
system("pause");
008E10DD push 8E3030h
008E10E2 call dword ptr [__imp__system (08E2078h)]
008E10E8 add esp,4
return 0;
请读者留意注释代码中数组元素地址的计算方法。需要注意的是:在C语言中,数组arr定义在main函数内,作为局部变量存储在堆栈空间,使用栈内基址寄存器ebp计算每个数组元素的偏移地址。
如何获取第i个元素,其实就是获取其数组首部地址,加上相对偏移就可以计算出第i个元素的存储位置。我们定义的是int型变量,所以这些元素都有共同的特点,它们都是4个字节大小,所以只要找到数组的首地址,就可以找到存储第i个元素的位置。
通过数组内存结构图的形式展示:
首地址
1 | 2 | 3 | 4 | 5 |
数组元素地址 = 首地址+偏移
第0个元素的地址 = 首地址+0*元素大小;
第1个元素的地址 = 首地址+1*元素大小;
第2个元素的地址 = 首地址+2*元素大小;
第3个元素的地址 = 首地址+3*元素大小;
第4个元素的地址 = 首地址+4*元素大小;
如果有i个元素:
第i个元素的地址 = 首地址+(i-1)*元素大小。
实验五十九:数组间赋值
在VS中新建项目8-2-2.c:
两个数组 arr1、arr2,arr1[6]={1,2,3,4,5,6};将数组 arr1 里的元素复制到数组 arr2中。
谨慎
既然是把一个数组的元素全部复制到另一个数组中,可不可以这样写呢?arr2=arr1;因为数组也是变量,给变量赋值不就是这样写的。如果你有这样的想法,那你就错了。数组名表示的是数组的起始地址,而不是数组的值。
我们来看正确的答案:
/*
数组间赋值
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr1[6] = { 1,2,3,4,5,6 };
int arr2[6] = { 0 };
//循环遍历数组元素并赋值
for (int i = 0; i < 6; i++)
{
arr2[i] = arr1[i];
printf("arr2[%d] = %d\n", i, arr2[i]);
}
system("pause");
return 0;
}
●输出结果:
arr2[0] = 1
arr2[1] = 2
arr2[2] = 3
arr2[3] = 4
arr2[4] = 5
arr2[5] = 6
请按任意键继续. . .
●代码解析:
int arr1[6]= {1,2,3,4,5,6};//定义arr1数组,并初始化。
int arr2[6]={0};//定义arr2数组,并将所有数组元素初始化为0。
使用for循环进行遍历数组中的元素。
1.当i = 0时,arr1[0] = 1,并把arr1[0]的元素赋给arr2[0],此时arr2[0]的元素为1;
输出 arr2[0] = 1;
2.当i = 1时,arr1[1] = 2,并把arr1[1]的元素赋给arr2[1],此时arr2[1]的元素为2;
输出 arr2[1] = 2;
3.当i = 2时,arr1[2] = 3,并把arr1[2]的元素赋给arr2[2],此时arr2[2]的元素为3;
输出 arr2[2] = 3;
4.当i = 3时,arr1[3] = 4,并把arr1[3]的元素赋给arr2[3],此时arr2[3]的元素为4;
输出 arr2[3] = 4;
5.当i = 4时,arr1[4] = 5,并把arr1[4]的元素赋给arr2[4],此时arr2[4]的元素为5;
输出 arr2[4] = 5;
6.当i = 5时,arr1[5] = 6,并把arr1[5]的元素赋给arr2[5],此时arr2[5]的元素为6;
输出 arr2[5] = 6;
实验六十:数组元素倒序排序
在VS中新建项目8-2-3.c:
定义int型数组,并输入6个元素,按倒序的方式排序。
arr[0]与arr[5]交换
arr[1]与arr[4]交换
Arr[2]与arr[3]交换
/*
数组元素倒序排序
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr[6] = { 0 };
int i;
for (i = 0; i < 6; i++)
{
printf("arr[%d]=", i);
scanf_s("%d", &arr[i]);//输出6个元素
}
for (i = 0; i < 3; i++) //交换
{
int temp = arr[i];//临时变量
arr[i] = arr[5 - i];
arr[5 - i] = temp;
}
printf("倒序:\n");
for (i = 0; i < 6; i++)//交换后的数组进行输出
{
printf("arr[%d]=%d\n", i, arr[i]);
}
system("pause");
return 0;
}
●输出结果:
arr[0]=1
arr[1]=2
arr[2]=3
arr[3]=4
arr[4]=5
arr[5]=6
倒序:
arr[0]=6
arr[1]=5
arr[2]=4
arr[3]=3
arr[4]=2
arr[5]=1
请按任意键继续. . .
●代码分析:
for(i=0;i<3;i++) //交换
{
int temp = arr[i];
arr[i] = arr[5-i];
arr[5-i] = temp;
}
主要分析这段代码:
1.当i=0时,把arr[0]的元素放入变量temp中,然后把arr[5-0]的值放入arr[0]中,再把变量temp的值赋值给arr[5],此时完成arr[0]与arr[5]的交换。
2.当i=1时,把arr[1]的元素放入变量temp中,然后把arr[4]的值放入arr[1]中,再把变量temp的值赋值给arr[4],此时完成arr[1]与arr[4]的交换。
3.当i=2时,把arr[2]的元素放入变量temp中,然后把arr[3]的值放入arr[2]中,再把变量temp的值赋值给arr[3],此时完成arr[2]与arr[3]的交换。
实验六十一:数组元素的删除
在VS中新建项目8-2-4.c:
怎样实现数组元素的删除呢?
(1)、查找要删除数字的下标;
(2)、从下标开始,后面一个覆盖前面一个元素;
(3)、数组的总长度-1。
注意
数组是一块连续的内存空间,一旦被定义,是不可以被改变大小的,因为在定义数组时,就已经写入内存中了,大小也被确定了。既然不能改变,那怎么删除呢?这属于障眼法,删除的元素会被它后面的元素覆盖掉,在控制台输出时,看似被删除了,但删除掉的那块内存已经被写入了,已分配的内存空间是不可能被抹掉的。
/*
数组元素的删除
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arr[] = { 25,11,22,44,33 };
int deleteArr = 0; //删除元素
int deleteIndex = -1;//删除下标 给一个不可能的初始值,容易判断
int i;
int count = 5;
printf("请输入要删除的元素:");
scanf_s("%d", &deleteArr);
for (i = 0; i < count; i++)
{
if (deleteArr == arr[i])
{
//记录下标
deleteIndex = i;
break;//找到后直接跳出循环
}
}
//未找到要删除的数组元素
if (-1 == deleteIndex)
{
printf("没有找到要删除的下标值");
}
else
{
//从下标开始,后面一个覆盖前面一个
for (i = deleteIndex; i < count - 1; i++)
{
arr[i] = arr[i + 1];
}
count--;//删除一个元素,其数组长度会减1
}
printf("删除后的数组:\n");
//遍历数组
for (i = 0; i < count; i++)
{
printf("%d ", arr[i]);
}
printf("\n删除后的原数组:\n");
//遍历数组
for (i = 0; i < count+1; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
●输出结果:
请输入要删除的元素:22
删除后的数组:
25 11 44 33
删除后的原数组:
25 11 44 33 33
请按任意键继续. . .
●代码分析:
删除前内存中的数组:
图8-5 删除前内存中的数组
图8-6 删除后内存中的数组
上图很好的诠释数组是一块连续的内存空间,一旦被定义,则是不可以被改变大小的。尽管我们在控制台显示输出的结果中已经没有了被删除的元素22,但是它申请的内存是存在的,后面一个数组元素44覆盖了22,同样再下一个数组元素33覆盖了44,最后一个数组元素33仍然保留在内存中。
实验六十二:插入数组元素
在VS中新建项目8-2-5.c:
/*
插入数组元素
*/
#include <stdio.h>
#include <stdlib.h>
#define NUM 6
int main(void) {
int arr[NUM] = {25,11,22,44,33};
int instetArr = 0;
int count = 5;
int i = 0;
printf("插入前的数组元素:\n");
for (i; i < NUM; i++)
{
printf("%d ", arr[i]);
}
printf("\n输入要插入的元素:");
scanf_s("%d", &instetArr);
arr[count++] = instetArr;
for (i = 0; i < count; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
●输出结果:
插入前的数组元素:
25 11 22 44 33 0
输入要插入的元素:66
25 11 22 44 33 66
请按任意键继续. . .
●代码分析:
如果要插入一个数组元素,必须保证有相应的内存空间。数组是静态内存分配,在编译器编译时,已经从数组名起始地址分配了固定的内存空间,不可以被改变。上述代码中使用#define宏定义了一个常量值NUM为6。表面初始化数组int arr[NUM] = {25,11,22,44,33};实际是int arr[NUM] = {25,11,22,44,33,0};输出的结果也证明了这一点。
插入的值66覆盖了数组最后一个元素0。
如果将66插入到数组的中间位置,将如何实现呢?
arr[NUM] = {25,11,66,22,44,33};
请读者参考8-2-4.c的代码独立完成。
练习
1、输入10名同学的成绩,并按从高到低的形式输出。
2、int arr[5] = {1,2,3,4,5}; 交换第二个元素和第三个元素。
3、int arr[4] = { 1, 2, 3, 4}; 求arr数组中元素的和、乘积。
4、使用数组表示下面的数据:
姓名 工资 工号
张三 10000.11 101
李四 9999.999 102
王五 8999.12 103
小李 12000 104
小王 15000 105
小章 14000.5 106
5、输入5名学生的分数并显示出他们的最高分和最低分。
6、把下面的汇编指令,写出对应的C语言代码。
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,64h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-64h]
0040101C mov ecx,19h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
00401028 mov dword ptr [ebp-14h],4Dh
0040102F mov dword ptr [ebp-10h],42h
00401036 mov dword ptr [ebp-0Ch],21h
0040103D mov dword ptr [ebp-8],63h
00401044 mov dword ptr [ebp-4],37h
0040104B mov dword ptr [ebp-18h],0
00401052 mov dword ptr [ebp-1Ch],0FFFFFFFFh
00401059 mov dword ptr [ebp-24h],5
00401060 push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xd2\xaa\xc9\xbe\xb3\xfd\xb5\xc4\xd4\xaa\xcb\xd8"
00401065 call printf (00401320)
0040106A add esp,4
0040106D lea eax,[ebp-18h]
00401070 push eax
00401071 push offset string "%d" (0042505c)
00401076 call scanf (004012c0)
0040107B add esp,8
0040107E mov dword ptr [ebp-20h],0
00401085 jmp main+80h (00401090)
00401087 mov ecx,dword ptr [ebp-20h]
0040108A add ecx,1
0040108D mov dword ptr [ebp-20h],ecx
00401090 mov edx,dword ptr [ebp-20h]
00401093 cmp edx,dword ptr [ebp-24h]
00401096 jge main+9Eh (004010ae)
00401098 mov eax,dword ptr [ebp-20h]
0040109B mov ecx,dword ptr [ebp-18h]
0040109E cmp ecx,dword ptr [ebp+eax*4-14h]
004010A2 jne main+9Ch (004010ac)
004010A4 mov edx,dword ptr [ebp-20h]
004010A7 mov dword ptr [ebp-1Ch],edx
004010AA jmp main+9Eh (004010ae)
004010AC jmp main+77h (00401087)
004010AE cmp dword ptr [ebp-1Ch],0FFh
004010B2 jne main+0B3h (004010c3)
004010B4 push offset string "\xc3\xbb\xd3\xd0\xd5\xd2\xb5\xbd\xd2\xaa\xc9\xbe\xb3\xfd\xb5\xc4\xcf\xc2\
004010B9 call printf (00401320)
004010BE add esp,4
004010C1 jmp main+0E8h (004010f8)
004010C3 mov eax,dword ptr [ebp-1Ch]
004010C6 mov dword ptr [ebp-20h],eax
004010C9 jmp main+0C4h (004010d4)
004010CB mov ecx,dword ptr [ebp-20h]
004010CE add ecx,1
004010D1 mov dword ptr [ebp-20h],ecx
004010D4 mov edx,dword ptr [ebp-24h]
004010D7 sub edx,1
004010DA cmp dword ptr [ebp-20h],edx
004010DD jge main+0DFh (004010ef)
004010DF mov eax,dword ptr [ebp-20h]
004010E2 mov ecx,dword ptr [ebp-20h]
004010E5 mov edx,dword ptr [ebp+ecx*4-10h]
004010E9 mov dword ptr [ebp+eax*4-14h],edx
004010ED jmp main+0BBh (004010cb)
004010EF mov eax,dword ptr [ebp-24h]
004010F2 sub eax,1
004010F5 mov dword ptr [ebp-24h],eax
004010F8 push offset string "\xc9\xbe\xb3\xfd\xba\xf3\xb5\xc4\xbd\xe1\xb9\xfb\xa3\xba\n" (0042502c)
004010FD call printf (00401320)
00401102 add esp,4
00401105 mov dword ptr [ebp-20h],0
0040110C jmp main+107h (00401117)
0040110E mov ecx,dword ptr [ebp-20h]
00401111 add ecx,1
00401114 mov dword ptr [ebp-20h],ecx
00401117 mov edx,dword ptr [ebp-20h]
0040111A cmp edx,dword ptr [ebp-24h]
0040111D jge main+126h (00401136)
0040111F mov eax,dword ptr [ebp-20h]
00401122 mov ecx,dword ptr [ebp+eax*4-14h]
00401126 push ecx
00401127 push offset string "%d " (00425024)
0040112C call printf (00401320)
00401131 add esp,8
00401134 jmp main+0FEh (0040110e)
00401136 push offset string "pause" (0042501c)
0040113B call system (004011b0)
00401140 add esp,4
00401143 xor eax,eax
本文摘自编程达人系列教材《汇编的角度——C语言》。