BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 7 | UU: 215

Nowe typy danych

Wachlarz dostępnych w C++ typów wbudowanych jest, jak wiemy, niezwykle bogaty. W połączeniu z możliwością fuzji wielu pojedynczych zmiennych do postaci wygodnych w użyciu tablic, daje nam to szerokie pole do popisu przy konstruowaniu własnych sposobów na przechowywanie danych.

Nabyte już doświadczenie oraz tytuł niniejszego podrozdziału sugeruje jednak, iż nie jest to wcale kres potencjału używanego przez nas języka. Przeciwnie: C++ oferuje nam możliwość tworzenia swoich własnych typów zmiennych, odpowiadających bardziej konkretnym potrzebom niż zwykłe liczby czy napisy.

Nie chodzi tu wcale o znaną i prostą instrukcję typedef , która umie jedynie produkować nowe nazwy dla już istniejących typów. Mam bowiem na myśli znacznie potężniejsze narzędzia, udostępniające dużo większe możliwości w tym zakresie.

Czy znaczy to również, że są one trudne do opanowania? Według mnie siedzący tutaj diabeł wcale nie jest taki straszny, jakim go malują ;D Absolutnie więc nie ma się czego bać!

Wyliczania nadszedł czas

Pierwszym z owych narzędzi, z którymi się zapoznamy, będą typy wyliczeniowe (ang. enumerated types ). Ujrzymy ich możliwe zastosowania oraz techniki użytkowania, a rozpoczniemy od przykładu z życia wziętego :)

Przydatność praktyczna

W praktyce często zdarza się sytuacja, kiedy chcemy ograniczyć możliwy zbiór wartości zmiennej do kilku(nastu/dziesięciu) ściśle ustalonych elementów. Jeżeli, przykładowo, tworzylibyśmy grę, w której pozwalamy graczowi jedynie na ruch w czterech kierunkach (góra, dół, lewo, prawo), z pewnością musielibyśmy przechowywać w jakiś sposób jego wybór. Służąca do tego zmienna przyjmowałaby więc jedną z czterech określonych wartości.

Jak możnaby osiągnąć taki efekt? Jednym z rozwiązań jest zastosowanie stałych, na przykład w taki sposób:

const int KIERUNEK_GORA = 1 ;
const int KIERUNEK_DOL = 2 ;
const int KIERUNEK_LEWO = 3 ;
const int KIERUNEK_PRAWO = 4 ;
int nKierunek;
nKierunek = PobierzWybranyPrzezGraczaKierunek();
switch (nKierunek)
{
case KIERUNEK_GORA: // porusz graczem w górę
case KIERUNEK_DOL: // porusz graczem w dół
case KIERUNEK_LEWO: // porusz graczem w lewo
case KIERUNEK_PRAWO: // porusz graczem w prawo
default : // a to co za kierunek? :)
}

Przy swoim obecnym stanie koderskiej wiedzy mógłbyś z powodzeniem użyć tego sposobu. Skoro jednak prezentujemy go w miejscu, z którego zaraz przejdziemy do omawiania nowych zagadnień, nie jest on pewnie zbyt dobry :)

Najpoważniejszym chyba mankamentem jest zupełna nieświadomość kompilatora co do specjalnego znaczenia zmiennej nKierunek . Traktuje ją więc identycznie, jak każdą inną liczbę całkowitą, pozwalając choćby na przypisanie podobne do tego:

nKierunek = 10 ;

Z punktu widzenia składni C++ jest ono całkowicie poprawne, ale dla nas byłby to niewątpliwy błąd. 10 nie oznacza bowiem żadnego z czterech ustalonych kierunków, więc wartość ta nie miałaby w naszym programie najmniejszego sensu!

Jak zatem podejść do tego problemu? Najlepszym wyjściem jest zdefiniowanie nowego typu danych, który będzie pozwalał na przechowywanie tylko kilku podanych wartości. Czynimy to w sposób następujący 1 :

enum DIRECTION { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT };

Tak oto stworzyliśmy typ wyliczeniowy zwany DIRECTION . Zmienne, które zadeklarujemy jako należące do tegoż typu, będą mogły przyjmować jedynie wartości wpisane przez nas w jego definicji . Są to DIR_UP , DIR_DOWN , DIR_LEFT i DIR_RIGHT , odpowiadające umówionym kierunkom. Pełnią one funkcję stałych - z tą różnicą, że nie musimy deklarować ich liczbowych wartości (gdyż i tak używać będziemy jedynie tych symbolicznych nazw).

Mamy więc nowy typ danych, wypadałoby zatem skorzystać z niego i zadeklarować jakąś zmienną:

DIRECTION Kierunek = PobierzWybranyPrzezGraczaKierunek();
switch (Kierunek)
{
case DIR_UP: // ...
case DIR_DOWN: // ...
// itd.
}

Deklaracja zmiennej należącej do naszego własnego typu nie różni się w widoczny sposób od podobnego działania podejmowanego dla typów wbudowanych. Możemy również dokonać jej inicjalizacji, co też od razu czynimy.

Kod ten będzie poprawny oczywiście tylko wtedy, gdy funkcja PobierzWybranyPrzezGraczaKierunek() będzie zwracała wartość będącą także typu DIRECTION .

Wszelkie wątpliwości powinna rozwiać instrukcja switch . Widać wyraźnie, że użyto jej w identyczny sposób jak wtedy, gdy korzystano jeszcze ze zwykłych stałych, deklarowanych oddzielnie.

Na czym więc polega różnica? Otóż tym razem niemożliwe jest przypisanie w rodzaju:

Kierunek = 20 ;

Kompilator nie pozwoli na nie, gdyż zmienna Kierunek podlega ograniczeniom swego typu DIRECTION . Określając go, ustaliliśmy, że może on reprezentować wyłącznie jedną z czterech podanych wartości, a 20 niewątpliwie nie jest którąś z nich :)

Tak więc teraz bezmyślny program kompilujący jest po naszej stronie i pomaga nam jak najwcześniej wyłapywać błędy związane z nieprawidłowymi wartościami niektórych zmiennych.

1 Nowe typy danych będę nazywał po angielsku, aby odróżnić je od zmiennych czy funkcji.

Definiowanie typu wyliczeniowego

Nie od rzeczy będzie teraz przyjrzenie się kawałkowi kodu, który wprowadza nam nowy typ wyliczeniowy. Oto i jego składnia:

enum nazwa_typu { stała_1 [ = wartość_1 ] ,
stała_2 [ = wartość_2 ] ,
stała_3 [ = wartość_3 ] ,
...
stała_n [ = wartość_n ] };

Słowo kluczowe enum (ang. enumerate - wyliczać) pełni rolę informującą: mówi, zarówno nam, jak i kompilatorowi, iż mamy tu do czynienia z definicją typu wyliczeniowego. Nazwę, którą chcemy nadać owemu typowi, piszemy zaraz za tym słowem; przyjęło się, aby używać do tego wielkich liter alfabetu.

Potem następuje częsty element w kodzie C++, czyli nawiasy klamrowe. Wewnątrz nich umieszczamy tym razem listę stałych - dozwolonych wartości typu wyliczeniowego. Jedynie one będą dopuszczone przez kompilator do przechowywania przez zmienne należące do definiowanego typu.

Tutaj również zaleca się, tak jak w przypadku zwykłych stałych (tworzonych poprzez const ), używanie wielkich liter. Dodatkowo, dobrze jest dodać do każdej nazwy odpowiedni przedrostek, powstały z nazwy typu, na przykład:

// przykładowy typ określający poziom trudności jakiejś gry
enum DIFFICULTY { DIF_EASY, DIF_MEDIUM, DIF_HARD };

Widać to było także w przykładowym typie DIRECTION .

Nie zapominajmy o średniku na końcu definicji typu wyliczeniowego!

Warto wiedzieć, że stałe, które wprowadzamy w definicji typu wyliczeniowego, reprezentują liczby całkowite i tak też są przez kompilator traktowane. Każdej z nich nadaje on kolejną wartość, poczynając zazwyczaj od zera.

Najczęściej nie przejmujemy się, jakie wartości odpowiadają poszczególnym stałym. Czasem jednak należy mieć to na uwadze - na przykład wtedy, gdy planujemy współpracę naszego typu z jakimiś zewnętrznymi bibliotekami. W takiej sytuacji możemy wyraźnie określić, jakie liczby są reprezentowane przez nasze stałe. Robimy to, wpisując wartość po znaku = i nazwie stałej.

Przykładowo, w zaprezentowanym na początku typie DIRECTION moglibyśmy przypisać każdemu wariantowi kod liczbowy odpowiedniego klawisza strzałki:

enum DIRECTION { DIR_UP = 38 ,
DIR_DOWN = 40 ,
DIR_LEFT = 37 ,
DIR_RIGHT = 39 };

Nie trzeba jednak wyraźnie określać wartości dla wszystkich stałych; możliwe jest ich sprecyzowanie tylko dla kilku. Dla pozostałych kompilator dobierze wtedy kolejne liczby, poczynając od tych narzuconych, tzn. zrobi coś takiego:

enum MYenum { ME_ONE, // 0
ME_TWO = 12 , // 12
ME_THREE, // 13
ME_FOUR, // 14
ME_FIVE = 26 , // 26
ME_SIX, // 27
ME_SEVEN }; // 28

Zazwyczaj nie trzeba o tym pamiętać, bo lepiej jest albo całkowicie zostawić przydzielanie wartości w gestii kompilatora, albo samemu dobrać je dla wszystkich stałych i nie utrudniać sobie życia ;)

Użycie typu wyliczeniowego

Typy wyliczeniowe zalicza się do typów liczbowych, podobnie jak int czy unsigned . Mimo to nie jest możliwe bezpośrednie przypisanie do zmiennej takiego typu liczby zapisanej wprost. Kompilator nie przepuści więc instrukcji podobnej do tej:

enum DECISION { YES = 1 , NO = 0 , DONT_KNOW = - 1 };
DECISION Decyzja = 0 ;

Zrobi tak nawet pomimo faktu, iż 0 odpowiada tutaj jednej ze stałych typu DECISION . C++ dba bowiem, aby typów enum używać zgodnie z ich przeznaczeniem, a nie jako zamienników dla zmiennych liczbowych. Powoduje to, że:

Do zmiennych wyliczeniowych możemy przypisywać wyłącznie odpowiadające im stałe. Niemożliwe jest nadanie im "zwykłych" wartości liczbowych.

Jeżeli jednak koniecznie potrzebujemy podobnego przypisania (bo np. odczytaliśmy liczbę z pliku lub uzyskaliśmy ją za pomocą jakiejś zewnętrznej funkcji), możemy salwować się rzutowaniem przy pomocy static_cast :

// zakładamy, że OdczytajWartosc() zwraca liczbę typu int lub podobną
Decyzja = static_cast <DECISION>(OdczytajWartosc());

Pamiętajmy aczkolwiek, żeby w zwykłych sytuacjach używać zdefiniowanych stałych. Inaczej całkowicie wypaczalibyśmy ideę typów wyliczeniowych.

Zastosowania

Ewentualni fani programów przykładowych mogą czuć się zawiedzeni, gdyż nie zaprezentuję żadnego krótkiego, kilkunastolinijkowego, dobitnego kodu obrazującego wykorzystanie typów wyliczeniowych w praktyce. Powód jest dość prosty: taki przykład miałby złożoność i celowość porównywalną do banalnych aplikacji dodających dwie liczby, z którymi stykaliśmy się na początku kursu. Zamiast tego pomówmy lepiej o zastosowaniach opisywanych typów w konstruowaniu "normalnych", przydatnych programów - także gier.

Do czego więc mogą przydać się typy wyliczeniowe? Tak naprawdę sposobów na ich konkretne użycie jest więcej niż ziaren piasku na pustyni; równie dobrze moglibyśmy, zadać pytanie w rodzaju "Jakie zastosowanie ma instrukcja if ?" :) Wszystko bowiem zależy od postawionego problemu oraz samego programisty. Istnieje jednak co najmniej kilka ogólnych sytuacji, w których skorzystanie z typów wyliczeniowych jest wręcz naturalne:

  • Przechowywanie informacji o stanie jakiegoś obiektu czy zjawiska.
    Przykładowo, jeżeli tworzymy grę przygodową, możemy wprowadzić nowy typ określający aktualnie wykonywaną przez gracza czynność: chodzenie, rozmowa, walka itd. Stosując przy tym instrukcję switch będziemy mogli w każdej klatce podejmować odpowiednie kroki sterujące konwersacją czy wymianą ciosów.
    Inny przykład to choćby odtwarzacz muzyczny. Wiadomo, że może on w danej chwili zajmować się odgrywaniem jakiegoś pliku, znajdować się w stanie pauzy czy też nie mieć wczytanego żadnego utworu i czekać na polecenia użytkownika. Te możliwe stany są dobrym materiałem na typ wyliczeniowy.

Wszystkie te i podobne sytuacje, z którymi można sobie radzić przy pomocy enum -ów, są przypadkami tzw. automatów o skończonej liczbie stanów (ang. finite state machine , FSM). Pojęcie to ma szczególne zastosowanie przy programowaniu sztucznej inteligencji, zatem jako (przyszły) programista gier będziesz się z nim czasem spotykał.

 

  • Ustawianie parametrów o ściśle określonym zbiorze wartości.
    Był już tu przytaczany dobry przykład na wykorzystanie typów wyliczeniowych właśnie w tym celu. Jest to oczywiście kwestia poziomu trudności jakiejś gry; zapisanie wyboru użytkownika wydaje się najbardziej naturalne właśnie przy użyciu zmiennej wyliczeniowej.
    Dobrym reprezentantem tej grupy zastosowań może być również sposób wyrównywania akapitu w edytorach tekstu. Ustawienia: "do lewej", do prawej", "do środka" czy "wyjustowanie" są przecież świetnym materiałem na odpowiedni enum .

  • Przekazywanie jednoznacznych komunikatów w ramach aplikacji.
    Nie tak dawno temu poznaliśmy typ bool , który może być używany między innymi do informowania o powodzeniu lub niepowodzeniu jakiejś operacji (zazwyczaj wykonywanej przez osobną funkcję). Taka czarno-biała informacja jest jednak mało użyteczna - w końcu jeżeli wystąpił jakiś błąd, to wypadałoby wiedzieć o nim coś więcej.
    Tutaj z pomocą przychodzą typy wyliczeniowe. Możemy bowiem zdefiniować sobie taki, który posłuży nam do identyfikowania ewentualnych błędów. Określając odpowiednie stałe dla braku pamięci, miejsca na dysku, nieistnienia pliku i innych czynników decydujących o niepowodzeniu pewnych działań, będziemy mogli je łatwo rozróżniać i raczyć użytkownika odpowiednimi komunikatami.

To tylko niektóre z licznych metod wykorzystywania typów wyliczeniowych w programowaniu. W miarę rozwoju swoich umiejętności sam odkryjesz dla nich mnóstwo specyficznych zastosowań i będziesz często z nich korzystał w pisanych kodach.

Upewnij się zatem, że dobrze rozumiesz, na czym one polegają i jak wygląda ich użycie w C++. To z pewnością sowicie zaprocentuje w przyszłości.

A kiedy uznasz, iż jesteś już gotowy, będziemy mogli przejść dalej :)

Kompleksowe typy

Tablice, opisane na początku tego rozdziału, nie są jedynym sposobem na modelowanie złożonych danych. Chociaż przydają się wtedy, gdy informacje mają jednorodną postać zestawu identycznych elementów, istnieje wiele sytuacji, w których potrzebne są inne rozwiązania.

Weźmy chociażby banalny, zdawałoby się, przykład książki adresowej. Na pierwszy rzut oka jest ona idealnym materiałem na prostą tablicę, której elementami byłyby jej kolejne pozycje - adresy.
Zauważmy jednak, że sama taka pojedyncza pozycja nie daje się sensownie przedstawić w postaci jednej zmiennej. Dane dotyczące jakiejś osoby obejmują przecież jej imię, nazwisko, ewentualnie pseudonim, adres e-mail, miejsce zamieszkania, telefon. Jest to przynajmniej kilka elementarnych informacji, z których każda wymagałaby oddzielnej zmiennej.

Podobnych przypadków jest w programowaniu mnóstwo i dlatego też dzisiejsze języki posiadają odpowiednie mechanizmy, pozwalające na wygodne przetwarzanie informacji o budowie hierarchicznej. Domyślasz się zapewne, że teraz właśnie rzucimy okiem na ofertę C++ w tym zakresie :)

Typy strukturalne i ich definiowanie

Wróćmy więc do naszego problemu książki adresowej, albo raczej listy kontaktów - najlepiej internetowych. Każda jej pozycja mogłaby się składać z takich oto trzech elementów:

  • nicka tudzież imienia i nazwiska danej osoby

  • jej adresu e-mail

  • numeru identyfikacyjnego w jakimś komunikatorze internetowym

Na przechowywanie tychże informacji potrzebujemy zatem dwóch łańcuchów znaków (po jednym na nick i adres) oraz jednej liczby całkowitej. Znamy oczywiście odpowiadające tym rodzajom danych typy zmiennych w C++: są to rzecz jasna std::string oraz int . Możemy więc użyć ich do utworzenia nowego, złożonego typu, reprezentującego w całości pojedynczy kontakt:

struct CONTACT
{
std::string strNick;
std::string strEmail;
int nNumerIM;
};

W ten właśnie sposób zdefiniowaliśmy typ strukturalny .

Typy strukturalne (zwane też w skrócie strukturami 1 ) to zestawy kilku zmiennych, należących do innych typów, z których każda posiada swoją własną i unikalną nazwę . Owe "podzmienne" nazywamy polami struktury.

Nasz nowonarodzony typ strukturalny składa się zatem z trzech pól, zaś każde z nich przechowuje jedynie elementarną informację. Zestawione razem reprezentują jednak złożoną daną o jakiejś osobie.

Struktury w akcji

Nie zapominajmy, że zdefiniowane przed chwilą "coś" o nazwie CONTACT jest nowym typem, a więc możemy skorzystać z niego tak samo, jak z innych typów w języku C++ (wbudowanych lub poznanych niedawno enum 'ów). Zadeklarujmy więc przy jego użyciu jakąś przykładową zmienną:

CONTACT Kontakt;

Logiczne byłoby teraz nadanie jej pewnej wartości. Pamiętamy jednak, że powyższy Kontakt to tak naprawdę trzy zmienne w jednym (coś jak szampon przeciwłupieżowy ;D). Niemożliwe jest zatem przypisanie mu zwykłej, "pojedynczej" wartości, właściwej typom skalarnym.

Możemy za to zająć się osobno każdym z jego pól. Są one znanymi nam bardzo dobrze tworami programistycznymi (napisem i liczbą), więc nie będziemy mieli z nimi najmniejszych kłopotów. Cóż zatem zrobić, aby się do nich dobrać?.

Skorzystamy ze specjalnego operatora wyłuskania , będącego zwykłą kropką ( . ). Pozwala on między innymi na uzyskanie dostępu do określonego pola w strukturze. Użycie go jest bardzo proste i dobrze widoczne na poniższym przykładzie:

// wypełnienie struktury danymi
Kontakt.strNick = "Hakier" ;

Kontakt.strEmail = " \n gigahaxxor@abc.pl Ten adres e-mail jest chroniony przed spamerami, włącz obsługę JavaScript w przeglądarce, by go zobaczyć " ;

Kontakt.nNumerIM = 192837465 ;

Postawienie kropki po nazwie struktury umożliwia nam niejako "wejście w jej głąb". W dobrych środowiskach programistycznych wyświetlana jest nawet lista wszystkich jej pól, jakby na potwierdzenie tego faktu oraz ułatwienie pisania dalszego kodu. Po kropce wprowadzamy więc nazwę pola, do którego chcemy się odwołać.

Wykonawszy ten prosty zabieg możemy zrobić ze wskazanym polem wszystko, co się nam żywnie podoba. W przykładzie powyżej czynimy doń zwykłe przypisanie wartości, lecz równie dobrze mogłoby to być jej odczytanie, użycie w wyrażeniu, przekazanie do funkcji, itp. Nie ma bowiem żadnej praktycznej różnicy w korzystaniu z pola struktury i ze zwykłej zmiennej tego samego typu - oczywiście poza faktem, iż to pierwsze jest tylko częścią większej całości.

Sądzę, że wszystko to powinno być dla ciebie w miarę jasne :)

Co uważniejsi czytelnicy (czyli pewnie zdecydowana większość ;D) być może zauważyli, iż nie jest to nasze pierwsze spotkanie z kropką w C++. Gdy zajmowaliśmy się dokładniej łańcuchami znaków, używaliśmy formułki napis .length() do pobrania długości tekstu.
Czy znaczy to, że typ std::string również należy do strukturalnych?. Cóż, sprawa jest generalnie dosyć złożona, jednak częściowo wyjaśni się już w następnym rozdziale. Na razie wiedz, że cel użycia operatora wyłuskania był tam podobny do aktualnie omawianego (czyli "wejścia w środek" zmiennej), chociaż wtedy nie chodziło nam wcale o odczytanie wartości jakiegoś pola. Sugerują to zresztą nawiasy wieńczące wyrażenie.
Pozwól jednak, abym chwilowo z braku czasu i miejsca nie zajmował się bliżej tym zagadnieniem. Jak już nadmieniłem, wrócimy do niego całkiem niedługo, zatem uzbrój się w cierpliwość :)

 

Spoglądając krytycznym okiem na trzy linijki kodu, które wykonują przypisania wartości do kolejnych pól struktury, możemy nabrać pewnych wątpliwości, czy aby składnia C++ jest rzeczywiście taka oszczędna, jaką się zdaje. Przecież wyraźnie widać, iż musieliśmy tutaj za w każdym wierszu wpisywać nieszczęsną nazwę struktury, czyli Kontakt ! Nie dałoby się czegoś z tym zrobić?
Kilka języków, w tym np. Delphi i Visual Basic, posiada bloki with , które odciążają nieco palce programisty i zezwalają na pisanie jedynie nazw pól struktur. Jakkolwiek jest to niewątpliwie wygodne, to czasem powoduje dość nieoczekiwane i niełatwe do wykrycia błędy logiczne. Wydaje się, że brak tego rodzaju instrukcji w C++ jest raczej rozsądnym skutkiem bilansu zysków i strat, co jednak nie przeszkadza mi osobiście uważać tego za pewien feler :D

Istnieje jeszcze jedna droga nadania początkowych wartości polom struktury, a jest nią naturalnie znana już szeroko inicjalizacja :) Ponieważ podobnie jak w przypadku tablic mamy tutaj do czynienia ze złożonymi zmiennymi, należy tedy posłużyć się odpowiednią formą inicjalizatora - taką, jak podana poniżej:

// inicjalizacja struktury
CONTACT Kontakt = { "MasterDisaster" , " \n md1337@ajajaj.com.pl Ten adres e-mail jest chroniony przed spamerami, włącz obsługę JavaScript w przeglądarce, by go zobaczyć " , 3141592 };

Używamy więc w znajomy sposób nawiasów klamrowych, umieszczając wewnątrz nich wyrażenia, które mają być przypisane kolejnym polom struktury. Należy przy tym pamiętać, by zachować taki sam porządek pól , jaki został określony w definicji typu strukturalnego. Inaczej możemy spodziewać się niespodziewanych błędów :)

Kolejność pól w definicji typu strukturalnego oraz w inicjalizacji należącej doń struktury musi być identyczna .

Uff, zdaje się, że w ferworze poznawania szczegółowych aspektów struktur zapomnieliśmy już całkiem o naszym pierwotnym zamyśle. Przypominam więc, iż było nim stworzenie elektronicznej wersji notesu z adresami, czyli po prostu listy internetowych kontaktów.

Nabyta wiedza nie pójdzie jednak na marne, gdyż teraz potrafimy już z łatwością wymyślić stosowne rozwiązanie pierwotnego problemu. Zasadniczą listą będzie po prostu odpowiednia tablica struktur :

const unsigned LICZBA_KONTAKTOW = 100 ;
CONTACT aKontakty[LICZBA_KONTAKTOW];

Jej elementami staną się dane poszczególnych osób zapisanych w naszej książce adresowej. Zestawione w jednowymiarową tablicę będą dokładnie tym, o co nam od początku chodziło :)


Schemat 11. Obrazowy model tablicy struktur

Metody obsługi takiej tablicy nie różnią się wiele od porównywalnych sposobów dla tablic składających się ze "zwykłych" zmiennych. Możemy więc łatwo napisać przykładową, prostą funkcję, która wyszukuje osobę o danym nicku:

int WyszukajKontakt(std::string strNick)
{
// przebiegnięcie po całej tablicy kontaktów przy pomocy pętli for
for ( unsigned i = 0 ; i < LICZBA_KONTAKTOW; ++i)
// porównywanie nicku każdej osoby z szukanym
if (aKontakty[i].strNick == strNick)
// zwrócenie indeksu pasującej osoby
return i;
// ewentualnie, jeśli nic nie znaleziono, zwracamy -1
return - 1 ;
}

Zwróćmy w niej szczególną uwagę na wyrażenie, poprzez które pobieramy pseudonimy kolejnych osób na naszej liście. Jest nim:

aKontakty[i].strNick

W zasadzie nie powinno być ono zaskoczeniem. Jak wiemy doskonale, aKontakty[i] zwraca nam i -ty element tablicy. U nas jest on strukturą, zatem dostanie się do jej konkretnego pola wymaga też użycia operatora wyłuskania. Czynimy to i uzyskujemy ostatecznie oczekiwany rezultat, który porównujemy z poszukiwanym nickiem.

W ten sposób przeglądamy naszą tablicę aż do momentu, gdy faktycznie znajdziemy poszukiwany kontakt. Wtedy też kończymy funkcję i oddajemy indeks znalezionego elementu jako jej wynik. W przypadku niepowodzenia zwracamy natomiast - 1 , która to liczba nie może być indeksem tablicy w C++.

Cała operacja wyszukiwania nie należy więc do szczególnie skomplikowanych :)

Odrobina formalizmu - nie zaszkodzi!

Przyszedł właśnie czas na uporządkowanie i usystematyzowanie posiadanych informacji o strukturach. Największym zainteresowaniem obdarzymy przeto reguły składniowe języka, towarzyszące ich wykorzystaniu.

Mimo tak groźnego wstępu nie opuszczaj niniejszego paragrafu, bo taka absencja z pewnością nie wyjdzie ci na dobre :)

Typ strukturalny definiujemy, używając słowa kluczowego struct (ang. structure - struktura). Składnia takiej definicji wygląda następująco:

struct nazwa_typu
{
typ_pola_1 nazwa_pola_1 ;
typ_pola_2 nazwa_pola_2 ;
typ_pola_3 nazwa_pola_3 ;
...
typ_pola_n nazwa_pola_n ;
};

Kolejne wiersze wewnątrz niej łudząco przypominają deklaracje zmiennych i tak też można je traktować. Pola struktury są przecież zawartymi w niej "podzmiennymi".

Całość tej listy pól ujmujemy oczywiście w stosowne do C++ nawiasy klamrowe.

Pamiętajmy, aby za końcowym nawiasem koniecznie umieścić średnik . Pomimo zbliżonego wyglądu definicja typu strukturalnego nie jest przecież funkcją i dlatego nie można zapominać o tym dodatkowym znaku.

Przykład wykorzystania struktury

To prawda, że używanie struktur dotyczy najczęściej dość złożonych zbiorów danych. Tym bardziej wydawałoby się, iż trudno o jakiś nietrywialny przykład zastosowania tegoż mechanizmu językowego w prostym programie.

Jest to jednak tylko część prawdy. Struktury występują bowiem bardzo często zarówno w standardowej bibliotece C++, jak i w innych, często używanych kodach - Windows API czy DirectX. Służą one nierzadko jako sposób na przekazywanie do i z funkcji dużej ilości wymaganych informacji. Zamiast kilkunastu parametrów lepiej przecież użyć jednego, kompleksowego, którym znacznie wygodniej jest operować.

My posłużymy się takim właśnie typem strukturalnym oraz kilkoma funkcjami pomocniczymi, aby zrealizować naszą prostą aplikację. Wszystkie te potrzebne elementy znajdziemy w pliku nagłówkowym c time , gdzie umieszczona jest także definicja typu tm :

struct tm
{
int tm_sec; // sekundy
int tm_min; // minuty
int tm_hour; // godziny
int tm_mday; // dzień miesiąca
int tm_mon; // miesiąc (0..11)
int tm_year; // rok (od 1900)
int tm_wday; // dzień tygodnia (0..6, gdzie 0 == niedziela)
int tm_yday; // dzień roku (0..365, gdzie 0 == 1 stycznia)
int tm_isdst; // czy jest aktywny czas letni?
};

Patrząc na nazwy jego pól oraz komentarze do nich, nietrudno uznać, iż typ ten ma za zadanie przechowywać datę i czas w formacie przyjaznym dla człowieka. To zaś prowadzi do wniosku, iż nasz program będzie wykonywał czynność związaną w jakiś sposób z upływem czasu. Istotnie tak jest, gdyż jego przeznaczeniem stanie się obliczanie biorytmu.

Biorytm to modny ostatnio zestaw parametrów, które określają aktualne możliwości psychofizyczne każdego człowieka. Według jego zwolenników, nasz potencjał fizyczny, emocjonalny i intelektualny waha się okresowo w cyklach o stałej długości, rozpoczynających się w chwili narodzin.


Wykres 1. Przykładowy biorytm autora tego tekstu :-)

Możliwe jest przy tym określenie liczbowej wartości każdego z trzech rodzajów biorytmu w danym dniu. Najczęściej przyjmuje się w tym celu przedział "procentowy", obejmujący liczby od -100 do +100.

Same obliczenia nie są szczególnie skomplikowane. Patrząc na wykres biorytmu, widzimy bowiem wyraźnie, iż ma on kształt trzech sinusoid, różniących się jedynie okresami. Wynoszą one tyle, ile długości trwania poszczególnych cykli biorytmu, a przedstawia je poniższa tabelka:

cykl

długość

fizyczny

23 dni

emocjonalny

28 dni

intelektualny

33 dni

Tabela 10. Długości cykli biorytmu

Uzbrojeni w te informacje możemy już napisać program, który zajmie się liczeniem biorytmu. Oczywiście nie przedstawi on wyników w postaci wykresu (w końcu mamy do dyspozycji jedynie konsolę), ale pozwoli zapoznać się z nimi w postaci liczbowej, która także nas zadowala :)

Spójrzmy zatem na ten spory kawałek kodu:

// Biorhytm - pobieranie aktualnego czasu w postaci struktury
// i użycie go do obliczania biorytmu
// typ wyliczeniowy, określający rodzaj biorytmu

enum BIORHYTM { BIO_PHYSICAL = 23 ,
BIO_EMOTIONAL = 28 ,
BIO_INTELECTUAL = 33 };
// pi :)
const double PI = 3.1415926538 ;
//----------------------------------------------------------------------
// funkcja wyliczająca dany rodzaj biorytmu
double Biorytm( double fDni, BIORHYTM Cykl)
{
return 100 * sin(( 2 * PI / Cykl) * fDni);
}
// funkcja main()
void main()
{
/* trzy struktury, przechowujące datę urodzenia delikwenta,
aktualny czas oraz różnicę pomiędzy nimi */
tm DataUrodzenia = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 };
tm AktualnyCzas = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 };
tm RoznicaCzasu = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 };
/* pytamy użytkownika o datę urodzenia */
std::cout << "Podaj date urodzenia" << std::endl;
// dzień
std::cout << "- dzien: " ;
std::cin >> DataUrodzenia.tm_mday;
// miesiąc - musimy odjąć 1, bo użytkownik poda go w systemie 1..12
std::cout << "- miesiac: " ;
std::cin >> DataUrodzenia.tm_mon;
DataUrodzenia.tm_mon--;
// rok - tutaj natomiast musimy odjąć 1900
std::cout << "- rok: " ;
std::cin >> DataUrodzenia.tm_year;
DataUrodzenia.tm_year -= 1900 ;
/* obliczamy liczbę przeżytych dni */
// pobieramy aktualny czas w postaci struktury
time_t Czas = time(NULL);
AktualnyCzas = *localtime(&Czas);
// obliczamy różnicę między nim a datą urodzenia
RoznicaCzasu.tm_mday = AktualnyCzas.tm_mday - DataUrodzenia.tm_mday;
RoznicaCzasu.tm_mon = AktualnyCzas.tm_mon - DataUrodzenia.tm_mon;
RoznicaCzasu.tm_year = AktualnyCzas.tm_year - DataUrodzenia.tm_year;
// przeliczamy to na dni
double fPrzezyteDni = RoznicaCzasu.tm_year * 365.25
+ RoznicaCzasu.tm_mon * 30.4375
+ RoznicaCzasu.tm_mday;
/* obliczamy biorytm i wyświelamy go */
// otóż i on
std::cout << std::endl;
std::cout << "Twoj biorytm" << std::endl;
std::cout << "- fizyczny: " << Biorytm(fPrzezyteDni, BIO_PHYSICAL)
<< std::endl;
std::cout << "- emocjonalny: " << Biorytm(fPrzezyteDni,
BIO_EMOTIONAL) << std::endl;
std::cout << "- intelektualny: " << Biorytm(fPrzezyteDni,
BIO_INTELECTUAL) << std::endl;
// czekamy na dowolny klawisz
getch();
}

Jaki jest efekt tego pokaźnych rozmiarów listingu? Są nim trzy wartości określające dzisiejszy biorytm osoby o podanej dacie urodzenia:

Screen 26. Efekt działania aplikacji obliczającej biorytm

Za jego wyznaczenie odpowiada prosta funkcja Biorytm() wraz towarzyszącym jej typem wyliczeniowym, określającym rodzaj biorytmu:

enum BIORHYTM { BIO_PHYSICAL = 23 ,
BIO_EMOTIONAL = 28 ,
BIO_INTELECTUAL = 33 };
double Biorytm( double fDni, BIORHYTM Cykl)
{
return 100 * sin(( 2 * PI / Cykl) * fDni);
}

Godną uwagi sztuczką, jaką tu zastosowano, jest nadanie stałym typu BIORHYTM wartości, będących jednocześnie długościami odpowiednich cykli biorytmu. Dzięki temu funkcja zachowuje przyjazną postać wywołania, na przykład Biorytm( liczba_dni , BIO_PHYSICAL) , a jednocześnie unikamy instrukcji switch wewnątrz niej.

Sama formułka licząca opiera się na ogólnym wzorze sinusoidy, tj.:


w którym A jest jej amplitudą, zaś T - okresem.
U nas okresem jest długość trwania poszczególnych cykli biorytmu, zaś amplituda 100 powoduje "rozciągnięcie" przedziału wartości do zwyczajowego <-100; +100>.

Stanowiąca większość kodu długa funkcja main() dzieli się na trzy części.

W pierwszej z nich pobieramy od użytkownika jego datę urodzenia i zapisujemy ją w strukturze o nazwie. DataUrodzenia :) Zauważmy, że używamy tutaj jej pól jako miejsca docelowego dla strumienia wejścia w identyczny sposób, jak to czyniliśmy dla pojedynczych zmiennych.

Po pobraniu musimy jeszcze odpowiednio zmodyfikować dane - tak, żeby spełniały wymagania podane w komentarzach przy definicji typu tm (chodzi tu o numerowanie miesięcy od zera oraz liczenie lat począwszy od roku 1900).

Kolejnym zadaniem jest obliczenie ilości dni, jaką dany osobnik przeżył już na tym świecie. W tym celu musimy najpierw pobrać aktualny czas, co też czynią dwie poniższe linijki:

time_t Czas = time(NULL);
AktualnyCzas = *localtime(&Czas);

W pierwszej z nich znana nam już funkcja time() uzyskuje czas w wewnętrznym formacie C++ 1 . Dopiero zawarta w drugim wierszu funkcja localtime() konwertuje go na zdatną do wykorzystania strukturę, którą przypisujemy do zmiennej AktualnyCzas .

Troszkę udziwnioną postać tej funkcji musisz na razie niestety zignorować :)

Dalej obliczamy różnicę między oboma czasami (zapisanymi w DataUrodzenia i AktualnyCzas ), odejmując od siebie liczby dni, miesięcy i lat. Otrzymany tą drogą wiek użytkownika musimy na koniec przeliczyć na pojedyncze dni, za co odpowiada wyrażenie:

double fPrzezyteDni = RoznicaCzasu.tm_year * 365.25
+ RoznicaCzasu.tm_mon * 30.4375
+ RoznicaCzasu.tm_mday;

Zastosowane tu liczby 365.25 i 30.4375 są średnimi ilościami dni w roku oraz w miesiącu. Uwalniają nas one od konieczności osobnego uwzględniania lat przestępnych w przeprowadzanych obliczeniach.

Wreszcie, ostatnie wiersze kodu obliczają biorytm, wywołując trzykrotnie funkcję o tej nazwie, i prezentują wyniki w klarownej postaci w oknie konsoli.

Działanie programu kończy się zaś na tradycyjnym getch() , które oczekuje na przyciśnięcie dowolnego klawisza. Po tym fakcie następuje już definitywny i nieodwołalny koniec :D

Tak oto przekonaliśmy się, że struktury warto znać nawet wtedy, gdy nie planujemy tworzenia aplikacji manewrujących skomplikowanymi danymi. Nie zdziw się zatem, że w dalszym ciągu tego kursu będziesz je całkiem często spotykał.

Unie

Drugim, znacznie rzadziej spotykanym rodzajem złożonych typów są unie .

Są one w pewnym sensie podobne do struktur, gdyż ich definicje stanowią także listy poszczególnych pól:

union nazwa_typu
{
typ_pola_1 nazwa_pola_1 ;
typ_pola_2 nazwa_pola_2 ;
typ_pola_3 nazwa_pola_3 ;
...
typ_pola_n nazwa_pola_n ;
};

Identycznie wyglądają również deklaracje zmiennych, należących do owych typów "unijnych", oraz odwołania do ich pól. Na czym więc polegają różnice?.

Przypomnijmy sobie, że struktura jest zestawem kilku odrębnych zmiennych, połączonych w jeden kompleks. Każde jego pole zachowuje się dokładnie tak, jakby było samodzielną zmienną, i posłusznie przechowuje przypisane mu wartości. Rozmiar struktury jest zaś co najmniej sumą rozmiarów wszystkich jej pól.

Unia opiera się na nieco innych zasadach. Zajmuje bowiem w pamięci jedynie tyle miejsca, żeby móc pomieścić swój największy element . Nie znaczy to wszak, iż w jakiś nadprzyrodzony sposób potrafi ona zmieścić w takim okrojonym obszarze wartości wszystkich pól. Przeciwnie, nawet nie próbuje tego robić. Zamiast tego obszary pamięci przeznaczone na wartości pól unii zwyczajnie nakładają się na siebie. Powoduje to, że:

W danej chwili tylko jedno pole unii zawiera poprawną wartość.

Do czego mogą się przydać takie dziwaczne twory? Cóż, ich zastosowania są dość swoiste, więc nieczęsto będziesz zmuszony do skorzystania z nich.

Jednym z przykładów może być jednak chęć zapewnienia kilku dróg dostępu do tych samych danych:

union VECTOR3
{
// w postaci trójelementowej tablicy

float v[ 3 ];
// lub poprzez odpowiednie zmienne x, y, z
struct
{
float x, y, z;
};
};

W powyższej unii, która ma przechowywać trójwymiarowy wektor, możliwe są dwa sposoby na odwołanie się do jego współrzędnych: poprzez pola x , y oraz z lub indeksy odpowiedniej tablicy v . Oba są równoważne:

VECTOR3 vWektor;
// poniższe dwie linijki robią to samo
vWektor.x = 1.0 ; vWektor.y = 5.0 ; vWektor.z = 0.0 ;
vWektor.v[ 0 ] = 1.0 ; vWektor.v[ 1 ] = 5.0 ; vWektor.v[ 2 ] = 0.0 ;

Taka unię możemy więc sobie obrazowo przedstawić chociażby poprzez niniejszy rysunek:


Schemat 12. Model przechowywania unii w pamięci operacyjnej

Elementy tablicy v oraz pola x , y , z niejako "wymieniają" między sobą wartości. Oczywiście jest to tylko pozorna wymiana, gdyż tak naprawdę chodzi po prostu o odwoływanie się do tego samego adresu w pamięci , jednak różnymi drogami .

Wewnątrz naszej unii umieściliśmy tzw. anonimową strukturę (nieopatrzoną żadną nazwą). Musieliśmy to zrobić, bo jeżeli wpisalibyśmy float x, y, z; bezpośrednio do definicji unii, każde z tych pól byłoby zależne od pozostałych i tylko jedno z nich miałoby poprawną wartość. Struktura natomiast łączy je w integralną całość.

 

Można zauważyć, że struktury i unie są jakby odpowiednikiem operacji logicznych - koniunkcji i alternatywy - w odniesieniu do budowania złożonych typów danych. Struktura pełni jak gdyby funkcję operatora && (pozwalając na niezależne istnienie wszystkim obejmowanym sobą zmiennym), zaś unia - operatora || (dopuszczając wyłącznie jedną daną). Zagnieżdżając frazy struct i union wewnątrz definicji kompleksowych typów możemy natomiast uzyskać bardziej skomplikowane kombinacje.
Naturalnie, rodzi się pytanie "Po co?", ale to już zupełnie inna kwestia ;)

 

Więcej informacji o uniach zainteresowani znajdą w unions.htm">MSDN .

***

Lektura kończącego się właśnie podrozdziału dała ci możliwość rozszerzania wachlarza standardowych typów C++ o takie, które mogą ci ułatwić tworzenie przyszłych aplikacji. Poznałeś więc typy wyliczeniowe, struktury oraz unie, uwalniając całkiem nowe możliwości programistyczne. Na pewno niejednokrotnie będziesz z nich korzystał.

1 Jest to liczba sekund, które upłynęły od północy 1 stycznia 1970 roku.

1 Zazwyczaj strukturami nazywamy już konkretne zmienne; u nas byłyby to więc rzeczywiste dane kontaktowe jakiejś osoby (czyli zmienne należące do zdefiniowanego właśnie typu CONTACT ). Czasem jednak pojęć "typ strukturalny" i "struktura" używa się zamiennie, a ich szczegółowe znaczenie zależy od kontekstu.

| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker