深入理解双指针(下)

转载:http://www.fenesky.com/blog/2014/07/03/pointers-to-pointers.html

 

本章中使用的程序是使用Linux的GCC编译出来的,所以汇编代码使用的是AT&T汇编指令,跟windows下使用Intel指令有所不同,详见AT&T与Intel汇编比较。同时,由于我是用的是64位机器,为了方便讲解32位的程序以及防止编译器对代码的优化影响我们对问题的分析,本章所讲解的所有代码编译选项为:gcc -m32 -O0。

概述

Pointers to Pointers:二级指针,我之前把它叫做双指针,比较专业的叫法是二级指针。二级指针是相对一级指针而言的。
二级指针一般用于函数参数传递:

addNode(Type** list);   

C语言参数值传递

很多C语言书上,对于参数的值传递都讲解的不是很清楚。对于值传递的理解有助于理解我们理解二级指针。

普通变量的值传递

先看看一段代码:

 1 #include <unistd.h>

 2 #include <stdio.h>

 3 #include <stdlib.h>

 4  void increase(int value)

 5 {

 6      value = value + 1;

 7 }

 8  

 9 int main(int argc, char** argv)

10  {

11      int count = 7;

12      increase(count);

13      printf("count = %d\n", count);

14  

15      return 0;

16  }

 

 

这段代码对应的汇编代码如下:

 1 080483e4 <increase>:

 2  80483e4:   55                      push   %ebp

 3  80483e5:   89 e5                   mov    %esp,%ebp

 4  80483e7:   83 45 08 01             addl   $0x1,0x8(%ebp)

 5  80483eb:   5d                      pop    %ebp

 6  80483ec:   c3                      ret    

 7 

 8 080483ed <main>:

 9  80483ed:   55                      push   %ebp

10  80483ee:   89 e5                   mov    %esp,%ebp

11  80483f0:   83 e4 f0                and    $0xfffffff0,%esp

12  80483f3:   83 ec 20                sub    $0x20,%esp

13  80483f6:   c7 44 24 1c 07 00 00    movl   $0x7,0x1c(%esp)

14  80483fd:   00  

15  80483fe:   8b 44 24 1c             mov    0x1c(%esp),%eax

16  8048402:   89 04 24                mov    %eax,(%esp)

17  8048405:   e8 da ff ff ff          call   80483e4 <increase>

18  //[...]

 

 

这段代码执行的结果 count = 7。 我是用gdb调试,打印ESP和count的地址如下:

(gdb) p $esp

$2 = (void *) 0xffffd2b0

(gdb) p &count

$3 = (int *) 0xffffd2cc

main函数内部的汇编如下:

sub $0x20,%esp #esp-0x20,栈向下生长0x20,用来存放局部变量 #在内存单元esp + 0x1c处存放7. #即count,我上面打印的 $3 - #2 = 0x1c. movl $0x7,0x1c(%esp) mov 0x1c(%esp),%eax #将内存单元0x1c即count变量的值copy到EAX寄存器中 mov %eax,(%esp) #copy count变量的内容到当前的ESP寄存器所指向的内存单元 call 80483e4 <increase> #调用increase函数

在我的机器上当前运行的ESP指针指向的内存单元是0xffffd2b0,栈向下生长了0x20,则当前栈桢(Stack Frame)的起始地址是0xffffd2b0到0xffffd2d0。count是局部变量,占用的是栈空间,上面gdb打印出来count的地址0xffffd2cc,正好落在main函数的栈桢内。

有一点需要注意的是,在increase调用之前,count变量被copy了一份放在当前ESP所指向内存单元0xffffd2b0,这个count就是为了用来传递参数用的。

接下来看看increase的汇编代码:

push %ebp #ebp压栈,保护上一个栈桢 mov %esp,%ebp #保护ESP addl $0x1,0x8(%ebp) #将copy出来的那个count变量+1 pop %ebp ret

increase的汇编代码比较简单,这里只需要解释下addl $0x1,0x8(%ebp)

由前面一句mov %esp,%ebp可以发现,此时EBP其实是指向栈顶。调用increase之前ESP是0xffffd2b0,由于调用increase需要将下一条IP指令压栈,则ESP = ESP - 0x04 = 0xffffd2ac。在进入increase之后,又执行了一句push %ebp,ESP = 0xffffd2ac - 0x04 = 0xffffd2a8。那么此时栈顶就是0xffffd2a8,EBP的内容就是0xffffd2a8。0x8(%ebp)表示的是EBP + 0x8处的内存单元:0xffffd2a8 + 8 = 0xffffd2b0出的内存单元。

addl $0x1,0x8(%ebp)这句汇编就是在内存单元0xffffd2b0处的内容加+1,最终将加一后的结果继续存放在0xffffd2b0处 。再回顾下,前面0xffffd2b0存放的内容:没错,就是copy出来的count。

看到这里,你会发现,在count传递到increase之后,一直都是在操作copy出来的那个count临时变量,而没有操作真正的count变量。可见,对于普通变量而言,参数的值传递就意味着只是简单的将变量copy了一份传递给函数,普通变量是无法改变外部原始变量的值。

指针的值传递(一级指针)

还是先看代码:

 1 #include <unistd.h>  2 #include <stdio.h>  3 #include <stdlib.h>  4  5 void increase(int* ptr)  6 {  7 *ptr = *ptr + 1;  8 }  9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 increase(&count); 14 printf("count = %d\n", count); 15 return 0; 16 }

这段代码对应的汇编代码如下:

080483e4 <increase>:  80483e4: 55 push %ebp  80483e5: 89 e5 mov %esp,%ebp  80483e7: 8b 45 08 mov 0x8(%ebp),%eax  80483ea: 8b 00 mov (%eax),%eax  80483ec: 8d 50 01 lea 0x1(%eax),%edx  80483ef: 8b 45 08 mov 0x8(%ebp),%eax  80483f2: 89 10 mov %edx,(%eax)  80483f4: 5d pop %ebp  80483f5: c3 ret 080483f6 <main>:  80483f6: 55 push %ebp  80483f7: 89 e5 mov %esp,%ebp  80483f9: 83 e4 f0 and $0xfffffff0,%esp  80483fc: 83 ec 20 sub $0x20,%esp  80483ff: c7 44 24 1c 07 00 00 movl $0x7,0x1c(%esp)  8048406: 00  8048407: 8d 44 24 1c lea 0x1c(%esp),%eax  804840b: 89 04 24 mov %eax,(%esp)  804840e: e8 d1 ff ff ff call 80483e4 <increase>  // [...]

这段代码的执行结果是8。
这段代码跟上一段代码的唯一区别是将count的地址传递给increase函数了。

main函数的汇编代码

push %ebp mov %esp,%ebp and $0xfffffff0,%esp sub $0x20,%esp movl $0x7,0x1c(%esp) lea 0x1c(%esp),%eax #将count变量的地址赋值给EAX mov %eax,(%esp) call 80483e4 <increase>

跟前面的main函数的唯一区别是lea 0x1c(%esp),%eax

看懂这段代码首先要补习下lea指令。lea指令跟mov指令很相似,区别在于lea类似于C语言中的&取地址。那么lea操作也只是简单的针对地址做加法而已,而不会针对这个地址单元取操作数。

那么这代码在调用increase函数之前,当前ESP所指向的内存单元的值是count变量的地址。而上一段代码在调用increase之前,当前ESP所指向的内存单元的值是count临时变量的值。

我们再来看看increase函数的汇编代码

push %ebp mov %esp,%ebp mov 0x8(%ebp),%eax #前面已经讲过了 # 取出EAX所指向的内存单元的值赋值给EAX # 也就是说执行此句话之后,EAX的内容是 # count变量的值,而不是地址。 mov (%eax),%eax lea 0x1(%eax),%edx #将EAX的内容加一,将加一后的结果存放到EDX mov 0x8(%ebp),%eax #重新将count变量的地址赋值给EAX #将EDX的内容存放到EAX所指向的内存单元 #就是将加一后的结果重新赋值给main函数里的count变量 mov %edx,(%eax) pop %ebp ret

理解这段汇编代码,需要记住一点,在调用increase之前,栈顶ESP所指向的内存单元的值是count变量的地址。之后,经过压栈IP,进入increase函数,再压栈EBP。则0x8(%ebp),EBP + 0x8表示的就是在调用increase前,栈顶所指向的内存单元,里面存放的是count变量的地址。也就是说mov 0x8(%ebp),%eax之后,EAX的内容就是count变量的地址。紧接着mov (%eax),%eax是现将EAX指向的内存单元的内容取出来存放到EAX中,此时EAX寄存器的内容已经不是地址了,而直接是count变量的值。然后对其做加一操作,存放到EDX当中。

下面是最关键的两句话:

mov 0x8(%ebp),%eax mov %edx,(%eax)

由于EBP + 0x8里面放的是count变量的地址,mov 0x8(%ebp),%eax之后,EAX中存放的就是count变量的地址。

EDX存放的是前面计算的结果,最后mov %edx,(%eax),将前面计算的结果重新存放到EAX所指向的内存单元,即重新给count变量赋值。

看到这里,你会发现,函数参数值传递,对于指针变量来说,也只是仅仅传递了一个内存地址,然后对这个内存地址进行操作。由于内存地址是进程级别的,所以,在函数内部 ,对地址所指向内容的修改,是可以带到函数外部的,是可以操作到函数外面的源变量的。

二级指针

我们改造下上面的代码

 1 #include <unistd.h>  2 #include <stdio.h>  3 #include <stdlib.h>  4 void increase(int* ptr)  5 {  6 *ptr = *ptr + 1;  7 ptr = NULL;  8 }  9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 int* countPtr = &count; 14 increase(countPtr); 15 printf("count = %d\n", count); 16 printf("countPtr = %p\n", countPtr); 17 return 0; 18 }

运行结果,count = 8,而countPtr则不是NULL。

运用前面的理论,其实很容易分析出问题。一级指针变量,也是一个普通变量,只不过这变量的值是一个内存单元的地址而已。countPtr在传递给increase之前,被copy到一个临时变量中,这个临时变量的值是一个地址,可以改变这个地址所在内存单元的值,但是无法改变外部的countPtr。

从这个结果可以得出一个结论:一级指针作为参数传递,可以改变外部变量的值,即一级指针所指向的内容,但是却无法改变指针本身(如countPtr)。

有了上面的理解基础,其实对于理解二级指针已经很容易了。

对于指针操作,有两个概念:

  • 引用:对应于C语言中的&取地址操作

Reference

  • 解引用:在C语言中,对应于->操作。

Dereference operator

对于一个普通变量,引用操作,得到的是一级指针。一级指针传递到函数内部,虽然这个一级指针的值会copy一份到临时变量,但是这个临时变量的内容是一个指针,通过->解引用一个地址可以修改该地址所指向的内存单元的值。

 

 

对于一个一级指针,引用操作,得到一个二级指针。相反,对于一个二级指针解引用得到一级指针,对于一个一级指针解引用得到原始变量。一级指针和二级指针的值都是指向一个内存单元,一级指针指向的内存单元存放的是源变量的值,二级指针指向的内存单元存放的是一级指针的地址。

二级指针一般用在需要修改函数外部指针的情况。因为函数外部的指针变量,只有通过二级指针解引用得到外部指针变量在内存单元的地址,修改这个地址所指向的内容即可。

我们针对上面的代码继续做修改

 1 #include <unistd.h>  2 #include <stdio.h>  3 #include <stdlib.h>  4 void increase(int** ptr)  5 {  6 **ptr = **ptr + 1;  7 *ptr = NULL;  8 }  9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 int* countPtr = &count; 14 increase(&countPtr); 15 16 printf("count = %d\n", count); 17 printf("countPtr = %p\n", countPtr); 18 return 0; 19 }

这段代码,运行结果count = 8, countPtr = NULL;

总结

首先,指针变量,它也是一个变量,在内存单元中也要占用内存空间。一级指针变量指向的内容是普通变量的值,二级指针变量指向的内容是一级指针变量的地址。

你可能感兴趣的:(指针)