Programowanie - C++

C++ – Cześć 13 kursu C/C++

13. Definicja własnych typów

Język C, poza wbudowanymi typami danych (np. int , czy float ), umożliwia także definicję własnych typów danych. W tym punkcie zostaną przedstawione różne aspekty tego tematu.

13.1. Typ wyliczeniowy

Typ wyliczeniowy nie jest typem danych w ścisłym tego słowa znaczeniu, gdyż jest to odpowiednik typu int . Ma on jednak ciekawą cechę, a mianowicie kolejne jego elementy możemy nazwać wedle swojego uznania. Jak zwykle prezentację nowych rzeczy zaczniemy od przykładowego programu:

#include <stdio.h>

void main(void)

{

enum {Pn, Wt, Sr, Czw, Pt, Sb=10, Nd} DzienTyg;

DzienTyg = Pn; printf(„Wartosc dla poniedzialku = %d
„, DzienTyg);

DzienTyg = Wt; printf(„Wartosc dla wtorku = %d
„, DzienTyg);

DzienTyg = Sr; printf(„Wartosc dla srody = %d
„, DzienTyg);

DzienTyg = Czw; printf(„Wartosc dla czwartku = %d
„, DzienTyg);

DzienTyg = Pt; printf(„Wartosc dla piatku = %d
„, DzienTyg);

DzienTyg = Sb; printf(„Wartosc dla soboty = %d
„, DzienTyg);

DzienTyg = Nd; printf(„Wartosc dla niedzieli = %d
„, DzienTyg);

}

W programie tym chcemy operować na zmiennej, która będzie przechowywać dzień tygodnia. Moglibyśmy po prostu zadeklarować ją jako int i przyjąć założenie, że poniedziałkowi odpowiada wartość zero, wtorkowi wartość jeden itd. Jednak, gdy przy dalszej rozbudowie programu chcielibyśmy przypisać tej zmiennej wartość „Środa” to musielibyśmy sobie przypominać jaka liczba jej odpowiada. Możemy jednak ułatwić sobie to zadanie dzięki zastosowaniu enum . Każdej kolejnej wartości możemy przydzielić identyfikator, który łatwiej będzie zapamiętać. Jak widzisz w naszym programie zadeklarowaliśmy identyfikatory „Pn”, „Wt”, „Sr” itd. enum już sam zadba o przydzielenie im konkretnych wartości – tzn. „Pn” będzie odpowiadało wartości zero, „Wt” jeden itd. Jeśli z jakiegoś powodu chciałbyś, aby od pewnego identyfikatora nastąpił przeskok i żeby liczenie zaczynało się od innej wartości to podajesz ją po znaku równości. W naszym przykładzie identyfikatorowi „Sb” przydzieliliśmy wartość dziesięć (pamiętaj, że kolejnym identyfikatorom będa odpowiadały zmienione już wartości – w naszym przykładzie niedzieli będzie przydzielona wartość jedenaście). Teraz zamiast pisać:

DzienTyg = 2;

możemy po prostu napisać:

DzienTyg = Sr;

Ważne jest jednak, abyś pamiętał, że mimo faktu przypisywania zmiennej wartości poprzez nadane identyfikatory, to nadal są to zwykłe liczby.

13.2. Typedef

Przedstawione w poprzednim podpunkcie enum mimo faktu, że mamy tam wpływ na nadawanie nazw identyfikatorom, nie deklaruje jednak nowego typu. Zmienna typu wyliczeniowego jest ciągle zmienną typu int . Do deklaracji nowego typu danych służy instrukcja typedef . A oto przykład jej użycia:

#include <stdio.h>

typedef float rzeczywista;

void main(void)

{

rzeczywista a=4.5;

printf(„%f
„, a);

}

Ogólna postać deklaracji to wygląda następująco:

typedef definicja_typu nazwa_nowego_typu;

W naszym przypadku przykład deklaracji nowego typu danych, któremu nadaliśmy nazwę „rzeczywista”, mamy w drugiej linijce programu. Określiliśmy tam, że nowy typ będzie po prostu typem float tylko ze zmienioną nazwą. Ponieważ identyfikator „rzeczywista” odpowiada od tego momentu nowemu typowi danych to możemy zadeklarować sobie zmienną tego typu. W funkcji main deklarujemy zmienną o nazwie a. Ponieważ jej typ został wyprowadzony z typu float to możemy używać jej tak, jakby była to zmienna typu float. W naszym programie po prostu ją wyświetlamy na ekranie.

Zastanawiasz się zapewne czemu służy to polecenie, skoro możemy po prostu używać wbudowanego typu float . Odpowiedzi są trzy. Po pierwsze, deklaracja ta może dotyczyć o wiele bardziej złożonego typu danych. Po drugie, dzięki temu możemy w łatwy sposób przenieść nasz program na inny kompilator lub system. Przypomnij sobie, że na przykład zmienna typu int może na jednym kompilatorze zajmować cztery bajty, a na innym tylko dwa. Możemy w łatwy sposób temu zaradzić poprzez deklarację naszego nowego typu o nazwie na przykład „MOJINT” i wszędzie go używać. Teraz jeśli chcielibyśmy go przenieśc na inny kompilator to wystarczy jedynie zmienić deklarację typu i gotowe ! Trzecim powodem może być chęć zwiększenia precyzji obliczeń. Jeśli program wszelkie obliczenia wykonywał na zmiennych typu float to w takim przypadku musielibyśmy zmienić wszelkie wystąpienia tego typu na typ double . A tak wystarczy jedynie zadeklarować nowy typ, na przykład „MOJFLOAT” i używać go zamiast float , a przy konieczności zwiekszenia precyzji zmienić jedynie deklarację typu „MOJFLOAT” tak, żeby wyprowadzony był z typu double .

13.3. Struktury

W tym punkcie powiemy sobie o strukturach (odpowiednikach pascalowych rekordów), czyli o złożonym typie danych. Jest to typ danych tworzony przez programistę, który jest kombinacją wcześniej zdefiniowanych typów, włączając w to, oprócz typów prostych, inne typy zdefiniowane przez programistę (także inne struktury). Zaczniemy, jak zwykle, od przykładowego programu:

#include <stdio.h>

typedef struct {

int godziny;

int minuty;

int sekundy;

} CZAS;

void main(void)

{

CZAS teraz;

int ile_sekund;

teraz.godziny = 23;

teraz.minuty = 53;

teraz.sekundy = 21;

printf(„Teraz jest %d:%d:%d
„, teraz.godziny, teraz.minuty, teraz.sekundy);

ile_sekund = teraz.sekundy + teraz.minuty*60 + teraz.godziny*3600;

printf(„Od poczatku dnia uplynelo %d sekund.
„, ile_sekund);

}

Analizę programu zaczniemy od miejsca definicji nowego typu danych, który tym razem nie będzie, tak jak ostatnio, tylko odpowiednikiem prostego typu, lecz całkowicie nowym, złożonym typem (strukturą). Ogólna postać definicji struktury wygląda następująco:

typedef struct {

typ nazwa_pola1;

typ nazwa_pola2;

.

.

.

typ nazwa_polaN;

} nazwa_struktury;

W naszym przypadku zdefiniowaliśmy sobie strukturę o nazwie CZAS zawierającą trzy pola typu int (godziny, minuty i sekundy), która została zaprojektowana do przechowywania informacji o konkretnym czasie (stąd nazwa 😉 Moglibyśmy co prawda te same informacje przechowywać w trzech osobnych zmiennych, ale co jeśli chcielibyśmy mieć dane o dwóch różnych godzinach ? Musielibyśmy dodać trzy nowe zmienne, co wkrótce doprowadziłoby do kompletnego chaosu. Struktura pozwala nam przechowywać potrzebne infromacje, przy czym wszystko znajduje sie w jednym miejscu – zamiast trzech, mamy tylko jedną zmienną. Przejdźmy dalej – na początku funkcji main widzimy deklarację zmiennej typu CZAS o nazwie „teraz”, która będzie przechowywać potrzebne nam informacje. Czyli narazie nic nowego. Natomiast w następnych trzech linijkach widzimy zupełnie nową kostrukcję. Przedstawia ona w jaki sposób odwołujemy się do poszczególnych pól struktury – czyli: podajemy nazwę zmiennej (u nas nazywa się ona „teraz”), potem stawiamy kropkę, a następnie podajemy nazwę pola, do którego się odnosimy. Poza tym, że do poszczególnych pól odwołujemy się w nowy sposób, możemy z nich korzystać tak jakby byłaby to normalna zmienna o danym typie – czyli możemy przypisywać jej wartość, czy też używać wszelkich operatorów, co zostało pokazane na przykładzie obliczania liczby sekund, które upłynęły od początku dnia.

Strukturę można także zdeklarować także w nieco inny sposób niż przedstawiony wcześniej, a mianowicie:

struct nazwa_struktury {

typ nazwa_pola1;

typ nazwa_pola2;

.

.

.

typ nazwa_polaN;

};

Jednak ja zalecam stosowanie tego pierwszego sposobu, gdyż przy deklaracji zmiennej nie wystarczy napisać tak jak w poprzednim przypadku:

nazwa_struktury nazwa_zmiennej;

ale należy zastosować nieco dłuższa składnię:

struct nazwa_struktury nazwa_zmiennej;

Poza tym faktem, oba sposoby deklaracji nie różnią się niczym.

13.4. Unie

Unie „z wyglądu” są bardzo podobne do znanych Ci już struktur. Inny jest jednak cel ich wykorzystania. Służą one mianowicie efektywnemu wykorzystaniu pamięci. Każde ich pole zajmuje fizycznie tą samą komórkę pamięci, z tego też względu, w danym momencie może być wykorzystywane tylko jedno z ich pól. Jeśli wydaje Ci się to dziwne to przyjrzyj się następującemu przykładowi:

#include <stdio.h>

typedef union {

float szybkosc_w_wezlach;

int szybkosc_w_km;

} POJAZD;

void main(void)

{

POJAZD samochod, statek;

samochod.szybkosc_w_km = 220;

statek.szybkosc_w_wezlach = 34.5;

printf(„Max. szybkosc samochodu to %d km/h
„, samochod.szybkosc_w_km);

printf(„Max. szbkosc statku to %3.1f wezlow
„, statek.szybkosc_w_wezlach);

}

Założeniem programu jest przechowywanie informacji o maksymalnej szybkości danego pojazdu. Jednak pojazdem może być zarówno samochód, który ma tą informację wyrażoną w km/h, jak również statek, w przypadku którego wyraża się ją w węzłach. Tak więc, gdybyśmy wykorzystali do tego celu strukturę to jedno z pól nigdy nie byłoby wykorzystywane, przez co tracilibyśmy miejsce w pamięci. Co prawda w tym przypadku byłyby to zaledwie cztery bajty, ale gdybyśmy mieli dużą tablicę takich struktur (o których powiemy później) to strata byłaby już znaczna. W takim przypadku możemy wykorzystać unię – zadeklarowaliśmy dwa pola, do których możemy odwoływać się używając różnych nazw, jednak tak na prawdę zajmują one tylko tyle miejsca w pamięci, ile zajmuję największy element (w naszym przypadku oba pola mają wielkość cztery bajty, więc unia zajmuje w pamięci także cztery bajty). Myślę, że nie trzeba omawiać problemu od strony skłdniowej – wystarczy powiedzieć, że składnia jest dokładnie taka sama jak w przypadku struktur. Jedyną różnicą jest to, że zamiast słowa kluczowego struct używamy słowa kluczowego union .

13.5. Pola bitowe

Pola bitowe są kolejnym odcinkiem z serii „Oszczędzanie pamięci”. Mają zastosowanie przy definicji struktur – przy pomocy tej konstrukcji możemy zadeklarować pole, którego wielkość będzie mniejsza niż jeden bajt (jeden lub kilka bitów – stąd nazwa). A oto przykład:

#include <stdio.h>

typedef struct {

unsigned char szyberdach : 1;

unsigned char abs : 1;

unsigned char ilosc_miejsc : 4;

} SAMOCHOD;

void main(void)

{

SAMOCHOD ford;

ford.szyberdach = 1;

ford.abs = 1;

ford.ilosc_miejsc = 2;

printf(„Ilosc miejsc : %d
„, ford.ilosc_miejsc);

if(ford.szyberdach) printf(„Posiada szyberdach.
„);

if(ford.abs) printf(„Posiada ABS.
„);

}

Program ten przechowuje i wyświetla informacje o samochodzie, a konkretnie o liczbie miejsc, o tym, czy posiada szyberdach i ABS. Normalnie potrzebowalibyśmy zadeklarować strukturę o trzech polach, która, przy zastosowaniu jednobajtowego typu char, zajęłaby trzy bajty w pamięci. Dzięki zastosowaniu pól bitowych wszystkie te informacje zajmują jeden, jedyny bajt pamięci (i to, w przeciwieństwie do unii, z możliwością odwoływania się do wszystkich pól jednocześnie). Jak to możliwe ? Otóż ograniczyliśmy zakres poszczególnych pól. Zastanówmy się, ile potrzeba miejsca w pamięci, aby przechować informację o fakcie wyposażenia, bądź nie, samochodu w ABS ? Są dwa możliwe stany – jest lub nie ma. Czyli innymi słowy jedynka, albo zero – wystarczy jeden bit ! To samo tyczy się szyberdachu. Jeśli chodzi o liczbę miejsc to w tym przypadku ograniczyłem liczbę możliwych wartości do 16 – po prostu przeznaczyłem na to pole cztery bity (a 2^4 = 16). W ten sposób zamiast użwyać trzech, struktura ta używa zaledwie jednego bajtu.

Od strony składniowej dostęp do pól bitowych jest identyczny, jak do tych „normalnych”. Jedyną różnicą jest sposób ich deklaracji – tzn. deklaruje się je tak samo, ale po typie i nazwie pola podaje się dodatkowo, po dwukropku, ilość bitów, które zamierzamy przeznaczyć na dane pole.