LEKCJA 14. Jak tworzyć i stosować struktury. ________________________________________________________________ W trakcie tej lekcji poznasz pojęcia: * Klasy zmiennej. * Struktury. * Pola bitowego. * Unii. Dowiesz się także więcej o operacjach logicznych. ________________________________________________________________ CO TO JEST KLASA ZMIENNEJ? W języku C i C++ programista ma większy wpływ na rozmieszczenie zmiennych w pamięci operacyjnej komputera i w rejestrach mikroprocesora. Może to mieć decydujący wpływ na dostępność danych z różnych miejsc programu i szybkość działania programu. Należy podkreślić, że TYP ZMIENNEJ (char, int, float itp.) decyduje o sposobie interpretacji przechowywanych w pamięci zer i jedynek, natomiast KLASA ZMIENNEJ decyduje o sposobie przechowywania zmiennej w pamięci. W C++ występują cztery klasy zmiennych. ZMIENNE STATYCZNE - static. Otrzymują stałą lokalizację w pamięci w momencie uruchamiania programu. Zachowują swoją wartość przez cały czas realizacji programu, chyba, że świadomie zażądamy zmiany tego stanu - np. instrukcją przypisania. Przykład deklaracji: static float liczba; W większości kompilatorów C++ zmienne statyczne, które nie zostały jawnie zainicjowane w programie, otrzymują po zadeklarowaniu wartość ZERO. ZMIENNE AUTOMATYCZNE - auto. Otrzymują przydział miejsca w pamięci dynamicznie - na stosie procesora, w momencie rozpoczęcia wykonania tego bloku programu, w którym zmienne te zostały zadeklarowane. Przydzielenie pamięci nie zwalnia nas z obowiązku zainicjowania zmiennej (wcześniej wartość zmiennej jest przypadkowa). Zmienne automatyczne "znikają" po zakończeniu wykonywania bloku. Pamięć im przydzielona zostaje zwolniona. Przykład: auto long suma; ZMIENNE REJESTROWE - register. Zmienne rejestrowe są także zmiennymi lokalnymi, widocznymi tylko wewnątrz tego bloku programu, w którym zostały zadeklarowane. C++ może wykorzystać dwa rejestry mikroprocesora - DI i SI do przechowywania zmiennych. Jeśli zadeklarujemy w programie więcej zmiennych jako zmienne rejestrowe - zostaną one umieszczone na stosie. Znaczne przyspieszenie działania programu powoduje wykorzystanie rejestru do przechowywania np. licznika pętli. Przykład: register int i; ..... for (i=1; i<1000; i++) {.....} ZMIENNE ZEWNĘTRZNE - extern. Jeśli zmienna została - raz i TYLKO RAZ - zadeklarowana w pojedynczym segmencie dużego programu, zostanie w tymże segmencie umieszczona w pamięci i potraktowana podobnie do zmiennych typu static. Po zastosowaniu w innych segmentach deklaracji extern zmienna ta może być dostępna w innym segmencie programu. Przykład: extern int NUMER; STRUKTURY. Poznane wcześniej tablice mogą zawierać wiele danych, ale wszystkie te dane muszą być tego samego typu. Dla zgrupowania powiązanych ze sobą logicznie danych różnego typu C/C++ stosuje STRUKTURY, deklarowane przy pomocy słowa struct. Kolejne pola struktury są umieszczane w pamięci zgodnie z kolejnością ich deklarowania. Strukturę, podobnie jak zmienną, MUSIMY ZADEKLAROWAĆ. Struktura jest objektem bardziej złożonym niż pojedyncza zmienna, więc i deklaracja struktury jest bardziej skomplikowana. Deklaracja struktury składa się z następujących elementów: 1. Słowo kluczowe struct (obowiązkowe). 2. Nazwa (opcjonalna). Jeśli podamy nazwę, to nazwa ta będzie oznaczać dany typ struktury. 3. Nawias klamrowy { 4. Deklaracje kolejnych składników struktury. 5. Nawias klamrowy } 6. Lista nazw struktur określonego powyżej typu (może zostać zadeklarowana oddzielnie). Przykład. Deklaracja ogólnego typu struktury i określenie wewnętrznej postaci struktury. struct Ludzie { char Imiona[30]; char Nazwisko[20]; int wiek; char pokrewienstwo[10] }; Jeśli określimy już typ struktury - czyli rodzaj, wielkość i przeznaczenie poszczególnych pól struktury, możemy dalej tworzyć - deklarować i inicjować konkretne struktury danego typu. Przykład. Deklaracja zmiennych - struktur tego samego typu. struct Ludzie Moi, Twoi, Jego, Jej, Szwagra; Deklarację struktur można połączyć. Przykład. Połączona deklaracja struktur. struct Ludzie { char pokrewienstwo[10]; char Imiona[30]; int wiek; } Moi, Twoi, Szwagra; Struktury statyczne * mają stałe miejsce w pamięci w trakcie całego programu; * są "widoczne" i dostępne w całym programie. Zadeklarujemy teraz typ struktury i zainicjujemy dwie struktury. Przykład. Zainicjowanie dwu struktur statycznych. struct Ludzie { char pokrewienstwo[10]; char Imiona[30]; int wiek; }; struct Ludzie Moi, Szwagra; static struct Ludzie Moi = { "Stryjek", "Walenty", 87 }; static struct Ludzie Szwagra = { "ciotka", "Ala", 21 }; Zapis static struct Ludzie Szwagra; oznacza: statyczna struktura typu "Ludzie" pod nazwą "Szwagra". Do struktury w całości możemy odwoływać się za pomocą jej nazwy a do poszczególnych elementów struktury poprzez nazwę struktury i nazwę pola struktury - ROZDZIELONE KROPKĄ ".". Zademonstrujmy to na przykładzie. Zwróć uwagę na różne sposoby przekazywania danych pomiędzy strukturami: C4.Wiek=Czlowiek2.Wiek; - przekazanie zawartości pojedynczego pola numerycznego; C4=Czlowiek3; - przekazanie zawartości całej struktury Czlowiek3 do C4. Przykład. Program manipulujący prostą strukturą. [P037.CPP] int main() { struct Ludzie { char Imie[20]; int Wiek; char Status[30]; char Tel_Nr[10]; }; static struct Ludzie Czlowiek1={"Ala", 7, "Ta, co ma Asa","?"}, Czlowiek2={"Patrycja", 13, "Corka", "8978987"}, Czlowiek3={"Krzysztof", 27, "Kolega z przedszkola", "23478"}; struct Ludzie C4, C5; C4=Czlowiek3; C4.Wiek=Czlowiek2.Wiek; C5=Czlowiek1; clrscr(); printf("%s %d %s\n", C4.Imie, C4.Wiek, C4.Status); printf("%s %s",C5.Imie, C5.Status); return 0; } Tablice mogą być elementami struktur, ale i odwrotnie - ze struktur, jak z cegiełek można tworzyć konstrukcje o wyższym stopniu złożoności - struktury struktur i tablice struktur. Jeśli tablica składa się z liczb typu int, to deklarujemy ją: int TABLICA[10]; jeśli tablica składa się ze struktur, to deklarujemy ją: struct TABLICA[50]; W przykładzie poniżej przedstawiono * deklarację jednowymiarowej tablicy LISTA[50], * elementami tablicy są struktury typu SCzlowiek, * jednym z elementów każdej struktury SCzlowiek jest struktura "niższego rzędu" typu Adres; [P038.CPP] int main() { struct Adres { char Ulica[30]; int Nr_Domu; int Nr_Mieszk; }; struct SCzlowiek { char Imie[20]; int Wiek; struct Adres Mieszkanie; }; struct SCzlowiek LISTA[50]; LISTA[1].Wiek=34; LISTA[1].Mieszkanie.Nr_Domu=29; printf("%d", LISTA[1].Mieszkanie.Nr_Domu); return 0; } Zapis printf("%d", LISTA[1].Mieszkanie.Nr_Domu oznacza: * wybierz element nr 1 z tablicy LISTA; (jak wynika z deklaracji tablicy, każdy jej element będzie miał wewnętrzną strukturę zorganizowaną tak, jak opisano w deklaracji struktury SCzlowiek); * wybierz ze struktury typu SCzlowiek pole Mieszkanie; (jak wynika z deklaracji, pole Mieszkanie będzie miało wewnętrzną organizację zgodną ze strukturą Adres); * ze struktury typu Adres wybierz pole Nr_Domu; * Wydrukuj zawartość pola pamięci interpretując ją jako liczbę typu int - w formacie %d. Słowo struktura tak doskonale pasuje, że chciałoby się powiedzieć: jeśli struktura struktur jest wielopoziomowa, to podobnie, jak przy wielowymiarowych tablicach, każdy poziom przy nadawaniu wartości musi zostać ujęty w dodatkową parę nawiasów klamrowych. [???] A CO Z ŁAŃCUCHAMI ZNAKOWYMI ? ________________________________________________________________ Język C++ oferuje do kopiowania łańcuchów znakowych specjalną funkcję strcpy(). Nazwa funkcji to skrót STRing CoPY (kopiuj łańcuch). Sposób wykorzystania tej funkcji: strcpy(Dokąd, Skąd); lub strcpy(Dokąd, "łańcuch znaków we własnej osobie"); Szczegóły - patrz Lekcja o łańcuchach znakowych. ________________________________________________________________ STRUKTURY I WSKAŹNIKI. Wskaźniki mogą wskazywać strukturę w całości lub element struktury. Język C/C++ oferuje specjalny operator -> który pozwala na odwoływanie się do elementów struktury. W przykładzie poniżej przedstawiono różne sposoby odwołania się do elementów trzech identycznych struktur STA, STB, STC. [P039.CPP] int main() { struct { char Tekst[20]; int Liczba1; float Liczba2; } STA, STB, STC, *Pointer; STA.Liczba1 = 1; STA.Liczba2 = 2.2; strcpy(STA.Tekst, "To jest tekst"); STB=STA; Pointer = &STC; Pointer->Liczba1 = 1; Pointer->Liczba2 = 2.2; strcpy(Pointer->Tekst, STA.Tekst); printf("\nLiczba1-STA Liczba2-STB Tekst-STC\n\n"); printf("%d\t", STA.Liczba1); printf("%f\t", STB.Liczba2); printf("%s", Pointer->Tekst); return 0; } Rozszyfrujmy zapis: strcpy(Pointer->Tekst, STA.Tekst); Skopiuj łańcuch znaków z pola Tekst struktury STA do pola Tekst struktury wskazywanej przez pointer. Prawda, że to całkiem proste? [???] CZY MUSIMY TO ROZDZIELAĆ ? ________________________________________________________________ Jak zauważyłeś, liczby moglibyśmy zapisywać także jako łańcuchy znaków, ale wtedy nie moglibyśmy wykonywać na tych liczbach działań. Konwersję liczba - łańcuch znaków lub odwrotnie łańcuch znaków - liczba wykonują w C specjalne funkcje np.: atoi() - Ascii TO Int.; itoa() - Int TO Ascii itp. Więcej informacji na ten temat i przykłady znajdziesz w dalszej części książki. ________________________________________________________________ Elementami struktury mogą być zmienne dowolnego typu, łądznie z innymi strukturami. Ciekawostka: ________________________________________________________________ Wskaźnik do deklarowanej struktury może być w języku C/C++ jak jeden z jej WŁASNYCH elementów. Jeśli wskaźnik wchodzący w skład struktury wskazuje na WŁASNĄ strukturę, to nazywa się to AUTOREFERENCJĄ STRUKTURY. ________________________________________________________________ POLA BITOWE. Często zdarza się, że jakaś zmienna ma zawężony zakres wartości. Dla przykładu zmienne logiczne (tzw. flagi) to zawsze tylko 0 lub 1. Wiek rzadko przekracza 255 lat a liczba dzieci zwykle nie jest większa niż 15. Nawet najbardziej niestali panowie nie zdążą ożenić się i rozwieść więcej niż 7 razy. Gdybyśmy zatem chcieli zapisać informacje * płeć 0 - mężczyzna, 1 - kobieta ( 1 bit ); * wiek 0 - 255 lat (8 bitów); * ilość dzieci 0 - 15 (4 bity); * kolejny numer małżeństwa 0 - 7 (3 bity); to przecież wszystkie te informacje mogą nam się zmieścić w jednym szesnastobitowym rejestrze lub w dwu bajtach pamięci. Takie kilka bitów wydzielone i mające określone znaczenie to właśnie pole bitowe. C++ pozwala także na uwzględnianie znaku w polach bitowych. Pola bitowe mogą być typu int i unsigned int (czyli takie jak w przykładzie poniżej). Jeśli jakieś dane chcemy przechowywać w postaci pola bitowego, w deklaracji struktury sygnalizujemy to dwukropkiem. Stwarza to dwie istotne możliwości: * bardziej ekonomicznego wykorzystania pamięci; * łatwego dodatkowego zaszyfrowania danych. [P040.CPP] //Pamietaj o dolaczeniu plikow naglowkowych ! int main() { struct USC { int Sex : 1; unsigned Wiek : 8; unsigned Dzieci : 4; unsigned Ktora : 3; } Facet; int bufor; clrscr(); Facet.Sex = 0; printf("\n Ile ma lat ? : "); scanf("%d", &bufor); Facet.Wiek = bufor; printf("\n Ktore malzenstwo ? : "); scanf("%d", &bufor); Facet.Ktora = bufor; printf("\n Ile dzieci ? : "); scanf("%d", &bufor); Facet.Dzieci = bufor; printf("\n\n"); if (Facet.Ktora) printf("Facet ma %d zone", Facet.Ktora); printf("\nPlec: Dzieci: Wiek (lat): \n\n"); printf("%d\t%d\t%d", Facet.Sex, Facet.Dzieci, Facet.Wiek); getch(); return 0; } Uruchom program i sprawdź co się stanie, jeśli Facet będzie miał np. 257 lat lub 123 żonę. Przekroczenie zadeklarowanego zakresu powoduje obcięcie części bitów. Aby uzyskać "wyrównanie" pola bitowego do początku słowa należy przed interesującym naspolem bitowym zdefiniować tzw. pole puste: * pole bitowe bez nazwy; * długość pola pustego powinna wynosić 0. Poniżej przedstawiam przykład pola bitowego zajmującego trzy kolejne słowa 16 bitowe. Dodanie pola pustego wymusza rozpoczęcie pola pole_IV od początku trzeciego słowa maszynowego (zakładamy, że pracujemy z komputerem 16 bitowym). struct { unsigned pole_I:4; unsigned pole_II:10; unsigned pole_III:4; unsigned :0; /* to jest pole puste */ unsigned pole_IV:5; } pole_przykladowe; Zwróć uwagę, że część bitów w drugim i trzecim słowie maszynowym nie zostanie wykorzystana. UNIE czyli ZMIENNE WARIANTOWE. Unie to specyficzne struktury, w których pola pamięci przeznaczone na objekty różnego typu nakładają się. Jeśli jakaś zmienna może być reprezentowana na kilka sposobów (wariantów) to sensowne jest przydzielenie jej nie struktury a unii. W danej chwili pole pamięci należące do unii może zawierać TYLKO JEDEN WARIANT. W przykładzie - albo cyfrę (która znakowo jest widziana jako znak ASCII o kodzie 2,3,4 itd.) albo napis. Do zadeklarowania unii służy słowo kluczowe union. [P041.CPP] #include "string.h" #include "stdio.h" int BUFOR, i; int main() { union { int Cyfra; char Napis[20]; } Unia; for (i=1; i<11; i++) { printf("\n Podaj liczbe jednocyfrowa: "); scanf("%d", &BUFOR); if (BUFOR<0 || BUFOR>9) strcpy(Unia.Napis, "TO NIE CYFRA !"); else Unia.Cyfra = BUFOR; printf("\n Pole jako Cyfra Pole jako Napis \n"); /* Tu wyswietlimy warianty: Pole jako cyfra i jako napis*/ /* Petla pozwoli Ci przeanalizowac wszystkie cyfry 0...9 */ printf(" %d\t\t\t%s", Unia.Cyfra, Unia.Napis); } return 0; } Pętla w przykładzie nie ma znaczenia. Służy tylko dla Twojej wygody - dzięki niej nie musisz uruchamiać programu przykładowego wielokrotnie. Podobnie zmienne BUFOR oraz i mają znaczenie pomocnicze. Zwróć uwagę, że nieprawidłowa interpretacja zawartości pola unii może spowodować wadliwe działanie programu. [Z] ________________________________________________________________ 1. W programie przykładowym zamień unię na strukturę. Porównaj działanie. 2 Przydziel na Wiek w strukturze Facet o jeden bit mniej. Ile lat może teraz mieć Facet ? 3. Zmodyfikuj program przykładowy tak, by napis o liczbie mężów/żon zależał od płci - pola Sex. 4. Zamieniwszy unię na strukturę w programie, sprawdź, czy wpływa to na wielkość pliku *.EXE. ________________________________________________________________ OPERACJE LOGICZNE. Zaczniemy od operacji logicznych na pojedynczych bitach liczb całkowitych. W C++ mamy do dyspozycji następujące operatory: ~€€€€Zaprzeczenie (NOT) ~0=1; ~1=0; |€€€€Suma (OR) 0|0=0; 0|1=1; 1|0=1; 1|1=1; &€€€€Iloczyn (AND) 0&0=0; 0&1=0; 1&0=0; 1&1=1; ^€€€€Alternatywa wyłączna ALBO...ALBO (XOR) €€€€€0^0=0; 0^1=1; 1^0=1; 1^1=0; <<€€€Przesunięcie bitów w lewo (Shift Left) €€€€€<< 00001000 = 00010000 dzieś. 8<<1=16 >>€€€Przesunięcie bitów w prawo (Shift Right) €€€€€>> 00001000 = 00000100 dzieś. 8>>2=2 Miło byłoby pooglądać to trochę dokładniej w przykładowych programach, ale potrzebne nam do tego będą funkcje. Zajmijmy się więc uważniej funkcjami.