|
概念 提到指针,我们都知道指针是用来存储一个变量的地址。所以,当我们定义了一个指向指针的指针的时候(pointer to pointer),我们也称之为二级指针,那针对于这个二级指针来说,第一级指针存放的是指向的变量的地址,第二级指针存放的是第一级指针的地址。可以用下面这张图表示他们之间的关系。6 s8 M4 ~/ w! r/ g" g
: u8 |- i4 C( A8 r( S/ g 一级指针与二级指针关系示例#include <stdio.h>2 [) S4 W4 ~4 a. d4 K4 \# B1 v上图所表达的意思也就是,一级指针变量 ptr1 存放的是 var 变量的地址,二级指针变量 ptr2 存放的是一级指针变量的地址。这也就是关于二级指针的相关概念。 9 n/ Q% C+ q3 |. v8 W int main(void)2 }5 c8 s) Y7 b { int a = 10; int *p = &a;4 Z( [, Y4 |/ ?- _4 N' I int **q = &p;+ U3 d% G/ }7 X9 |0 h printf("a = %d\n",a);" W- l: y2 }7 ?: a9 U9 L1 d. B printf("&a = %p\n",&a);! Z; s1 X3 U/ }( l printf("p = %p\n",p); printf("&p = %p\n",&p);+ K/ j c0 w9 s" w& I' h printf("*p = %d\n",*p);' u" L. Z: `9 y% \3 r printf("q = %p\n",q); printf("&q = %p\n",&q);# x+ w d6 B0 N" _ printf("*q = %p\n",*q); printf("**q = %d\n",**q);1 c: o4 l% l! r( T- y- F3 m }5 R* c$ r' K* H. K9 T4 O 下图是代码运行的结果:
5 _; s& e. J r9 b9 F) D q = &p;结果也很明显了,一级指针变量 p 存放的是变量 a 的地址,二级指针变量 q 存放的是一级指针变量 p 的地址,所以根据以上结果也能得出下面的等式: *q = p = &a; **q = *p = a; 在了解了上述一级指针和二级指针的一个关系之后,我们再来看另外一个例子:2 D7 E; `* x$ K0 |; ^6 ` int main(void)" E# q3 V# ` I- Y现在有如下代码: { int **ipp;& L8 l7 G6 c$ A$ r% F; k int i = 5,j = 6,k = 7;1 q" W6 ]/ |8 n; ` int *ip1 = &i,*ip2 = &j; 2 {: ^$ i$ M7 T, M3 B7 h1 W } 如果这个时候,我们加了这么一句代码: ipp = &ip1;那么上述所涉及到的数据之间的关系是这样的:
" C6 j7 O. S6 R3 L. \4 k *ipp = ip2;5 \* u" A3 m% d% r1 W. e8 f( V根据上面这个图我们也可以知道,对于 ipp 的两次解引用的结果是 i 的值,也就是说 **ipp = 5,我想对于这个的理解并不困难,如果我继续在这个基础上添加代码,注意,是在上条代码的基础上添加如下代码: 在这条代码的作用下,数据关系图就发生了改变,改变如下所示:" |5 J. m9 F& H3 u
对于上述的变化来说,我们增加的代码改变的是 *ipp 的值,也就是说 ipp 的值是不会发生改变的,既然 ipp 的值不会发生改变,那么 ipp 指向 ip1 的关系不会发生改变,我们增加的代码改变了 *ipp 的值,那么也就是说改变了一级指针指向的值,而 ip2 是指向 j 的,所以也就有了上述的变化。 紧接着我们继续在第一条增加的代码的基础上重新增加一条代码,增加的代码如下: 那么这个时候所对应的数据关系图如下图所示:
这个原理和刚才的一样,不在这里赘述了。 那再讲述了上述的基本概念之后,我们知道二级指针变量是用于存放一级指针变量的地址的,那么在具体的实际应用中,又在什么地方可以用到二级指针呢?下面来看一个 C 语言函数传址调用的例子。7 e: e* D- Y( Z5 S8 [' P void swap(int *a,int *b)$ F: [8 _% _9 t# Z! J我们在刚学习指针的时候,都会碰到如下这样一个例子: {0 U8 I: b& g8 M2 a7 {* m3 E int temp;+ o' o9 r8 Y2 n4 ?% d5 m8 R temp = *a; *a = *b;+ C5 K8 l+ d$ |0 O( j' C *b = temp;5 X6 o, N1 v, z" V+ E) o+ L) w } 之所以在定义函数时,把函数的形参定义为指针,而非如下这样的形式: void swap(int a,int b);是因为C 语言在进行函数调用的时候,是将实参的值复制一份,并将其副本传递到函数调用里,如果形参定义的不是指针,那么在函数内部改变数值,不会对实参本来的值发生改变。而将形参定义成了指针的话,那么传到函数里面的值虽然是实参地址的一个副本,但是地址里存的值发生了改变,也就导致实参本来的值也发生了改变。 #include <stdlib.h>$ ?! ?& d' T# H- c/ X0 x' o有了上述分析的基础上,我们知道,如果要在一个函数内改变一个数的值,那么就需要将形参定义为指针。同样的,如果我们要在一个函数内改变一个指针的值,我们就需要将形参定义了二级指针,下面来看这样一个例子: int allocstr(int len,char **retptr) { char *p = malloc(len + 1);/*加 1 是为了 '\0' */- F8 R# N) Y# c. P if (p = NULL) return 0;5 T) q6 n: n# H$ _- @ *retptr = p;! ^8 @+ v7 \/ B# j( p+ ` return 1;/ r* y- t, k$ e- G1 u2 k2 } } 在调用的时候,是像下面这样子进行调用的: char *string = "hello world!"char *copystr;1 z$ x* b+ k l9 R% L1 B if (allostr(strlen(string),©str))2 n6 j. n: i1 K- M C strcpy(copystr,string); else printf("out of memory!\n");, p9 `7 b+ A/ {4 a2 m 上述这个例子就是涉及到字符串拷贝的一个实际的例子,因为我们要在 allostr 里改变指针变量 copystr 的值(要使用 malloc 分配内存),那么就需要把 copystr 的地址传到函数里,那么这个时候,所定义的函数形参也就需要是二级指针了。 二级指针在单链表中的应用首先,我们有这样一个单链表的数据结构: typedef struct ListNode# t6 E1 X, O8 B4 D' a{ int data;# _% S2 c1 o# K( a struct ListNode *next; }ListNode;9 f4 o. D1 M c% g/ l 依据这样一个数据结构,假定我们创建了一个如下所示的一个单链表:6 I/ k: z& Z5 j( L% [2 Z
那么我们如果要删除链表中的一个结点的时候,第一时间采用的可能是如下所示的代码: {( D( g3 N$ H: [, k ListNode *pre = NULL; ListNode *entry; for (entry = head; entry != NULL; entry = entry->next) 6 L& H" V& {- M/ S% H3 n& s { if (entry->data == target) {8 f* L: ~9 h( M3 q5 t /* 判断删除的结点是否是第一个结点*/ if (entry == head) 5 p- q: x0 l5 Q" F* T, q head = entry->next; else pre->next = entry->next; # x% P: F! l* Q3 ^/ E) A- A free(entry);* _7 O( l- r. P# t7 K break;/ x; ~ i# b3 a } pre = entry; }) K7 E8 K5 }( h" O& B! l! [3 i return head;( d' S2 Y, D6 v3 D9 v }, {5 k0 G w% A& d 上述代码所述的删除结点的思路遵循如下图所示的原理,首先是关于当所要删除的结点是第一个结点的时候,删除结点示意图如下所示:
N, C- r1 h2 \' E$ D- d$ I) B 如果要删除的结点不是处在第一个结点的位置,那么删除结点的原理示意图如下图所示:& d# W5 K+ p; A( H
上述就是一个使用一级指针操作链表的一个简单地例子,自己在理解这个例子的时候,也存在几个对我来说的难点,笔者写下来和大家分享一下,首先,
上面的例子中,在删除单链表的结点的时候,我们形参采用的是一级指针的方式,在这个过程中,还需要引入 pre 指针来解决这个问题,还有一种很巧妙的方法,利用了二级指针的特性解决了结点删除的问题,在这个过程中,运用二级指针,不需要进行删除第一个结点的判断。具体代码如下: void find_and_delete2(ListNode **head,int target)! g- A" ]# v n( b8 K{8 A- l6 u+ k6 |5 M6 i; P for (; *head != NULL; head = &(*head)->next) {7 O: a; Q4 v3 m' Z z if ((*head)->data == target)+ J1 G" P: F$ g9 }* p { (*head) = (*head)->next;$ d0 B) F' s9 P6 h Z+ B, y# W break;$ r& h$ T4 U, Q6 I. n } : g( i! K+ y. m* S% [ j1 X } 9 b. b2 t3 L) N* }/ o' E }; S, N7 g0 C/ |) ]# U% P, {! {: i 上述的代码没有创建任何局部变量,直接利用 head 进行遍历链表,因为其是二级指针,这样子进行遍历在函数结束后不会改变其本身的链表结构。然后,在进行删除的时候,(*head) 在函数结束后是会保持其在函数内的变化值的,所以也就完成了结点的删除。 结论上述就是关于二级指针的相关内容,总体来说,二级指针也是指针,用指针的思想来处理这个问题就好,区别只是在于一级指针是由于存放普通变量的地址的,二级指针是用于存放指针变量的地址的。另外需要注意的就是 C 语言在进行函数调用时,实参的传递采用的是实参值的一份拷贝。如果要在函数内部改变变量的值,就要传入指针,如果要在函数内部改变指针的值,就需要传入二级指针。 |
| 二级三级......都不在话下,都是套娃! |
微信公众号
手机版