リンクされたリストがノード内にノードを格納する代わりにポインタを使用する理由


121

私はJavaで広範囲に及ぶ前にリンクリストを扱ってきましたが、C ++は非常に初めてです。私はプロジェクトで与えられたこのノードクラスをうまく使っていました

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

しかし、私はあまりよく答えられなかった一つの質問をしました。なぜ使用する必要があるのですか

Node *m_next;

リストの次のノードを指すのではなく

Node m_next;

ポインタバージョンを使用する方がよいことを理解しています。事実について議論するつもりはありませんが、なぜそれが優れているのかはわかりません。私は、ポインターがメモリ割り当てにどのように優れているかについて明確な答えが得られなかったので、ここにいる誰かがそれをよりよく理解するのに役立つかどうか疑問に思っていました。


14
@selfごめんなさい?すべてがポインタである言語にリンクリストがないのはなぜですか?
AngewはSO

41
オブジェクトポインタと参照の点で、CおよびC ++がJavaとどのように異なるかを確認することが重要です。 Node m_nextはノードへの参照ではなく、ノード全体のストレージですNode
ブライアンカイン

41
@self Javaには、明示的に使用しないポインタがあります。
m0meni

27
ずっと下に亀がいるという選択肢はありません。狂気はどこかで終わらなければならない。
WhozCraig 2015

26
Javaについて知っていることはすべて忘れください。C ++とJavaは、基本的に異なる方法でメモリを処理します。書籍の推奨事項については、この質問を参照してください。1つ選んで読んでください。あなたは私たちすべてに大きな恩恵をもたらすでしょう。
Rob K

回答:


218

それはより良いだけではなく、それが唯一の可能な方法です。

Node オブジェクトを内部に格納した場合、どうなるでしょsizeof(Node)うか?それはsizeof(int) + sizeof(Node)、に等しいsizeof(int) + (sizeof(int) + sizeof(Node))、に等しいsizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node)))、など無限大になります。

そのようなオブジェクトは存在できません。それは不可能です。


25
*遅延評価されない限り。厳密な評価ではなく、無限リストが可能です。
Carcigenicate 2015

55
@Carcigenicateこれは、Nodeオブジェクトの関数を評価/実行するためのものではありません。Nodeのすべてのインスタンスのメモリレイアウトについてです。これは、評価を行う前に、コンパイル時に決定する必要があります。
Peteris

6
@DavidKこれを行うことは論理的に不可能です。ここにはポインター(実際には間接参照)が必要です。言語がポインターを非表示にできることを確認してください。
Voo

2
@David混乱しています。最初に、それは論理的に不可能であることに同意しますが、それを熟考したいですか?CまたはC ++のすべてを削除します。私が見る限り、どの言語で夢見ることは不可能です。その構造は定義上無限の再帰であり、ある程度の間接性がなければ、それを壊すことはできません。
Voo、2015

13
@benjamin私は実際に指摘しました(そうでなければ誰かがこれを持ち出すことを知っていたので-助けにはならなかった)Haskellは作成時にサンクを割り当てたので、これらのサンクが私たちに必要な間接性を与えるため、これは機能します。これは変装した追加データのあるポインタにすぎません...
Voo

178

Javaで

Node m_node

別のノードへのポインタを格納します。あなたはそれについて選択の余地はありません。C ++では

Node *m_node

同じことを意味します。違いは、C ++では、オブジェクトへのポインタではなく、実際にオブジェクトを格納できることです。そのため、ポインタが必要だと言わなければなりません。C ++の場合:

Node m_node

ここにノードを保存することを意味します(そしてそれは明らかにリストでは機能しません-再帰的に定義された構造に終わります)。


2
@SalmanA私はすでにこれを知っていました。私は、受け入れられた答えがはるかによく説明されているポインターがないとなぜそれが機能しないのか知りたいだけでした。
m0meni

3
@ AR7彼らは2つの異なるアプローチのもとで、どちらも同じような説明をしています。これを「通常の」変数として宣言した場合、コンストラクターが初めて呼び出されたときに、コンストラクターが新しいインスタンスにインスタンス化されます。しかし、インスタンス化が完了する前-最初のコンストラクタが完了する前に-メンバーNodeの独自のコンストラクタが呼び出され、別の新しいインスタンスがインスタンス化されます...そして、無限の疑似再帰が発生します。これは、パフォーマンスの問題であるため、完全に厳密で文字通りの意味でのサイズの問題ではありませ
Panzercrisis 2015

しかし、あなたが本当に望んでいるのは、リストの次のものを指す方法に過ぎず、Node実際に最初のものの中にあるものではありませんNode。つまり、基本的にJavaがプリミティブではなくオブジェクトを処理する方法であるポインターを作成します。メソッドを呼び出したり、変数を作成したりすると、Javaはオブジェクトのコピーやオブジェクト自体を保存しません。オブジェクトへの参照を格納します。オブジェクトへの参照は、基本的には、キディグローブが少し巻かれたポインターです。これは、両方の答えが本質的に言っていることです。
Panzercrisis 2015

サイズや速度の問題ではなく、不可能性の問題です。含まれるNodeオブジェクトには、Nodeオブジェクトを含むNodeオブジェクトが含まれます...実際にはコンパイルできません
pm100

3
@Panzercrisis両方が同じ説明をしていることを知っています。ただし、このアプローチは、C ++でポインターがどのように機能するか、Javaでポインターがどのように処理されるかについてすでに理解していることに焦点を当てていたため、あまり役に立ちませんでした。受け入れられた答えは、サイズを計算できないためにポインターを使用しないことが不可能である理由を具体的に説明しました。一方、これは「再帰的に定義された構造」として、漠然と残しました。PSあなたが書いたばかりのあなたの説明は、両方よりもそれをよりよく説明します:D。
m0meni

38

C ++はJavaではありません。あなたが書くとき

Node m_next;

Javaでは、それは書くことと同じです

Node* m_next;

C ++で。Javaではポインタは暗黙的ですが、C ++では明示的です。あなたが書くなら

Node m_next;

C ++では、Node定義しているオブジェクト内にインスタンスを配置します。常にそこにあり、省略できません。割り当てnewも削除もできません。この効果をJavaで達成することは不可能であり、同じ構文でJavaが行うこととはまったく異なります。


1
スーパーノードがノードを拡張する場合、Javaで同様のものを取得するにはおそらく「拡張」します。スーパーノードはノードのすべての属性を含み、すべての追加スペースを予約する必要があります。したがって、Javaでは「Node extends Node」を実行できません
Falco

@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)ます。
ビットマスク

4
@bitmask有効なサイズが実数に存在ません。超無限を許可する場合、aleph_0動作します。(
あまりに知識が豊富である:

2
@k_gさて、C / C ++標準では、の戻り値sizeofは符号なし整数型であることを義務付けているため、超有限または実際のサイズの希望さえあります。(さらに教訓的です!:p)
トーマス

@Thomas:自然数でさえあることを指摘する人もいるでしょう。(-pedantic top:pを超える)
ビットマスク

1
実際、Nodeこのスニペットの最後まで定義されていないため、実際には使用できません。まだ宣言されていないクラスへのポインタを暗黙的に前方宣言することを許可することは、そのような構造を可能にするために、常に明示的にポインタをキャストする必要なしに、言語によって許可される小さなチートです。
osa 2015

13

後者(Node m_next)にはノードを含める必要あります。それはそれを指さないでしょう。そして、要素のリンクはありません。


3
さらに悪いことに、オブジェクトに同じタイプのオブジェクトを含めることは論理的に不可能です。
マイクシーモア

それはノードを含むノードを含むノードであるので、技術的にリンクしているのではないでしょうか?
m0meni

9
@ AR7:いいえ、封じ込めとは、文字通りオブジェクトの中にあり、リンクされていないことを意味します。
マイクシーモア

9

あなたが説明するアプローチは、C ++だけでなく、その(主に)サブセット言語Cとも互換性があります。Cスタイルのリンクリストの開発を学ぶことは、低レベルのプログラミング手法(手動のメモリ管理など)を紹介する良い方法ですが、一般に、最新のC ++開発のベストプラクティスではありません

以下では、C ++でアイテムのリストを管理する方法に4つのバリエーションを実装しました。

  1. raw_pointer_demoあなたと同じアプローチを使用します-生のポインタの使用で必要な手動のメモリ管理。ここでのC ++の使用は、syntactic-sugar専用であり、使用されるアプローチは、その他の点ではC言語と互換性があります。
  2. ではshared_pointer_demo、リストの管理は依然として手作業で行われているが、メモリ管理が自動(生のポインタを使用していません)。これは、おそらくJavaで経験したこととよく似ています。
  3. std_list_demo標準ライブラリlistコンテナを使用します。これは、独自のライブラリをローリングするのではなく、既存のライブラリに依存する場合に、どれほど簡単になるかを示しています。
  4. std_vector_demo標準ライブラリvectorコンテナを使用します。これは、単一の連続したメモリ割り当てでリストストレージを管理します。つまり、個々の要素へのポインタはありません。特定のかなり極端なケースでは、これは著しく非効率になることがあります。ただし、一般的なケースでは、これはC ++でのリスト管理の推奨されるベストプラクティスです

注意:これらすべての中で、raw_pointer_demo実際に必要なのは、メモリの「リーク」を回避するためにリストを明示的に破棄することだけです。他の3つのメソッドは、コンテナーがスコープ外になると(関数の終了時に)、リストとその内容を自動的に破棄します。要点: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 ++の最も興味深い言語レベルの違いの1つに対処しています。Javaでは、型のオブジェクトを宣言するNodeと、個別に割り当てられたオブジェクトへの参照が暗黙的に使用されます。C ++では、参照(ポインター)割り当てと直接(スタック)割り当てのどちらかを選択できるため、区別を明示的に処理する必要があります。ほとんどの場合、リスト要素ではなく、直接割り当てを使用します。
ブレントブラッドバーン2017年

std :: dequeの可能性も推奨しなかった理由がわかりません。
ブレントブラッドバーン

8

概観

C ++でオブジェクトを参照して割り当てる方法は2つありますが、Javaでは1つしかありません。

これを説明するために、次の図は、オブジェクトがメモリに格納される方法を示しています。

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 (...)

両方の方法の違いを確認すると、最初の方法では住所項目が顧客内に割り当てられているのに対して、2番目の方法では各住所を明示的に作成する必要があることがわかります。

警告: Javaはこの2番目の手法のようにオブジェクトをメモリに割り当てますが、構文は最初の方法のようであり、「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

リンクされたリストがノードの内部にノードを格納する代わりにポインタを使用するのはなぜですか?

もちろん、ささいな答えがあります。

あるノードを次のノードにポインターでリンクしなかった場合、それらはリンクリストではありません。

リンクされたリストが存在するのは、オブジェクトをチェーンできるようにしたいからです。たとえば、すでにどこかにオブジェクトが存在しています。たとえば、実際のオブジェクト(コピーではない)をキューの最後に置きます。それはリンクを追加することによって達成されます、すでにキューにある最後の要素から、エントリへのをます。マシン用語では、それは次の要素のアドレスで単語を埋めることです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.