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

关于二级指针的相关内容

[复制链接]
gaosmile 发布时间:2020-6-24 13:42
概念
提到指针,我们都知道指针是用来存储一个变量的地址。所以,当我们定义了一个指向指针的指针的时候(pointer to pointer),我们也称之为二级指针,那针对于这个二级指针来说,第一级指针存放的是指向的变量的地址,第二级指针存放的是第一级指针的地址。可以用下面这张图表示他们之间的关系。
微信图片_20200624133701.png 二级指针关系图

上图所表达的意思也就是,一级指针变量 ptr1 存放的是 var 变量的地址,二级指针变量 ptr2 存放的是一级指针变量的地址。这也就是关于二级指针的相关概念。
一级指针与二级指针关系示例#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;
    int **q = &p;

    printf("a   = %d\n",a);
    printf("&a  = %p\n",&a);
    printf("p   = %p\n",p);
    printf("&p  = %p\n",&p);
    printf("*p  = %d\n",*p);
    printf("q   = %p\n",q);
    printf("&q  = %p\n",&q);
    printf("*q  = %p\n",*q);
    printf("**q = %d\n",**q);
}

下图是代码运行的结果:
微信图片_20200624133704.png 代码运行结果截图

结果也很明显了,一级指针变量 p 存放的是变量 a 的地址,二级指针变量 q 存放的是一级指针变量 p 的地址,所以根据以上结果也能得出下面的等式:
q = &p;
*q = p = &a;
**q = *p = a;

在了解了上述一级指针和二级指针的一个关系之后,我们再来看另外一个例子:
现在有如下代码:
int main(void)
{
    int **ipp;
    int i = 5,j = 6,k = 7;
    int *ip1 = &i,*ip2 = &j;   
}

如果这个时候,我们加了这么一句代码:
ipp = &ip1;
那么上述所涉及到的数据之间的关系是这样的:
微信图片_20200624133709.png 变量关系图

根据上面这个图我们也可以知道,对于 ipp 的两次解引用的结果是 i 的值,也就是说 **ipp = 5,我想对于这个的理解并不困难,如果我继续在这个基础上添加代码,注意,是在上条代码的基础上添加如下代码
*ipp = ip2;
在这条代码的作用下,数据关系图就发生了改变,改变如下所示:
微信图片_20200624133712.png 数据关系图

对于上述的变化来说,我们增加的代码改变的是 *ipp 的值,也就是说 ipp 的值是不会发生改变的,既然 ipp 的值不会发生改变,那么 ipp 指向 ip1 的关系不会发生改变,我们增加的代码改变了 *ipp 的值,那么也就是说改变了一级指针指向的值,而 ip2 是指向 j 的,所以也就有了上述的变化。
紧接着我们继续在第一条增加的代码的基础上重新增加一条代码,增加的代码如下:
*ipp = &k;
那么这个时候所对应的数据关系图如下图所示:
微信图片_20200624133715.png 数据关系图

这个原理和刚才的一样,不在这里赘述了。
二级指针的应用
那再讲述了上述的基本概念之后,我们知道二级指针变量是用于存放一级指针变量的地址的,那么在具体的实际应用中,又在什么地方可以用到二级指针呢?下面来看一个 C 语言函数传址调用的例子。
我们在刚学习指针的时候,都会碰到如下这样一个例子:
void swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

之所以在定义函数时,把函数的形参定义为指针,而非如下这样的形式:
void swap(int a,int b);
是因为C 语言在进行函数调用的时候,是将实参的值复制一份,并将其副本传递到函数调用里,如果形参定义的不是指针,那么在函数内部改变数值,不会对实参本来的值发生改变。而将形参定义成了指针的话,那么传到函数里面的值虽然是实参地址的一个副本,但是地址里存的值发生了改变,也就导致实参本来的值也发生了改变。
有了上述分析的基础上,我们知道,如果要在一个函数内改变一个数的值,那么就需要将形参定义为指针。同样的,如果我们要在一个函数内改变一个指针的值,我们就需要将形参定义了二级指针,下面来看这样一个例子:
#include <stdlib.h>

int allocstr(int len,char **retptr)
{
    char *p = malloc(len + 1);/*加 1 是为了 '\0' */
    if (p = NULL)
        return 0;
    *retptr = p;
    return 1;
}

在调用的时候,是像下面这样子进行调用的:
char *string = "hello world!"
char *copystr;
if (allostr(strlen(string),&copystr))
    strcpy(copystr,string);
else
    printf("out of memory!\n");

上述这个例子就是涉及到字符串拷贝的一个实际的例子,因为我们要在 allostr 里改变指针变量 copystr 的值(要使用 malloc 分配内存),那么就需要把 copystr 的地址传到函数里,那么这个时候,所定义的函数形参也就需要是二级指针了。
二级指针在单链表中的应用
首先,我们有这样一个单链表的数据结构:
typedef struct ListNode
{
    int data;
    struct ListNode *next;
}ListNode;

依据这样一个数据结构,假定我们创建了一个如下所示的一个单链表:
微信图片_20200624133719.png 单链表

那么我们如果要删除链表中的一个结点的时候,第一时间采用的可能是如下所示的代码:
ListNode *find_and_delete(ListNode *head,int target)
{
    ListNode *pre = NULL;
    ListNode *entry;

    for (entry = head; entry != NULL; entry = entry->next)
    {
        if (entry->data == target)
        {
            /* 判断删除的结点是否是第一个结点*/
            if (entry == head)
                head = entry->next;
            else
                pre->next = entry->next;

            free(entry);
            break;
        }
        pre = entry;
    }
    return head;
}

上述代码所述的删除结点的思路遵循如下图所示的原理,首先是关于当所要删除的结点是第一个结点的时候,删除结点示意图如下所示:
微信图片_20200624133722.png 第一个结点删除原理

如果要删除的结点不是处在第一个结点的位置,那么删除结点的原理示意图如下图所示:


微信图片_20200624133725.png 普通结点删除

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

二级指针在单链表结点删除的应用
上面的例子中,在删除单链表的结点的时候,我们形参采用的是一级指针的方式,在这个过程中,还需要引入 pre 指针来解决这个问题,还有一种很巧妙的方法,利用了二级指针的特性解决了结点删除的问题,在这个过程中,运用二级指针,不需要进行删除第一个结点的判断。具体代码如下:
void find_and_delete2(ListNode **head,int target)
{
    for (; *head != NULL; head = &(*head)->next)
    {
        if ((*head)->data == target)
        {
            (*head) = (*head)->next;
            break;
        }   
    }   
}

上述的代码没有创建任何局部变量,直接利用 head 进行遍历链表,因为其是二级指针,这样子进行遍历在函数结束后不会改变其本身的链表结构。然后,在进行删除的时候,(*head) 在函数结束后是会保持其在函数内的变化值的,所以也就完成了结点的删除。
结论
上述就是关于二级指针的相关内容,总体来说,二级指针也是指针,用指针的思想来处理这个问题就好,区别只是在于一级指针是由于存放普通变量的地址的,二级指针是用于存放指针变量的地址的。另外需要注意的就是 C 语言在进行函数调用时,实参的传递采用的是实参值的一份拷贝。如果要在函数内部改变变量的值,就要传入指针,如果要在函数内部改变指针的值,就需要传入二级指针。
收藏 1 评论1 发布时间:2020-6-24 13:42

举报

1个回答
cdlongbo 回答时间:2020-6-24 14:10:08
二级三级......都不在话下,都是套娃!

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版