在C语言当中,指针箭头"->"看起来是简单的,然而,好多人在学到链表之际,会被它难住。此符号从本质上来说,那是从一个结构体指针里把内部数据取出的快捷途径,要理解它呀,得先弄明白变量、指针、结构体这三层概念方可,不然的话,看链表代码就好似看天书一般。
变量不是房间里会变的东西
有不少人认为变量乃是值会发生变化的数据,只是这样的理解太过浅显,变量的实质是一块固定的内存空间,它具备不变的地址,空间当中存放的内容能够被改变,就如同某间教室这个房间一般,每天前来上课的学生并不相同,然而教室的位置以及大小却是未变的,变量的地址就等同于这间房的门牌号,变量的值恰似房间里的学生,平常我们运用变量名直接对学生进行操作,指针却能够让我们依据门牌号去找到房间。
声明一个整型变量int a,系统会分配一块内存,这块内存是4字节,给它贴上名叫"a"的标签。当你写下a = 5,是将5放进这块空间。但要是写int p = &a,p这个指针变量所存的是a那块空间的地址编号,例如0x7fff1234。此时p就是门牌号的记录本,顺着门牌号找到房间后看到的学生是a,而不是p。
结构体把多个房间打包成别墅
将不同类型数据组合起来的结构体,好似把几个功能各异的房间连接成一栋别墅,例如定义一个含有学号、姓名、成绩的学生结构体,此三个数据于内存之中是连续存放的,在用普通变量struct Student stu时,能够直接借由stu.id去访问学号,然而当使用指针struct Student p指向stu时,便无法再靠着p.id去访问,原因在于p是一个地址,并非结构体自身。
到了这个时候,"->"便发挥其作用了。p->id等同于(p).id,其含义是,首先依据p当中的地址去寻找到那座别墅,接着再获取别墅里的id房间。当编译器对这个符号进行处理之际,会自动去计算结构体成员的偏移量,就像id之处在结构体起始位置偏移0字节,name偏移4字节,成绩偏移20字节。p->id所做的便是取p所指向的地址加上0字节之后的内容。
链表里的箭头是串起别墅的锁链
链表的节点结构体当中,除去存放数据之外,有一个同类型的指针成员是必须存在的,该指针成员用于指向下一个节点,典型的定义呈现为struct Node{ int data; struct Node next; };以上情况里,next被当作一个指针变量,其所存储的内容是下一个节点的地址。要是你存在一个指针p,它指向当前节点,那么p->next所做的便是取出这个节点里所存储的下一个节点的地址,而p = p->next呢,就是将下一个节点的地址赋予p,从而让p指向接下来的那个节点。
对链表进行遍历的过程,便是持续不断地重复这样的操作。起始的时候,p指向头节点,通过访问p->data来获取数据,接着,p = p->next从而移动到下一个节点,一直到p变成NULL才停止。此操作容易出错的原因,在于有不少人将p->next与p弄混淆了。p是当前节点的地址,p->next是下一个节点的地址,它们两者的类型是相同的,然而含义却不一样。
指针的定位器功能决定了链表的灵活性
和数组不一样,链表并不在内存里连续存放,它的节点能够分散于各处。每个节点当中的next指针属于定位器,记录着下一个节点的准确地址。当你写下p = p->next的时候,这就等同于把定位器里的坐标更新成下一个节点的坐标。这种离散存储使得链表能够动态地进行节点的增删操作,相较于数组而言,不需要提前去申请连续的大块内存。
在内存当中,一个链表的节点,有可能位居地址0x1000、0x2050、0x1A80的位置,这些位置彼此并不是相邻的。然而借助next指针串联起来,便能够从一个节点找寻到下一个节点。针对p->next这个操作而言,在底层实际上是读取当前节点地址偏移某个字节之处的值,而这个值就是下一个节点的地址。要是next所存储的是0x2050,那么p->next就等同于0x2050,当赋值给p之后,p便指向了那个崭新的地址。
遍历链表时箭头的使用要区分两种情况
于链表操作里头,存在两种用场会用到箭头。其一乃是去访问当前节点的成员,就好比说p->data、p->next这样的情况,此时p是节点指针来着,箭头是用以取成员的。其二是将next的值赋予p自身,也就是说p = p->next,在这个时候,等号右边的p->next所取出的乃是地址值,等号左边的p是身为存放指针的变量。好多人把这两步给合并成一句话,很容易就忽略掉p->next本身也是属于一个指针类型的值。
写链表代码之际,时常会出现的一个错误乃是,混淆p以及p->next的类型,要是p属于struct Node类型,那么p->next同样是struct Node *类型,此二者能够相互进行赋值,然而倘若你写下p->next = p,那就致使当前节点的next指向自身,从而形成环,在进行遍历时会陷入死循环,要是你写下p = p,那便什么都未曾改变,正确的移动指针唯有p = p->next这一种书写方式。
掌握箭头含义才能看懂链表核心操作
链表所进行的插入操作以及删除操作,均是依赖于对箭头作出正确使用的。举例来说,当于节点prev的后方插入一个全新节点new时,那就需要书写new->next = prev->next; prev->next = new;。其中第一句是要使得新节点的next去指向prev原本的下一个节点,而第二句是要让prev的next去指向新节点。要是这两句书写的顺序颠倒了,那么便会造成后面的节点丢失的情况。要是不借助箭头,直接写成new.next = prev.next,那语法立马就错了,这是因为new是用于指向的指针并非结构体变量呀。
要是在删除节点之际,设若当前的这个节点恰恰就是prev,那么此时就要去删除prev->next所指向的那个节点,其写法应该是prev->next=prev->next->next。在这一句话当中,可以看到两次都用到了箭头,先是通过取prev->next从而获得了那个待删除节点的地址,接着,针对这个已经获取到的地址再去取->next进而得到下下个节点的地址,最终,把这个辛辛苦苦得到的地址赋予给prev->next。而想要弄明白这个链条的关键所在,就是要清晰地分辨出每个箭头究竟是从哪一个指针起始去取的。
瞧见这儿,你理应已然清楚p = p->next并非啥神奇法术,它仅是把当前节点所存的下一个节点的地址提取出来,放置进p这个Pointer变量里头,使得p指向接下来的节点。接着问个事儿:要是链表节点里存在不止一个指针成员,像既有next又有prev,那么于删除节点之际就得分别处置这俩指针,你能够撰写出正确的代码不?欢迎在评论区分享你的写法。