对于C语言的参数传递都是值传递,当传传递一个指针给函数的时,其实质上还是值传递,除非使用双指针。
在讲双指针之前,还是先讲讲关于C语言函数调用的本质。
函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行,包括局部变量的空间分配与回收,都是通过栈来实现的。绝大多数CPU上的程序实现使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。当函数A调用函数B的时候,会把A的变量和参数压入到栈中,然后接着将B的变量和参数,局部变量压入到栈中。然后当A调用B是,B其实是在栈中取得A传递的参数和值,从而达到值传递的效果。
以一个交换2个数的值的函数调用为例。
void swap ( int *a, int *b ){ int c; c = *a; *a = *b; *b = c; }
int main(int argc, char **argv){ int a,b; a = 16; b = 32;
swap( &a, &b); return ( a - b ); }
|
那么,这段代码编译成汇编语言之后,除了会有代码段,数据段,堆栈,那么在调用的时候,会把main函数的参数变量压入main函数的栈帧,然后接着会压入swap函数的局部变量和参数。
那么按照刚才上述理论,编译成汇编语言以后,这个图就是函数调用的时候内存形态。
有了上面的图和理论基础,再来讨论双重指针的问题。当定义的时候,只有一个*号的时候,我们叫它一级指针。**个星号的叫二级指针。
当我们使用一级指针的时候,我们试图使用下述错误代码来实现2个数交换
void swap ( int *a, int *b ){ int *temp; temp = NULL; temp = a; a = b; b = temp; }
int main ( int argc, char **argv ){ int a,b; a = 16; b = 32; swap(&a, &b); return ( a - b ); }
|
这种方式按照理论上来说,是想通过调用swap函数,在swap函数内部,实现将交换&a,&b,即交换a和b的地址来达到目的。这样绝对不可以。因为当把,a,b的地址传到swap函数之后,按照上述栈帧图的结构来看,最终swap函数值通过栈指针来实现的,当swap使用的时候,还是把a,b的地址复制到寄存器中才能运算。那么,大家也许就明白了,swap把a,b的地址复制到寄存器中,然后运算,相当于抱着a,b的副本跑了,然后去操作,这些所有针对,a,b副本的操作管main函数中的a,b什么事?当swap返回之后,这些寄存器或者是栈空间随着swap的然会而释放了,而main函数的a,b没发生任何变法。所以上述代码是错误的,无法实现你想要的功能。
当我们用二级指针来实现上述功能的时候有就可以达到效果。
void swap ( int **a, int **b ){ int *tmp = NULL; tmp = *a; *a = *b; *b = tmp; }
int main () .... ....
|
这个时候,你会发现就能实现达到交换的目的。
这就是双指针神奇的功能,突破C语言传值的概念。那么,双指针是如何达到效果的呢?
当我们申明 **a之后,其实双指针变量a其实已经存在了。那么在内存中的效果如下图
再来看这个图,p就是这里**a种的a.当我们申明**p之后,p就已经存在了。其实这个bridge也已经存在了,那么我们要做的就是bridge中放我们要操作的数的地址。也就是&incom;那么,其实这样操作*bridge就是操作&incom也就是&b啊,这个一级指针没什么区别啊。
请注意,对于一级指针,我们要操作的是就是b,那么按照汇编语言的规则,就要把b放到寄存器或者栈中去操作,我们相当于复制了一个副本去操作,等我们操作完了,返回函数,这些寄存器,栈等都释放了,main中什么也没发生。但是如果我们用双指针,二级指针就不一样了。我们操作的是bridge.我们只是机械的复制一个bridge的内容到寄存器或者到栈中,而没有实际的去复制imcom的内容,我们只是告诉bridge,你要指向一个叫incom的地址,也就是说,bridge的内容要是incom的地址,即&incom;所以,就达到目的了。所以,双指针让参数传递具有穿透力。
双指针主要用在但我们想一个A函数传递参数的时候,但是我们希望在A内部对参数做任何修改都能保存起来,那么就是用双指针吧。
举个例子;
我们在做链表的时候,我们肯定希望在用一个函数creatLink(...)函数来增加链表节点。那么我们可以有2种方法来实现
第一种,用一级指针
typedef struct node{ ... ... }list;
node *create(list *l){ list *head; head = l; malloc...//为节点申请内存空间
... ... //操作
return head; }
int main(...){ ... ... list *listhead createList(listhead); ....
//以后的任何操作,我们都要考虑,我们是否拿到的是链表头指针,到底哪个是链表波的头指针,我们是否要renturn下来返回链表头指针??等。。。。 }
|
这样做可以达到删除增加节点的目的,但是,在任何情况下,我们的操作都得死死地抓住头指针,也即是我们增加删除节点后,任何对链表长度的修改,我们都要将链表头指针返回,即 return head;所以,我们要通过这个函数最后获得头指针,抓住他,死死地 抓住他,然后操作。
第二种方法:用双指针,也即是二级指针。
typedef struct node{ ... ... }list;
void create(list **l){ list *head; head = l; malloc...//为节点申请内存空间
... ... //操作
}
int main(...){ ... ... list *st createList(st) .... .... //以后的任何操作,不管是删除还是插入,我们不需要考虑,我们是否已经return head了,不需要,我们在任何情况下,对链表的操作都只需要使用st来完成,因为,st就是链表的头指针,不变,因为在申明st的时候,已经为st分配一个地址空间,它是存在的,一直存在,直到main函数结束
}
|