Чому пов'язані списки використовують покажчики замість того, щоб зберігати вузли всередині вузлів


121

Я раніше широко працював із пов'язаними списками на Java, але я дуже новачок у C ++. Я використовував цей клас вузлів, який мені було дано в проекті просто чудово

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

але у мене було одне запитання, на яке не дуже відповіли. Чому це потрібно використовувати

Node *m_next;

щоб вказати на наступний вузол у списку замість

Node m_next;

Я розумію, що краще використовувати версію вказівника; Я не збираюся сперечатися з фактами, але не знаю, чому це краще. Я отримав не настільки чітку відповідь про те, як вказівник краще для розподілу пам’яті, і мені було цікаво, чи хтось тут може допомогти мені зрозуміти це краще.


14
@ Ви пробачте мене? Чому б мова, де все вказівник, не мала пов'язаних списків?
Ендже вже не пишається SO

41
Важливо відзначити, наскільки C і C ++ відрізняються від Java з точки зору покажчиків об'єктів від посилань. Node m_nextне є посиланням на вузол, це зберігання для самого Nodeсебе.
Брайан Каїн

41
@self Java має вказівники, ви просто не використовуєте їх явно.
m0meni

27
Черепахи аж донизу - це не варіант. Божевілля має десь закінчитися.
WhozCraig

26
Будь ласка, забудьте все, що знаєте про Java. C ++ та Java обробляють пам'ять принципово різними способами. Перегляньте це питання, щоб вибрати рекомендації для книги , і прочитайте її. Ти зробиш усім нам величезну прихильність.
Роб К

Відповіді:


218

Це не просто краще, це єдино можливий спосіб.

Якби ви зберігали Node предмет всередині себе, що б це sizeof(Node)було? Було б sizeof(int) + sizeof(Node), що було б рівним sizeof(int) + (sizeof(int) + sizeof(Node)), якому було б sizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node)))до нескінченності тощо.

Такий об'єкт не може існувати. Це неможливо .


25
* Якщо тільки це не ліниво оцінено. Нескінченні списки можливі, тільки не з суворою оцінкою.
Carcigenicate

55
@Carcigenicate мова не йде про оцінку / виконання певної функції на об'єкті Node - це про макет пам'яті кожного екземпляра Node, який повинен бути визначений під час компіляції, перш ніж будь-яка оцінка може відбутися.
Петерсіс

6
@DavidK Зробити це логічно неможливо. Тут вам потрібен вказівник (ну справді непрямий) - впевнений, що мова може приховати його від вас, але, врешті-решт, цього не обійти.
Voo

2
@David Я розгублений. Спочатку ви погоджуєтесь, що це логічно неможливо, але потім ви хочете це споглядати? Видаліть що-небудь з C або C ++ - це неможливо жодною мовою, про яку ви могли коли-небудь мріяти, наскільки я бачу. Ця структура за визначенням є нескінченною рекурсією, і без деякого рівня опосередкованості ми не можемо її зламати.
Voo

13
@ Бенджамін я насправді зазначив (тому що я знав, що інакше хтось підніме це - добре, це не допомогло), що Haskell виділив грона під час створення, і, отже, це працює, тому що ці грози дають нам необхідний нам непрямий характер. Це не що інше, як вказівник із додатковими даними у маскуванні ...
Voo

178

На Java

Node m_node

зберігає вказівник на інший вузол. У вас немає вибору щодо цього. В C ++

Node *m_node

означає те саме. Різниця полягає в тому, що в C ++ ви можете фактично зберігати об'єкт на відміну від вказівника на нього. Ось чому ви повинні сказати, що хочете вказівник. В C ++:

Node m_node

означає зберегти вузол прямо тут (і це очевидно не може працювати для списку - у вас виходить рекурсивно визначена структура).


2
@SalmanA Я це вже знав. Мені просто хотілося знати, чому це не спрацює без вказівника, що пояснює прийняту відповідь набагато краще.
m0meni

3
@ AR7 Вони обидва дають одне і те ж пояснення, лише за двома різними підходами. Якщо ви оголосили його як "звичайну" змінну, то вперше, коли конструктор буде викликаний, це дозволить це зробити новим екземпляром. Але перед тим, як закінчити його інстанціювання - до того, як буде закінчений перший конструктор - Nodeбуде викликаний власний конструктор члена , який створить інший новий екземпляр ... і ви отримаєте нескінченну псевдорекурсію. Це насправді не стільки питання розміру в абсолютно суворих і буквальних термінах, скільки це питання продуктивності.
Panzercrisis

Але все, чого ти справді хочеш, - це лише спосіб вказати на те, що є у списку, а не на Nodeте, що є насправді всередині першого Node. Таким чином, ви створюєте вказівник, який по суті є тим, як Java обробляє об'єкти, на відміну від примітивів. Коли ви викликаєте метод або робите змінну, Java не зберігає копію об'єкта або навіть самого об'єкта; він зберігає посилання на об'єкт, який, по суті, є вказівником з трохи закрученою рукавичкою, що навколо нього. Це те, що по суті говорять обидві відповіді.
Panzercrisis

це не розмір чи швидкість - це питання неможливості. Включений об'єкт Node включав би об'єкт Node, який включав би Node object ... Насправді його неможливо скомпілювати
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, ймовірно, було б "розширюється", якщо SuperNode розширює Node, SuperNodes включає всі атрибути Node і повинен резервувати весь додатковий простір. Тож на Java не можна робити "Node extends Node"
Falco

@Falco Правда, успадкування - це форма включення базових класів на місці. Однак, оскільки Java не допускає багаторазового успадкування (на відміну від C ++), ви можете витягнути лише екземпляр одного іншого попереднього класу шляхом успадкування. Тому я не вважав би спадщину заміною включення на місце.
cmaster - відновити моніку

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працює. (Просто будучи надмірно педантичним :-))
k_g

2
@k_g Добре, що стандарт C / C ++ передбачає, що повернене значення sizeofє непідписаним цілісним типом, тому існує надія на безмежні або навіть реальні розміри. (будучи ще більш педантичним!: p)
Томас,

@Thomas: Можна навіть зазначити, що туди йдуть навіть Природні числа. (Перехід над -педагогічним верхом: p)
бітмаска

1
Насправді, Nodeнавіть не визначено до кінця цього фрагмента, тому ви не можете реально використовувати його всередині. Дозволити імпліцитно вказувати вперед покажчики до ще недекларованого класу - це невелика хитра, яка дозволена мовою для того, щоб зробити такі структури можливими, без необхідності постійно чітко вказувати покажчики.
оса

13

Останній ( Node m_next) повинен містити вузол. Це не вказувало б на це. І тоді не було б зв’язку елементів.


3
Гірше, логічно було б неможливо, щоб об'єкт містив щось одного типу.
Майк Сеймур

Чи технічно все ще не було б з'єднання, оскільки це був би вузол, що містить вузол, що містить вузол тощо?
m0meni

9
@ AR7: Ні, стримування означає, що він буквально знаходиться всередині об'єкта, не пов'язаний з ним.
Майк Сеймур

9

Підхід , який ви описуєте сумісний не тільки з C ++, але і з його ( в основному) мови підмножина C . Навчитися розробляти пов'язаний список у стилі С - це хороший спосіб познайомитись із методами програмування низького рівня (наприклад, ручним управлінням пам'яттю), але це, як правило, не найкраща практика для сучасної розробки C ++.

Нижче я реалізував чотири варіанти, як керувати списком елементів у C ++.

  1. raw_pointer_demoвикористовує той же підхід, що і ваш - ручне управління пам’яттю, необхідне з використанням необроблених покажчиків. Використання 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

Чому зв'язані списки використовують покажчики замість того, щоб зберігати вузли всередині вузлів?

Звичайно, є тривіальна відповідь.

Якби вони не прив’язали один вузол до наступного за допомогою вказівника, вони не зв'язали б списки .

Існування пов'язаних списків як речі є тим, що ми хочемо мати можливість об'єднати об'єкти разом. Наприклад: у нас вже є об’єкт звідкись. Тепер ми хочемо поставити цей фактичний об’єкт (а не копію) наприкінці черги, наприклад. Це досягається додаванням посилання з останнього елемента, який уже знаходиться в черзі, до запису, який ми додаємо. У машинному плані це заповнення слова адресою наступного елемента.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.