你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

关于二级指针的相关内容

[复制链接]
gaosmile 发布时间:2020-6-24 13:42
概念
提到指针,我们都知道指针是用来存储一个变量的地址。所以,当我们定义了一个指向指针的指针的时候(pointer to pointer),我们也称之为二级指针,那针对于这个二级指针来说,第一级指针存放的是指向的变量的地址,第二级指针存放的是第一级指针的地址。可以用下面这张图表示他们之间的关系。6 s8 M4 ~/ w! r/ g" g
微信图片_20200624133701.png 二级指针关系图
: u8 |- i4 C( A8 r( S/ g
上图所表达的意思也就是,一级指针变量 ptr1 存放的是 var 变量的地址,二级指针变量 ptr2 存放的是一级指针变量的地址。这也就是关于二级指针的相关概念。
一级指针与二级指针关系示例#include <stdio.h>2 [) S4 W4 ~4 a. d4 K4 \# B1 v
9 n/ Q% C+ q3 |. v8 W
int main(void)2 }5 c8 s) Y7 b
{
) \8 B0 e: p; G& Z    int a = 10;
% H0 ?. b$ [! j6 H6 I    int *p = &a;4 Z( [, Y4 |/ ?- _4 N' I
    int **q = &p;+ U3 d% G/ }7 X9 |0 h

# w" t% }* ?) q6 \& b* X8 V) 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);
  l( u8 F" u7 X/ w    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);
) D. A7 Q/ ~  b. O) v  ^    printf("&q  = %p\n",&q);# x+ w  d6 B0 N" _
    printf("*q  = %p\n",*q);
+ i4 W8 E, Y' @4 q( w    printf("**q = %d\n",**q);1 c: o4 l% l! r( T- y- F3 m
}
5 R* c$ r' K* H. K9 T4 O
下图是代码运行的结果:
1 H: G) h* a9 I" U8 n: \: k7 Q
微信图片_20200624133704.png 代码运行结果截图
5 _; s& e. J  r9 b9 F) D
结果也很明显了,一级指针变量 p 存放的是变量 a 的地址,二级指针变量 q 存放的是一级指针变量 p 的地址,所以根据以上结果也能得出下面的等式:
q = &p;
/ R9 w* o1 m/ w! c7 N( A4 p9 J*q = p = &a;
, }) H( H( n  k4 q) S! _! m/ ~+ w**q = *p = a;

7 g, M4 I6 [+ R7 e, U# p* c8 v
在了解了上述一级指针和二级指针的一个关系之后,我们再来看另外一个例子:2 D7 E; `* x$ K0 |; ^6 `
现在有如下代码:
int main(void)" E# q3 V# `  I- Y
{
1 a7 ]- T, }4 S5 [. X' c  {2 n    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
}

1 U) |! c5 ~/ [) N5 `5 {% |
如果这个时候,我们加了这么一句代码:
ipp = &ip1;
$ q1 P+ J8 f( `  Z. S6 g
那么上述所涉及到的数据之间的关系是这样的:
6 z/ @. j2 d: |% V& q
微信图片_20200624133709.png 变量关系图
" C6 j7 O. S6 R3 L. \4 k
根据上面这个图我们也可以知道,对于 ipp 的两次解引用的结果是 i 的值,也就是说 **ipp = 5,我想对于这个的理解并不困难,如果我继续在这个基础上添加代码,注意,是在上条代码的基础上添加如下代码:
*ipp = ip2;5 \* u" A3 m% d% r1 W. e8 f( V
在这条代码的作用下,数据关系图就发生了改变,改变如下所示:" |5 J. m9 F& H3 u
微信图片_20200624133712.png 数据关系图

  I6 e5 G  v2 Q- x& \* y对于上述的变化来说,我们增加的代码改变的是 *ipp 的值,也就是说 ipp 的值是不会发生改变的,既然 ipp 的值不会发生改变,那么 ipp 指向 ip1 的关系不会发生改变,我们增加的代码改变了 *ipp 的值,那么也就是说改变了一级指针指向的值,而 ip2 是指向 j 的,所以也就有了上述的变化。
3 {4 K7 S* b- N# ]5 A) q8 r紧接着我们继续在第一条增加的代码的基础上重新增加一条代码,增加的代码如下:
*ipp = &k;9 t4 z3 ~" z- i2 m" N$ e. j* H; U
那么这个时候所对应的数据关系图如下图所示:
4 v) I2 \& E# x" p
微信图片_20200624133715.png 数据关系图

: Z( c, P; ~; h% g这个原理和刚才的一样,不在这里赘述了。
二级指针的应用
那再讲述了上述的基本概念之后,我们知道二级指针变量是用于存放一级指针变量的地址的,那么在具体的实际应用中,又在什么地方可以用到二级指针呢?下面来看一个 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;
( D! Z$ ^3 I( R! V% c0 P" Z    *a = *b;+ C5 K8 l+ d$ |0 O( j' C
    *b = temp;5 X6 o, N1 v, z" V+ E) o+ L) w
}

+ }9 u8 k" N$ K3 m7 A7 C. r, Q' r
之所以在定义函数时,把函数的形参定义为指针,而非如下这样的形式:
void swap(int a,int b);
" p. Z* h5 m$ l# |$ a0 Q" r. G; O
是因为C 语言在进行函数调用的时候,是将实参的值复制一份,并将其副本传递到函数调用里,如果形参定义的不是指针,那么在函数内部改变数值,不会对实参本来的值发生改变。而将形参定义成了指针的话,那么传到函数里面的值虽然是实参地址的一个副本,但是地址里存的值发生了改变,也就导致实参本来的值也发生了改变。
$ G$ V6 {, d' I有了上述分析的基础上,我们知道,如果要在一个函数内改变一个数的值,那么就需要将形参定义为指针。同样的,如果我们要在一个函数内改变一个指针的值,我们就需要将形参定义了二级指针,下面来看这样一个例子:
#include <stdlib.h>$ ?! ?& d' T# H- c/ X0 x' o

+ b* y% K6 @9 W( \3 K" a; iint allocstr(int len,char **retptr)
" d0 a8 m: O/ c# O{
* ?- l) j$ |# I6 i9 f3 r8 Q    char *p = malloc(len + 1);/*加 1 是为了 '\0' */- F8 R# N) Y# c. P
    if (p = NULL)
3 \3 b! b9 D$ I5 v$ y: k        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 }
}

9 x, _" D; I! g1 w# Y! t- [
在调用的时候,是像下面这样子进行调用的:
char *string = "hello world!"
' i4 I0 e( U3 i4 V/ ?1 Schar *copystr;1 z$ x* b+ k  l9 R% L1 B
if (allostr(strlen(string),&copystr))2 n6 j. n: i1 K- M  C
    strcpy(copystr,string);
" g1 j7 N/ W& v/ M. _6 |7 K4 Pelse
+ F9 L- ~- V4 c: h% k+ E  h; ~( G    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
{
# h. J" S0 j' g( A( H& \) o& l    int data;# _% S2 c1 o# K( a
    struct ListNode *next;
" p6 L6 F2 I5 m}ListNode;
9 f4 o. D1 M  c% g/ l
依据这样一个数据结构,假定我们创建了一个如下所示的一个单链表:6 I/ k: z& Z5 j( L% [2 Z
微信图片_20200624133719.png 单链表

9 B' u. i! K# J那么我们如果要删除链表中的一个结点的时候,第一时间采用的可能是如下所示的代码:
ListNode *find_and_delete(ListNode *head,int target)7 ^) ?. I) l: F8 l+ h7 K; M
{( D( g3 N$ H: [, k
    ListNode *pre = NULL;
" H' r+ |4 e/ y    ListNode *entry;
9 ~0 F  M8 r) H& u
2 a/ i7 `5 K* A# x" a! W    for (entry = head; entry != NULL; entry = entry->next) 6 L& H" V& {- M/ S% H3 n& s
    {
# C" P4 D3 \3 g0 t        if (entry->data == target)
6 y# J8 G8 @* C2 {        {8 f* L: ~9 h( M3 q5 t
            /* 判断删除的结点是否是第一个结点*/
+ \& [# _6 X3 `( O$ A            if (entry == head) 5 p- q: x0 l5 Q" F* T, q
                head = entry->next;
: u8 {% u/ w' t0 h            else
; S) Y8 ^  R+ k, B' K  x8 x                pre->next = entry->next;
/ X$ T7 _; s, N# x% P: F! l* Q3 ^/ E) A- A
            free(entry);* _7 O( l- r. P# t7 K
            break;/ x; ~  i# b3 a
        }
- r, K. d  |% }6 d$ ]0 E        pre = entry;
, G4 A. l& Q! M3 H( f    }) K7 E8 K5 }( h" O& B! l! [3 i
    return head;( d' S2 Y, D6 v3 D9 v
}
, {5 k0 G  w% A& d
上述代码所述的删除结点的思路遵循如下图所示的原理,首先是关于当所要删除的结点是第一个结点的时候,删除结点示意图如下所示:
8 N" s1 [! k- i1 F
微信图片_20200624133722.png 第一个结点删除原理
  N, C- r1 h2 \' E$ D- d$ I) B
如果要删除的结点不是处在第一个结点的位置,那么删除结点的原理示意图如下图所示:
& d# W5 K+ p; A( H

- C6 X5 x* M! [# ]6 `: _- N
微信图片_20200624133725.png 普通结点删除

! X; S: n3 t2 j5 o8 [# _上述就是一个使用一级指针操作链表的一个简单地例子,自己在理解这个例子的时候,也存在几个对我来说的难点,笔者写下来和大家分享一下,首先,
  • 第一个难点就是头指针,在图中画的头指针指向了第一个结点,图中所示的头指针并没有数据域,只是单单地指向了第一个结点,在代码中的 head 指针变量却有数据域,并且就是第一个结点的数据,这个概念的理解其实是对于指针的理解,head 指向了第一个结点,一定注意在这里的 head 是头指针,并不是头结点。(这是笔者个人的理解,如果大家有不同的看法,欢迎各位朋友添加笔者微信共同探讨)。
  • 第二个难点就是上述函数中,函数有一个返回值,返回了头指针。为什么要返回呢?是因为当前传入函数的形参是一级指针,在函数内部改变 head ,在函数运行结束时,head 值并不会发生改变,所以要返回。
  • 第三个难点,那么为什么链表操作中,又能够删除中间的结点呢?是因为虽然 传进去的 head 是一级指针,但是 head 结构体成员内的 next 是一个指针,那这样的话,对于 next 成员来说它是一个二级指针,对于他的变化,在函数结束时是会产生改变的,所以可以删除中间的结点。

    9 p/ j+ o  I6 k5 E7 R# k
二级指针在单链表结点删除的应用
上面的例子中,在删除单链表的结点的时候,我们形参采用的是一级指针的方式,在这个过程中,还需要引入 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 C6 T8 v: o, z    {7 O: a; Q4 v3 m' Z  z
        if ((*head)->data == target)+ J1 G" P: F$ g9 }* p
        {
" T( K, y' a; n4 T            (*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 语言在进行函数调用时,实参的传递采用的是实参值的一份拷贝。如果要在函数内部改变变量的值,就要传入指针,如果要在函数内部改变指针的值,就需要传入二级指针。
赞 收藏 1 评论1 发布时间:2020-6-24 13:42

举报

1个回答
cdlongbo 回答时间:2020-6-24 14:10:08
二级三级......都不在话下,都是套娃!
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版