今天在做leetcode203:移除链表元素时,反复遇到了报错:runtime error: member access within null pointer of type ‘ListNode’ (solution.cpp),报错提示的意思是试图访问’ListNode空指针类型的成员,就浅浅记录一下修复bug的过程吧。。。。

刚开始的代码是这样的,逻辑是先建立一个头结点放到链表头部,这样就可以统一链表结点删除的操作了,然后创建ListNode类型指针cur,初始化其指向头结点的下一个结点,利用while循环遍历链表,当cur指针指向Null时停止遍历。然后就报错了…

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyNode=new ListNode(0);
        dummyNode->next=head;
        ListNode* cur=dummyNode->next;
        ListNode* tmp=nullptr;
        while(cur!=nullptr){
            if(cur->val==val){
                tmp=cur->next;
                cur->next=cur->next->next;
                delete tmp;
                cur=cur->next;
            }else   cur=cur->next;
        }
        head=dummyNode->next;
        delete dummyNode;
        return head;
    }
};

报错的代码行是:cur->next=cur->next->next;,报错提示的意思是试图访问’ListNode空指针类型的成员,然后才发现原因出在:当移除的元素位于链表最后一个时,此时cur指向最后一个结点,cur->next为空,而访问cur->nex->next就是访问空指针的成员变量了

那么怎么解决这个问题呢?我最开始想到的办法是是增加判断语句,当cur指向的是最后一个结点时,就不执行
cur->next=cur->next->next;操作,而是将前一个结点的next指针置为空,但是因为cur指向的是当前结点,所以没有办法操作上一个结点,这个办法好像行不通。

于是我参考了代码随想录中的参考答案如下:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

可以发现这个代码与我的代码不同有两点:第一是cur指针初始化时指向的是头结点,而我的代码cur指向是指向头结点的下一个结点,即原始链表的第一个结点,第二是while循环中的条件为cur->next != NULL,即当cur结点的下一个结点不为空时继续循环,而我的代码是cur!=nullptr,即当前结点不为空时继续循环。

区别感觉很小,但是代码随想录中的这份代码并不会报错,原因就在这份代码的循环条件是cur->next != NULL,这样保证了cur->next不为空,因此就不会出现访问空指针的成员变量的情况;而且判断某结点的值时利用的是cur->next ->val进行判断,而不是让cur指针直接指向该结点,即利用cur->val判断,=这样做的好处是判断某一个结点的值时同时也能保留对其上一个结点进行修改的权利,这样当需要删除的元素在链表中最后一个时,可以很方便的将其前一个结点的next指针置为nullptr

于是我参考代码随想录把原始代码改成了下面的代码,但是一时脑瘫的在if(cur->next->val==val)的作用域最后多加了一句cur=cur->next;,以为此时指针也要后移一位,结果又遇到了同样的报错,而且报错转移到了if(cur->next->val==val)这一行,最后才反应过来,如果待删除元素位于链表最后,当执行完cur->next=cur->next->next时,cur->next为空,如果这时执行cur=cur->next;,则cur为空,等到下一次循环判断时,又会访问空指针成员变量,因此这一句不能加,同一个坑踩两次了属于是…而且在需要删除的结点情况下,cur指针不用后移,因为下一次循环cur->next访问的就是被删除节点的下一个结点了,这个逻辑当时也没理清。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyNode=new ListNode(0);
        dummyNode->next=head;
        ListNode* cur=dummyNode;
        ListNode* tmp=nullptr;
        while(cur->next!=nullptr){
            if(cur->next->val==val){
                tmp=cur->next;
                cur->next=cur->next->next;
                delete tmp;
                cur=cur->next;
            }else   cur=cur->next;
        }
        head=dummyNode->next;
        delete dummyNode;
        return head;
    }
};

最后总结来说:
1.利用让cur指针的移动范围为:[头结点,倒数第二个结点],比起范围为[第二个结点,最后一个结点]更加合理,可以有效处理待删除元素在链表尾部的问题,也能够避免bug的发生
2.使用链表时要严格检查有没有访问空指针的成员,特别要考虑待删除元素在链表边界的特殊情况

ps:如果细心的话可以发现代码随想录中判断空指针用的是Null,而我的代码里用的是nullptr,当时觉得有些疑惑,这里再简单总结一下在C++中Null和nullptr的区别:用Null表示空指针是C语言中遗留下来的传统,但在C++中可能会引起问题,因此在C++11中引入了nullptr表示空指针,如果要在C++中表示空指针,那么使用nullptr而不是Null.。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐