Struktury - część 1 - grupowanie danych za pomocą struktur

Wprowadzenie

W zasadzie przedstawiłem Ci już wszystkie "zwyczajne" typy danych jakie występują w języku C++. Za pomocą poznanych typów danych możesz używać w swoich programach zarówno liczb, liter, jak i napisów, czyli w zasadzie mogłoby się wydawać, że wszystkiego, co może Ci być potrzebne w Twoich programach

Warto jednak w tym momencie zastanowić się do czego ma służyć programowanie i języki programowania, z których jednym jest język C++. Okazuje się, że programy najczęściej mają służyć do rozwiązywania problemów mających związek z rzeczywistością i do modelowania, symulowania rzeczywistości.

Jak wiadomo w rzeczywistości nie wszystko jest liczbą, literą czy napisem. Tak naprawdę prawie wszystko z czym spotykamy się na co dzień składa się co najmniej z kilku elementów. Nawet zwykła książka zawiera tytuł, pewną liczbę stron, spis treści, rozdziały i strony.

To samo, gdybyśmy chcieli w mniej lub bardziej dokładny sposób opisać danego człowieka. Każdy człowiek ma nazwisko, imię (imiona), wiek, adres zamieszkania, wzrost itd.

Gdybyśmy chcieli napisać program, który umożliwiałby zapisywanie danych o pewnej grupie ludzi, to mogłoby się okazać to nie takie wygodne. Bo tak naprawdę, w jaki sposób należałoby przedstawić pojedynczego człowieka w naszym programie?

Oczywiście do przedstawienia nazwiska w języku C++ wystarczyłby typ napisowy, dla imienia również typ napisowy, dla wzrostu typ całkowitoliczbowy itd. Jednak dopiero połączenie tych kilku atrybutów jakimi są imię, nazwisko, wzrost itd. umożliwiłoby w pełni opisanie cech charakterystycznych dla danego człowieka.

Struktury - idealne rozwiązanie

Jak się już zapewne domyślasz, rozwiązaniem takiego problemu są struktury - typ języka C++, który był już wprowadzony w języku C, jednak w prostszej postaci. Struktury umożliwiają bowiem przechowywanie kilku informacji o jednym elemencie (obiekcie), dzięki czemu operowanie na jednym obiekcie (w tym przypadku człowieku czy książce) bardziej odzwierciedla rzeczywistość.

Zastanówmy się jednak, w jaki sposób można by przechowywać informacje o człowieku, gdyby w języku C++ struktur nie było. Gdybyśmy mieli w programie tylko jednego człowieka, wystarczyłoby utworzyć zmienną typu string, która przechowywałaby imię, kolejną zmienną typu string, która przechowywałaby nazwisko, zmienną typu int, która przechowywałaby wiek itd. Czyli rozwiązanie byłoby dość proste.

Z kolei, gdybyśmy chcieli w programie mieć możliwość przechowywania danych o kilku osobach, należałoby utworzyć kilka tablic. Pierwsza tablica przechowywałaby zmienne typu string - nazwiska. Druga przechowywałaby zmienne typu string - imiona. Kolejna zmienne typu int - wiek człowieka itd. Wszystkie tablice powinny mieć taki sam rozmiar. Wówczas gdybyśmy chcieli poznać informacje o pierwszym wprowadzonym człowieku, wybralibyśmy pierwszy element z tablicy przechowującej nazwiska, pierwszy element z tablicy przechowującej imiona, pierwszy element z tablicy przechowującej wiek itd.

Jak się zatem okazuje, bez struktur dałoby się zrealizować w sumie wszystkie wymagane operacje, jednak wówczas nasz program nie odzwierciedlałby tak dobrze rzeczywistości. Co bowiem miałaby w rzeczywistości znaczyć tablica nazwisk ludzi, tablica imion, czy tablica wieku ludzi?

W przypadku gdy o jednym obiekcie (w tym przypadku człowieku) chcielibyśmy przechowywać znacznie więcej informacji i byłyby one znacznie bardziej skomplikowane (np. kod DNA), mogłoby się okazać, że nawet dla nas - autorów programu, wszystko stałoby się niejasne i za bardzo zagmatwane.

Struktury - podstawy

Jako, że w języku C++ struktury są bardzo skomplikowanym typem danych, zaznaczę, że w tej lekcji przedstawię Ci wyłącznie bardzo podstawowy zarys czym są struktury. Tak naprawdę ta koncepcja będzie identyczna z koncepcją przedstawioną w języku C, w którym struktury były dokładnie takie, jak przedstawię Ci to w tej lekcji.

Oczywiście przedstawiony model działa również w języku C++, z tym tylko, że jest to ułamek tego, co struktury "potrafią" w języku C++. W rzeczywistości struktury w języku C++ są klasami z jedną niewielką różnicą dotyczącą zasięgu. Wszystko to zrozumiesz, kiedy przeczytasz lekcję o klasach i programowaniu obiektowym.

Jak już wspominałem, celem struktury jest zgrupowanie pewnych elementów i utworzenie w ten sposób nowego typu danych. Schematycznie definicję nowej struktury możemy zapisać następująco:

struct NazwaStruktury
{
  nazwaTypu1 nazwaZmiennej1;
  nazwaTypu2 nazwaZmiennej2;
  ...
};

Przede wszystkim zwróć uwagę na znajdujący się po nawiasie klamrowym średnik. Jest on tam potrzebny i bez niego kompilacja programu się nie powiedzie.

Zauważ też, że nazwa struktury została rozpoczęta z dużej litery. Co prawda nie jest to obowiązkiem, ale przyjęło się, że nazwy tworzonych struktur rozpoczyna się dużą literą.

Czas przejść do wnętrza struktury. Między nawiasami klamrowymi określamy, z czego składa się struktura. Każdy element jest parą składającą się z nazwy typu przechowywanego oraz z nazwy tej zmiennej. Typy zmiennych będących elementami struktury mogą być dowolne - mogą być to typy proste tj. int, char, string itd., ale również typy tworzone przez użytkownika np. typ wyliczeniowy czy inna struktura.

Liczba elementów struktury nie jest ograniczona. Struktura może składać się tylko z jednego elementu, ale wówczas sens tworzenia takiej struktury staje pod dużym znakiem zapytania.

Do tej pory pokazałem Ci jak utworzyć nową strukturę. Ale typów danych nie tworzy się dla samej przyjemności tworzenia. Zazwyczaj po utworzeniu typu chcemy stworzyć zmienną tego typu, żeby później móc przeprowadzać na niej wybrane operacje. Schematycznie utworzenie zmiennej typu strukturalnego możemy zrealizować następująco:

NazwaStruktury nazwaZmiennej;

lub

struct NazwaStruktury nazwaZmiennej;

Oba sposoby tworzenia zmiennej typu strukturalnego są równoważne, chociaż warto przyjąć od razu pewną konwencję i stosować w swoich programach albo sposób pierwszy albo drugi.

W rzeczywistości sposób pierwszy jest to sposób wprowadzony w języku C++ i jak zapewne wiesz, ten sposób nie różni się od sposobu tworzenia zmiennej typu podstawowego (np. int). Dla mnie największą wadą stosowania tej konwencji jest to, że edytory nie podświetlają samej nazwy typu na inny kolor, co sprawia, że w bardziej skomplikowanych programach, kod staje się nieco mniej czytelny.

Z kolei drugi sposób został wprowadzony w języku C i działa nadal w obecnej wersji języka C++, chociaż nie jest powiedziane, że będzie działał w przyszłych wersjach. Jeśli piszemy programy w języku C++, które będziemy następnie przenosić do języka C, należy używać właśnie tego sposobu, bowiem w języku C, dodatkowe słowo struct jest wymagane w momencie tworzenia zmiennej typu strukturalnego. Dodatkowo słowo struct jest podświetlane w edytorach programistycznych, dlatego też taki kod jest nieco bardziej czytelny.

Jeśli chodzi o mnie, stosuję drugą metodę, głównie w związku ze wspomnianym przeze mnie podświetlaniem słowa struct, co przyspiesza orientację w kodzie programu. Zdaję sobie jednak sprawę, że metoda ta jest nieco przestarzała, jednak świadomie jej używam i zdaję sobie sprawę z ewentualnych przyszłych konsekwencji (niekompilowanie programów w przyszłych wersjach języka).

Sposoby definicji struktury

W poprzednim paragrafie przedstawiłem Ci jeden ze sposobów definicji struktury i utworzenia zmiennej typu strukturalnego. Okazuje się jednak, że istnieją jeszcze dwie inne metody. Umieszczę je wszystkie tutaj i wyjaśnię, które z nich są godne polecenia.

Metoda 1:

struct NazwaStruktury
{
  nazwaTypu1 nazwaZmiennej1;
  nazwaTypu2 nazwaZmiennej2;
  ...
};

// Utworzenie zmiennej:
NazwaStruktury nazwaZmiennej; // lub struct NazwaStruktury nazwaZmiennej;

Metoda 2:

struct NazwaStruktury
{
  nazwaTypu1 nazwaZmiennej1;
  nazwaTypu2 nazwaZmiennej2;
  ...
} nazwaZmiennej;

Metoda 3:

struct
{
  nazwaTypu1 nazwaZmiennej1;
  nazwaTypu2 nazwaZmiennej2;
  ...
} nazwaZmiennej;

Czas na wyjaśnienie poszczególnych metod. W metodzie pierwszej tworzymy strukturę o nazwie NazwaStruktury (zwróć uwagę, że struktura ma nazwę). Następnie tworzymy zmienną nowo utworzonego typu. Zmienne nowego typu będziemy mogli utworzyć tak naprawdę w dowolnym miejscu bieżącego zasięgu, czyli na obecnym etapie kursu - w każdym miejscu programu.

Druga metoda jest bardzo podobna do pierwszej. Struktura ma również nazwę, ale od razu tworzymy zmienną typu strukturalnego. Oprócz tego, że zmienną (lub kilka zmiennych) tworzymy w momencie definicji struktury, to zmienne nowego typu będziemy mogli utworzyć tak naprawdę w dowolnym miejscu bieżącego zasięgu, czyli na obecnym etapie kursu - w każdym miejscu programu.

W trzeciej metodzie definiujemy strukturę i od razu tworzymy zmienną (lub kilka zmiennych) nowego typu. Zauważ, że tym razem struktura jest nienazwana, bowiem nie określamy jej nazwy. Jest to pewne skrócenie zapisu, jednak kosztem skrócenia zapisu, ponosimy bardzo wysoką cenę. Zmienne nowego typu możemy utworzyć wyłącznie w momencie definicji struktury. W dalszej części programu utworzenie zmiennej naszego typu strukturalnego będzie niemożliwe, bowiem nasz typ nie ma nazwy i w ten sposób tracimy do niego dostęp.

Podsumowując, warto stosować w swoich programach jedynie pierwszy lub drugi zapis. Trzeci zapis można zastosować wyłącznie wtedy, gdy jesteśmy pewni, że nie będziemy tworzyć zmiennych nowego typu w dalszej części programu oraz że nie będziemy musieli w programie wykorzystać nazwy typu (na przykład przy przekazywaniu zmiennej do funkcji). Ponieważ tak naprawdę nigdy nie możemy być tego pewni, dlatego najlepiej zapomnieć o tej metodzie i zawsze nadawać strukturze jakąś nazwę.

Przykłady struktur

Jak na razie pojawiały się tylko schematy, czas pokazać Ci jak może wyglądać rzeczywista struktura. Większość przykładów to tylko pokazanie przykładowych struktur, więc nie będą to na razie programy, bowiem wiesz jeszcze za mało o strukturach.

Przykład 1:

struct NajprostszaStruktura
{
  int liczba;
};
struct NajprostszaStruktura a;

Oto przykład najprostszej struktury, która zawiera tylko jeden element. W praktyce takich struktur się nie stosuje przy takim podejściu do struktur, jakie prezentuję w tej lekcji (taka struktura może natomiast mieć sens, gdy traktujemy struktury jako klasy).

Przykład 2:

enum kolory {niebieski, zielony, czerwony, srebrny, czarny };

struct Samochod
{
  int predkosc; // liczba ujemna to np. jazda do tylu
  unsigned short int iloscBiegow;
  kolory kolor;
 
} mojeAuto;

W tym przypadku utworzyliśmy zmienną mojeAuto typu Samochod. Zauważ, że element kolor jest typu kolory, który jest typem wyliczeniowym zdefiniowanym przed definicją struktury.

Przykład 3:

enum tematyka { kryminalna, scienceFiction, romans, lektura, przygodowa};

struct Ksiazka
{
  string tytul;
  unsigned int ileStron;
  unsigned int ileRozdzialow;
  tematyka typ;
  unsigned int cena;
};

Ksiazka mojaNowaKsiazka;

W powyższym przykładzie także użyliśmy typu wyliczeniowego. Sama struktura może obrazować dane o książce w domowej biblioteczce.

Przykład 4:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
  struct Osoba
  {
     string nazwisko;
     string imie;
     unsigned short int wiek;
     char plec;
  };
  Osoba uczen;
 
  cout <<"Suma typow prostych to "<<sizeof(string)<<'+'<<sizeof(string)<<'+'
       <<sizeof(unsigned short int)<<'+'<<sizeof(char)<<'='
       <<2*sizeof(string)+sizeof(unsigned short int)+sizeof(char)<<endl;

  cout <<"Rozmiar typu Osoba wynosi "<<sizeof(Osoba)<<endl;
  cout <<"Rozmiar zmiennej uczen, ktora jest typu Osoba to "
       << sizeof(uczen)<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 21.1

W tym przykładowym programie utworzyliśmy strukturę o nazwie Osoba i wypisaliśmy jej rozmiar. Przy okazji wypisaliśmy również rozmiar sumy jej elementów i jak widać, rozmiar samej struktury jest o jeden większy od sumy rozmiarów jej składowych. Jak widać rozmiary się nieco różnią, jednak dla nas nie będzie to miało większego znaczenia dlaczego tak jest i czym to jest spowodowane (prawdę mówiąc różnice w rozmiarze nie zawsze wynoszą jeden bajt, ale nie udało mi się znaleźć informacji, z czego to wynika).

Teraz przyjrzyj się wszystkim przykładom (z wyjątkiem pierwszego). Zauważ, że stworzyliśmy struktury, które opisują jakiś przedmiot lub postać z naszego życia. Zawartość struktur jest dość jasna i powiązana z przedmiotem (postacią), który opisuje. W ten oto sposób osiągnęliśmy częściowo to, na czym nam zależało - język programowania wspiera odwzorowywanie rzeczywistości.

Atrybuty struktury i odwołania do atrybutów

Do tej pory przedstawiłem Ci sposób tworzenia struktur i deklaracji zmiennych typu strukturalnego. Czas jednak nauczyć się korzystać z tych zmiennych.

Jak już wiesz, każda struktura składa się z czegoś, co do tej pory nazywaliśmy elementami. Od tej pory będziemy używać nazwy atrybut albo składowa (zamiennie). Tak więc przykładowa książka w trzecim przykładzie poprzedniego paragrafu ma atrybut ileStron, ma również atrybut tytul itd.

Aby odwołać się do atrybutu danej zmiennej musimy mieć oczywiście utworzoną strukturę oraz zmienną typu strukturalnego. Wtedy schematycznie odwołanie do atrybutu możemy zapisać następująco:

nazwaZmiennej.nazwaAtrybutu

Zatem aby dostać się do atrybutu danej zmiennej, piszemy nazwę zmiennej, operator dostępu do składowej (kropkę) oraz nazwę atrybutu. W ten oto sposób możemy przeprowadzać wszystkie dopuszczalne operacje na atrybutach danej zmiennej, które są oczywiście dozwolone dla danego typu atrybutu - możemy zarówno odczytywać zawartość atrybutów, jak i zapisywać tam wartości dokładnie tak samo jak robimy to w przypadku zwykłych zmiennych.

Poniżej znajduje się przykładowy program, dokonujący prostych operacji na strukturze Ksiazka, przedstawionej wcześniej w tej lekcji:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
 enum tematyka { kryminalna, scienceFiction, romans, lektura, przygodowa};
 struct Ksiazka
 {
    string tytul;
    unsigned int ileStron;
    unsigned int ileRozdzialow;
    tematyka typ;
    double cena;
 } mojaNowaKsiazka;

 mojaNowaKsiazka.tytul="Programowanie podstawowe";
 mojaNowaKsiazka.ileStron=1210;
 mojaNowaKsiazka.ileRozdzialow=28;
 mojaNowaKsiazka.typ=lektura;
 mojaNowaKsiazka.cena=120;

 cout <<"-----------Informacje o Twojej ksiazce--------------"<<endl;
 cout <<"Tytul: "<<mojaNowaKsiazka.tytul<<endl;
 cout <<"Ilosc stron: "<<mojaNowaKsiazka.ileStron<<endl;
 cout <<"Ilosc rozdzialow: "<<mojaNowaKsiazka.ileRozdzialow<<endl;
 cout <<"Typ ksiazki: ";
 switch (mojaNowaKsiazka.typ)
 {
    case kryminalna:     cout <<"kryminalna";
                                 break;
    case scienceFiction: cout <<"science-fiction";
                                 break;
    case romans:         cout <<"romans";
                                 break;
    case lektura:        cout <<"lektura";
                                 break;
    case przygodowa:     cout <<"przygodowa";
                                 break;
    default:             cout <<"Nieznany typ (MOZLIWY BLAD PROGRAMU)";
 }

 cout <<endl;
 cout <<"Cena ksiazki: "<<mojaNowaKsiazka.cena<<endl;
 cout <<"-----------------------------------------------------"<<endl;

 // zmieniamy cene np. z powodu rabatu o 10%
 mojaNowaKsiazka.cena=0.9*mojaNowaKsiazka.cena;
 // okazuje sie ze ksiazka zawiera 20 stron mniej
 mojaNowaKsiazka.ileStron-=20;
 // tytul jest troche inny (zawiera dodatkowo "czesc 1");
 mojaNowaKsiazka.tytul+=" czesc 1";

 // wypisujemy tylko zmienione dane o ksiazce
 cout <<"Cena ksiazki: "<<mojaNowaKsiazka.cena<<endl;
 cout <<"Ilosc stron: "<<mojaNowaKsiazka.ileStron<<endl;
 cout <<"Tytul: "<<mojaNowaKsiazka.tytul<<endl;

 cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
 getchar();  
 return 0;
}
program nr 21.2

Jak zatem widzisz, za pomocą operatora dostępu do składowej przeprowadziliśmy różne operacje na zmiennej mojaNowaKsiazka. Najpierw przypisaliśmy tam wartości, później odczytaliśmy i wypisaliśmy dane, następnie dokonaliśmy zmian niektórych atrybutów i znów dokonaliśmy odczytania. Wszystkie operacje są identyczne tak jak byśmy operowali na zwykłych zmiennych. Inny jest tylko zapis - niestety trochę dłuższy.

Przedstawię Ci jeszcze jeden przykład wykorzystania struktur. W poniższym przykładzie wartości atrybutów zmiennej typu strukturalnego będą pobierane z klawiatury.

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
 struct Osoba
 {
    string nazwisko;
    string imie;
    unsigned short int wiek;
    char plec;
 };
 Osoba uzytkownik;
 cout <<"Trwa pobieranie danych o Tobie..."<<endl<<endl;

 cout <<"Wprowadz swoje nazwisko: ";
 cin >> uzytkownik.nazwisko;
 cin.ignore();

 cout <<"Wprowadz swoje imie: ";
 cin >> uzytkownik.imie;
 cin.ignore();
 
 cout <<"Wprowadz swoj wiek: ";
 cin >> uzytkownik.wiek;
 cin.ignore();

 // chcemy byc pewni, ze uzytkownik wprowadzil k/K lub m/M
 do
 {
    cout <<"Wprowadz swoja plec (k/m): ";
    cin >>uzytkownik.plec;
    cin.ignore();
 } while (uzytkownik.plec!='k' && uzytkownik.plec!='K' && uzytkownik.plec!='m' &&
   uzytkownik.plec!='M');

 cout <<endl<<"Oto dane zebrane o Tobie"<<endl;
 cout <<"------------------------"<<endl;
 cout <<"Imie: "<<uzytkownik.imie<<endl;
 cout <<"Nazwisko: "<<uzytkownik.nazwisko<<endl;  
 cout <<"Wiek: "<<uzytkownik.wiek<<endl;
 cout <<"Plec: "<< ((uzytkownik.plec=='k' || uzytkownik.plec=='K') ? "kobieta" :
        "mezczyzna") <<endl;
 cout <<"------------------------"<<endl;


 cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
 getchar();  
 return 0;
}
program nr 21.3

Sam program jest dość łatwy z wyjątkiem dwóch miejsc. Zauważ, że dla pobierania płci użyto pętli do while. Konstrukcję taką zastosowano dlatego, aby było pewne, że użytkownik wpisał k, K, m lub M. Gdyby taka pętla nie pojawiła się, wówczas użytkownik mógłby wpisać dowolny znak, np. literę j i nie było by wiadomo jakiej jest płci.

Drugim miejscem budzącym wątpliwość jest miejsce wypisywania płci użytkownika. Zastosowano tutaj operator warunkowy, aby przypomnieć Ci, że coś takiego jak operator warunkowy, istnieje. Jeśli nie pamiętasz jak się go używa, sugeruję Ci dla Twojego dobra powrócić do lekcji "Operator warunkowy i instrukcja switch" i odświeżyć nieco pamięć.

Natomiast wszelkie operacje przeprowadzane na składowych zmiennej typu strukturalnego nie powinny budzić już żadnych wątpliwości - takie same odwołania pojawiły się już w poprzednim programie.

Podsumowanie

W tej lekcji przedstawiłem Ci, jakie możliwości daje typ strukturalny. Dzięki strukturom łatwiej opisywać rzeczywiste przedmioty i osoby oraz grupować dane ich dotyczące.

Gorąco zachęcam do przeczytania kolejnej lekcji, bowiem temat struktur będzie tam kontynuowany i pojawią się bardziej zaawansowane zastosowania tego typu danych.

powrót