第8章 数组和指针的寻址

虽然数组和指针都是针对地址操作,但它们有许多不同之处。数组是相同数据类型的数据集合,以线性方式连续存储在内存中;而指针只是一个保存地址值的4字节变量。在使用中,数组名是一个地址常量值,保存数组首元素地址,不可修改,只能以此为基地址访问内存数据;而指针却是一个变量,只要修改指针中所保存的地址数据,就可以随意访问,不受约束。本章将深入介绍数组的构成以及两种寻址方式。(关于指针的讲解见2.5节。)

8.1 数组在函数内

当在函数内定义数组时,如果无其他声明,该数组即为局部变量,拥有局部变量的所有特性。数组中的数据在内存中的存储是线性连续的,其数据排列顺序由低地址到高地址,数组名称表示该数组的首地址,如:

int nArray[5]={1,2,3,4,5};

此数组为5个int类型数据的集合,其占用的内存空间大小为sizeof(数据类型)*数组中元素个数,即4*5=20字节。如果数组nArray第一项所在地址为0x0012FF00,那么第二项所在地址为0x0012FF04,其寻址方式与指针相同(见2.5.2节)。这样看上去很像是在函数内连续定义了5个int类型的变量,但也不完全相同。通过代码清单8-1的分析,我们将能够找出它们之间的不同之处。

代码清单8-1 数组与局部变量对比—Debug版


//C++源码说明:数组与局部变量定义以及初始化

void main(){

//整型数组定义,并初始化各成员

int nArray[5]={1,2,3,4,5};

//定义5个局部整型变量,分别初始化为1、2、3、4、5

int nOne=1;

int nTwo=2;

int nThree=3;

int nFour=4;

int nFive=5;

}

//C++源码与对应汇编代码讲解

int nArray[5]={1,2,3,4,5};

0040B498 mov dword ptr[ebp-14h],1;赋值数组第一项为:1

0040B49F mov dword ptr[ebp-10h],2;赋值数组第二项为:2

0040B4A6 mov dword ptr[ebp-0Ch],3;赋值数组第三项为:3

0040B4AD mov dword ptr[ebp-8],4;赋值数组第四项为:4

0040B4B4 mov dword ptr[ebp-4],5;赋值数组第五项为:5

int nOne=1;

0040B4BB mov dword ptr[ebp-18h],1;赋值第一个局部变量为:1

int nTwo=2;

0040B4C2 mov dword ptr[ebp-1Ch],2;赋值第二个局部变量为:2

int nThree=3;

0040B4C9 mov dword ptr[ebp-20h],3;赋值第三个局部变量为:3

int nFour=4;

0040B4D0 mov dword ptr[ebp-24h],4;赋值第四个局部变量为:4

int nFive=5;

0040B4D7 mov dword ptr[ebp-28h],5;赋值第五个局部变量为:5


在代码清单8-1中,连续定义的为同一类型的变量,这一点和数组相同。但是,这几个局部变量的类型不同时,将更容易区分出它们与数组间的不同之处。将代码清单8-1中的5个局部变量修改为如下所示。


char cChar='A';

float fFloat=1.0f;

short sShort=1;

int nInt=2;

double dDouble=2.0f;


再次编译调试,查看它们在汇编代码中的表现形式:


char cChar='A';

0040104B mov byte ptr[ebp-18h],41h

float fFloat=1.0f;

0040104F mov dword ptr[ebp-1Ch],3F800000h

short sShort=1;

;这里是VC++6.0反汇编引擎的一个错误,offset main+4Ah(0040105a)其实是1

;00401056 mov word ptr[ebp-20h],1

00401056 mov word ptr[ebp-20h],offset main+4Ah(0040105a)

int nInt=2;

0040105C mov dword ptr[ebp-24h],2

double dDouble=2.0f;

00401063 mov dword ptr[ebp-2Ch],0

0040106A mov dword ptr[ebp-28h],40000000h


从以上代码中可以看出,每一次为局部变量赋值时的类型都不相同,根据此特征即可判断这些局部变量不是数组中的元素,因为数组中的各项元素为同一类型数据,以此便可区分局部变量与数组。

对于数组的识别,应判断数据在内存中是否连续并且类型是否一致,均符合即可将此段数据视为数组。全局数组的识别非常简单,具体请见8.3节的讲解。

数组在Release版下不会有太大变化。类似于局部变量的优化方案,在寻址的过程中,数组不同于局部变量,不会因为被赋予了常量值而使用常量传播,如图8-1所示。

图 8-1 局部数组的定义和初始化—Release版

在图8-1中,连续使用了5个4字节的内存地址,依次赋值整型数据1、2、3、4、5。IDA下的标号var_14为常量-14,执行“esp+1Ch+var_14”后这里访问的地址值最小,因此这里为数组的首地址。双击标号“var_14”定位到标号定义处,在IDA下单击此标号,按键盘上的“*”键,以标号“var_14”所标示的地址为数组首地址,每个数组元素以4字节大小向后解释5个数据作为数组元素,如图8-2所示。

图 8-2 使用IDA标识数据元素

成功解释后,选取标号“var_14”并使用“N”键将标号重新命名为“nArray”。此时,程序中所有用到该数组标号的地方将全部被修改,如图8-3所示。

图 8-3 解释后的数组标号使用

学习了数组,就不得不提一下字符串。在C++中,字符串本身就是数组,根据约定,该数组的最后一个数据统一使用0作为字符串结束符。在VC++6.0编译器下,为字符类型的数组赋值(初始化)其实是复制字符串的过程。这里并不是单字节复制,而是每次复制4字节的数据。两个内存间的数据传递需要借用寄存器,而每个寄存器一次性可以保存4字节的数据,如果以单字节的方式复制就会浪费掉3字节的空间,而且多次数据传递也会降低执行效率,所以编译器采用4字节的复制方式,如代码清单8-2所示。

代码清单8-2 将字符数组初始化为字符串—Debug版片段1


char szHello[]="Hello World";

00401028 mov eax,[string"Hello World"(0041f01c)]

0040102D mov dword ptr[ebp-0Ch],eax

00401030 mov ecx, dword ptr[string"Hello World"+4(0041f020)]

00401036 mov dword ptr[ebp-8],ecx

00401039 mov edx, dword ptr[string"Hello World"+8(0041f024)]

0040103F mov dword ptr[ebp-4],edx


在代码清单8-2中,使用了eax、ecx、edx这三个寄存器,将常量字符串分为3段共12字节,每个寄存器保存4字节的数据,并依次复制到字符数组szHello中。代码清单8-2中的字符串长度为12字节,即4的倍数。当字符串的长度不为4的倍数时,又如何以4字节的方式复制数据呢?这个问题很好解决,只要在最后一次不等于4字节的数据复制过程中按照1或者2字节的方式复制即可。代码清单8-3显示了两者的区别。

代码清单8-3 将字符数组初始化为字符串—Debug版片段2


char szHello[]="Hello Worl";//将原字符串中的字符'd'去掉

00401028 mov eax,[string"Hello World"(0041f01c)]

0040102D mov dword ptr[ebp-0Ch],eax

00401030 mov ecx, dword ptr[string"Hello World"+4(0041f020)]

00401036 mov dword ptr[ebp-8],ecx

00401039 mov dx, word ptr[string"Hello World"+8(0041f024)]

00401040 mov word ptr[ebp-4],dx

00401044 mov al,[string"Hello World"+0Ah(0041f026)]

00401049 mov byte ptr[ebp-2],al


在代码清单8-3中,字符串的前8字节数据的复制过程没有变化,最后3字节的字符数据被拆分为两部分,先复制2字节的数据,然后再复制剩余的1字节的数据。

通过对代码清单8-2和代码清单8-3的分析,读者了解了字符数组被初始化为字符串的全过程。我们将通过8.2节进一步了解数组作为函数参数是如何传递的。