前提知识
这个问题比较像函数传递时的值传递和引用传递类似,具体可以看下面两段代码,方便理解后面双指针。可以看到输出结果左侧还是6,而右侧输出0,这就是关键所在
。形参m只是实参a的一个赋值的变量,形参我们都知道是函数调用时候才分配内存单元,当函数调用完毕后,形参就会被释放掉,所以左侧程序可以这么理解:定义一个a变量,它的值为6,当把a作为实参传进test这个函数时,系统会定义一个变量m,并且把a的值“6”赋给了m,然后又执行m=0。所以,到整个程序结束,m=0,a=6,所以a的值根本就没有发生改变。而右侧代码,a和m指向同一个地址,因此在test函数里m的修改对于main函数里的a同样有效。
void test(int m){ void test(int *m){
m = 0; *m = 0;
} }
int main(){ int main(){
int a = 6; int a = 6;
test(a); test(&a);
cout<<a<<endl; cout<<a<<endl;
return 0; return 0;
} }
若一个变量想通过函数来改变本身的值,将本身作为参数传递是不成功的,只有传递本身的指针(地址)才能达到这样的效果。
所以后面我们创建链表时,传递的是**双指针
**,这就是为什么参数是双指针的原因。
变量内存中都有自己的地址
我们来看下面一张图,就比较容易理解,只要是变量它都会有自己的地址(指针),即使是指针变量。
然后,指针它就是用来存地址的,只有两部分,一部分是附带自己的地址,一部分是存别人的地址
初始化链表需要双指针
定义链表节点如下代码所示:
struct ListNode {
int val;
ListNode *next;
ListNode(int x,ListNode *t) : val(x), next(t) {}
};
现在又两段初始化链表头结点的代码,执行结果完全不一样:
void test(){ void test(){
ListNode *head = NULL; ListNode *head = NULL;
init(&head); init(head);
print(head); print(head);
} }
void init(ListNode **node){ void init(ListNode *node){
*node = new ListNode(10,NULL); node = new ListNode(10,NULL);
} }
void print(ListNode *head){ void print(ListNode *head){
while(head != NULL){ while(head != NULL){
cout<<head->val<<"->"; cout<<head->val<<"->";
head = head->next; head = head->next;
} }
cout<<endl; cout<<endl;
} }
可以看出执行结果,左侧使用二级指针成功初始化插入节点,而右侧不行。如果传的是一级指针,那么仅仅只是head和node所指的是同一块内存区域,而node和head本身并不是同一个指针,函数执行完,node指针也被释放掉,而新生成的节点在内存中孤零零,没有人指向它。
使用双指针情况
下面来进行详细图解一下:
首先看使用双指针情况,对于在test函数中定义的head节点,定义了一个指针,指向ListNode 类型,其实也就是整个链表的头指针,只不过还没有赋值。
ListNode *head = NULL;
下面调用节点初始化函数,head本身就是指针,现在&head
即为取地址,相当于将head指针的地址传递过去,刚好对应于初始化函数void init(ListNode **node)
中的二级指针,其中**node
表示指针的指针。内侧的指针*node
指向链表,即对应于*head
,用来初始化,外侧的**head
指向链表指针,相当于上面讲的指针传递,这样才可以把init函数中node的修改作为参数传回给主程序。
init(&head);
其原型函数是void init(ListNode **node)
图示如下:
其实*node
就是头指针head的值了,加*
号就代表指针的值,new会申请一个结点,然后返回结点的首地址,其实这个新生成的结点是没有名字的,为了方便假设为x。具体图示如下图所示
*node = new ListNode(10,NULL);
为了更清晰说明我们写代码打印每一个地址,代码如下以及执行结果
void test(){
ListNode *head = NULL;
cout<<"1."<<&head<<endl;
init(&head);
cout<<"7."<<head<<endl;
print(head);
}
void init(ListNode **node){
cout<<"2."<<&(*node)<<endl;
cout<<"3."<<*node<<endl;
cout<<"4."<<(node)<<endl;
*node = new ListNode(10,NULL);
cout<<"5."<<(*node)<<endl;
cout<<"6."<<node<<endl;
}
下面使用图示对每步操作,地址变化做详细说明:
使用单指针情况
如果理解上面的情况,再分析右侧代码不使用双指针,就比较好分析了。编译器总是要为函数的每个参数制作临时副本,比如看输出的1和2,看到head和node的地址都不一样,在函数init中node指向生成的头指针,然而函数结束后,head还是原来那样没什么变化,node被销毁释放掉,至于新生成的结点,则是孤零零的在内存区里瑟瑟发抖,等待有人来指向它,这就是需要双指针原因。用了双指针,head指向了新生成的结点,node被释放掉,皆大欢喜。
void test(){
ListNode *head = NULL;
cout<<"1."<<&head<<endl;
init(head);
cout<<"7."<<head<<endl;
print(head);
}
void init(ListNode *node){
cout<<"2."<<&node<<endl;
cout<<"3."<<node<<endl;
node = new ListNode(10,NULL);
cout<<"4."<<node<<endl;
cout<<"5."<<&node<<endl;
cout<<"6."<<node->val<<endl;
}
具体图示如下: