Typ logiczny i typ wyliczeniowy. Kolejne typy danych w języku C++

Wprowadzenie

Znasz już podstawowe typy danych i operatory. W ostatnim czasie przedstawiłem Ci tablice, pętle i wiele różnych przydatnych instrukcji. W tej lekcji poznasz 2 kolejne typy danych, z czego jeden jest bardzo ważny i używasz go nieświadomie niemal przez cały czas trwania tego kursu.

Typ logiczny

Typ logiczny jest tak naprawdę jednym z najprostszych typów języka C++. Zmienna typu logicznego może bowiem przyjmować tylko dwie wartości: prawdę albo fałsz.

Typ logiczny w C++ to:

bool - typ logiczny (przyjmuje tylko 2 wartości: true lub false)

Typ logiczny jest w rzeczywistości bardzo często wykorzystywany, nawet jeśli nie używaliśmy do tej pory zmiennej typu bool. Tak naprawdę w każdym wyrażeniu logicznym, kiedy mamy operatory && lub || oraz kiedy używamy instrukcji warunkowej lub operatora warunkowego, tam tak naprawdę jest wykorzystywany typ logiczny.

Zacznijmy jednak od początku. Oto prosty przykład, w którym tworzymy zmienną typu bool i wypisujemy jej wartość na ekran.

#include <iostream>

using namespace std;

int main()
{
  bool logiczny;
 
  logiczny=true;
  cout <<"Zmienna logiczny ma wartosc "<<logiczny<<endl;
 
  logiczny=false;
  cout <<"Zmienna logiczny ma teraz wartosc "<<logiczny<<endl;
 
  logiczny=2;
  cout <<"Zmienna logiczny ma teraz wartosc "<<logiczny<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();
  return 0;  
}
program nr 20.1

W przykładowym programie można tak naprawdę dostrzec wiele wydawałoby się dziwnych rzeczy. Przede wszystkim, kiedy zmienna ma wartość true, na ekran zostaje wypisane 1, a kiedy zmienna ma wartość false na ekran zostaje wypisane 0.

Faktycznie - tak zostało przyjęte w języku C++, jednak w rzeczywistości to nie będzie miało dla Ciebie żadnego znaczenia. Tak naprawdę poza celami poznawczymi, tak jak tutaj, nikt nie wypisuje wartości zmiennej typu logicznego na ekran, bo nie jest to tak naprawdę w żaden sposób uzasadnione.

Dziwniejszą sprawą jest to, że zmiennej zostaje przypisana wartość liczbowa 2, mimo że wspominałem, że zmienna typu logicznego może przyjmować tylko dwie wartości: false albo true. Rzeczywiście tak właśnie jest, jednak dokonując przypisania jakiejkolwiek wartości niezerowej do zmiennej logicznej, zostaje ona potraktowana jako prawda, czyli true (Tylko 0 jest traktowane jako fałsz, czyli false).

Potwierdza to wynik po wypisaniu na ekran - zmienna nie ma bowiem wartości 2, tylko 1, a 1 jak już wspomniałem przy wypisaniu wartości zmiennej typu logicznego, oznacza po prostu true. Jak więc widzisz wszystko się zgadza.

Typ logiczny a instrukcje warunkowe

W rzeczywistości wszędzie tam, gdzie są stosowane instrukcje warunkowe if, operatory warunkowe, czy instrukcja switch, tam niejawnie jest stosowany typ logiczny.

Przykładowo, wyrażenie 2<3 zwraca wartość logiczną true, a wyrażenie 5!=5 zwraca wartość logiczną false. Właśnie tego typu wyrażenia są stosowane we wspomnianych przeze mnie konstrukcjach. Na dodatek, również we wszystkich typach pętli jako warunki końcowe pojawiają się wyrażenia, które w rzeczywistości zwracają true lub false.

Mam nadzieję, że tym sposobem przekonałem Cię, że typ logiczny jest bardzo powszechnie stosowany w języku C++. Jest on zarazem bardzo prostym typem, więc nie ma co dłużej zastanawiać się nad jego znaczeniem, tylko po prostu świadomie go używać w swoich programach.

Jeśli spojrzysz na program znajdujący się w lekcji "Instrukcja break w C++." to zwróć uwagę na zmienną end. To właśnie typowa zmienna typu logicznego, chociaż wtedy użyliśmy typu unsigned int z prostego powodu, że typ logiczny nie był Ci wtedy jeszcze znany.

Postaraj się teraz zmienić program tak, aby został użyty typ logiczny a działanie się nie zmieniło. Zmień również przypisania wartości na wartości typowe dla typu logicznego (true i false zamiast 1 i 0).

Typ wyliczeniowy - wprowadzenie

Wszystkie typy, które do tej pory udało Ci się poznać charakteryzowała jedna wspólna cecha - do zmiennej tego typu mogliśmy przypisać wszystkie dopuszczalne wartości dla danego typu. Dla typu całkowitego mogliśmy przypisać dowolną liczbę całkowitą z pewnego zakresu, a do zmiennej typu znakowego mogliśmy przypisać dowolny znak.

Oczywiście takie podejście może wydawać Ci się całkiem poprawne. Rzeczywiście, w wielu przypadkach jest to pożądane, a wręcz idealne rozwiązanie. Co jednak, gdybyśmy chcieli ograniczyć ten zbiór wartości? Co gdybyśmy chcieli na przykład stworzyć nowy typ, dla którego są dopuszczalne tylko 3 wartości?

Po pierwsze, takie podejście może być uzasadnione. Powiedzmy, że chcemy na przykład napisać program o różnych pojazdach i uznajemy, że w programie chcemy mieć pojazdy należące do jednego z trzech typów (np. bezsilnikowe, jednosilnikowe i wielosilnikowe). Nie chcemy mieć możliwości przypisywania do typu pojazdu żadnej innej wartości.

Możemy podejść w różny sposób do problemu. Na przykład możemy założyć, że będziemy zawsze pamiętać, że nie wolno nam przypisać do pewnej zmiennej typu np. całkowitego innych wartości poza 0 (bezsilnikowe), 1 (jednosilnikowe) i 2(wielosilnikowe). Nie muszę Cię chyba zapewniać, że jest to podejście zupełnie nie na miejscu, bowiem nawet Ty w końcu zapomnisz, co znaczy 0, co 1 a co 2 i w końcu przypiszesz do zmiennej inną wartość np. 3.

Trochę innym podejściem byłoby sprawdzanie za pomocą instrukcji warunkowej, czy przypisujemy do zmiennej dopuszczalną wartość, a jeśli nie, to podjęcie pewnych środków zaradczych. To rozwiązanie oczywiście jest nieco lepsze, jednak nadal nie było by tym czego oczekujemy.

Przede wszystkim musielibyśmy wszędzie za pomocą instrukcji warunkowej sprawdzać, czy wartość jest dopuszczalna dla naszego "nowego typu" - nie muszę Cię przekonywać, że zaciemniło by to w znacznym stopniu kod Twojego programu.

Dodatkowo, nadal zmiennej przypisywalibyśmy zwykłe liczby typu 0, 1, 2, co przy pierwszym spojrzeniu na kod Twojego programu, nie znaczy oczywiście zupełnie nic. Należało by stworzyć gdzieś na początku programu komentarz, że 0 to pojazdy bezsilnikowe, 1 to pojazdy jednosilnikowe, a 2 to pojazdy wielosilnikowe.

Tak naprawdę za każdym razem, kiedy Ty lub ktoś inny chciałby dodać coś do programu, musiałby sobie ponownie przypominać co znaczy 0, co znaczy 1, a co znaczy 2.

Typ wyliczeniowy pozwoli Ci natomiast tworzyć nowe typy danych i to bez większego wysiłku. Dodatkowo, całą odpowiedzialność za poprawność typu danych przerzucisz na kompilator!

Typ wyliczeniowy - podstawy

Wiesz już w przybliżeniu do czego służy typ wyliczeniowy - za jego pomocą tworzysz typy danych, dla których zmienne mogą przyjmować tylko pewną określoną liczbę wartości. To kompilator a nie Ty zatroszczy się, aby nie przypisać do zmiennej nowego typu żadnej innej wartości.

Schematycznie definicję typu wyliczeniowego zapisujemy tak:

enum nazwaTypu {pierwszaWartosc, drugaWartosc, ..., ostatniaWartosc};

Od razu zaznaczę, że jest to uproszczona definicja typu, jednak w naszym przykładzie dotyczącym pojazdów, taka definicja mogłaby w zupełności wystarczyć.

nazwaTypu to wymyślona przez nas nazwa typu, a wartości to nazwy dopuszczalnych wartości dla naszego typu, ale muszą to być zwykłe nazwy - takie jak nazwy zmiennych. Niestety nie możemy tutaj napisać 0, 1 czy 2, bowiem nie są to poprawne nazwy zmiennych (a w rzeczywistości poprawne nazwy ).

Poniżej przedstawiam przykładowy program z użyciem typu wyliczeniowego:

#include <iostream>

using namespace std;

int main()
{
  // utworzenie typu
  enum typPojazdu {bezsilnikowe, jednosilnikowe, wielosilnikowe};
 
  // utworzenie zmiennej noweg typu
  typPojazdu mojPojazd; // lub enum typPojazdu mojPojazd;
 
  mojPojazd=bezsilnikowe; // przypisanie wartosci zmiennej
 
  if (mojPojazd==bezsilnikowe)
     cout <<"Twoj pojazd nie ma silnika."<<endl;
  else  
     cout <<"Twoj pojazd ma silnik."<<endl;
 
  mojPojazd=wielosilnikowe; // przypisanie nowej wartosci zmiennej
 
  if (mojPojazd==jednosilnikowe)
     cout <<"A teraz Twoj pojazd ma tylko jeden silnik."<<endl;
  else
     cout <<"A teraz Twoj pojazd ma wiecej niz jeden silnik."<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();
  return 0;  
}
program nr 20.2

Jak widzisz, program jest całkiem prosty - na początku tworzymy nasz typ wyliczeniowy o nazwie typPojazdu z trzema dopuszczalnymi wartościami. Później tworzymy zmienną naszego nowego typu (zauważ, że w tym miejscu można, choć nie trzeba dodać po raz drugi słowo enum).

Następnie, jak widzisz, dokonujemy przypisania wartości do zmiennej. Spróbuj przypisać do zmiennej jakiekolwiek inne wartości (poza tymi trzema wymienionymi w definicji typu) i zobaczysz, że kompilator zaprotestuje. Zauważ też, że wszystkie inne operacje są jak w przypadku zwykłego typu - możemy bez problemu dokonywać chociażby operacji porównania.

Zanim wyjaśnię Ci do czego może posłużyć rzeczywista wartość całkowita, postaram Ci się przybliżyć kilka reguł dotyczących przypisywania wartości w typie numerycznym.

Po pierwsze, nie musimy koniecznie określać wartości całkowitych (tak właśnie zrobiliśmy w pierwszym przykładzie), bowiem może to nam nie być do niczego potrzebne.

Wartości przypisane poszczególnym wartościom naszego typu mogą być tylko całkowite - niestety nie można przypisywać tam wartości zmiennoprzecinkowych, a wszystko to dlatego, że typ wyliczeniowy jest typem całkowitym (a nie na przykład typem float).

Jeśli nie określimy początkowej wartości całkowitej, to domyślnie wyliczanie zacznie się od 0.

Każda następna wartość będzie o 1 większa, o ile nie określimy inaczej. Dlatego też jeśli nie określimy w ogóle wartości to pierwsza wartość 0, a każda następna jest o 1 większa, czyli 1, 2, 3 itd.

Możemy w dowolnym miejscu określić rzeczywistą wartość całkowitą, pamiętając, że jeśli dla następnej wartości nie będzie określona rzeczywista wartość całkowita, to wówczas będzie ona o 1 większa od podanej jawnie.

Rzeczywiste wartości całkowite mogą się powtarzać - zazwyczaj jednak i tak określisz różne wartości, ale czasami ta cecha może się przydać.

Teraz trochę przykładów, żeby zobrazować Ci przedstawione powyżej reguły i upewnić się, że wszystko rozumiesz. Nie będę już ich komentował:

enum mojTyp {a, b, c, d, e, f, g}; // wartosci: 0, 1, 2, 3, 4, 5, 6

enum mojTyp {a=3, b, c, d, e, f, g}; // wartosci: 3, 4, 5, 6, 7, 8, 9

enum mojTyp {a, b, c, d=0, e, f, g}; // wartosci: 0, 1, 2, 0, 1, 2, 3

enum mojTyp {a=3, b, c=3, d, e=2, f, g=7}; // wartosci: 3, 4, 3, 4, 2, 3, 7

enum mojTyp {a=3, b=3, c=3, d=3, e=3, f=3, g=3}; // wartosci: 3, 3, 3, 3, 3, 3, 3

enum mojTyp {a=60, b=0, c, d, e, f, g}; // wartosci: 60, 0, 1, 2, 3, 4, 5

Mam nadzieję, że powyższe przykłady rozjaśniły Ci, w jaki sposób są wyznaczane wartości dla poszczególnych elementów.

Mimo, że jak już wiesz, zmienna typu wyliczeniowego tak naprawdę przyjmuje rzeczywistości całkowite, to jednak nie będziemy mogli przypisać do niej wartości całkowitej. Jest to właśnie tą dużą zaletą - osiągnęliśmy to co chcieliśmy.

W powyższych przykładach nawet jeśli a miałoby wartość 0, to do zmiennej możemy przypisać tylko a, natomiast wartości 0 przypisać nie możemy. Dzięki temu uniknęliśmy przypisywania nic nie znaczących wartości i konieczności czytania komentarzy, co oznacza np. wartość 0 dla danego typu wyliczeniowego.

Wykorzystanie wartości dla typu wyliczeniowego

Chcę Ci w końcu pokazać, do czego mogą się przydać również wartości całkowite zmiennych typu wyliczeniowego. W tym celu rozszerzymy nasz typ pojazdów i założymy, że dla podanej prędkości chcemy określić jaką część prędkości generuje jeden silnik.

W powyższym przykładzie zauważysz, że dokonując zwykłych operacji arytmetycznych typu dodawanie, mnożenie itd. (tutaj akurat dzielenie) zmiennej typu wyliczeniowego ze zwykłymi liczbami całkowitymi, taka zmienna zachowuje się właśnie jak zwykła liczba, czyli jej wartość całkowita może zostać wykorzystana.

#include <iostream>

using namespace std;

int main()
{
  int ktory;
  int predkosc;
  float predkoscSilnika;
  enum typPojazdu { bezsilnikowe=0, jednosilnikowe, dwusilnikowe, czterosilnikowe=4,
                    wielosilnikowe=10};
 
  enum typPojazdu mojPojazd;
 
  cout <<"Nacisnij cyfre odpowiadajaca Twojemu typowi pojazdu i ENTER"<<endl;
  // te wartosci tutaj NIE MUSZA sie zgadzac z wartosciami w typie wyliczeniowym
  cout <<"1 - Pojazd bezsilinikowy"<<endl;
  cout <<"2 - Pojazd jednosilnikowy"<<endl;
  cout <<"3 - Pojazd dwusilnikowy"<<endl;
  cout <<"4 - Pojazd czterosinilnikowy"<<endl;
  cout <<"5 - Pojazd wielosilnikowy"<<endl;
  cin >>ktory;
  cin.ignore();
  switch (ktory)
  {
     case 1: mojPojazd=bezsilnikowe; break;
     case 2: mojPojazd=jednosilnikowe; break;
     case 3: mojPojazd=dwusilnikowe; break;
     case 4: mojPojazd=czterosilnikowe; break;
     case 5: mojPojazd=wielosilnikowe; break;
     default: mojPojazd=bezsilnikowe;          
  }
  cout <<"Podaj predkosc Twojego pojazdu (wieksza od zera): ";
  cin >>predkosc;
  cin.ignore();
 
  if (mojPojazd==bezsilnikowe)
     predkoscSilnika=0;
  else
     predkoscSilnika=predkosc/mojPojazd;
 
  if (predkoscSilnika==0)
     cout <<"Gratulacje. Cala predkosc "<<predkosc
          <<" tworzysz swoimi nogami"<<endl;
  else
     cout <<"Predkosc jednego silnika to okolo "<<predkoscSilnika
          <<" bo "<<predkoscSilnika<<'*'<<mojPojazd<<'='<<predkosc<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();
  return 0;  
}
program nr 20.4

Podsumowanie

W tej lekcji przedstawiłem Ci dwa nowe typy danych: typ logiczny i typ wyliczeniowy. Oba typy są ważne w języku C++, a bez typu logicznego tak naprawdę nie da się w ogóle obejść. Dlatego jeśli nie wszystko udało Ci się zrozumieć, zachęcam do ponownego przeczytania tej lekcji.

powrót