【问题标题】:Pointer to pointer assignment and dereference in C++指向 C++ 中的指针分配和取消引用的指针
【发布时间】:2021-02-04 15:43:17
【问题描述】:

假设我有一个如下所示的链表节点:

struct ListNode {
  int val;
  ListNode *next;
  ListNode(int x) : val(x), next(NULL) {}
};

目标是编写一个函数来删除单链表中的一个节点。一种在恒定时间内执行此操作的有效方法是这样的:

void deleteNode(ListNode* node) {
    auto *tmp = node->next;
    *node = *tmp;
    delete tmp;
}

这可行,但为什么我们需要取消引用指针?

如果node 是指针,tmp 是指针,为什么需要取消引用?为什么我不能node = tmp

【问题讨论】:

  • 分配给函数的(非引用)参数在函数外部无效。指针没有什么特别之处。
  • 如果我理解正确,那么您实际上是在删除 tmp 之前将 tmp_node 复制到当前节点。
  • 如果*node = *tmp 复制了值,我如何才能移动node 的地址(没有任何副本)?
  • “一种有效的方法”我对此表示怀疑。复制指向的对象而不是操作指针仅在具有非复杂赋值运算符的非常小的对象的情况下才有效...使用指向要删除的节点 preceeding 的指针,事情将是效率更高。请解释为什么这不是一个选项。
  • @Adrian C++ 中一个不可变的绝对规则,在任何情况下都不能改变对象的地址。

标签: c++ pointers dereference


【解决方案1】:

在执行*node=*tmp 时,您将*tmp 的所有字节复制到*node 中,因此 node->val 现在拥有 tmp->valnode->next 现在拥有 tmp->next

node 的旧内容已被遗忘(这很正常,因为您想摆脱此节点)但您将下一个节点的内容保存在同一位置。 然后,如果您删除下一个节点(通过tmp 知道),您不会丢失其内容(它已保存在上一个节点中)。

【讨论】:

    【解决方案2】:

    让我们分解deleteNode 函数的三行代码:

        auto *tmp = node->next;
    

    这将创建一个局部变量tmp,它将是传递的node 参数的next 字段的副本。这是一个指向列表中下一个结构的指针,一旦我们复制了它,我们就可以擦除或覆盖该成员。

        *node = *tmp;
    

    这会将tmp所指向的结构的实际数据(即列表中的下一个节点)复制到当前 em> 节点,覆盖next 字段。我们需要取消引用 both 指针,以便复制指向的结构的值。

        delete tmp;
    

    这将删除给定列表中的“下一个”节点。但是,我们已经将所有它的数据(包括它的next 成员)复制到我们当前的节点中,所以我们修改后的列表现在(有效地)从原始列表中的第二个开始(传递的参数);值得注意的是,*nodenext 字段现在将是最初存储在 node->next->next 中的地址——因此,我们已经“跳过”了列表中的一个条目(第二个)并将其删除。

    【讨论】:

      【解决方案3】:

      你不能只写 node = tmp 的原因是因为这不会改变你的函数之外的任何东西。

      给定这个链表

      node0 -> node1 -> node2 -> node3
      

      如果你想删除 node1,期望的结果是

      node0 -> node2 -> node3
      

      如果您不想主动修改node0中的指针值(即地址next),则必须将node2中的值移动到node1所在的位置。

      【讨论】:

        【解决方案4】:

        为什么我不能node = tmp

        你可以这样做,但它不会做任何有用的事情。 nodedeleteNode 中的局部变量。由于它是一个指针,您可以使用该本地指针来修改它所指向的内容,但修改指针本身在函数之外没有任何影响。

        实际上指针在这方面并没有什么不同。当你有时,你也无法从外部观察到任何影响

        void foo(int x) {
            x = 42;
        }
        

        传递引用是不同的:

        void bar(int& x) {
            x = 42;
        }
        int a = 0;
        bar(a);    // now a == 42 
        

        指针也一样:

        void bar_ptr(int*& x) {
              x = nullptr;
        }
        int* b = &a;
        bar_ptr(b);   // now b == nullptr
        

        【讨论】:

          【解决方案5】:

          如果您执行node = tmp,然后删除tmp,您将删除ListNodenode 指向。

          【讨论】:

            【解决方案6】:
            1. 正如其他人指出的那样,node = tmp 只是更改了参数(局部变量)
            2. *node = *tmp是复制ListNode的内容,相当于 node.val = tmp.val; node.next = tmp.next
            3. 此函数实际上删除了下一个元素 - 它可以工作,但它会使下一个指针无效(如果有将 node->next 引用为指针的东西,它现在是一个悬空指针)

            【讨论】:

            • 我不认为下一个指针悬空(因为第 2 点)
            • 好吧,我的意思是它可以工作,但是当指针已经在代码的其他部分使用时应该小心。可能我们不会期望指针在其上一个节点被删除时失效。
            【解决方案7】:

            你的函数真正做的是,它不会从参数中删除节点,而是下一个节点,用跟随者覆盖当前节点。

            指针的取消引用就像memcpy() 一样,将数据从下一个节点移动到当前节点。您不是在复制指针,而是在复制它指向的数据。

            这样你可以用相同的节点指针重复调用函数,它会沿着链向下移动。 但是,由于您没有检查指针,因此最后一个节点可能有一个 NULL 指针,并且会在取消引用时崩溃。

            所以你需要这样做

             if (tmp)
                 *node = *tmp;
            

            例子:

            typedef struct list
            {
                struct list *next;
                int value;
            } List;
            
            void deleteNext(List* node)
            {
                auto *tmp = node->next;
            
                if(tmp)
                    *node = *tmp;
            
                delete tmp;
            }
            int main(int argc, char *argv[])
            {
                List *l0 = new List;
                List *l1 = new List;
                l0->value = 0;
                l0->next = l1;
                l1->value = 1;
                l1->next = NULL;
            
                deleteNext(l0);
                deleteNext(l0); // Without the 'if' it will crash here.
            
                return 0;
            }
            

            【讨论】:

              【解决方案8】:

              但是为什么我们需要取消引用指针呢?

              让我们探索如果我们不通过指针间接会发生什么:

              auto *tmp = node->next;
              node = tmp;
              delete tmp;
              

              这相当于只是

              delete node->next;
              

              // resulting structure
              previous    node             next (deleted)     next next (leaked)
              1---------->2----dangling--->_                  4
              
              // desired structure that we get from the correct code
              previous    node             next (deleted)     next next
                                           _
              1-----------3---------------------------------->4
              

              所以,我们最终会删除错误的节点,并且在应该删除的节点中有一个悬空指针。


              请注意,在尝试删除最后一个节点时,即使是正确的 inirecting 版本也会被破坏。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2015-02-13
                • 2013-01-04
                • 1970-01-01
                • 2011-12-10
                • 1970-01-01
                • 1970-01-01
                • 2012-03-25
                相关资源
                最近更新 更多