Wcześniej intensywnie pracowałem z listami połączonymi w Javie, ale jestem bardzo nowy w C ++. Używałem tej klasy węzła, która została mi przekazana w projekcie
class Node
{
public:
Node(int data);
int m_data;
Node *m_next;
};
ale miałem jedno pytanie, na które nie udzielono zbyt dobrej odpowiedzi. Dlaczego konieczne jest użycie
Node *m_next;
wskazywać na następny węzeł na liście zamiast
Node m_next;
Rozumiem, że lepiej jest użyć wersji wskaźnikowej; Nie będę dyskutować o faktach, ale nie wiem, dlaczego jest lepiej. Otrzymałem niezbyt jasną odpowiedź na temat tego, w jaki sposób wskaźnik jest lepszy do alokacji pamięci i zastanawiałem się, czy ktoś tutaj mógłby mi pomóc lepiej to zrozumieć.
c++
pointers
linked-list
m0meni
źródło
źródło
Node m_next
nie jest odniesieniem do węzła, jest magazynem dla całegoNode
siebie.Odpowiedzi:
To nie tylko lepsze, to jedyny możliwy sposób.
Co by było, gdybyś przechowywał w sobie jakiś
Node
przedmiotsizeof(Node)
? Byłobysizeof(int) + sizeof(Node)
, co byłoby równesizeof(int) + (sizeof(int) + sizeof(Node))
, które byłoby równesizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node)))
itd. Nieskończoności.Taki obiekt nie może istnieć. To niemożliwe .
źródło
W Javie
przechowuje wskaźnik do innego węzła. Nie masz co do tego wyboru. W C ++
oznacza to samo. Różnica polega na tym, że w C ++ można faktycznie przechowywać obiekt, a nie wskaźnik do niego. Dlatego musisz powiedzieć, że chcesz mieć wskaźnik. W C ++:
oznacza przechowywanie węzła tutaj (i to wyraźnie nie może działać dla listy - kończy się rekurencyjnie zdefiniowaną strukturą).
źródło
Node
zostanie wywołany własny konstruktor członka , który utworzy instancję kolejnej nowej instancji ... i otrzymasz nieskończoną pseudorekurencję. Nie jest to tak bardzo kwestia rozmiaru w całkowicie ścisłych i dosłownych kategoriach, jak kwestia wydajności.Node
tego, który faktycznie znajduje się wewnątrz pierwszegoNode
. Tworzysz więc wskaźnik, który zasadniczo jest sposobem, w jaki Java obsługuje obiekty, w przeciwieństwie do prymitywów. Kiedy wywołujesz metodę lub tworzysz zmienną, Java nie przechowuje kopii obiektu ani nawet samego obiektu; przechowuje odniesienie do obiektu, który jest zasadniczo wskaźnikiem z owiniętą wokół niego odrobiną dziecięcej rękawiczki. Oto, co zasadniczo mówią obie odpowiedzi.C ++ to nie Java. Kiedy piszesz
w Javie to to samo, co pisanie
w C ++. W Javie wskaźnik jest niejawny, w C ++ jest jawny. Jeśli piszesz
w C ++ umieszczasz instancję
Node
bezpośrednio wewnątrz definiowanego obiektu. Jest zawsze obecny i nie można go pominąć, nie można go przypisaćnew
i nie można go usunąć. Ten efekt jest niemożliwy do osiągnięcia w Javie i jest zupełnie inny niż to, co robi Java z tą samą składnią.źródło
Używasz wskaźnika, w przeciwnym razie twój kod:
… Nie może się skompilować, ponieważ kompilator nie może obliczyć rozmiaru
Node
. Dzieje się tak, ponieważ zależy to od siebie - co oznacza, że kompilator nie może zdecydować, ile pamięci zajmie.źródło
k == sizeof(Node)
blokady, a Twój typ zawiera dane, musiałby również przechowywać tensizeof(Node) = k + sizeof(Data) = sizeof(Node) + sizeof(Data)
i wtedysizeof(Node) > sizeof(Node)
.aleph_0
działa. (Po prostu zbyt pedantyczny :-))sizeof
była typem całkowitym bez znaku, więc istnieje nadzieja na rozmiary nieskończone, a nawet rzeczywiste. (będąc jeszcze bardziej pedantycznym!: p)Node
nie jest nawet zdefiniowany przed końcem tego fragmentu, więc tak naprawdę nie można go używać w środku. Zezwolenie na niejawne deklarowanie w przód wskaźników do jeszcze niezadeklarowanej klasy jest małym oszustwem, na które pozwala język, aby umożliwić takie struktury, bez konieczności jawnego rzutowania wskaźników przez cały czas.Ta ostatnia (
Node m_next
) musiałaby zawierać węzeł. To by na to nie wskazywało. I nie byłoby wtedy łączenia elementów.źródło
Podejście, które można opisać jest kompatybilny nie tylko z C ++, ale także z jego (w większości) języka podzbiór C . Nauka tworzenia listy połączonej w stylu C jest dobrym sposobem na zapoznanie się z technikami programowania niskiego poziomu (takimi jak ręczne zarządzanie pamięcią), ale generalnie nie jest to najlepsza praktyka w przypadku nowoczesnego programowania w języku C ++.
Poniżej zaimplementowałem cztery warianty zarządzania listą elementów w C ++.
raw_pointer_demo
stosuje takie samo podejście jak Twoje - ręczne zarządzanie pamięcią wymagane przy użyciu surowych wskaźników. Użycie C ++ jest tutaj tylko dla cukru syntaktycznego , a zastosowane podejście jest poza tym zgodne z językiem C.shared_pointer_demo
liście zarządzanie nadal odbywa się ręcznie, ale zarządzanie pamięcią jest automatyczne (nie używa surowych wskaźników). Jest to bardzo podobne do tego, czego prawdopodobnie doświadczyłeś z Javą.std_list_demo
używalist
kontenera biblioteki standardowej . To pokazuje, o ile łatwiejsze staje się to, jeśli polegasz na istniejących bibliotekach zamiast zmieniać własne.std_vector_demo
używavector
kontenera biblioteki standardowej . To zarządza przechowywaniem listy w jednej ciągłej alokacji pamięci. Innymi słowy, nie ma wskaźników do poszczególnych elementów. W niektórych, raczej skrajnych przypadkach, może to stać się znacznie nieefektywne. Jednak w typowych przypadkach jest to zalecana najlepsza praktyka zarządzania listami w C ++ .Uwaga: ze wszystkich z nich, tylko
raw_pointer_demo
faktycznie wymaga, aby lista została jawnie zniszczona, aby uniknąć „wycieku” pamięci. Pozostałe trzy metody automatycznie niszczą listę i jej zawartość, gdy kontener znajdzie się poza zakresem (na zakończenie funkcji). Chodzi o to, że: C ++ może być pod tym względem bardzo podobny do języka Java - ale tylko wtedy, gdy zdecydujesz się rozwijać swój program przy użyciu dostępnych narzędzi wysokiego poziomu.źródło
Node_reference
deklaracja odnosi się do jednej z najbardziej interesujących różnic na poziomie języka między Javą a C ++. W Javie zadeklarowanie obiektu typuNode
spowodowałoby niejawne użycie odwołania do oddzielnie przydzielonego obiektu. W C ++ masz wybór między odniesieniem (wskaźnikiem) a bezpośrednim (stosem) przydziałem - więc musisz jawnie obsłużyć rozróżnienie. W większości przypadków użyjesz bezpośredniego przydziału, chociaż nie dla elementów listy.Przegląd
Istnieją 2 sposoby odwoływania się i przydzielania obiektów w C ++, podczas gdy w Javie jest tylko jeden.
Aby to wyjaśnić, poniższe diagramy pokazują, jak obiekty są przechowywane w pamięci.
1.1 Elementy C ++ bez wskaźników
Ostrzeżenie : składnia języka C ++ używana w tym przykładzie jest podobna do składni języka Java. Ale alokacja pamięci jest inna.
1.2 Elementy C ++ używające wskaźników
Jeśli sprawdzisz różnicę między tymi dwoma sposobami, zobaczysz, że w pierwszej technice pozycja adresu jest przypisana do klienta, podczas gdy w drugiej musisz jawnie utworzyć każdy adres.
Ostrzeżenie: Java alokuje obiekty w pamięci jak ta druga technika, ale składnia jest podobna do pierwszej, co może być mylące dla nowicjuszy w C ++.
Realizacja
Twoja przykładowa lista może być podobna do poniższego przykładu.
Podsumowanie
Ponieważ lista połączona ma zmienną liczbę elementów, pamięć jest przydzielana w miarę potrzeb i w miarę dostępności.
AKTUALIZACJA:
Warto również wspomnieć, jak skomentował @haccks w swoim poście.
Czasami odniesienia lub wskaźniki do obiektów wskazują elementy zagnieżdżone (znane też jako „Kompozycja UML”).
Czasami odwołania lub wskaźniki obiektów wskazują elementy zewnętrzne (inaczej „Agregacja UML”).
Jednak zagnieżdżonych elementów tej samej klasy nie można zastosować przy użyciu techniki „bez wskaźnika”.
źródło
Na marginesie, jeśli pierwszy element członkowski klasy lub struktury jest następnym wskaźnikiem (więc żadne funkcje wirtualne ani żadna inna cecha klasy, która oznaczałaby, że next nie jest pierwszym członkiem klasy lub struktury), to może używać "bazowej" klasy lub struktury z tylko następnym wskaźnikiem i używać wspólnego kodu do podstawowych połączonych operacji na listach, takich jak dołączanie, wstawianie przed, pobieranie z przodu, ... Dzieje się tak, ponieważ C / C ++ gwarantuje, że adres pierwszego elementu członkowskiego klasy lub struktury jest taki sam, jak adres klasy lub struktury. Klasa lub struktura węzła bazowego miałaby tylko następny wskaźnik do użycia przez podstawowe funkcje listy połączonej, a następnie w razie potrzeby do konwersji między typem węzła podstawowego a „pochodnymi” typami węzłów zostanie użyte rzutowanie typów. Nota boczna - w C ++, jeśli klasa węzła bazowego ma tylko następny wskaźnik,
źródło
Powodem jest to, że kiedy tworzysz
Node
obiekt, kompilator musi przydzielić pamięć dla tego obiektu i w tym celu obliczany jest rozmiar obiektu.Kompilator zna rozmiar wskaźnika do dowolnego typu dlatego można obliczyć rozmiar wskaźnika obiektu za pomocą wskaźnika odwołującego się do siebie.
Jeśli
Node m_node
jest używane zamiast tego, kompilator nie ma pojęcia o rozmiarzeNode
i utknie w nieskończonej rekurencji obliczeńsizeof(Node)
. Zawsze pamiętaj: klasa nie może zawierać elementu członkowskiego własnego typu .źródło
Ponieważ to w C ++
jest odpowiednikiem tego w Javie
gdzie obaj tworzą nowy obiekt
MyClass
przy użyciu domyślnego konstruktora.źródło
Jest oczywiście banalna odpowiedź.
Jeśli oni nie odwołują jednego węzła do drugiego przez wskaźnik, nie byłby powiązany list .
Istnienie połączonych list jest rzeczą, ponieważ chcemy mieć możliwość łączenia obiektów w łańcuchy. Na przykład: skądś już mamy obiekt. Teraz chcemy na przykład umieścić ten rzeczywisty obiekt (nie kopię) na końcu kolejki. Osiąga się to poprzez dodanie linku z ostatniego elementu już w kolejce do wpisu, który dodajemy. W kategoriach maszynowych jest to wypełnienie słowa z adresem następnego elementu.
źródło