使用C++中的Templates实现链表

Linked list implementation using Templates in C++

本文关键字:实现 链表 Templates 中的 C++ 使用      更新时间:2023-10-16

我是C++的新手,最近一直在研究数据结构。我以以下方式创建了链接列表:

class Random{
private:
   struct Node{
          int data;
          Node* next;
         };
} 

但我遇到了一段代码,它以以下方式做同样的事情:

template<Typename T>
struct Listnode {
      T data;
      shared_ptr<ListNode<T>> next;
};

我查了一下,发现当我们想要有多种数据类型时,我们会使用模板。像现在一样,我们可以使用intdoublefloat而不是"T"。而在前一种情况下,我们只能使用int。然而,我不明白如何:

Node* next

与相同

shared_ptr<ListNode<T>> next

以及这些将如何命名,我知道对于前者,我们使用:

Node->next = new Node;
Node->data = randomdata;

前一种方式是如何运作的。这两个实现的另一个问题是,哪一个更好,为什么?

T*ptr;form是声明指向内存的指针的基本方法,该内存持有类型为T的值。这种指针由数组T[]的基地址、new T()、new T[]或其他方法初始化。

正如你现在所看到的,有很多方法可以分配指针所指向的内存。这是释放所用内存的陷阱之一。您应该使用delete、delete[],还是我们指向的内存甚至不是我们分配的?如果我们忘记释放已分配的内存,或者试图访问已释放的内存,该怎么办?

=>使用原始指针,错误很容易发生!

聪明的人来拯救我们!智能指针,如std::unique_ptr,std::sharedptr为我们封装了这些原始指针,并处理类型安全的内存管理。因此,当超出范围时,unique_ptr中的内存会自动释放。如果不存在对shared_ptr的引用,那么它也适用。

我总是建议尽可能使用c++的智能指针!您应该使用哪种类型的智能指针取决于您想要实现的链表类型(例如,如果也支持循环列表)。顺便说一句,你有没有想过std::vector或std::list?

第二种形式是一种"智能"指针。大多数使用现代c++的代码都应该使用它们。

使用原始(非智能)指针,当对象超出范围时,您必须记住进行new/delete或new[]/delete[]的配对。在构造函数/析构函数的简单情况下,这并不是一个太大的负担。但是,当您在函数中使用指针,而该函数抛出异常时,释放指针会变得有点棘手。

智能指针有多种类型。独特的、共同的和脆弱的。唯一性是指只在一个地方使用的一次性对象(如对象或函数)。Shared适用于多个对象使用同一指针/资源的情况,并且您只想在指针的最后一个所有者超出范围时调用所分配对象的析构函数。弱点是指资源由其他人管理,并且在具有弱指针的对象超出范围后,指向的资源应该继续存在的情况(它们也是避免防止GC和导致内存泄漏的循环分配所必需的)。

聪明的指针是一件好事,你应该好好读一读(Stroustrups的书很棒)。现在很少有需要裸指针的情况。

正如Karoly Horvath所说,这不是一回事:

  • T*是指向类型为T的对象的"普通"指针,它在内存中存储一个地址,并隐式地存储我们可以在该地址找到的对象的类型(这对于知道目标内存的大小等很有用)
  • std::shared_ptr<T>是一个属于"智能指针"类别的对象,被称为"智能",因为它们可以通过跟踪对该内存位置的引用数量来管理被指向的内存。这意味着在实际操作中,当代码在运行时不再使用动态分配的内存时,它将为您释放

我想说的是,对于一个简单的链表(单链表或双链表),不需要使用shared_ptr s。它可能很有用,例如,对于具有动态递归结构的图。不过,为了通用性起见,最好使用节点的模板版本:

template <typename T>
struct ListNode
{
    T data;
    ListNode<T> *next;
};

首先让我们清除一些垃圾:

  1. 学习和理解链表的原始实现是很好的,因为您将在生产代码中遇到它们(出于许多好的或坏的原因)
  2. 如果你不得不在代码中使用"侵入性"链表,模板和"智能指针"会让你省去麻烦。(IMO)
  3. 集合类/模板几乎总能为您提供更好的服务

有了以上注意事项:

std::shared_ptr是一个"智能指针",它封装了一个原始指针(通常通过调用operator new产生),并将RAII风格的语义与一个允许底层对象的多个持有者引用内容而不会使其消失的契约一起添加到混合中。(正确使用时。)

链表"只是"程序在不移动数据的情况下遵循(希望但不要求)同构类型的惯例。使用("旧学校",而不是)链接列表所有链接管理都是您的责任。要么忘记,要么分心并"忘记"释放资源是非常容易的。这是导致许多夜晚调试和令人讨厌的事情(称为"内存泄漏")的原因。

混合"模板链接列表"的"更好"之处在于减少了资源管理责任。它并没有被消除。它将有助于减少忘记删除已分配节点的内存泄漏类型。它不会消除由循环引用引起的"内存泄漏"。(这种情况下,需要"外部参与者"来打破循环链,并且在"真实"代码中可能非常复杂。)

std::shared_ptr定义了运算符,允许您"假装"与std::shared指针正在管理的内容进行交互。这样,代码在视觉上看起来基本相同,但类(/可能)代表您隐藏了一点复杂性。(指针检查等)

哪个更好?国际海事组织也没有。然而,如果只在这两者之间进行选择,在大多数情况下,我绝对更喜欢"更智能的指针"版本,而不是"自己动手"风格。

如果是我,我会选择另一个集装箱。然而,如果您的意图是了解如何实现这些容器的基本原理(这是一件好事!),这并不是您想要听到的答案。:-)