Struktury - część 2 - tablice struktur w języku C++

Wprowadzenie

W tej lekcji postaram Ci się przybliżyć struktury oraz przedstawić nieco bardziej zaawansowane sposoby ich użycia i definicji. Po raz kolejny pragnę zaznaczyć, że cały czas będę operował na uproszczonym modelu struktur, który działa zarówno w języku C, jak i C++.

Tablice w strukturach

Ponieważ struktury mają w jakimś stopniu odzwierciedlać rzeczywistość, to nie można zakładać, że wszystkie dane w strukturze są prostymi typami danych. Jednym z typów, który może się w strukturze pojawić jest tablica.

Odzwierciedlenie tego w rzeczywistości jest dość proste. Gdybyśmy chcieli utworzyć strukturę klasa (w sensie klasa, którą tworzą uczniowie w jakiejś szkole), to wówczas tak naprawdę struktura klasa powinna zawierać tablicę nazwisk uczniów w klasie lub jeszcze lepiej tablicę struktur uczeń (takim przypadkiem zajmiemy się w dalszej części tej lekcji).

Niestety na tym poziomie wiedzy, realizacja nawet tak prostego przykładu nie jest jeszcze możliwa, bowiem tablica w języku C++ musi mieć ściśle określony rozmiar. A niestety na początku tworzenia naszej struktury klasa, nie wiadomo ile będzie w niej uczniów (Możemy oczywiście założyć, że będzie maksimum 40 uczniów i utworzyć tablicę o 40 elementach, ale w ten sposób możemy zmarnować sporą część pamięci operacyjnej).

Dlatego też, na razie zajmiemy się nieco prostszym przypadkiem. Załóżmy, że mamy strukturę samochód i że rozpatrujemy samochody 4-kołowe. Podczas jazdy samochodem może się oczywiście zdarzyć awaria koła (np. przebicie opony, urwanie koła itd.) i my chcielibyśmy w jakiś sposób zasygnalizować, że z którymś z kół stało się coś niedobrego.

Do przechowywania stanu kół użyjemy tablicy 4-elementowej - tutaj możemy użyć tablicy, bowiem rozpatrujemy zawsze samochody 4-kołowe. Każde koło może być albo sprawne (wartość logiczna true) albo niesprawne (wartość logiczna false).

W programie użyjemy dodatkowo wbudowaną funkcję rand, która generuje pseudolosowe liczby całkowite. Przede wszystkim, aby funkcja rand działała zgodnie z naszymi oczekiwaniami, należy przed jej użyciem jednokrotnie użyć zapisu:

srand(static_cast<unsigned>(time(0)));

Jest to tak jakby inicjalizacja dla losowania liczb i jeśli ta linia zostałaby pominięta, wówczas funkcja losowa przy każdym uruchomieniu programu losowałaby identyczną liczbę.

Z kolei zapis:

rand()%4;

oznacza, że uzyskamy losową liczbę całkowitą z przedziału 0 - 3, czyli 0, 1, 2 lub 3. Funkcja rand losuje bowiem liczbę z bardzo dużego przedziału i aby otrzymać żądaną liczbę, należy dokonać dzielenia modulo za pomocą operatora %.

W samym programie wprowadzimy założenie, że samochód może jeździć tak długo, jak ma uszkodzone co najwyżej jedno koło. Gdy drugie koło zostaje uszkodzone, wówczas samochód kończy jazdę.

Niech nie przeraża Cię dość długi kod - większość elementów jest już Ci dobrze znana. Celem tego kodu jest to, aby Twoją uwagę zwróciły odwołania do tablicy kolo, która jest składową Samochodu i żeby było dla Ciebie jasne, że w takim przypadku tak samo jak w przypadku innych tablic możemy dokonywać ich przeglądania za pomocą pętli (tutaj akurat pętli for).

#include <iostream>

using namespace std;

int main()
{
struct Samochod
{
   bool kolo[4];
   string nazwa;
   int niesprawne; // ilosc niesprawnch kol
   unsigned long int punkty; // punkty zebrane w trakcie wyscigu
   unsigned int okrazenia; // ilosc przejechanych okrazen
};
Samochod wyscigowka;
unsigned int nrKola, szczescie;  

srand(static_cast<unsigned>(time(0)));

cout << "Podaj nazwe Twojego samochodu: ";
cin >> wyscigowka.nazwa;
cin.ignore();

// na poczatku wszystkie kola sa sprawne
cout <<"Stan kol przed wyscigiem: ";
for (unsigned int i=0;i<4;++i)  
{
   wyscigowka.kolo[i]=true;
   cout <<wyscigowka.kolo[i]<<' ';
}
wyscigowka.niesprawne=0; // zadne kolo nie jest uszkodzone  
wyscigowka.punkty=0; // zdobyte 0 punktow
wyscigowka.okrazenia=0; // przejechano 0 okrazen

cout <<endl<<endl<<"Rozpoczyna sie wyscig......."<<endl;      
do
{
     do // bo nie da sie uszkodzic tego samego kola dwa razy
     {
         nrKola=rand()%4;        
     } while (wyscigowka.kolo[nrKola]==false);
     szczescie=rand()%3;
     if (szczescie==0) // nie mamy szczescia
     {
         wyscigowka.kolo[nrKola]=false; // uszkodzenie
         ++wyscigowka.niesprawne; // wieksza liczba kol jest niesprawna
         cout <<"Kolo nr "<<(nrKola+1)<<" zostalo uszkodzone"<<endl;            
     }
     else if (szczescie==1)
         wyscigowka.punkty+=5;
     else // wiadomo, ze szczescie wynosi 2
         wyscigowka.punkty+=10;        
     ++wyscigowka.okrazenia;
} while (wyscigowka.niesprawne<2); // mozna jezdzic z jednym niesprawnym kolem

cout <<endl<<"Stan kol po wyscigu: ";
for (unsigned int i=0;i<4;++i)  
   cout <<wyscigowka.kolo[i]<<' ';

cout <<endl<<endl<<"-------Podsumowanie dla samochodu "<<wyscigowka.nazwa
       << "----------"<<endl;
cout <<"Przejechano "<<wyscigowka.okrazenia<<" okrazen"<<endl;
cout <<"Zdobyto "<<wyscigowka.punkty<<" punktow"<<endl;
cout <<"Uszkodzonych kol: "<<wyscigowka.niesprawne<<endl; //zawsze 2

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

Tablice struktur

Oprócz tego, że tablice mogą być składową struktury, to bardzo często chcemy utworzyć tablicę struktur. Jest to bardzo naturalne zjawisko, bowiem jeśli mamy strukturę książka, to tak naprawdę zazwyczaj chcemy jej użyć do jakiegoś zbioru książek - do książek znajdujących się w bibliotece, do przeczytanych w życiu książek itp.

W poprzednim programie urządziliśmy "wyścig" jednego samochodu. Możemy chcieć zorganizować wyścig większej liczby samochodów i zobaczyć, jakie są ich wyniki oraz ewentualnie wybrać zwycięzcę.

Poprzez modyfikację (i rozwinięcie) poprzedniego programu uzyskamy następujący program:

#include <iostream>

using namespace std;

int main()
{
  const unsigned int maxIlosc=500;
 
 
  struct Samochod
  {
     bool kolo[4];
     int niesprawne; // ilosc niesprawnch kol
     unsigned long int punkty; // punkty zebrane w trakcie wyscigu
     unsigned int okrazenia; // ilosc przejechanych okrazen
  };
  unsigned int ilosc; // ilosc pojazdow w grze
 
  Samochod wyscigowka[maxIlosc]; // tablica o rozmiarze maxIlosc samochodow
 
 
  cout <<"Podaj ilosc samochodow: "; // nie podawaj zbyt duzych liczb
  cin >>ilosc;
  cin.ignore();
 
  if (ilosc>maxIlosc)
  {
     cout <<"Niestety podana liczba jest za duza. Maksymalna ilosc samochodow to "
          <<maxIlosc<<endl;
     cout <<"Nacisnij ENTER aby zakonczyc..."<<endl;
     getchar();
     return 0;
  }
 
 
 
  unsigned int nrKola, szczescie;  
  unsigned int max; // nr samochodu o maksymalnej liczbie zebranych punktow
  srand(static_cast<unsigned>(time(0)));
 
  // ustawiamy parametry poczatkowe wszystkich wyscigowek
  for (unsigned int i=0; i<ilosc;++i)
  {
     for (unsigned int j=0;j<4;++j)  
         wyscigowka[i].kolo[j]=true;
     wyscigowka[i].niesprawne=0; // zadne kolo nie jest uszkodzone  
     wyscigowka[i].punkty=0; // zdobyte 0 punktow
     wyscigowka[i].okrazenia=0; // przejechano 0 okrazen
  }
 
 
  max=0; // nr najlepszego samochodu
  for (unsigned int i=0;i<ilosc;++i)
  {
      cout <<"Wyscigowka nr "<<(i+1)<<endl;
      do
      {
         do // bo nie da sie uszkodzic tego samego kola dwa razy
         {
            nrKola=rand()%4;        
         } while (wyscigowka[i].kolo[nrKola]==false);
         szczescie=rand()%3;
         if (szczescie==0) // nie mamy szczescia
         {
            wyscigowka[i].kolo[nrKola]=false; // uszkodzenie
            ++wyscigowka[i].niesprawne; // wieksza liczba kol jest niesprawna
            cout <<"Kolo nr "<<(nrKola+1)<<" zostalo uszkodzone"<<endl;            
         }
         else if (szczescie==1)
            wyscigowka[i].punkty+=5;
         else // wiadomo, ze szczescie wynosi 2
           wyscigowka[i].punkty+=10;        
         ++wyscigowka[i].okrazenia;
      } while (wyscigowka[i].niesprawne<2);
 
        cout <<endl<<endl<<"--Podsumowanie dla samochodu nr "<<(i+1)<<"--"<<endl;
        cout <<"Przejechano "<<wyscigowka[i].okrazenia<<" okrazen"<<endl;
        cout <<"Zdobyto "<<wyscigowka[i].punkty<<" punktow"<<endl;
 
      // sprawdzamy czy wynik jest lepszy od najlepszego wyniku
      if (wyscigowka[i].punkty>wyscigowka[max].punkty)
         max=i; // najlepszy staje sie obecny  
  }
 
  cout <<endl<<endl<<"Oto zwyciezca wyscigu - pojazd nr "<<(max+1)<<endl<<endl;  
  cout <<"Zwyciezca przejechal "<<wyscigowka[max].okrazenia<<" okrazen"<<endl;
  cout <<"Zwyciezca zdobyl "<<wyscigowka[max].punkty<<" punktow"<<endl;
  cout <<"Zwyciezca uszkodzil "<<wyscigowka[max].niesprawne<<" kola "<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 22.2

Zwróć uwagę na odwołania do poszczególnych wyścigówek. Po nazwie wyścigówki pojawia się jej indeks w nawiasie kwadratowym (tak jak w zwykłej tablicy), a dopiero po indeksie pojawia się kropka i odwołujemy się do składowych, które też mogą być tablicami (tak jak tutaj tablica kolo).

W programie dodatkowo sprawdzamy, która wyścigówka jest najlepsza, poprzez przechowywanie indeksu najlepszego samochodu.

Zwróć uwagę, że mimo że podajemy liczbę wyścigówek za pomocą klawiatury, to wcześniej tworzymy tablicę wyścigówek dla pewnej wartości maxIlosc. Wynika to z tego, że tablice w języku C++ muszą mieć określoną wielkość już w momencie kompilacji. Dlatego też w naszym programie przyjmujemy z góry maksymalną ilość wyścigówek, które jesteśmy w stanie obsłużyć, a następnie pozwalamy użytkownikowi wprowadzić rzeczywistą liczbę wyścigówek. Jeśli wprowadzona przez użytkownika liczba jest większa od przez nas zakładanej, kończymy nasz program.

Taki program jak ten, jeśli założymy dużą liczbę wyścigówek i podamy dużą liczbę wyścigówek, zajmie już dość sporo miejsca w pamięci i może działać nawet minutę lub dłużej na wolniejszych komputerach, dlatego też podczas testowania radzę nie podawać zbyt dużych liczb (ja przetestowałem dla 5000 wyścigówek). Pomijam fakt, że w rzeczywistości, gdyby program miał robić tylko to co robi, można by w ogóle zrezygnować z tablicy, dzięki czemu program zająłby znacznie mniej miejsca w pamięci.

Zagnieżdżanie struktur w strukturach

Mam nadzieję, że struktury Ci się już spodobały, bo teraz pojawi się ich kilka jednocześnie w jednym programie. Co prawda, w początkowych swoich programach, nie będziesz raczej zbyt często używać kilku struktur jednocześnie, jednak w bardzo skomplikowanych programach do przechowywania i zarządzania danymi, wiedza odnośnie zagnieżdżonych struktur niewątpliwie Ci się przyda.

Jak już dobrze wiesz, za pomocą struktur możemy grupować dane o jakimś elemencie. Niekiedy jednak tych danych może być tak dużo, że łatwo się w nich pogubić i tak naprawdę tracimy kontrolę nad przejrzystością naszej struktury.

W takim przypadku lepiej stworzyć dodatkowe struktury, które pogrupują pewne dane w sposób logiczny i przejrzysty i dzięki temu ułatwią poruszanie się po całej dużej strukturze.

Załóżmy, że mamy początkowo taką strukturę:

struct Osoba
{
  string nazwisko;
  string imie;
  int wiek;
  char plec;
};

Teraz chcielibyśmy rozbudować naszą strukturę, aby umożliwiała ona przechowywanie informacji o przeciętnym dorosłym człowieku. Przydałby się zatem adres zamieszkania w postaci nazwy ulicy, numeru ulicy i numeru mieszkania/domu oraz czynsz za mieszkanie. Dodatkowo, ponieważ prawie każdy dorosły człowiek pracuje, przydałyby się również dane dotyczące pracy, tj. zajmowane stanowisko, uzyskiwana pensja, a także nazwa ulicy, numer ulicy i numer mieszkania/domu.

Po dodaniu tych wszystkich atrybutów nasza struktura wyglądałaby mniej więcej tak:

struct Osoba
{
  string nazwisko;
  string imie;
  int wiek;
  char plec;
  string UlicaMieszkanie;
  unsigned int NrUlicyMieszkanie;
  unsigned int NrMieszkaniaMieszkanie;
  unsigned int czynsz;
  string stanowisko;
  unsigned int pensja;
  string UlicaPraca;
  unsigned int NrUlicyPraca;
  unsigned int NrMieszkaniaPraca;
};

Nie wiem jak dla Ciebie, ale dla mnie taka struktura wygląda wręcz okropnie. Mamy w niej aż 12 atrybutów, a nazwy atrybutów są długie i trudne do zapamiętania. Dodatkowo atrybuty nie są między sobą w żaden sposób logicznie powiązane.

Jeśli o strukturze myślimy tak, jak to przedstawiam Tobie dotychczasowo, to za wszelką cenę należy unikać takich struktur, bowiem posługiwanie się nimi jest niewygodne i taka struktura zamiast pomagać, tak naprawdę daje nam bardzo niewiele korzyści.

Zajmiemy się zatem przebudowaniem naszej struktury, a tak naprawdę utworzymy dwie dodatkowe struktury, które sprawią, że atrybuty naszej dotychczasowej struktury zostaną logicznie podzielone. W ten oto sposób uzyskamy następujący odpowiednik poprzedniej struktury:

struct DanePraca
{
  string stanowisko;
  unsigned int pensja;
  string ulica;
  unsigned int numer;
  unsigned int lokal;  
};
struct DaneMieszkanie
{
  string ulica;
  unsigned int numer;
  unsigned int lokal;    
  unsigned int czynsz;
};
struct Osoba
{
  string nazwisko;
  string imie;
  int wiek;
  char plec;
  struct DanePraca praca;
  struct DaneMieszkanie mieszkanie;  
};

Taka struktura jest już zdecydowanie prostsza do zarządzania i wygodniejsza. Co prawda dostęp do niektórych danych będzie dłuższy, bowiem będziemy musieli najpierw się odwołać do głównej struktury, a później do następnej, jednak dzięki podziałowi atrybutów mogliśmy zastosować krótsze nazwy atrybutów, które niewątpliwie będzie łatwiej zapamiętać.

W przykładowej hierarchii struktur, gdybyśmy chcieli utworzyć nową osobę o nazwie student i dostać się do jego stanowiska i zapisać tam dane o tym, że student jest praktykantem, zapisać dane, że student mieszka na ulicy Poziomkowej oraz, że ma 24 lata, to musielibyśmy postąpić następująco:

Osoba student;
student.praca.stanowisko="praktykant";
student.mieszkanie.ulica="Poziomkowa";
student.wiek=24;

Mimo, że obecna struktura jest już całkiem dobra, to mimo to warto było by się zastanowić nad wydzieleniem jeszcze jednej struktury, która przechowywałaby zarówno ulicę, numer ulicy, jak i numer lokalu. Co prawda nie jest to aż tak bardzo konieczne w tym przypadku, jednak gdybyśmy potrzebowali uzyskać więcej danych o danej osobie, to wówczas po raz kolejny, uzyskalibyśmy przejrzystość.

Po proponowanej zmianie hierarchia struktur wyglądałaby następująco:

struct DaneAdres
{
  string ulica;
  unsigned int numer;
  unsigned int lokal;  
};
struct DanePraca
{
  string stanowisko;
  unsigned int pensja;
  struct DaneAdres adres;
};
struct DaneMieszkanie
{
  struct DaneAdres adres;
  unsigned int czynsz;
};
struct Osoba
{
  string nazwisko;
  string imie;
  int wiek;
  char plec;
  struct DanePraca praca;
  struct DaneMieszkanie mieszkanie;  
};

a wcześniejsze przykładowe odwołania wyglądałyby teraz tak:

Osoba student;
student.praca.stanowisko="praktykant";
student.mieszkanie.adres.ulica="Poziomkowa";
student.wiek=24;

Jako ostatni przykład dotyczący struktur umieszczę przykład oparty na przedstawionej strukturze Osoba, z tym tylko, że będziemy mogli mieć kilka osób, czyli użyjemy tablicy struktur. Oto przykład:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
  const unsigned int ileMax=10;
 
 
  struct DaneAdres
  {
     string ulica;
     unsigned int numer;
     unsigned int lokal;  
  };
  struct DanePraca
  {
     string stanowisko;
     unsigned int pensja;
     struct DaneAdres adres;
  };
  struct DaneMieszkanie
  {
     struct DaneAdres adres;
     unsigned int czynsz;
  };
  struct Osoba
  {
     string nazwisko;
     string imie;
     int wiek;
     char plec;
     struct DanePraca praca;
     struct DaneMieszkanie mieszkanie;  
  };
 
  Osoba pracownik[ileMax]; // tablica struktur
 
 
  unsigned int ile; // ilosc osob;
 
  cout <<"Podaj ile osob chcesz wprowadzic: ";
  cin >>ile;
  cin.ignore();
 
  if (ile>ileMax)
  {
     cout <<"Niestety podana liczba jest za duza. Maksymalna ilosc osob to "
          <<ileMax<<endl;
     cout <<"Nacisnij ENTER aby zakonczyc..."<<endl;
     getchar();
     return 0;
  }
 
 
 
  for (unsigned int i=0;i<ile;++i) // dla kazdego pracownika
  {
     cout <<endl<<"DANE O PRACOWNIKU "<<(i+1)<<endl;
     // pobieramy dane, ale tylko niektore
     cout <<"Podaj imie: ";
     cin >>pracownik[i].imie;
     cin.ignore();
     cout <<"Podaj nazwisko: ";
     cin >>pracownik[i].nazwisko;
     cin.ignore();
     cout <<"Podaj wysokosc czynszu: ";
     cin >>pracownik[i].mieszkanie.czynsz;
     cin.ignore();
     cout <<"Podaj numer lokalu mieszkania: ";
     cin >>pracownik[i].mieszkanie.adres.lokal;
     cin.ignore();
     cout <<"Podaj nazwe ulicy miejsca pracy: ";
     cin >>pracownik[i].praca.adres.ulica;
     cin.ignore();      
     cout <<"Podaj wysokosc pensji: ";
     cin >>pracownik[i].praca.pensja;
     cin.ignore();
  }
 
  cout <<endl<<endl<<"Oto dane o pracownikach:"<<endl<<endl;
  for (unsigned int i=0;i<ile;++i) // dla kazdego pracownika
  {
     // wypisujemy dane, ktore pobralismy
     cout <<endl<<"DANE O PRACOWNIKU "<<(i+1)<<endl;
     cout << pracownik[i].imie<<' '<<pracownik[i].nazwisko<<endl;
     cout <<pracownik[i].mieszkanie.czynsz<<' '
          <<pracownik[i].mieszkanie.adres.lokal<<endl;
     cout << pracownik[i].praca.adres.ulica <<' '
          << pracownik[i].praca.pensja<<endl;
  }
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 22.3

Podsumowanie

W tej lekcji przedstawiłem Ci bardziej zaawansowane operacje na strukturach danych i sposób dostępu do atrybutów w przypadku zagnieżdżonych struktur.

Jeśli udało Ci się zrozumieć wszystkie zamieszczone tutaj przykłady, nie masz się czego obawiać - wiesz wszystko o strukturach w takiej wersji, jak to było w języku C i rozumiesz jak się dostać do atrybutów struktur języka C++ (czyli znasz już pewną część struktur w języku C++ i zarazem klas).

powrót