为什么链表使用指针而不是将节点存储在节点内部

Why do linked lists use pointers instead of storing nodes inside of nodes

本文关键字:节点 存储 内部 链表 指针 为什么      更新时间:2023-10-16

我以前在Java中广泛使用链表,但我对C++很陌生。我正在使用在项目中提供给我的这个节点类,它很好

class Node
{
  public:
   Node(int data);
   int m_data;
   Node *m_next;
};

但我有一个问题没有得到很好的回答。为什么有必要使用

Node *m_next;

指向列表中的下一个节点,而不是

Node m_next;

我知道最好使用指针版本;我不会争论事实,但我不知道为什么它更好。关于指针如何更好地用于内存分配,我得到了一个不太明确的答案,我想知道这里是否有人可以帮助我更好地理解这一点。

这不仅更好,而且是唯一可能的方法。

如果将Node对象存储在其中,sizeof(Node)会是什么?它将是sizeof(int) + sizeof(Node),等于sizeof(int) + (sizeof(int) + sizeof(Node)),等于sizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node)))等无穷大。

这样的对象是不可能存在的。这是不可能的

在 Java 中

Node m_node

存储指向另一个节点的指针。你别无选择。在C++

Node *m_node

意思是一样的。不同之处在于,在C++中,您实际上可以存储对象,而不是指向它的指针。这就是为什么你必须说你想要一个指针。C++:

Node m_node

意味着将节点存储在这里(这显然不适用于列表 - 你最终会得到一个递归定义的结构(。

C++不是Java。当你写的时候

Node m_next;

在Java中,这与编写相同

Node* m_next;

在C++。在Java中,指针是隐式的,在C++中它是显式的。如果你写

Node m_next;

在C++中,您将Node的实例放在要定义的对象中。它始终存在,不能省略,不能与new一起分配,也不能删除。这种效果在Java中是不可能实现的,它与Java使用相同的语法所做的完全不同。

使用指针,否则代码:

class Node
{
   //etc
   Node m_next; //non-pointer
};

不会编译,因为编译器无法计算Node的大小。这是因为它依赖于自身——这意味着编译器无法决定它将消耗多少内存。

后者(Node m_next(必须包含节点。 它不会指向它。 这样就不会有元素的链接。

您描述的方法不仅与C++兼容,而且与其(大部分(子集语言 C 兼容。 学习开发 C 样式的链接列表是向自己介绍低级编程技术(如手动内存管理(的好方法,但它通常不是现代C++开发的最佳实践。

下面,我实现了四种关于如何在C++中管理项目列表的变体。

  1. raw_pointer_demo使用与您相同的方法 - 使用原始指针需要手动内存管理。 这里使用C++仅用于语法糖,所使用的方法在其他方面与 C 语言兼容。
  2. shared_pointer_demo列表管理仍然是手动完成的,但内存管理是自动的(不使用原始指针(。这与你可能在 Java 上经历过的非常相似。
  3. std_list_demo使用标准库list容器。这表明,如果您依赖现有库而不是滚动自己的库,事情会变得容易得多。
  4. std_vector_demo使用标准库vector容器。这将在单个连续内存分配中管理列表存储。换句话说,没有指向单个元素的指针。对于某些相当极端的情况,这可能会变得非常低效。但是,对于典型情况,这是C++中列表管理的推荐最佳做法。

注意:在所有这些中,只有raw_pointer_demo实际上要求显式销毁列表以避免"泄漏"内存。 其他三种方法将在容器超出范围时(在函数结束时(自动销毁列表及其内容。关键是:在这方面,C++能够非常"类似Java"——但前提是你选择使用你可以使用的高级工具来开发你的程序。


/*BINFMTCXX: -Wall -Werror -std=c++11
*/
#include <iostream>
#include <algorithm>
#include <string>
#include <list>
#include <vector>
#include <memory>
using std::cerr;

/** Brief   Create a list, show it, then destroy it */
void raw_pointer_demo()
{
    cerr << "n" << "raw_pointer_demo()..." << "n";
    struct Node
    {
        Node(int data, Node *next) : data(data), next(next) {}
        int data;
        Node *next;
    };
    Node * items = 0;
    items = new Node(1,items);
    items = new Node(7,items);
    items = new Node(3,items);
    items = new Node(9,items);
    for (Node *i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr << "n";
    // Erase the entire list
    while (items) {
        Node *temp = items;
        items = items->next;
        delete temp;
    }
}

raw_pointer_demo()...
9, 3, 7, 1

/** Brief   Create a list, show it, then destroy it */
void shared_pointer_demo()
{
    cerr << "n" << "shared_pointer_demo()..." << "n";
    struct Node; // Forward declaration of 'Node' required for typedef
    typedef std::shared_ptr<Node> Node_reference;
    struct Node
    {
        Node(int data, std::shared_ptr<Node> next ) : data(data), next(next) {}
        int data;
        Node_reference next;
    };
    Node_reference items = 0;
    items.reset( new Node(1,items) );
    items.reset( new Node(7,items) );
    items.reset( new Node(3,items) );
    items.reset( new Node(9,items) );
    for (Node_reference i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr<<"n";
    // Erase the entire list
    while (items)
        items = items->next;
}

shared_pointer_demo()...
9, 3, 7, 1

/** Brief   Show the contents of a standard container */
template< typename C >
void show(std::string const & msg, C const & container)
{
    cerr << msg;
    bool first = true;
    for ( int i : container )
        cerr << (first?" ":", ") << i, first = false;
    cerr<<"n";
}

/** Brief  Create a list, manipulate it, then destroy it */
void std_list_demo()
{
    cerr << "n" << "std_list_demo()..." << "n";
    // Initial list of integers
    std::list<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );
    // Insert '8' before '3'
    items.insert(std::find( items.begin(), items.end(), 3), 8);
    show("B: ", items);
    // Sort the list
    items.sort();
    show( "C: ", items);
    // Erase '7'
    items.erase(std::find(items.begin(), items.end(), 7));
    show("D: ", items);
    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_list_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

/** brief  Create a list, manipulate it, then destroy it */
void std_vector_demo()
{
    cerr << "n" << "std_vector_demo()..." << "n";
    // Initial list of integers
    std::vector<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );
    // Insert '8' before '3'
    items.insert(std::find(items.begin(), items.end(), 3), 8);
    show( "B: ", items );
    // Sort the list
    sort(items.begin(), items.end());
    show("C: ", items);
    // Erase '7'
    items.erase( std::find( items.begin(), items.end(), 7 ) );
    show("D: ", items);
    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_vector_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

int main()
{
    raw_pointer_demo();
    shared_pointer_demo();
    std_list_demo();
    std_vector_demo();
}

概述

在 C++ 中引用和分配对象有两种方法,而在 Java 中只有一种方法。

为了解释这一点,下图显示了对象如何在内存中存储。

1.1 C++ 没有指针的项目

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};
class CustomerClass
{
  public:
    int          Code;
    char[50]     FirstName;
    char[50]     LastName;
    // "Address" IS NOT A pointer !!!
    AddressClass Address;
};
int main(...)
{
   CustomerClass MyCustomer();
     MyCustomer.Code = 1;
     strcpy(MyCustomer.FirstName, "John");
     strcpy(MyCustomer.LastName, "Doe");
     MyCustomer.Address.Code = 2;
     strcpy(MyCustomer.Address.Street, "Blue River");
     strcpy(MyCustomer.Address.Number, "2231 A");
   return 0;
} // int main (...)
.......................................
..+---------------------------------+..
..|          AddressClass           |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: Street            |..
..| [+] char[10]: Number            |..
..| [+] char[50]: POBox             |..
..| [+] char[50]: City              |..
..| [+] char[50]: State             |..
..| [+] char[50]: Country           |..
..+---------------------------------+..
.......................................
..+---------------------------------+..
..|          CustomerClass          |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: FirstName         |..
..| [+] char[50]: LastName          |..
..+---------------------------------+..
..| [+] AddressClass: Address       |..
..| +-----------------------------+ |..
..| | [+] int:      Code          | |..
..| | [+] char[50]: Street        | |..
..| | [+] char[10]: Number        | |..
..| | [+] char[50]: POBox         | |..
..| | [+] char[50]: City          | |..
..| | [+] char[50]: State         | |..
..| | [+] char[50]: Country       | |..
..| +-----------------------------+ |..
..+---------------------------------+..
.......................................

警告:此示例中使用的C++语法类似于 Java 中的语法。但是,内存分配是不同的。

1.2 使用指针C++项

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};
class CustomerClass
{
  public:
    int           Code;
    char[50]      FirstName;
    char[50]      LastName;
    // "Address" IS A pointer !!!
    AddressClass* Address;
};
.......................................
..+-----------------------------+......
..|        AddressClass         +<--+..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: Street        |...|..
..| [+] char[10]: Number        |...|..
..| [+] char[50]: POBox         |...|..
..| [+] char[50]: City          |...|..
..| [+] char[50]: State         |...|..
..| [+] char[50]: Country       |...|..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|         CustomerClass       |...|..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: FirstName     |...|..
..| [+] char[50]: LastName      |...|..
..| [+] AddressClass*: Address  +---+..
..+-----------------------------+......
.......................................
int main(...)
{
   CustomerClass* MyCustomer = new CustomerClass();
     MyCustomer->Code = 1;
     strcpy(MyCustomer->FirstName, "John");
     strcpy(MyCustomer->LastName, "Doe");
     AddressClass* MyCustomer->Address = new AddressClass();
     MyCustomer->Address->Code = 2;
     strcpy(MyCustomer->Address->Street, "Blue River");
     strcpy(MyCustomer->Address->Number, "2231 A");
     free MyCustomer->Address();
     free MyCustomer();
   return 0;
} // int main (...)

如果你检查两种方式之间的差异,你会看到,在第一种技术中,地址项在客户内部分配,而第二种方式,您必须显式创建每个地址。

警告:Java 像第二种技术一样在内存中分配对象,但是,语法类似于第一种方法,这可能会让"C++"的新手感到困惑。

实现

因此,您的列表示例可能类似于以下示例。

class Node
{
  public:
   Node(int data);
   int m_data;
   Node *m_next;
};
.......................................
..+-----------------------------+......
..|            Node             |......
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................v..
...................................[X].
.......................................

总结

由于链表具有可变数量的项目,因此会根据需要和可用情况分配内存。

更新:

同样值得一提的是,正如@haccks在他的帖子中评论的那样。

有时,引用或对象指针表示嵌套项目(又名"U.M.L. 组合"(。

有时,引用或对象指针表示外部项目(又名"U.M.L. 聚合"(。

但是,同一类的嵌套项不能使用"无指针"技术应用。

附带说明一下,如果类或结构的第一个成员是下一个指针(因此没有虚拟函数或类的任何其他功能意味着 next 不是类或结构的第一个成员(,那么您可以使用"基"类或结构体,只需下一个指针, 并使用通用代码进行基本的链表操作,如追加、插入之前、从前面检索......这是因为C/C++保证类或结构的第一个成员的地址与类或结构的地址相同。基节点类或结构将只有一个指针供基本链表函数使用,然后根据需要使用类型转换在基节点类型和"派生"节点类型之间进行转换。旁注 - 在C++中,如果基节点类只有一个下一个指针,那么我假设派生类不能有虚函数。

为什么在链表中使用指针更好?

原因是,当您创建Node对象时,编译器必须为该对象分配内存,并为此计算对象的大小。
编译器已知任何类型的指针大小,因此可以使用自引用指针计算对象的大小。

如果使用Node m_node,则编译器不知道Node的大小,并且它将卡在计算sizeof(Node)无限递归中。永远记住:类不能包含其自己的类型成员

因为这在C++

int main (..)
{
    MyClass myObject;
    // or
    MyClass * myObjectPointer = new MyClass();
    ..
}

Java 中等效于此

public static void main (..)
{
    MyClass myObjectReference = new MyClass();
}

其中它们都使用默认构造函数创建一个新的MyClass对象。

为什么链表使用指针而不是将节点存储在节点?

当然有一个微不足道的答案。

如果它们没有通过指针将一个节点链接到下一个节点,它们就不会成为链表

链表作为一个东西的存在是因为我们希望能够将对象链接在一起。 例如:我们已经有一个来自某个地方的对象。 例如,我们现在想把那个实际的对象(而不是副本(放在队列的末尾。 这是通过添加从队列中已有的最后一个元素到我们正在添加的条目的链接来实现的。 用机器术语来说,这是用下一个元素的地址填充一个单词。