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


121

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

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

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

Node *m_next;

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

Node m_next;

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


14
@self请原谅我吗?为什么在没有什么指针的语言中没有链接列表?
Angew不再为2015年

41
请务必注意,C和C ++在对象指针和引用方面与Java有何不同。 Node m_next不是对节点的引用,而是整个节点的存储Node
Brian Cain 2015年

41
@self Java确实有一些指针,只是您没有明确使用它们。
m0meni 2015年

27
龟一路下跌不是一个选项。疯狂必须在某个地方结束。
WhozCraig 2015年

26
请忘记有关Java的所有知识。C ++和Java以根本不同的方式处理内存。去看这个问题以获取建议书,然后阅读。您将为我们所有人带来巨大的好处。
Rob K

Answers:


218

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

如果您将Node 对象存储在其内部,那将sizeof(Node)是什么?这将是sizeof(int) + sizeof(Node),这将是等于sizeof(int) + (sizeof(int) + sizeof(Node)),这将是等于sizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node)))等无穷大。

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


25
*除非对其进行懒惰的评估。无限列表是可能的,只是没有严格的评估。
Carcigenicate's

55
@Carcigenicate不是关于评估/执行Node对象上的某些功能,而是关于Node的每个实例的内存布局,必须在编译时确定,然后才能进行任何评估。
彼得斯,2015年

6
@DavidK这样做在逻辑上是不可能的。您在这里需要一个指针(实际上是一个间接指针)-确保该语言可以向您隐藏它,但是最后,这没有办法。
Voo 2015年

2
@David我很困惑。首先,您同意从逻辑上讲这是不可能的,但是您想考虑一下吗?删除任何C或C ++-就我所知,用您可能梦dream 以求的任何语言,这都是不可能的。根据定义,该结构是无限递归的,并且没有某种程度的间接性,我们不能破坏它。
Voo 2015年

13
我实际上指出了@benjamin(因为我知道否则有人会提出这个建议-并没有帮助),Haskell在创建时分配了thunk,因此之所以有效,是因为这些thunk给了我们所需的间接性。这不过是一个伪装着多余数据的指针……
Voo 2015年

178

在Java中

Node m_node

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

Node *m_node

意味着同一件事。区别在于,在C ++中,您实际上可以存储对象而不是指向它的指针。这就是为什么您必须说想要指针的原因。在C ++中:

Node m_node

意味着将节点存储在此处(并且显然不能用于列表-您最终得到的是递归定义的结构)。


2
@SalmanA我已经知道这一点。我只是想知道为什么没有指针就无法工作,而这正是公认的答案所能更好地解释的。
m0meni 2015年

3
@ AR7两种方式都给出了相同的解释。如果您将其声明为“常规”变量,则首次调用构造函数时,它将实例化为新实例。但是在完成实例化之前-在第一个实例的构造函数完成之前- Node将调用成员自己的构造函数,这将实例化另一个新实例...您将获得无尽的伪递归。从性能和角度来看,从严格意义上讲,这实际上并不是一个大小问题。
Panzercrisis

但是,您真正想要的只是指向列表中下一个对象的一种方法,而不是第一个Node中的对象Node。因此,您将创建一个指针,这实际上是Java处理对象的方式,而不是基元。当您调用方法或创建变量时,Java不会存储对象的副本,甚至不存储对象本身。它存储了对一个对象的引用,该对象本质上是一个指针,并在其周围包裹了一点小孩子的手套。这就是两个答案的本质所在。
Panzercrisis

它不是大小或速度问题-这是不可能的问题。所包含的Node对象将包含一个Node对象,而该Node对象将包含一个Node对象...实际上,不可能对其进行编译
pm100 2015年

3
@Panzercrisis我知道他们两个都给出了相同的解释。但是,这种方法对我没有帮助,因为它专注于我已经了解的内容:指针在C ++中如何工作以及指针在Java中如何处理。公认的答案专门针对为什么不使用指针是不可能的,因为无法计算大小。另一方面,它被模糊地保留为“递归定义的结构”。PS您刚刚写的解释比两个都更好地解释了:D。
m0meni 2015年

38

C ++不是Java。当你写

Node m_next;

在Java中,这与编写相同

Node* m_next;

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

Node m_next;

在C ++中,您Node在要定义的对象中放置了一个right 实例。它始终在那里,不能省略,不能new与它一起分配,也不能删除。这种效果在Java中是无法实现的,并且与Java在相同语法下的效果完全不同。


1
如果SuperNode扩展Node,则在Java中获得类似的东西可能是“扩展”,SuperNodes包含Node的所有属性,并且必须保留所有额外的空间。因此,在Java中,您无法执行“节点扩展节点”
Falco 2015年

@Falco是的,继承是基类就地包含的一种形式。但是,由于Java不允许多重继承(与C ++不同),因此您只能通过继承引入其他单个现有类的实例。这就是为什么我不认为继承可以代替就地成员包含。
cmaster-恢复莫妮卡2015年

27

您使用一个指针,否则您的代码:

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

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


5
更糟糕的是,不存在有效的大小:如果k == sizeof(Node)保持并且您的类型具有数据,则还必须保持,sizeof(Node) = k + sizeof(Data) = sizeof(Node) + sizeof(Data)然后再保持sizeof(Node) > sizeof(Node)
bitmask

4
@bitmask 实数中没有有效的大小。如果允许跨无限,则aleph_0可以使用。(只是太学究了:
k_g 2015年

2
@k_g好吧,C / C ++标准规定,返回值必须sizeof是无符号整数类型,因此希望能够实现超限甚至实数大小。(甚至更加学究!:p)
Thomas

@托马斯:甚至有人指出甚至还有自然数。(遍历-pedantic顶部:p)
bitmask

1
实际上,Node在此代码段结束之前甚至没有定义它,因此您不能在其中真正使用它。允许一个人隐式地向前声明一个尚未声明的类的指针是该语言所允许的一种作弊,以便使这种结构成为可能,而无需一直都显式地转换指针。
osa 2015年

13

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


3
更糟糕的是,对象在逻辑上不可能包含相同类型的东西。
Mike Seymour 2015年

从技术上讲,是否还会存在链接,因为它将是一个包含有节点的节点,依此类推?
m0meni

9
@ AR7:不,包含意味着它实际上位于对象内部,没有链接到对象。
Mike Seymour 2015年

9

您所描述的方法不仅与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();
}

Node_reference上面的声明解决了Java和C ++之间最有趣的语言级差异之一。在Java中,声明类型的对象Node将隐式使用对单独分配的对象的引用。在C ++中,您可以选择引用(指针)分配还是直接(堆栈)分配-因此您必须明确处理区别。在大多数情况下,您将使用直接分配,尽管不用于列表元素。
布伦特·布拉德本

不知道为什么我不建议使用std :: deque的可能性。
布伦特·布拉德本

8

总览

在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在他的帖子中评论。

有时,引用或对象指针指示嵌套的项目(也称为“ UML组成”)。

有时,引用或对象指针指示外部项(也称为“ UML聚合”)。

但是,同一类别的嵌套项目无法通过“无指针”技术应用。


7

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


6

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

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

如果Node m_node使用,则编译器不知道的大小,Node它将陷入计算的无限递归sizeof(Node)。切记:类不能包含其自身类型的成员


5

因为这在C ++中

int main (..)
{
    MyClass myObject;

    // or

    MyClass * myObjectPointer = new MyClass();

    ..
}

等效于Java

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

他们两个都MyClass使用默认构造函数创建一个新对象。


0

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

当然有一个简单的答案。

如果他们没有通过指针一个节点链接到下一个节点,那么它们将不会是链接列表

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.