我之前一直有一个疑问,对数组名取地址和数组名的值为什么是一样的。单纯听别人口头讲述这个东西其实我觉得还是有点难以理解,我觉得从汇编的角度来看待这个问题,非常好理解。
int main()
{
int a = 1;
int c[10] = {1,2,3,4,5,6};
return 0;
}
上面这样的一段简单的代码,我们从汇编的角度来看一些,首先要说明的是,c语言中的标识符(也就是变量名),其实是没有地址空间的,也就是说他其实是不占空间的,只是他代表的值占据了空间。为什么叫标识符,因为他只是一个标识的作用,它就相当于一个地址,该地址的值是该标识符代表的是的地址。
就拿上面的a来说,我们画图进行理解
上面的a = 0xff1122并不是说a的值等于0xff1122,而是说a这个标识符是等同于这个地址值,他是一个替代作用,这一点需要理解。然后我们对变量a的访问对应到汇编上就等同于
MOV AL,[ff1122H]
就是将ff1122这个地址保存的值传送到AL寄存器中,所以说a这个标识符其实就是一个助计的作用,他代表的就是一个地址。
mov指令的作用就是将[]内的地址值位置中保存的值拿出来。
注意,汇编语言中其实也是有变量名的,为了避免这个问题继续循环下去,我们进行了简化,尽量站在机器的角度去看待问题。
你总不能让我给你写机器码吧。
CPU进行运算的时候是看不懂什么变量名的,对他来说一个指令中只能有操作数,而操作数具有不同的类型。
8086/8088的指令系统中的操作数主要有3种类型:立即数操作数(即常数)、寄存器操作数(如通用寄存器AX、段寄存器DS)和存储器操作数(存放在内存中的数据)。
所以我们写的变量名什么的,编译之后生成的机器码中没有这样的东西。
知道这些之后其实对于上面标识符的替代作用就能理解清楚了。
有了上面的基础之后,我们再来理解为什么数组名和数组名取地址的值相同。
我们现在也把数组名想象成标识符,他也是代表一个地址,但是这个地址与之前的地址是不同的,地址也是有类型的,其实也不能叫地址有类型,其实地址本身来说就是一个值,他的类型是由操作他的指令来确定的,操作他的指令在该地址上往后读几个位,就代表了该地址值所代表的类型。所以说,所有的标识符其实都是代表一个普通的地址值,但是对于该值的操作指令不同,也就展现出了不同的类型。
回到数组名,数组名代表的其实就是数组的首地址值,只是我们对于该地址值的指令与对上面a这个表示符所代表的地址值的指令不一样,就显示出了不同的结果。
上面我们说的a标识符等同于0xff1122,那么我们在程序中对a这个标识符取地址,其实就是将a标识符代表的地址给了你。
然后直接读取a的值的时候,就是读取a这个标识符代表的地址指向的值,然后根据不同的操作指令(不同的操作指令决定了指令要处理的数据有多长),从该地址开始往后读不同的位数。
看一段代码:然后我们利用GCC生成汇编代码,来看汇编是如何处理这段程序的
int a = 1;
int c[10] = {1,2,3,4,5,6};
printf("%d",a);
printf("%d",&a);
printf("%d",c);
printf("%d",&c);
生成的汇编代码,8,9这些有数字的行不用管,代表下面的汇编在c语言中代表的是哪一行,这只是codeBlock为了帮助我们对照代码进行的对应处理。
;8 : printf("%d",a);
0x4013d2 mov 0x3c(%esp),%eax
0x4013d6 mov %eax,0x4(%esp)
0x4013da movl $0x408024,(%esp)
0x4013e1 call 0x401350 <printf>
;9 : printf("%d",&a);
0x4013e6 lea 0x3c(%esp),%eax
0x4013ea mov %eax,0x4(%esp)
0x4013ee movl $0x408024,(%esp)
0x4013f5 call 0x401350 <printf>
;10 : printf("%d",c);
0x4013fa lea 0x14(%esp),%eax
0x4013fe mov %eax,0x4(%esp)
0x401402 movl $0x408024,(%esp)
0x401409 call 0x401350 <printf>
;11 : printf("%d",&c);
0x40140e lea 0x14(%esp),%eax
0x401412 mov %eax,0x4(%esp)
0x401416 movl $0x408024,(%esp)
0x40141d call 0x401350 <printf>
esp是一个指针寄存器,其内部的值指向栈顶的位置。0x3c就是偏移地址,最终操作的地址就是esp+0x3c。
lea可以将有效地址传送到指定的的寄存器,也就是将上面的esp+0x3c这个地址值传送到指定的寄存器。
mov 0x3c(%esp),%eax的意思就是将栈顶偏移0x3c位置的值保存到%eax寄存器中,我们就可以看出,直接输出a的时候,是将a这个标识符所代表的地址位置保存的值给输出。
lea 0x3c(%esp),%eax的意思就是将a标识符代表地址的偏移值保存到%eax寄存器中,也就是将a所代表的地址保存到%eax寄存器中,然后进行输出,最终输出的就是变量a的地址。
然后再看下面对于数组c的操作,我们发现我们直接输出a的时候,其也是用的lea指令,为什么,因为不能直接将c标识符所代表的地址位置的值进行输出,这是一个数组,他要怎么输出,这是有语义错误的,但是帮助我们进行了转换,也就是直接把c所代表的地址值进行了输出。
&c就好理解,c本来就代表的是这个数组,lea就是将数组的地址偏移量放到了%eax寄存器中。然后后面就进行了输出。
在来看一段代码,我们直接输出c[0],看汇编是什么样子
;12 : printf("%d",c[0]);
0x401422 mov 0x14(%esp),%eax
0x401426 mov %eax,0x4(%esp)
0x40142a movl $0x408024,(%esp)
0x401431 call 0x401350 <printf>
看到了没有,这次就用上的是mov而不是上面的lea,mov就是取 esp+0x14 地址指向的值放到%eax寄存器。