ProType Warez Strona Główna
FAQ  Szukaj  Użytkownicy  Grupy  Statystyki Rejestracja  Zaloguj  Album  Download

Poprzedni temat :: Następny temat
Kurs C++ - cz.10
Autor Wiadomość
Fili:P
Administrator



Dołączył: 19 Sie 2010
Posty: 44
Poziom: 5
HP: 0/81
 0%
MP: 38/38
 100%
EXP: 6/13
 46%
Wysłany: 2010-11-09, 09:03   Kurs C++ - cz.10  

[] Struktury



Czasami patrząc na używane przez nas zmienne zauważamy, że są one ze sobą ściśle powiązane. Na przykład tworząc prostą grę możemy mieć takie zmienne:


string imie;
int zycie;
int typ_broni;
int amunicja;
int poziom;
int pozycja_x;
int pozycja_y;


Wszystkie podane wyżej zmienne służą do opisywania stanu postaci gracza, z wyjątkiem jednej: zmiennej poziom. Język C++ daje nam możliwość "grupowania" takich zmiennych w jednym bloku, opatrzonym słowem struct oraz nazwą:


struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
int pozycja_x;
int pozycja_y;
};
 
int poziom;


W ten sposób stworzyliśmy strukturę SGracz. Pozornie to tylko więcej pisaniny, ale praktyka pokazuje, że takie wyodrębnianie związanych ze sobą zmiennych znacznie ułatwia pisanie programów. Taki ogólny wzorzec (paradygmat) znany jest pod nazwą programowania strukturalnego i został rozwinięty głównie przez programistów C, oczywiście można z niego skorzystać również w C++.

Zadeklarowanie struktury SGracz wiąże się z powstaniem nowego typu danych. Możemy następnie tworzyć zmienne tego nowego typu:


SGracz gracz;

Na tym etapie widać już pierwszą korzyść ze stosowania struktur. Jeśli zamarzy nam się w grze tryb dla wielu graczy, to nie będziemy teraz musieli deklarować dla każdego z nich osobnego zestawu zmiennych: imie, zycie, typ_broni itd. Wystarczy zadeklarować strukturę (typ danych), a następnie dla każdego z graczy po JEDNEJ zmiennej tego typu:


SGracz gracz1, gracz2;


Jeśli zechcemy mieć w grze milion graczy, to też nie ma problemu; można utworzyć tablicę struktur:


SGracz gracze [1000000];

Można również deklarować zmienne bezpośrednio po deklaracji typu (struktury):


struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
int pozycja_x;
int pozycja_y;
} gracz1, gracz2, pozostali_gracze[5];


...ale nie jest to raczej zalecane.

Warto zauważyć, że nazwa naszej struktury zaczyna się od dużej literki S. Nie jest to bynajmniej wymagane, ale wielu programistów tak postępuje (lub podobnie), by kod był czytelniejszy - nazw typów nie powinno się mylić z nazwami zmiennych.




[] Dostęp do składowych


Jak teraz dostać się do zmiennych wchodzących w skład struktury? Bardzo prosto - podajemy nazwę zmiennej strukturalnej, kropkę i po niej nazwę zmiennej składowej:


gracz1.imie = "Wacek";
gracz1.zycie = 100;
gracz1.typ_broni = 0;


Ponieważ zmienna będąca strukturą w gruncie rzeczy nie różni się od innych zmiennych, można do niej też utworzyć wskaźnik:


SGracz* gracz_ptr = NULL;

Działa on w zasadzie tak samo, jak wskaźniki do "wbudowanych" typów danych. Można go "przerobić" na zmienną używając operatora dereferencji (gwiazdki) i tym samym uzyskać dostęp do zmiennych składowych struktury:


(*gracz_ptr).imie = "Szarik"; // (1)


Oczywiście zbytnie nagromadzenie nawiasów i innych operatorów znakomicie utrudnia zrozumienie, o co chodzi w danym fragmencie kodu, więc wymyślono też inny sposób na "dotarcie" do składowej wskazywanej struktury:


gracz_ptr->imie = "Szarik"; // (2)

Instrukcja (2) robi dokładnie to samo, co (1). Łatwo zapamiętać: na "zwykłych" zmiennych będących strukturą stosujemy kropkę, na wskaźnikach - strzałkę (oczywiście strzałka jest operatorem dwuznakowym i nie ma nic wspólnego ani z operatorem '-', ani z '>').




[] Inicjalizacja struktury



W języku C++ przewidziano sposób na "szybkie" zainicjowanie pól (składowych zmiennych) struktury. Jest analogiczny do inicjalizacji tablic:


SGracz gracz = {"Rambo", 10, 0, 0, 1, 1};


Niestety, podobnie jak w przypadku tablic, sposób ten ma pewne ograniczenia. Zadziała świetnie, jeśli składowymi będą zmienne typu int, float czy innych "wbudowanych" typów. Zadziała też dla typu string, jak widać na powyższym przykładzie, jednak tylko dlatego, że twórcy typu string specjalnie przygotowali go do tego typu operacji – szczegóły poznamy w rozdziale o klasach.

Nie jesteśmy zmuszeni podczas inicjalizacji podawać wszystkich wartości, np.:


SGracz gracz = {"Rambo"};


W tym przypadku jedynie składowa imie zostanie zainicjowana jawnie wartością "Rambo", natomiast pozostałe – wartościami domyślnymi, tj. generowanymi przez domyślny konstruktor. O konstruktorach jeszcze nie rozmawialiśmy, ale dla typu int wartością domyślną będzie na pewno 0 i tak też w ostatnim przykładzie zostaną ustawione zmienne zycie, typ_broni i tak dalej.

Warto podkreślić, że inicjalizacja i przypisanie to zdecydowanie nie to samo i coś takiego:


gracz = {"Rambo", 10, 0, 0, 1, 1};


...nie jest wykonalne.

Ponieważ nic nie stoi na przeszkodzie, by struktura zawierała inne struktury, możemy zrobić coś takiego:


struct SPozycja
{
int x;
int y;
};
 
struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
SPozycja pozycja;
};


Jest to całkiem sensowne, ponieważ nie tylko gracze, ale również wiele innych obiektów w naszej grze może mieć pozycję (przeciwnicy, pociski, przeszkody etc.). Dostęp do takich zagnieżdżonych składowych jest taki:


SGracz gracz;
gracz.pozycja.x = 5;

Zagnieżdżanie działa również dobrze z "szybką" inicjalizacją:



gracz = {"Rambo", 10, 0, 0, {1, 1} };


Zauważmy, że w tym przypadku mamy dodatkową korzyść z zastosowania zagnieżdżenia - w inicjalizacji od razu widać, które z liczb odnoszą się do pozycji gracza (wcześniej nie było to takie oczywiste).




[] Upakowanie danych w pamięci


Pamiętamy, że za pomocą operatora sizeof można sprawdzić rozmiar typu danych. Działa on również ze strukturami. Na razie stworzyliśmy dwie – SPozycja i SGracz, pomierzmy je więc:


cout << "sizeof SPozycja: " << sizeof (SPozycja) << endl;
cout << "sizeof SGracz: " << sizeof (SGracz) << endl;


Typowy "wynik" to 8 bajtów dla SPozycja i 52 bajty dla SGracz. W pierwszym przypadku nie ma wątpliwości; dwie zmienne int po 4 bajty każda. Drugi przypadek jest nieco bardziej złożony. Mamy tu 5 zmiennych typu int (w tym dwie ze struktury SPozycja), więc razem 20 bajtów. Wychodzi na to, że typ string zajmuje w pamięci 32 bajty. Nie do końca jest to prawdą, ale tym zajmiemy się kiedy indziej. Na razie przyjmijmy, że string ma 32 bajty i zajmijmy się dodawaniem do struktury SGracz zmiennych innego typu:


struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
SPozycja pozycja;
char rasa;
};


W naszej grze pojawiła się możliwość wyboru rasy głównej postaci. Załóżmy, że jest tylko kilka ras, więc nie potrzebujemy zbyt dużej zmiennej do przechowywania tej informacji – char w zupełności wystarczy. Ale jaki to będzie miało wpływ na rozmiar struktury? Sprawdźmy ponownie:


cout << "sizeof SGracz: " << sizeof (SGracz) << endl;


Widzimy, że struktura "urosła" aż o 4 bajty, mimo że wyrażenie sizeof(char) zwraca nam 1. Dzieje się tak dlatego, że większość popularnych kompilatorów C++ wyrównuje rozmiar struktury, by ten był podzielny przez 4. Ma to potencjalne znaczenie dla szybkości działania naszych programów, za to utrudnia nam życie, jeśli chcemy oszczędzać pamięć. Z tego powodu warto się dwa razy zastanowić, czy zmienna typu char w tym akurat przypadku na pewno będzie lepsza od int. Ale jeśli dojdziemy do wniosku, że jednak będzie lepsza, to zapewne zechcemy poznać sposoby na "zmuszenie" kompilatora do nadania strukturze "właściwego" rozmiaru.

I tutaj pojawia się problem, ponieważ standard C++ nie przewiduje na chwilę obecną żadnego sposobu. Nie znaczy to jednak, że taki sposób nie istnieje. W Visual C++ możemy na przykład zrobić tak:


#pragma pack (1)

Po wstawieniu takiego czegoś nasze struktury będą miały taki rozmiar, jaki by wynikał "z logiki" (53 w przypadku SGracz). Oczywiście nie można sobie wstawić dowolnej liczby, np. 3. Dozwolone wielkości to 1, 2, 4, 8 i 16.

Dyrektywa taka odnosi się do WSZYSTKICH struktur, jakie od tej deklarujemy; jednak raczej nie zaleca się takiej drastycznej ingerencji w działanie kompilatora. Przy pisaniu większych programów nie wyjdzie nam to na zdrowie. Znacznie lepiej modyfikować wyrównanie indywidualnie dla każdej struktury, co umożliwia nam rozszerzona postać dyrektywy #pragma pack:


#pragma pack (push: 1)
struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
SPozycja pozycja;
char rasa;
};
#pragma pack (pop)


Rozszerzona dyrektywa #pragma pack działa na zasadzie stosu – parametr push powoduje wrzucenie na stos nowej wartości, do której będą wyrównywane struktury (tutaj: 1), parametr pop powoduje zdjęcie ostatniej wartości ze stosu. Jeśli nie zagnieździmy tych dyrektyw, to na stosie będzie zawsze najwyżej jedna wartość, więc zdjęcie jej ze stosu "przywraca" domyślny sposób wyrównywania rozmiaru struktury. Tak oto zmieniamy wyrównanie TYLKO dla struktury SGracz, podczas gdy ewentualne inne struktury w naszej grze będą wyrównywane "normalnie", czyli do 4 bajtów.

Inny wiodący kompilator, GCC, również obsługuje #pragma pack, ale nie dla wszystkich platform. Jeśli trafimy na taką, dla której ta dyrektywa nie działa, można się pod GCC poratować jeszcze czymś takim:


struct SGracz
{
string imie;
int zycie;
int typ_broni;
int amunicja;
SPozycja pozycja;
char rasa;
} __attribute__((packed));


Deklaracja ta spowoduje pod GCC wyłączenie wyrównania w danej strukturze (analogicznie jak #pragma pack(1)).




[] Operacje na pamięci


[] Ciekawostka: Unie



Istnieje specjalny rodzaj struktur, który stworzony został z myślą o oszczędzaniu pamięci. Zamiast słowa struct do jego zadeklarowania używamy słowa union. Poza tym drobiazgiem istnieje zasadnicza różnica pomiędzy strukturą a unią: wszystkie jej składowe współdzielą ten sam obszar pamięci. Co to znaczy? Popatrzmy na prosty przykład:


union Unia
{
int PoleInt;
char PoleChar;
};
 
Unia u;
u.PoleInt = 5;
 
cout << "u.PoleInt = " << u.PoleInt << endl;
 
u.PoleChar = 0;
 
cout << "u.PoleInt = " << u.PoleInt << endl;


Najpiew zainicjowaliśmy pole PoleInt wartością 5 i wypisaliśmy ją na ekranie. Do tej pory wszystko jest tak samo, jak byłoby w przypadku struktury. Ale następna linijka modyfikuje składową PoleChar, ustawiając ją na 0. Wypisując na ekran u.PoleInt ponownie dostrzeżemy, że jej wartość została zniszczona przez poprzednią operację; teraz PoleInt wynosi 0, podobnie jak PoleChar. Dlaczego? Ponieważ oba pola mają wspólną komórkę pamięci!

Oczywiście, zmienna typu char jest "mniejsza" niż int. Dlatego jeśli do u.PoleInt wpiszemy teraz 1000, to u.PoleChar nie stanie się nagle równe 1000, tylko -24, ponieważ wzięty pod uwagę będzie tylko pierwszy bajt naszej unii. W każdym razie obydwa pola korzystają wspólnie z tego pierwszego bajtu.

Warto "zbadać" unię operatorem sizeof:


cout << "sizeof Unia: " << sizeof (Unia) << endl;


Wynik to 4, ponieważ tyle wynosi rozmiar największej składowej unii. Gdyby do unii wstawić pole typu SPozycja, wynik byłby równy 8, ponieważ największą składową byłoby wtedy pole typu SPozycja. Z kolei umieszczenie w unii wyłącznie pól typu char:


union Unia
{
char PoleChar;
char PoleChar2;
char PoleChar3;
};


...sprawi, że test sizeof da nam wynik 1. Tak więc domyślne wyrównywanie rozmiaru do wielokrotności 4 nie dotyczy unii.

Jakie jeszcze różnice między uniami a strukturami wynikają z tej zasadniczej różnicy, o której wspomnieliśmy na początku (współdzielenie pamięci)? Jest ich kilka, ale o większości z nich będziemy mogli porozmawiać dopiero wtedy, gdy poznamy klasy. Na razie warto wiedzieć o dwóch. Po pierwsze, nie można zainicjować unii "szybkim" sposobem, czyli:


Unia u = { 1, 2, 3 }; // błąd


Jest to w sumie oczywiste, gdyż i tak nie mielibyśmy żadnego pożytku z wartości 1, która natychmiast zostałaby "zniszczona" przez wartość 2, po czym ta również zostałaby nadpisana przez 3. Za to możemy uczynić tak:


Unia u = { 1 };

Jest to dla nas na razie jedyny sposób inicjalizacji składowych unii.

Drugą istotną różnicę widać w przykładzie:


union Unia
{
string PoleString; // oj...
char PoleChar2;
};


Pole typu string nie może być niestety (a może stety?) składową unii. To samo dotyczy wszystkich typów danych posiadających konstruktor. Póki nie wiemy wiele o konstruktorach, musimy zadowolić się wytłumaczeniem, że tylko proste typy danych "nadają się" do unii. To ograniczenie można obejść stosując wskaźnik do danego typu, np:


union Unia
{
string* PoleString; // ...ale tak może być
char PoleChar2;
};


Należy jednak uważać przy stosowaniu wskaźników, bo mogą z tego wynikać różne komplikacje i trudne do wykrycia błędy.

Mówimy o uniach jako o ciekawostce, ponieważ nie są one często używane. Komplikacje wynikające ze współdzielenia pamięci przez pola przewyższają zwykle korzyści, jakie daje zastosowanie unii (oszczędność pamięci). Zawsze jednak warto wiedzieć o istnieniu takich tworów. A nuż się przydadzą?


[] Ciekawostka: Pola bitowe


Pola bitowe to specjalny typ pola w strukturze (może być też w klasie). Określa on, że dany składnik ma zajmować określoną liczbę bitów (nie bajtów!). Oto przykład:


struct SDane
{
unsigned int poleBitowe :5;
};


W powyższym przykładzie pole poleBitowe będzie się mieścić dokładnie na 5 bitach. Definicja pola bitowego jest bardzo podobna do "zwykłego" pola, z tym że po jego nazwie następuje dwukropek, a po nim ilość bitów, którą ma zajmować obiekt. Wartość ta musi być większa lub równa 0. Pole bitowe musi być typu całkowitego czyli char,int we wszystkich wariantach long,short oraz signed,unsigned, a także bool i enum.

Pola bitowe nie są jednak tak przewidywalne, jak by się mogło wydawać. Spójrzmy na poniższy przykład:


struct SDane
{
unsigned int liczba :4;
unsigned int numer :2;
bool widoczny :1;
bool uruchomiony :1;
};


Powyższa struktura składa się z czterech pól które razem dają nam 8 bitów, czyli 1 bajt. Wydawało by się najbardziej rozsądne po prostu umieszczenie tego w jednym bajcie, jednak nie jest to w żaden sposób określone przez Standard C++! Oznacza to, że każdy kompilator może rozmieścić bity "po swojemu". Jednak w większości popularnych kompilatorów istnieją dość rozsądne zasady rozmieszczania bitów:


  • Dane są umieszczane po kolei, w takiej kolejności, w jakiej są deklarowane w klasie.
  • Jeżeli pole nie mieści się w całości w słowie, to jest kontynuowane w kolejnym słowie.
  • Najczęściej umieszczanie danych zaczyna się od prawego brzegu słowa.

Pola bitowa są najczęściej używane do zaoszczędzenia pamięci. Jednak mają też sporo ograniczeń:


  • Nie można pobrać adresu pola bitowego, a co za tym idzie
  • Nie można na nie pokazywać wskaźnikiem

  • Pole bitowe nie może być static
  • Pole bitowe nie może mieć referencji

Pomimo to warto wiedzieć o takich tworach - czasem mogą się przydać ;-).

 
     
Wyświetl posty z ostatnich:   
Odpowiedz do tematu
Nie możesz pisać nowych tematów
Możesz odpowiadać w tematach
Nie możesz zmieniać swoich postów
Nie możesz usuwać swoich postów
Nie możesz głosować w ankietach
Dodaj temat do Ulubionych
Wersja do druku

Skocz do:  
Szybka odpowiedź
Użytkownik: 


Wygaśnie za Dni
 
 
 
 
 
 
 

Powered by phpBB modified by Przemo © 2003 phpBB Group
Template bLock v 0.2 modified by Nasedo

Strona wygenerowana w 5.6 sekund. Zapytań do SQL: 11