【问题标题】:Implementing copy constructor in a single linked list C++在单链表 C++ 中实现复制构造函数
【发布时间】:2017-09-13 18:00:16
【问题描述】:

我有一个 C++ 代码:

#include <iostream>
using namespace std;
struct Node;

typedef Node *NodePtr;

struct Node
{
    int Item;
    NodePtr Next;
};

class LinkedList
{
public:
    LinkedList();                   // default constructor
    ~LinkedList();                  // destructor
    void AddTail(int);              // adds item to tail
private:
    NodePtr Head;
};

LinkedList::LinkedList()
{
    Head = NULL; //declare head as null
}
//Adding on tail
void LinkedList::AddTail(int Item)
{
    NodePtr Crnt;
    NodePtr node = new Node;
    node->Item = Item;
    node->Next = NULL;
//if head is in null declare the added node as head
    if (Head == NULL)
    {
        Head = node;
    }
    else
    { //set the current to head, move the current node to current next node
        Crnt = Head;
        while (Crnt->Next != NULL)
        {
            Crnt = Crnt->Next;
        }
        //Add item to the tail of the linked list
        Crnt->Next = node;
    }
}

int main()
{
    LinkedList la;
    la.AddTail(30);
    la.AddTail(60);
    la.AddTail(90);
    LinkedList lb;
    return 0;
}

所以我的问题是如何实现一个复制构造函数(假设在对象 lb 上),该构造函数对列表参数进行深层复制,并添加代码以在空列表和非空列表上测试复制构造函数? 提前致谢。

【问题讨论】:

  • 简单的方法应该是迭代要复制的列表并为列表中的每个元素调用AddTail 方法。但是,嘿,如果你想要一个复制构造函数,你会想要一个赋值运算符来配合它。 May I suggest the Copy and Swap Idiom?
  • 你能把代码贴出来吗?
  • @Patrick,请发布您尝试实现复制构造函数的代码

标签: c++ constructor linked-list destructor copy-constructor


【解决方案1】:

试试这个(https://ideone.com/9lywXc 使用您发布的原始代码)

LinkedList::LinkedList(const LinkedList& other):Head(nullptr)
{
    cout << "copy constructor called:\n";
    if(other.Head == nullptr) return;
    NodePtr dummyHead = new Node;
    NodePtr curr = dummyHead;
    NodePtr othcurr = other.Head;
    for(; othcurr!=nullptr; othcurr = othcurr->Next)
    {
        curr->Next = new Node;
        curr = curr->Next;
        cout << (curr->Item = othcurr->Item) << ",";
        curr->Next = nullptr;
    }
    Head = dummyHead->Next;
    delete dummyHead;
}


int main()
{
    LinkedList la;
    la.AddTail(30);
    la.AddTail(60);
    la.AddTail(90);
    LinkedList lb(la);
    return 0;
}

输出:

复制构造函数调用:

30,60,90,

【讨论】:

  • 有效,但是对于可以在几行代码中完成的事情来说工作量太大了。
  • 我还没有足够的信誉来评论你的帖子。我还需要4个。我喜欢您的回复,除了使用 AddTail 的复制构造函数具有 O(n^2) 复杂性。对添加的每个新节点进行完整列表遍历。是的,可以使用尾指针等,但我想在不升级提问者的情况下给出一个可用且可破译的答案。不过,我认为你的整体信息更好。
  • 更多的 N Log N,但是是的。没有声称它是有效的,只是易于编写。坦率地说,这是您对链表效率最不担心的问题。您应该看到它在缓存中的表现不佳。 Stroustrup 有一篇很棒的文章:isocpp.org/blog/2014/06/stroustrup-lists 视频非常值得一看。
  • 一个更智能的添加功能,允许您指定插入位置,也使复制更快NodePtr Add(NodePtr after, int value) 将短路几乎所有的遍历并匹配您的性能。
  • 是的。追逐指针,我知道。 Chandler carruth 在youtube.com/watch?v=fHNmRkzxHWs 中的链表上也发疯了,这应该使用智能指针等,如果你正在复制列表,那么你为什么要这样做。无论哪种方式,asker 现在都有多种方式和观点来做到这一点
【解决方案2】:

编程的一大规则是不要重复自己 (DRY)。如果您有一个添加功能并且您知道它可以工作,请继续将其用于与添加相关的工作。这意味着继续添加愚蠢和多才多艺的内容符合您的最大利益。

应用 DRY 理念,假设 AddTail 方法正常工作,复制构造函数非常简单:为源列表中的每个节点调用 AddTail

LinkedList::LinkedList(const LinkedList & src):Head(nullptr)
{
    NodePtr node = src.Head;
    while (node != nullptr)
    {
        AddTail(node->Item);
        node = node->Next;
    }
}

由于Copy and Swap Idiom,有了一个有效的复制构造函数,赋值运算符也简单得可笑:

LinkedList & LinkedList::operator=(LinkedList src) 
// pass by reference performs the copy
{
    std::swap(Head, src.Head); // now just swap the head of the copy 
                               // for the head of the source
    return *this;
} // destructor fires for src and cleans up all the nodes that were on this list 

要完成The Rule of Three 三重奏,我们需要一个析构函数。这像 复制构造函数是 DRY 的一个应用程序:一遍又一遍地调用你的节点删除函数,直到列表为空。删除函数几乎是任何链表的必然要求,所以在这里我假设有一个叫做Remove

LinkedList::~LinkedList()
{
    while (Head != nullptr)
    {
        NodePtr temp = Head;
        Remove(Head);
        delete temp;
    }
}

因此,现在基于链接列表无法运行的两个功能,我们已经实现了基本维护所需的所有其他功能。您只需要经过测试且无错误的 AddRemove 函数,其余的几乎都是免费的。

而且因为AddTail 函数碰到了我的一个小豌豆……这里有一个大大降低函数复杂度的技巧:

void LinkedList::AddTail(int Item)
{
    NodePtr *Crnt = &Head; // instead of pointing where Head points, point at 
                           // Head now we don't care if it is head or any 
                           // other node's Next. They are all abstracted to 
                           // the same thing: A pointer to where the next 
                           // node can be found        
    while (*Crnt != NULL) // keep looking until end of list
    {
        Crnt = &(*Crnt)->Next; // by pointing at the next Next pointer
    }
    //Add item to the tail of the linked list
    NodePtr node = new Node;
    node->Item = Item;
    node->Next = NULL;
    *Crnt = node; // Now just plop the new node over top of the terminating NULL
}

Remove 函数(我未实现)使用相同的指针到指针技巧。

【讨论】:

    【解决方案3】:

    这是对 user4581301 先前答案的改进,因此复制构造函数在输入列表大小上为 O(n)。在封装列表类中使用一个额外的指针跟踪尾部:

    class LinkedList
    {
    public:
        LinkedList():Head(nullptr),Tail(nullptr){}
        LinkedList(const LinkedList& other);
        ~LinkedList() = default;                  // destructor
        void AddTail(int);              // adds item to tail
    private:
        NodePtr Head;
    
        //KEEP TRACK OF TAIL POINTER WITH EXTRA MEMBER
        NodePtr Tail;
    };
    
    //via user4581301 response
    LinkedList::LinkedList(const LinkedList & src):Head(nullptr),Tail(nullptr)
    {
        NodePtr node = src.Head;
        while (node != nullptr)
        {
            AddTail(node->Item);
            node = node->Next;
        }
    }
    
    //Adding on tail
    void LinkedList::AddTail(int Item)
    {
        NodePtr np = new Node{Item,nullptr};
        if(Tail == nullptr && Head == nullptr)
            Head = Tail = np;
        else
        {
            Tail->Next = np;
            Tail = Tail->Next;
        }
    }
    

    【讨论】:

    • 谢谢,答案很好,但我对:LinkedList::LinkedList(const LinkedList &amp; src):Head(nullptr),Tail(nullptr) 中的含义感到困惑。听说过::,但没有听说过:
    • 它称为构造函数初始化列表。它是直接初始化成员变量的方法,而不是通过 { foo = 5; 在构造函数主体中初始化//...etc } 从技术上讲,构造函数主体在初始化列表执行其初始化之后运行。 en.cppreference.com/w/cpp/language/initializer_listcprogramming.com/tutorial/initialization-lists-c++.html
    • @Patrick 初始化列表是 C++ 中最重要和最少教的概念之一。它允许您在进入构造函数的主体之前初始化成员变量和基类。在进入构造函数的主体之前,必须完全构造所有成员,有时会导致您对相同的对象进行两次初始化。在某些情况下可以节省大量时间。
    【解决方案4】:

    关于测试: 您可以添加功能以吐出列表的内容。或者您可以将其子类化为测试扩展。或者你可以像这样打破封装,使用 operator

    class LinkedList
    {
    public:
        LinkedList():Head(nullptr),Tail(nullptr){}
        LinkedList(const LinkedList& other);
        ~LinkedList() = default;                  // destructor
        void AddTail(int);              // adds item to tail
    
        //breaks encapsulation but make some nice sugar to look inside
        friend ostream& operator<<(ostream& s, LinkedList& l)
        {
            s << "list contents: ";
            NodePtr c = l.Head;
            for(; c!=nullptr;c=c->Next)
                s << c->Item << " ";
            s << endl;
            return s;
        }
    private:
        NodePtr Head;
        NodePtr Tail;
    };
    

    这样

    int main()
    {
        LinkedList la;
        la.AddTail(30);
        la.AddTail(60);
        la.AddTail(90);
        LinkedList lb(la);
        cout << lb;
        return 0;
    }
    

    吐出: 列表内容:30 60 90

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-06-17
      • 2012-08-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多