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