Listy powiązane zorientowane obiektowo
Transkrypt
Listy powiązane zorientowane obiektowo
Rozdział 3. Dziedziczenie, polimorfizm oraz wielokrotne wykorzystanie kodu Listy powiązane zorientowane obiektowo Aby zilustrować potęgę polimorfizmu, przeanalizujmy zorientowaną obiektowo listę powiązaną. Jak zapewne wiesz, lista powiązana jest strukturą danych, zaprojektowaną do przechowywania nieokreślonej liczby obiektów. Istnieje oczywiście możliwość przechowywania obiektów w tablicy, lecz tablice muszą posiadać określony rozmiar. Jeżeli zatem nie wiesz dokładnie, ilu obiektów będziesz potrzebował do stworzenia całego projektu, korzystanie z tablicy może nie być najlepszym rozwiązaniem. Zakładając zbyt dużą tablicę tracisz pamięć, zakładając zbyt małą ryzykujesz, iż nie zdoła ona pomieścić wszystkich elementów. Właśnie z tych powodów warto skorzystać z list powiązanych. Projektowanie listy powiązanej Lista powiązana jest zazwyczaj implementowana jako łańcuch węzłów. Każdy węzeł (ang, node) wskazuje na obiekt (Twoje Dane) oraz na następny węzeł listy. Gdy łańcuch nie zawiera już więcej węzłów, wówczas węzeł wskazuje na zero – rysunek 3.1. Węzeł Węzeł Węzeł Dane Dane Dane Zero Rys. 3.1. Lista powiązana Aby każdy węzeł posiadał określoną funkcję, należy utworzyć trzy typy: LinkedList (ListaPowiązana), TailNode (OgonWęzłów) i InternalNode (WęzełWewnętrzny). Typ LinkedList udostępnia klientowi dostęp do wpisu listy, TailNode wskazuje na koniec listy, a InternalNode przechowuje bieżące dane. Wspólne cechy trzech typów możemy zamieścić w klasie bazowej Node, zawierającej dwie metody: Insert() i Show(). Insert() umieszcza obiekt danych w liście, natomiast Show() wyświetla wartość danych. Zachodzącą pomiędzy obiektami relację przedstawia rysunek 3.2 Węzeł +Insert(): Node +Show(): void ListaPowiązana -myNext: Node WęzełWewnętrzny -myNext(): Node -myData: Data Rys. 3.2. Hierarchia dziedziczenia węzła 1 OgonWęzełów Część I Programowanie zorientowane obiektowo Jesse Liberty C++. Księga eksperta. Zarówno węzeł LinkedList, jak i InternalNode posiadają zmienną obiektu Node, nazywaną myNext. Wskaźnik ten używany jest do znajdywania następnego węzła w liście. Jak zapewne zauważyłeś, omawiana lista jest listą jednokierunkową – oznacza to, że każdy węzeł może wskazywać jedynie węzeł kolejny (nie ma możliwości wskazania węzła poprzedniego). Spróbujmy teraz utworzyć klasę Data. Klasa ta będzie potrzebować jakiejś wartości (np. typu integer) i metody pozwalającej na porównanie dwóch obiektów (w naszym przypadku porównanie będzie polegało na określeniu, która wartość jest większa). Chcielibyśmy również, aby obiekt mógł wyświetlać swoją zawartość. W omawianym projekcie powiedzieliśmy, że LinkedList jest specjalnym rodzajem węzła, odpowiedzialnym za przedstawianie interfejsu listy klientowi. Czy przeczy to zasadzie, że dziedziczenie reprezentowane jest przez relację jest? Niezupełnie – LinkedList jest specjalnym typem węzła zaznaczającym początek listy. W rzeczywistości konstrukcja listy jest wirtualna (abstrakcyjna). Dlatego też, obiekt LinkedList przedstawia faktycznie dostęp do listy. Implementacja listy powiązanej Tworzenie węzła LinkedList pociąga za sobą tworzenie obiektu TailNode: LinkedList::LinkedList() { nextNode = new TailNode; } Dlatego nawet pusta lista zawiera dwa obiekty: LinkedList i TailNode – nie ma natomiast obiektu InternalNode, przechowującego dane (rysunek 3.3). OgonWęzłów ListaPowiązana Rys. 3.3. Pusta lista Po umieszczeniu danych w węźle LinkedList, węzeł natychmiast wystawia wskaźnik dla następnego węzła: Node * LinkedList::Insert(Data * theData) 2 Rozdział 3. Dziedziczenie, polimorfizm oraz wielokrotne wykorzystanie kodu { nextNode = nextNode->Insert(theData); return this; } Wywołanie funkcji Insert() jest informacją dla węzła TailNode o konieczności dodania nowego węzła do listy. Nowy węzeł jest wstawiany na przedostatniej pozycji listy (przed TailNode). Każdy nowy obiekt umieszczany na przedostatnim miejscu listy musi być, zgodnie z definicją, najmniejszym obiektem (w sensie porównywania reprezentowanej przez niego wartości). Umieszczenie nowego obiektu na liście polega na utworzeniu węzła InternalNode, którego konstruktor posiada dwa argumenty: wskaźnik do danych oraz do następnego węzła (w naszym przypadku drugim argumentem jest wskaźnik do TailNode). Node *TailNode::Insert(Data * theData) { InternalNode * dataNode = new InternalNode (theData, this); return dataNode; } Nowo utworzony InternalNode zawiera wskaźnik do danych i do TailNode. TailNode po utworzeniu węzła zwraca do niego wskaźnik, zatem w omawianym przypadku zwraca on do węzła LinkedList wskaźnik do InternalNode. LinkedList przypisuje własny wskaźnik następnego węzła do zwróconej wartości z nextNode->Insert(). Cała ta sytuacja została przedstawiona na rysunku 3.4. WęzłełWewnętrzny OgonWęzłów ListaPowiązana Dane Rys. 3.4. Po dodaniu nowego węzła Wskaźnik określa miejsce przesłania danych – w obecnej sytuacji, po dodaniu nowego elementu, nie będzie to już TailNode, lecz InternalNode. InternalNode, pobierając obiekt Data, porównuje swoje własne dane z danymi nowego węzła: Node * InternalNode::Insert(Data *theData) { int result = myData->Compare(*theData); } 3 Część I Programowanie zorientowane obiektowo Jesse Liberty C++. Księga eksperta. Nowy obiekt musi być najmniejszym istniejących węzłów. Jeżeli spełnia to założenie, to InternalNode przesyła obiekt Data do następnego węzła listy: case kSmaller: { nextNode = nextNode->Insert(theData); return this; } Wartość danych każdego wewnętrznego węzła jest sprawdzana. Jeżeli nowe dane są najmniejszymi danymi listy, węzeł może przejść w TailNode i zostać umieszczony jako ostatni węzeł w liście. W przeciwnym wypadku, gdy InternalNode zawiera dane mniejsze niż nowy obiekt, wówczas właśnie ten węzeł wewnętrzny jest odpowiedzialny za umieszczenie nowych danych w liście. W tym przypadku InternalNode (oznaczmy go chwilowo numerem 1) tworzy nowy węzeł (2), który ma za zadanie wskazywać obiekt, który został utworzony (czyli węzeł 1). Obrazując tą sytuację, nowy węzeł zostaje utworzony przed aktualnym obiektem InternalNode. Wskaźnik do nowego węzła jest zwracany przez InternalNode, tak aby za pomocą wywołania Insert() można było przyłączyć nowy obiekt do listy. case kSame: // przepada case kLarger: // nowe dane umieszczam przed sobą { InternalNode * dataNode = new InternalNode(theData, this); return dataNode; } W przykładzie przedstawiona jest sytuacja, w której obiekt zawierający tę samą wartość co obiekt aktualny, traktowany jest tak, jak gdyby zawierał wartość większą (z dwu obiektów zawierających tę samą wartość wcześniej w liście występuje ten, który został później do niej dopisany). Oczywiście w łatwy sposób można zrezygnować z takiego rozwiązania, zmieniając odpowiedni kod implementacji. Po umieszczeniu obiektu w liście, program pyta użytkownika o podanie nowych danych – na końcu użytkownik musi określić moment zakończenia wprowadzania nowych elementów. Mając gotową listę możemy spowodować jej wyświetlenie: virtual void Show() { nextNode->Show() } W przypadku pustej listy wystąpi wskazanie na TailNode; funkcja Show() nic nie wyświetli. Jednak, gdy lista nie będzie pusta, wskazany zostanie węzeł wewnętrzny. Działanie funkcji Show() przedstawione jest w poniższym przykładzie: virtual void Show() { myData->Show(); nextNode->Show(); } Każdy wewnętrzny węzeł wyświetla wówczas swoje dane i odsyła funkcję Show() do następnego węzła, z którym jest powiązany. Procedurę wyświetlania kończy osiągnięcie przez funkcję obiektu TailNode. Kiedy przychodzi pora na usunięcie listy, należy wywołać destruktor, usuwający kolejny węzeł: ~LinkedList() { delete nextNode; } 4 Rozdział 3. Dziedziczenie, polimorfizm oraz wielokrotne wykorzystanie kodu W ten sposób kolejno usuwa się wszystkie węzły listy, aż do napotkania obiektu TailNode – występuje tu „efekt domino”: skasowanie jednego węzła powoduje skasowanie kolejnego. Nie ma przy tym potrzeby pamiętania ilości węzłów umieszczonych w liście. Wszystkie wyżej omówione zagadnienia, zostały przedstawione w listingu 3.1 Listing 3.1. Lista powiązana zorientowana obiektowo #include <iostream> using namespace std; enum {kSmaller, kLarger, kSame}; class Data { public: Data(int val):dataValue(val){} virtual ~Data(){} virtual int Compare(const Data &); virtual void Show(){cout << dataValue << endl;} private: int dataValue; }; int Data::Compare(const Data & theOtherData) { if (dataValue < theOtherData.dataValue) return kSmaller; if (dataValue > theOtherData.dataValue) return kLarger; else return kSame; } class Node // abstrakcyjny typ danych { public: Node(){} virtual ~Node(){} virtual Node * Insert (Data * theData) = 0; virtual void Show() = 0; private: }; class InternalNode: public Node { public: InternalNode(Data * theData, Node * next); ~InternalNode() {delete nextNode; delete myData;} virtual Node * Insert(Data * theData); virtual void Show() {myData->Show(); nextNode->Show(); } private: Data * myData; Node * nextNode; }; InternalNode::InternalNode(Data * theData, Node * next): myData(theData), nextNode(next) { } 5 Część I Programowanie zorientowane obiektowo Jesse Liberty C++. Księga eksperta. Node * InternalNode::Insert(Data *theData) { int result = myData->Compare(*theData); switch(result) { case kSame: // przepada case kLarger: // nowe dane umieszczam przed sobą { InternalNode * dataNode = new InternalNode(theData, this); return dataNode; } case kSmaller: { nextNode = nextNode->Insert(theData); return this; } } return this; } class TailNode: public Node { public: TailNode(){} ~TailNode(){} virtual Node * Insert(Data * theData); virtual void Show() {} private: }; Node *TailNode::Insert(Data * theData) { InternalNode * dataNode = new InternalNode (theData, this); return dataNode; } class LinkedList: public Node { public: LinkedList(); ~LinkedList() {delete nextNode;} virtual Node * Insert(Data * theData); virtual void Show() {nextNode->Show();} private: Node *nextNode; }; LinkedList::LinkedList() { nextNode = new TailNode; } Node * LinkedList::Insert(Data * theData) { nextNode = nextNode->Insert(theData); return this; } int main() { 6 Rozdział 3. Dziedziczenie, polimorfizm oraz wielokrotne wykorzystanie kodu Data * pData; int val; LinkedList l1; for (;;) { cout << "Jaka wartosc chcesz dodac do listy?? (wpisz 0 aby zakonczyc): "; cin >> val; if (!val) break; pData = new Data(val); l1.Insert(pData); } cout << "\n\n"; l1.Show(); cout << "\n\n"; return 0; } Każdy obiekt jest odpowiedzialny tylko za jeden dany obszar, a wszystkie węzły traktowane są przez niego polimorficznie. Dzięki wyszczególnieniu obiektów LinkedList, InternalNode oraz TailNode możemy utworzyć bardzo scentralizowaną aplikację, której kod jest znacznie łatwiejszy do modyfikacji. Najważniejszą częścią kodu, z perspektywy zrozumienia polimorfizmu, jest sposób wywołania węzła: nextNode->Insert(). Każdy obiekt (za wyjątkiem TailNode), wie tylko tyle, że posiada wskaźnik do kolejnego węzła; nie wie natomiast, czy jest to TailNode, czy InternalNode. Można więc powiedzieć, że obiekt wywołujący nie wie, co wywołuje, lecz wie tylko o samym wydarzeniu. Procedura wyświetlania zawartości listy polega natomiast na wywołaniu węzła, który wyświetla swoją zawartość i przekazuje wywołanie do następnego węzła. Proces kończy się na osiągnięciu przez funkcję obiektu TailNode. 7