Skocz do zawartości

Saurons

Members
  • Postów

    28
  • Rejestracja

  • Ostatnia wizyta

Ostatnie wizyty

Blok z ostatnimi odwiedzającymi dany profil jest wyłączony i nie jest wyświetlany użytkownikom.

Osiągnięcia Saurons

  1. W tym artykule dowiesz sie jak bawic sie kolorami wykorzystujac do tego nasza starenka VGA. Co prawda pewnie nikt z was nie ma juz prawdziwej VGA, ale chyba kazda z tych dzisiejszych super-mega-rakiet potrafi VGA emulowac (np. tryb 13h). Na poczatek troche teorii. Obraz na ekranie jest rysowany tak (w duuuzym uproszczeniu oczywiscie), ze karta bierze sobie zawartosc kolejnej komorki pamieci z segmentu 0a000h i znajduje odpowiedni dla tej zawartosci kolor z palety. Jezeli w komorce pierwszej bedzie np. 36, to na monitorze zobaczymy w tym miejscu kropke w kolorze jaki jest zdefiniowany na 36 miejscu w palecie. Kolor definiujemy przez okreslenie nasycenia 3 barwami - czerwona, zielona i niebieska. (jak sie przyblizysz odpowiednio blisko do monitora (albo lepiej TV) to na pewno zobaczysz takie male kolorowe kropeczki - wlasnie w tych kolorach). W trybie 13h mozemy ustalic wartosci RGB na 0-63. Przy ustawieniu 0,0,0 mamy kolor idealnie czarny, przy 63,63,63 - idealna biel. Color najbardziej czerwony to 63,0,0. Kolor zolty to mieszanka zielonego i czerwonego - czyli np. 50,50,0 - im mniejsze beda wartosci tym bardziej bedzie on przypominal brazowy. Mozesz sie tak bawic tymi kombinacjami caly dzien :-) A jak definiuje sie kolor w palecie za pomoca assemblera? Bardzo prosto :-) Musimy tylko wyslac do portu 03c8h numer koloru, ktory chcemy zdefiniowac, a nastepnie do 03c9h po kolei trzy wartosci - R, G i B. Aby wyslac cos do portu uzywamy instrukcji: gdzie dx to numer portu a al wartosc tam wysylana Aby ustawic kolor tla na niebieski trzeba wykonac nastepujacy kod: Jest jeszcze jedna rzecz ktora powinienes wiedziec. Po ustawieniu koloru n - karta jest gotowa do przyjmowania ustawien koloru n+1. Nie musimy wiec ciagle wpisywac numeru koloru do 03c8h. Wystarczy ciagle pisac do 03c9h.
  2. Stworzymy prostą procedurę by zrozumieć na czym polega jej działanie: procedure Rysuj(x1, y1, x2, y2: Integer; kolor: TColor); begin Form1.Canvas.Brush.Color:=kolor; Form1.Canvas.Rectangle(x1, y1, x2, y2); end; powyższa procedura rysuje kwadrat o podanym kolorze i w określonym obszarze. Aby wykorzystać procedurę Rysuj potrzebny nam będzie przycisk. Wstaw go na formę i w zdarzeniu OnClick wpisz: rysuj(10, 10, 50, 50, clGreen); Uruchom program i kliknij na przycisku. Zapewne zauważyłeś, że na formie został narysowany zielony kwadrat. Spytasz: "I po to są procedury". To jest tylko przykład. Rozpatrzmy taki przypadek: narysuj 100 kwadratów o różnych kolorach i w różnych miejscach. Kod procedury Rysuj zajmuje 2 linijki, a jej wykonanie tylko 1. A co gdyby procedura miała wykonać operacje mieszczące się w 100 linjikach kodu? Jak widać rozbudowany kod możemy sprowadzić do jednej linijki i wykorzystywać go w kazdym miejscu programu. Podobne do procedur są funkcje. Różnią się tym że deklarujemy jej wynik, tzn. czy ma to być tekst, liczba itp. Najlepiej coś wyjaśnić na przykładzie: function CzyZero(l1, l2: integer) : string; begin if l1-l2=0 then Result:='Wynikiem jest zero' else Result:='Wynikiem nie jest zero'; end; Teraz wstaw na formę Button i dwa Labele. Pod zdarzenie OnClick przycisku wpisz: Label1.Caption := CzyZero(10,234); Label2.Caption := CzyZero(10,10); Jak widać nasza funkcja działa poprawnie. Zwraca ona wyniku odejmowania tylko odpowiedni tekst. Wynik wykonania funkcji podajemy po słowie Result:=. Teraz zajmiemy się modułem. Jest to plik, który zawiera procedury i funkcje. Stworzymy moduł, który zawiera powyższe przykłady. Zamknij wszystkie projekty i utwórz nowy unit (File -> New -> Unit). Skasuj jego zawartość, wklej poniższy kod i przyjżyj się mu: unit test; interface { informujemy moduł, że korzysta on z grafiki } uses Graphics; { informujem moduł jakie zawiera funkcje i procedury } function CzyZero(l1, l2: integer) : string; procedure Rysuj(can: TCanvas; x1, y1, x2, y2: Integer; kolor: TColor); implementation { nasza funkcja} function CzyZero(l1, l2: integer) : string; begin if l1-l2=0 then Result:='Wynikiem jest zero' else Result:='Wynikiem nie jest zero'; end; { nasza procedura. Została lekko zmieniona (can: TCanvas). Jako że nie mamy formy, musimy poinformować procedurę na czym ma rysować kwadrat. O Canvasie można poczytać w rozdziale 14} procedure Rysuj(can: TCanvas; x1, y1, x2, y2: Integer; kolor: TColor); begin can.Brush.Color:=kolor; can.Rectangle(x1, y1, x2, y2); end; end. Zapisz go do jakiegoś katalogu (np c: est) jako "test". Teraz utwórz nową aplikację (File -> New -> Application) i zapisz ją do tego samego folderu . Na formę wstaw Button i Label. Do deklaracji "uses" programu dodaj test. Teraz w zdarzeniu OnClick przycisku wpisz: rysuj(Form1.Canvas, 10, 10, 50, 50, clMaroon); Label1.Caption := CzyZero(100, 50); Jeżeli wwszystko wykonałeś prawidłowo to ujżysz taki sam efekt jak przy wykonaniu procedury Rysuj i funkcji CzyZero w podanych wcześniej przykładach. Jeżeli mamy wiele funkcji i procedur, z których korzystamy w naszym programie możemy utworzyć moduł. Po co? Zauważ, że kod programu staje się bardziej przejrzysty i estetyczny.
  3. Cel Nasz program ma przypominać budzik. Po wpisaniu godziny program sprawdza czy jeszcze nie nadeszła. W przypadku jeśli taka sytuacja zaistniała wyświetla komunikat. Potrzebne komponenty : Metoda 1) Wstawiamy komponenty, wypisane w powyższej tabeli i zmieniamy im właściwość name na taką jaka jest w kolumnie "Nazwa" 2) Do obsługi funkcji OnTimer komponetu TTimer dodajemy kod: 4) Uruchamiamy program. Kod źródłowy
  4. Okna komunikatów służą do wyświetlania informacji oraz pobierania danych od użytkownika. Delphi posiada sześć rodzajów komunikatów, ale zajmiemy się tylko najczęściej używanymi. ShowMessage Funkcja wyświetla proste okno. Jego tytuł zawiera nazwę naszego programu i przycisk 'OK'. ShowMessage('To jest mój komunikat'); ShowMessagePos Jest to funkcja podobna do ShowMessage. Posiada ona dwa dodatkowe parametry, które służą do określenia pozycji okna. ShowMessagePos('To jest mój komunikat', 10, 10); MessageBox Jest to najlepsze okno komunikatów. Pozwala na zdefiniowanie rodzajów przycisków, przycików itp. Funkcja wywołująca ma taką postać: Application.MessageBox('Tekst Okna', 'Tytuł Okna', Przyciski+Rysunek); Mały przykładzik: Application.MessageBox('To jest moje okienko', 'Informacja', MB_OK + MB_ICONInformation); Poniżej przedstawiona jest lista dostępnych kombinacji przycisków: MB_ABORTRETRYIGNORE - okno z przyciskami 'Przerwij', 'Ponów próbę', 'Zignoruj'. MB_OK - okno z przyciskiem 'OK'. MB_OKCANCEL - okno z przyciskami 'OK', 'Anuluj'. MB_RETRYCANCEL - okno z przyciskami 'Ponów próbę', 'Anuluj'. MB_YESNO - okno z przyciskami 'Tak', 'Nie'. MB_YESNOCANCEL - okno z przyciskami 'Tak', 'Nie', 'Anuluj'. Możemy także określić typ obrazka w oknie: MB_ICONWarning - żółty trójkąt z wykrzyknikiem. MB_ICONError - czerwone koło z iksem. MB_ICONInformation - biały dymek z literą i. MB_ICONQuestion - biały dymek ze znakiem zapytania. Funkcja MessageBox zwraca wynik. To znaczy, że po kliknięciu na przycisku nasz program jest informowany, który z nich wybrał użytkownik. Mały przykładzik (potrzebny jest Button i Label): W zdarzeniu OnClick Buttona: case Application.MessageBox('Który klawisz wybierzesz?', 'Pytanie', MB_YESNO+MB_IconQuestion) of IDYES : Label1.Caption:='Wybrałeś TAK'; IDNO : Label1.Caption:='Wybrałeś NIE'; end; Wynikiem może być także liczba. Poniżej przedstawione są możliwe wyniki (w nawiasach podane są wyniki w postaci liczbowej): IDOK (1) wybrano przycisk 'OK'. IDCANCEL (2) wybrano przycisk 'Anuluj'. IDABORT (3) wybrano przycisk 'Przerwij'. IDRETRY (4) wybrano przycisk 'Ponów próbę'. IDIGNORE (5) wybrano przycisk 'Zignoruj'; IDYES (6) wybrano przycisk 'Tak'. IDNO (7) wybrano przycisk 'Nie'.
  5. Cel Chcemy uzyskać efekt, aby po wpisaniu liczb w oba pola i wciśnięciu przycisku została wyświetlona suma tych liczb.BR> Potrzebne komponenty : Nazwa Klasa Edit1 TEdit Edit2 TEdit Label1 TLabel Label2 TLabel Label3 TLabel Button1 TButton Metoda 1) Wstawiamy komponenty, wypisane w powyższej tabeli i zmieniamy im właściwość name na taką jaka jest w kolumnie "Nazwa" 2) Zmieniamy właściwości Caption lub Text według własnego uznania lub sugerując się rysunkiem 3) Do obsługi funkcji OnClick przycisku dodajemy poniższy kod: 4) Uruchamiamy program. Porady Zmieniając znak działania na +, -, /, * zmienia się sposób wykonywania działania. Kod źródłowy Masz problem? napisz !
  6. Ten program też jest prościutki, ale potrafi już coś zrobić. Wyświetla na ekranie tekst i czeka na naciśnięcie klawisza. .model tiny .code tekst db 'To jest chyba tekst$' start: mov ax,@data mov ds,ax mov ah,09h mov dx,offset tekst int 21h mov ah,01h int 21h mov ah,4ch int 21h .stack 512 end start Jak widzisz początek programu jest podobny - określamy model i zaczynamy segment kodu. Jednak zaraz potem deklarujemy zmienną. Jest to łancuch znaków, czyli bajtów. Definicja zmiennej ma postać: identyfikator typ wartość . Deklarujemy więc zmienną tekst, składającą się z bajtów i przyjmującą wartość 'To jest chyba tekst$'. Inne typy zmiennych to dw - word (słowo = 2 bajty), dd - double word (podwójne słowo = 4 bajty), jest jeszcze dq - czyli aż 10 bajtów. Jeżeli chcemy aby zmienna przymowała wartości rzeczywiste a nie całkowite (i tym samym aby działać na niej za pomocą koprocesora) - musi to być co najmniej dd. Pierwsze dwa rozkazy programu wpisują do ds numer segmentu @data. Pamiętasz chyba, że w modelu tiny segment danych jest jednocześnie segmentem kodu. Tak więc po prostu wpisujemy ten jedyny segment do ds, aby procesor miał dostęp do zmiennych znajdujących się w tym segmencie. Następne trzy rozkazy to wywołanie funkcji 09h ms-dosu. Ta funkcja służy do wysłania na ekran ciągu znaków zakończonego znakiem dolara '$'. Adres ciągu znaków znajduje się w parze rejestrów ds:dx. Do ds wysłaliśmy już odpowiednią wartość - do dx trzeba jeszcze tylko podesłać offset naszej zmiennej. Funkcja 01h ms-dosu czeka na klawisz. Tak jak w poprzednim programie - musisz myśleć o tym, by procesor się nie pogubił w odmętach pamięci. Jeżeli nie zakończysz łancucha znaków znakiem '$' to ms-dos będzie wysyłał na ekran wszystko aż do napotkania takiego znaku. Tym razem nie grozi ci np. formatowanie twardego dysku ale za to będziesz miał mnóstwo śmieci na ekranie.
  7. W tym artykule przedstawię najprostszy możliwy program, który tak właściwie nie będzie robić nic, poza zajmowaniem pamięci i miejsca na dysku :-) Nawet taki program trzeba jednak napisać i wcale nie zajmie on 0 bajtów... dzięki niemu poznasz szkielet programu assemblerowego i łatwiej ci będzie zrozumieć kolejne przykłady. Program "nie rób nic" wygląda tak: .model tiny .code start: mov ah,4ch int 21h .stack 512 end start Jak widzisz - na początku określany jest model programu. Do wyboru jest kilka, między innymi tiny, small, medium, huge... model tiny charakteryzuje się tym, że cały program wraz z danymi musi zmieścić się w jednym segmencie - czyli (w trybie rzeczywistym) - 64Kb. Nie przejmuj się tym na razie :-) W drugiej linijce dyrektywa .code rozpoczyna segment kodu. Jest to zarazem segment danych (ponieważ używamy modelu tiny). Możemy więc spokojnie od tego miejsca umieszczać zarówno zmienne jak i kod programu. Następnie widzimy etykietę start. Pod koniec programu określamy od którego miejsca ma zacząć się wykonanie - to znaczy skąd ma zacząć pobierać rozkazy procesor na początku wykonywania naszego programu - wybrana została właśnie etykieta start. Następne dwie linijki to rozkazy procesora . Jak widzisz są aż dwa. Pierwszy to mov a drugi - int. Przygotuj się teraz - bo poznasz pierwsze rozkazy swojego procesora. Rozkaz mov a,b wysyła b do a. Zarówno a jak i b mogą być miejscem w pamięci bądź też rejestrem - z jednym zastrzeżeniem: nie można przesyłać danych wprost z pamięci do pamięci. Drugi z tych rozkazów int x wywołuje przerwanie o numerze x. Co oznacza więc ten kod? Do rejestru ah wpisywana jest wartość 4ch, a następnie wywoływane jest przerwanie 21h. W zaufaniu powiem ci, że 21h to przerwanie ms-dosu. Służy one do korzystania z funkcji systemu operacyjnego. Numer funkcji podajemy w ah, a funkcja 4ch oznacza zakończenie programu. Przed zakończeniem programu umieściłem jeszcze dyrektywę .stack. Nie jest ona tu niezbędna. Mówi o tym, ile miejsca zostanie przeznaczone na stos. W tym programie stosu nie używamy, ale w każdym prawdziwym programie stosu używa się bardzo często. 512 bajtów wystarcza dla małych programików, jednak gdy korzystamy np. z rekurencji stos kończy się bardzo szybko - a w assemblerze nie otrzymamy miłego komunikatu w okienku - zostanie po prostu zamazana pamięć - nie wiadomo jakie mogą być tego skutki. Być może zostaną wykonane losowe rozkazy. A czym to grozi chyba nie muszę tłumaczyć :-) Pozostało jeszcze wytłumaczyć po co są właściwie te dwie instrukcje w programie. Może wystarczyłoby nie umieszczać żadnych instrukcji? Otóż wtedy procesor skoczyłby do etykiety start i zaczął wykonywać to, co by się tam znajdywało... a chyba nikt nie jest w stanie przewidzieć co by się tam mogło znaleźć... W najlepszym przypadku program się po prostu zawiesi. Jak skompilować ten program? Jeżeli korzystasz z Turbo Assemblera firmy Borland, musisz najpierw skompilować plik źródłowy tasm first.asm a potem to skonsolidować tlink first.obj . W wyniku otrzymasz plik first.exe, który jest gotowy do uruchomienia.
  8. Co to takiego te tablice? Wyobraź sobie komponent StringGrid (wiersze i kolumny), w którym wpisane są różne liczby. W każdej chwili możesz odczytać zawartość danej komórki (np. [5,5]). Tablica to coś podobnego, jest to zmienna zawierająca zdefiniowaną ilość zmiennych liczbowych, tekstowych itp. Najczęściej używanymi są tablice jedno- i dwuwymiarowe. Tablica jednowymiarowa to nic innego jak ciąg zmiennych (np: 1,2,3,4,5,6..n), natomiast tablica dwuwymiarowa wyglądem przypomina wspomniany już komponent StringGrid lub okno M$ Excel. Przejdziemy do przykładu, który powinien wam wszystko uporządkować: var tab: array[1...10] of integer; x: integer; begin for x:=1 to 10 do begin tab[x]:=x; end; Label1.Caption:=IntToStr(tab[4]); end; Czas na tłumaczenie deklarujemy zmienną tab, która jest tablicą mogącą przechowywać 10 liczb całkowitych. Oczywiście możemy zadeklarować tablicę mogącą przechowywać mniej lub więcej danych. Następnie wypełniamy każdy element tablicy liczbami. Na koniec wyświetlamy na komponencie Label1 wartość czwartej komórki naszej tabeli. Teraz przykład tablicy dwuwymiarowej: var tab: array[1...10, 1...10] of Integer; x,y: Integer; begin for x:=1 to 10 do begin for y:=1 to 10 do begin tab[y,x]:=x; end; end; Label1.Caption:=IntToStr(tab[5,5]); end; Deklarujemy zmienną tab, podobnie jak w poprzednim przykładzie. Zapewne zauważyłeś że zawartość kwadratowego nawiasu trochę się różni od tego w tablicy jednowymiarowej. Jak sama nazwa mówi mamy dwa wymiary czyli oś x (w poziomie) i y (w pionie). Inaczej też odwołujemy się do zawartości danej komórki takiej tablicy, musimy podać pozycję komórki y i x. Zapewne zadajesz sobie teraz pytanie po co komu tablice? Wbrew pozorom są one bardzo przydatne. Często wykorzystuje się je w grach komputerowych do opisywania świata gry. Np. ich zawartość stanowią liczby 1 i 0, gdzie 0 to wolna droga (gracz może iść) a 1 to przeszkoda (gracz nie ma przejścia). Jak zapewne zauważyłeś(aś) (pozdro dla uczącej się Ani ) zawsze deklarujemy rozmiar tabeli (kwadratowe nawiasy). Począwszy od Delphi 4 istnieje takie coś, jak tablica dynamiczna. Podając zmienną tablicy nie musimy podawać jej rozmiarów, robimy to dopiero w dalszej części programu korzystając z funkcji SetLength, np.: var tab: array of integer; begin SetLength(tab, 10); end; Deklarujemy tablice, a następnie przypisujemy jej wielkość 11 elementów (komórki liczymy od 0). To wszystko, jeżeli nadal uważacie że jest to niepotrzebny wynalazek to ściągnijcie sobie z DA źródła moich gierek (Wyciek, Gąsienica itp.), które oparte są właśnie na tablicach.
  9. Wyjątki to mechanizmy umożliwiające aplikacji powrót do normalnego działania po wystąpieniu błędu. Jest to rzecz prosta więc przejdziemy do przykładu: var plik: TextFile; begin AssignFile(plik, 'plik.abc'); try reset(plik); try write('tekst'); finally CloseFile(plik); end; except ShowMessage('Błąd wejścia/wyjścia'); end; end; W konstrukcji "try...finally" wpierw wykonywane są operacje zawarte pomiędzy klauzulami try i finally. Po ich zakończeniu wykonywane są zadania z klauzuli finally i end, wykonywane są niezależnie od tego jaki był skutek wykonania pierwszej klauzuli. Klauzula except i end służy do obsłużenia wyjątku. Rzecz jest prosta dlatego nie będę się tu rozpisywał.
  10. Operatory to symbole służące do manipulowania danymi. Zajmiemy się pięcioma rodzajami operatorów. Operator przypisania (:=) służy do przypisania zmiennej wartości np.: liczba := 5; Instrukcja przypisuje zmiennej liczba wartość 5. Operatory porównania (=, <>, <, >, <=, >=) Służą do stwierdzenia równości, nierówności lub porównania pod względem relacji mniejszości dwóch wartości. Mówiąc po ludzku sprawdzają czy np. liczby są takie same, różne itp.; np.: if x>2 then Label1.Caption:='zmienna x jest większa od 2'; Operatory logiczne (and, or, not) Realizują operacje wynikające z algebry Boole'a. Inaczej mówiąc testują kilka warunków, np.: if (a=1) and (b=3) then Label1.Caption:='Coś'; - jeżeli a=1 i b=3 to... Poniższa tabela przedstawia poznane operatory: TYP ZNAK Przypisanie := Równe = Nierówne <> Mniejsze < Mniejsze-równe <= Większe-równe >= Logiczne "i" and Logiczne "lub" or Zaprzeczenie not Operatory arytmetyczne służą do dodawania, odejmowania itd. itp. np.: x:=5+5; Operatory zmniejszenia lub zwiększenia powodują zwiększenie lub zmniejszenie wartości liczbowej, np.: x:=5; Inc(x) - w wyniku otzrymamy x:=6; lub Inc(x,5) - zwiększamy x o 5, w wyniku otrzymamy x:=10; Czas na tabelkę: TYP ZNAK Dodawanie + Odejmowanie - Mnożenie * Dzielenie rzeczywiste / Dzielenie całkowite div Reszta z dzielenia mod Zwiększenie Inc() Zmniejszenie Dec()
  11. Asembler (ang. assemble – składać) to program dokonujący tłumaczenia języka asemblera na język maszynowy, czyli tzw. asemblacji. Jest to swoisty odpowiednik kompilacji dla języków wyższych poziomów. Program tworzony w innych językach programowania niż asembler jest zwykle kompilowany do języka maszynowego (wyniku pracy asemblera), a następnie zamieniany na kod binarny przez program asemblera. Powtarzające się często schematy programistyczne oraz wstawiane fragmenty kodu doprowadziły do powstania tzw. makroasemblerów, które rozszerzają asemblery o obsługę makr przed właściwą asemblacją, co zbliża je nieco do pierwszych wersji języka C. Do najpopularniejszych odmian języka asemblera, ze względu na popularność architektury Intela znanej pod nazwą x86, zaliczyć można Asembler x86. Do najpopularniejszych asemblerów zalicza się NASM, TASM oraz MASM, jak również FASM i GASM. Kod [edytuj] Przykładowe polecenia (mnemoniki) w języku Asembler x86: mov ax, 0D625h mov es, ax ; wprowadź do rejestru segmentowego ES wartość z AX wynoszącą D625 szesnastkowo (54821 dziesiętnie) mov al, 24 mov ah, 0 ; załaduj do Ten akurat temat nie jest mój Reszta moja Pozdrawiam Saurons
  12. Konstruktory W poprzedniej lekcji musieliśmy utworzyć obiekt kowbojskie klasy PaczkaZapalek przed funkcją main, bo tylko obiekty globalne są zerowane przez kompilator na początku programu. Obiekty utworzone lokalnie [np. wewnątrz funkcji main] nie są zerowane i zawierają przypadkowe wartości. Czy oznacza to, że wszystkie składniki takich obiektów trzeba sobie za każdym razem poustawiać ręcznie? No więc, na szczęście nie ;-). Z pomocą przychodzą nam właśnie konstruktory, które poznasz w tym odcinku kursu. Co to jest konstruktor? Konstruktor to specjalna funkcja składowa w klasie, którą kompilator wywołuje automatycznie przy tworzeniu każdego obiektu tej klasy. Najważniejszym przeznaczeniem konstruktora jest... uwaga uwaga... nadawanie składnikom obiektu początkowych wartości! :-) Gdy umieścisz w klasie konstruktor, nie będziesz już musieć ręcznie ustawiać składników każdego tworzonego obiektu, bo kompilator zrobi to za ciebie automagicznie! ;-D I tu ważna uwaga! Wbrew temu, co myślą niektórzy, konstruktor nie tworzy obiektu w pamięci. Rezerwacja pamięci dla obiektu to przecież działka kompilatora. Konstruktor wkracza do akcji już po przydzieleniu pamięci dla obiektu, i jedynie nadaje jego składnikom początkową wartość. Jest czymś w rodzaju "dekoratora wnętrz" ;-). Pewnie już nie możesz się doczekać, kiedy powiem ci jak umieścić w klasie taką specjalną funkcję ;-J. Już tłumaczę. Funkcja taka wyróżnia się tym, że nazywa się tak samo jak klasa, oraz nie ma typu zwracanej wartości. I wcale nie chodzi mi o to, że typem tym jest void. Konstruktor wogóle nie ma typu! Zresztą najlepiej wyjaśni to poniższy przykład: class PaczkaZapalek { private: int ile_zapalek; public: PaczkaZapalek(); //Konstruktor !!!!! void Dodaj(int ile); void Wyjmij(int ile); int Pokaz() { return ile_zapalek; } }; Umieściliśmy w klasie zapowiedź konstruktora. Jego zadaniem będzie wypełnienie pudełka po brzegi, bo na co komu puste pudełko. Teraz kolej na napisanie definicji tego konstruktora, czyli co on ma robić: PaczkaZapalek::PaczkaZapalek() { ile_zapalek = 40; } Od tej chwili gdziekolwiek byśmy nie utworzyli obiektu klasy PaczkaZapalek, dostaniemy pudełko zawierające równo 40 zapałek :-). Taki konstruktor programiści nazywają domyślnym, bo nie musimy mu mówić, jakie wartości mają mieć składniki obiektu. Jako dowód możesz sprawdzić, co zrobi poniższy kod umieszczony w funkcji main: PaczkaZapalek kowbojskie; //Tutaj po cichu zadziała konstruktor cout << "W paczce jest " << kowbojskie.Pokaz(); cout << " zapalek kowbojskich" << endl; Konstruktory z parametrami Czasami chcielibyśmy dostać pudełko z inną początkową ilością zapałek. Do tego także przydaje się konstruktor i zaraz dopiszemy sobie taki w naszej klasie. class PaczkaZapalek { private: int ile_zapalek; public: PaczkaZapalek(); //Konstruktor domyślny PaczkaZapalek(int pocz); //Konstruktor z parametrem void Dodaj(int ile); void Wyjmij(int ile); int Pokaz() { return ile_zapalek; } }; Nie, nie widzisz podwójnie ;-). Tam naprawdę są dwa konstruktory. Konstruktor, podobnie jak każda inna funkcja, może być przeładowywany. Kompilator sam się domyśli, którego z nich użyć. Jeśli użyjesz znanego ci już zapisu: PaczkaZapalek firmowa; to wiadomo, ruszy do pracy konstruktor domyśly. A jak zrobić, żeby zadziałał ten drugi? Przypomnij sobie, w jaki sposób inicjalizowało się zwykłą zmienną typu int: int liczba = 13; Tak się tworzyło liczbę i od razu nadawało się jej wartość początkową [inicjowało]. Powiedziałem nieco wcześniej, że konstruktor służy do nadawania wartości początkowej składnikom obiektu. Dlatego taki sam zapis zadziała dla naszego pudełka zapałek. PaczkaZapalek czeskie = 38; Wbrew pozorom to nie jest przypisanie. Przecież obiekt czeskie jest dopiero tworzony! Jeśli pamiętasz jeszcze, co pisałem o inicjalizacji w odcinku Zmienne i typy, ten znak równości na pewno cię nie zmylił ;-). Tak, to jest inicjalizacja. Dlatego rusza do pracy konstruktor klasy PaczkaZapalek. Jeśli nie bardzo to widać, możesz także jawnie go wywołać, stosując taki zapis: PaczkaZapalek czeskie = PaczkaZapalek(38); Teraz lepiej widać tam ten konstruktor i w jaki sposób dostaje on tą liczbę jako parametr. Dla kompilatora jednak nie ma to znaczenia. Nie musisz wywoływać konstruktora jawnie, bo i tak zostanie on wywołany. Możesz nawet napisać tak: PaczkaZapalek czeskie(38); Ten właśnie zapis jest najczęściej stosowany, bo jest najbardziej wygodny i pozwala na podawanie konstruktorowi większej ilości parametrów w nawiasie [oddzielonych przecinkami]. Zastanówmy się teraz, jak mogłoby wyglądać ciało naszego drugiego konstruktora. Najprościej pewnie jakoś tak: PaczkaZapalek::PaczkaZapalek(int pocz) { ile_zapalek = pocz; } Takie coś jednak pozwala ustawić początkowo dowolną ilość zapałek, nawet więcej niż się zmieści ;-P. Trzeba dorzucić jakieś sprawdzanie, czy parametr nie przekracza pojemności pudełka. Niepełnosprytny programista pewnie wsadziłby tam jakąś instrukcję if itp., ale po co? Czy nie lepiej wykorzystać to, co już mamy? :-) Looknij se: PaczkaZapalek::PaczkaZapalek(int pocz) { ile_zapalek = 0; Dodaj(pocz); } Najpierw zerujemy składnik ile_zapalek, a następnie wykorzystujemy funkcję Dodaj, która już potrafi sprawdzić czy nie wsadzamy do pudełka więcej, niż się da. Sprytnie, co nie? ;-) A dla cfaniaków mam jeszcze jeden "wałek": można zupełnie wywalić konstruktor domyślny robiąc taki oto przekręt: class PaczkaZapalek { private: int ile_zapalek; public: PaczkaZapalek(int pocz=40); //Konstruktor z parametrem domyśnym void Dodaj(int ile); void Wyjmij(int ile); int Pokaz() { return ile_zapalek; } }; Konstruktor domyślny to przecież taki, który można wywołać bez parametrów. Powyższy konstruktor da się tak wywołać dzięki parametrowi z domyślną wartością. Gdy przy tworzeniu obiektu nie określimy początkowej ilości zapałek, domyślnie w pudełku będzie ich 40. Co jeszcze potrafi konstruktor? To jeszcze nie wszystkie zalety stosowania konstruktorów. Drugim ważnym zastosowaniem jest konwersja, czyli zamiana. Jak pewnie pamiętasz, w naszej klasie PaczkaZapalek umieściliśmy konstruktor pobierający typ int. Od tamtej chwili obiekty naszej klasy mogą być budowane na podstawie liczb typu int. Gdy będziemy chcieli utworzyć nasz obiekt tak: PaczkaZapalek ruskie(36); to konstruktor stworzy obiekt ruskie posługując się tą liczbą. Mówiąc w języku programistów, obiekt klasy int zostanie skonwertowany na postać obiektu naszej klasy. Jeśli w swojej klasie umieścisz konstruktor pobierający obiekt jakiejś innej klasy, to od tej chwili możesz budować obiekty swojej klasy na podstawie obiektów tej innej klasy. Zrób teraz mały eksperyment - wykomentuj zapowiedź i ciało konstruktora i spróbuj skompilować program z taką instrukcją w funkcji main: PaczkaZapalek nowa = 45; Kompilator wywali błąd, bo nie będzie wiedział, jak zamienić obiekt klasy int na obiekt twojej klasy. Żeby taki zapis był możliwy, musi istnieć konstruktor pobierający int.
  13. Klasy i obiekty Czym się różni wróbel? Dotychczas programowaliśmy w stylu strukturalnym [proceduralnym], a więc rozbijaliśmy problem na mniejsze problemy, które łatwiej jest rozwiązać. Program opierał się na funkcjach, które operowały na strukturach danych, jednak dane i operacje na nich wykonywane nie były ze sobą w żaden sposób powiązane. Na przykład nic nie stawało na przeszkodzie, by do funkcji liczącej sumę liczb typu int wrzucić pojemność cysterny i temperaturę powietrza. Ten sposób programowania okazał się mało wygodny i podatny na błędy, dlatego wymyślono inny [moim zdaniem dużo lepszy ;-)] styl programowania, zwany programowaniem obiektowym [ang. OOP - Object Oriented Programming]. Programowanie obiektowe jest wygodniejsze i bardziej dla nas naturalne, bo pozwala nam na modelowanie obiektów i ich zachowań znanych nam ze świata rzeczywistego. Jeśli na przykład piszemy edytor tekstu, to chcielibyśmy jakoś zamodelować w programie kartkę papieru. Pisząc grę chcielibyśmy mieć model ekranu, bitmap, obiektów gry itp. W programowaniu obiektowym funkcje [zwane w OOP metodami] zostały powiązane w jedną całość z danymi [zwanymi właściwościami] na których te funkcje operują, dzięki czemu takie "agregaty" łatwiej odpowiadają rzeczywistym obiektom. Trudniej jest się też pomylić i wysłać do funkcji nieodpowiednie parametry. Inną zaletą programowania obiektowego jest hermetyzacja informacji. Oznacza to tyle, że dane, które nie powinny być widoczne na zewnątrz modelowanego obiektu, są w nim ukrywane. Gdy weźmiesz odbiornik radiowy to na zewnątrz jego obudowy wyprowadzone są tylko te pokrętła, które są potrzebne. Reszta jest ukryta wewnątrz żeby nie dało się przypadkowo rozregulować radia kręcąc byle czym. Zresztą gdybyśmy dostali radio bez obudowy, to zajęło by nam dużo czasu zanim połapalibyśmy się, czym pokręcić żeby podgłośnić. Ukrywanie danych w programowaniu obiektowym ułatwia więc korzystanie z obiektów [bo ukrywa ich wewnętrzne "trybiki"] oraz zwiększa bezpieczeństwo [bo nie pozwala na majstrowanie :-)]. Ostatnią już chyba zaletą OOP jest polimorfizm. Polega on na tym, że kod jakby sam przystosowuje się do tego, na jakich obiektach przyszło mu pracować. Czyli mówiąc prościej orientuje się według obiektów. Godne uwagi jest szczególnie to, że program praktycznie bez żadnych zmian w kodzie jest już przygotowany do obsługi nawet takich obiektów, których jeszcze nie wymyślono! :-o Jest to chyba najważniejsza zaleta programowania obiektowego. No, dość już tej teorii, przechodzimy do praktyki. Za chwilę dowiesz się, jak można zastosować ten nowy styl programowania do języka C++. Jak w tytule Programowanie obiektowe ma ułatwiać modelowanie rzeczywistych obiektów. Zamodelujmy więc sobie jakiś taki obiekt, na przykład.. pudełko zapałek :-). Przeciętne pudełko zapałek zawiera około 40 zapałek. Możemy wyjmować zapałki dotąd, aż nie zostanie ani jedna. Dalsze wyjmowanie nie ma sensu [no bo niby co mamy wyjmować? ;-P]. Nasz program powinien o tym wiedzieć i nie pozwolić na dalsze wyjmowanie. Podobnie z wkładaniem zapałek - zmieści się tylko 40 i ani jednej więcej. W stylu proceduralnym robiło się zmienną globalną przechowującą ilość zapałek i zestaw funkcji do wyjmowania i wkładania. Jednak mimo iż funkcje takie będą "idiotoodporne", to nic nie stoi na przeszkodzie, by "na chama" zmienić tą globalną zmienną na przykład na 1000 i cały program jest do wykopania :-(. Na szczęście istnieje programowanie obiektowe, spróbujmy więc go wykorzystać. W języku C++ są jak wiadomo typy wbudowane, takie jak int, char i pozostałe. Ale wśród nich nie ma czegoś takiego jak pudełko zapałek. Na szczęście język ten pozwala na definiowanie i wykorzystywanie własnych typów danych! 8-) Do tego celu służą klasy. Klasa jest jakby projektem technicznym, który określa z czego obiekt się składa, co możemy z nim robić i jak on na to zareaguje. Jest szablonem, według którego można taśmowo budować obiekty. Klasa jako taka nie istnieje w pamięci, jest tylko jakby nowym typem danych, który użytkownik dodał do kompilatora. Dopiero utworzenie obiektu tej klasy powoduje przydzielenie pamięci, tak jak to było przy deklarowaniu zmiennych. Klasa jest podobna do struktury, tylko że posiada pewne dodatkowe własności. Jedną z nich jest możliwość umieszczania w niej nie tylko danych, ale i funkcji [metod], które na tych danych pracują. [Dla ciekawskich: w nowych kompilatorach struktury też to potrafią, ale i tak wszyscy używają klas ;-P]. Zróbmy więc sobie klasę PaczkaZapalek, która będzie modelem naszego pudełka i jednocześnie nowym typem danych. class PaczkaZapalek { public: int ile_zapalek; //Właściwość - ilość zapałek w pudełku void Dodaj(int ile); //Metoda dodaje zapałki do pudełka void Wyjmij(int ile); //Metoda wyjmuje zapałki z pudełka }; Definicja klasy rozpoczyna się od słówka class i znajduje się w nawiasach klamrowych. Zauważ że podobnie jak przy strukturze, całą definicję kończy średnik! Wiele dziwacznych błędów bierze się stąd, że programista zapomina dać tego średnika. Wewnątrz nawiasów klamrowych są składniki, którymi mogą być zmienne i funkcje. Etykietą public zajmiemy się za chwilę. Jak widzisz, obie funkcje działające na pudełku zapałek należą teraz do niego i są jego nieodłączną częścią. Narazie tylko zapowiedzieliśmy kompilatorowi, jakie funkcje należą do obiektów tej klasy, za to nie wspomnieliśmy, jak one działają! Zanim stworzymy pierwszy obiekt tej klasy, musimy napisać definicje tych funkcji. Weźmy na przykład pierwszą z nich: void PaczkaZapalek::Dodaj(int ile) { if (ile <= 0) return; ile_zapalek += ile; if (ile_zapalek > 40) ile_zapalek = 40; //Wyrównanie do 40, gdy za dużo } Przyjrzyj się nagłówkowi funkcji. Widzisz coś nowego? Tak, jest tutaj nazwa klasy, operator zakresu i dopiero nazwa funkcji. A wszystko po to, żeby kompilator wiedział, dla której klasy definiujemy funkcję. Bo przecież może być inna klasa, która ma funkcję składową o takiej samej nazwie i kompilator musi je jakoś rozróżnić. Reszta jest ci już pewnie znana. Ciało funkcji też jest dosyć proste - jeśli dodajemy jakieś zapałki, to zwiększamy odpowiednio wewnętrzną zmienną ile_zapalek. Gdy zapałek jest za dużo, wyrównujemy do 40. A tak wygląda definicja drugiej funkcji: void PaczkaZapalek::Wyjmij(int ile) { if (ile <= 0) return; ile_zapalek -= ile; if (ile_zapalek < 0) ile_zapalek = 0; //Wyrównanie do 0, gdy za mało } No. Mamy już zdefiniowane obie funkcje, możemy już tworzyć obiekty klasy PaczkaZapalek! :-). A robi się to w taki sam sposób, jak przy definiowaniu każdej innej zmiennej typu wbudowanego. Dopisz przed funkcją main poniższą linijkę: PaczkaZapalek kowbojskie; //Globalna paczka zapałek Ponieważ jest to obiekt globalny, kompilator wyzeruje zarezerwowaną dla niego pamięć, więc pudełko na początku będzie zawierać 0 zapałek. Gdybyśmy zadeklarowali ten obiekt wewnątrz funkcji main, mógłby zawierać początkowo przypadkowe dane, a tego chyba nie chcecie [jak mawiał Arnie z "13 posterunku" ;-D]. Mamy obiekt kowbojskie klasy PaczkaZapalek. Jak się do niego dobrać? Podobnie jak do struktury, czyli używając kropki. Na przykład żeby zobaczyć, ile w pudełku jest zapałek, wystarczy sprawdzić jego właściwość ile_zapalek w taki oto sposób: cout << "W pudelku jest " << kowbojskie.ile_zapalek << " zapalek.\n"; Podobnie korzystamy z metod obiektu, czyli jego funkcji składowych. kowbojskie.Dodaj(3); //Dodajemy 3 zapałki cout << "Dodalismy 3 zapalki. Teraz jest "; cout << kowbojskie.ile_zapalek << " zapalek.\n"; Wywoływanie metod obiektu przypomina trochę wydawanie mu poleceń i zadawanie mu pytań. Prawda, że takie podejście jest fajniejsze? ;-) Poeksperymentuj teraz samodzielnie. Spróbuj poczęstować funkcje błędnymi wartościami i zobacz, czy zachowają się inteligentnie. A teraz spróbuj zmienić zawartość właściwości ile_zapalek na jakąś kosmiczną liczbę. Udało się? TAK?! :-o Pewnie myślisz teraz: "A miało być tak pięknie.. Dlaczego obiekt pozwala na majstrowanie przy jego wewnętrznych danych?!" Hmm.. No cóż.. A powiedzieliśmy kompilatorowi, które dane mają być dostępne z zewnątrz, a które nie? Raczej nie bardzo ;-). Lepiej więc czym prędzej to zróbmy. Składniki prywatne i publiczne Każda klasa pozwala nam określić, które składniki będą widoczne z zewnątrz, a które będą prywatnymi właściwościami klasy [takimi, o których nie powinniśmy wiedzieć, żeby czegoś nie popsuć]. Do ustalania dostępu w klasie służą trzy etykietki: private [z ang. prywatny], protected [z ang. chroniony], oraz znane ci już public [z ang. publiczny]. Składniki prywatne są widoczne tylko wewnątrz obiektu. Mogą więc na nich operować tylko funkcje składowe tego obiektu. Nawet inne obiekty tej samej klasy nie mogą ich sobie nawzajem podglądać czy zmieniać. Składniki publiczne to te, które widać z zewnątrz. Widzą je pozostałe obiekty i można je dowolnie zmieniać i używać. Natomiast o składnikach chronionych powiemy sobie innym razem, a narazie są dla nas tym samym, czym składniki prywatne. Domyślnie wszystkie składniki klasy są prywatne, więc jeśli nie podasz żadnej etykiety dostępu, będą właśnie prywatne. Ale na co nam obiekt, z którego nic nie wystaje na zewnątrz i nie ma takiej siły, żeby się do niego dobrać? Zazwyczaj więc wszystkie właściwości robi się prywatne, a metody robi się publiczne, bo one wiedzą, jak się bezpiecznie obejść z prywatnymi składnikami i nie narobią nam kiermaszu ;-). Czasami niektóre funkcje można uczynić prywatnymi, na przykład jeśli są to jakieś funkcje pomocnicze które klasa wykorzystuje na swój własny tajemniczy sposób. Wróćmy teraz do naszego pudełka zapałek. Uczynimy składnik ile_zapalek składnikiem prywatnym, żeby uniemożliwić niepowołany dostęp do niego. Jedynie metody Dodaj i Wyjmij będą mogły go modyfikować. Żeby jednak mieć możliwość sprawdzenia, ile zapałek jest w pudełku [przecież teraz jest to składnik prywatny!], dodamy jeszcze jedną metodę, która nam to powie. Zauważ, że nie dodajemy żadnej metody pozwalającej na ustawienie dowolnej wartości dla składnika ile_zapalek, czyli mamy to czego chcieliśmy - bezpieczeństwo. class PaczkaZapalek { private: int ile_zapalek; //Właściwość - ilość zapałek w pudełku public: void Dodaj(int ile); //Metoda dodaje zapałki do pudełka void Wyjmij(int ile); //Metoda wyjmuje zapałki z pudełka int Pokaz() { return ile_zapalek; } //Metoda pokazuje ile jest zapałek }; Czy widzisz coś interesującego? Dlaczego zdefiniowaliśmy metodę Pokaz wewnątrz definicji klasy i co nam to daje? No więc taki sposób definicji metody powoduje, że jest ona funkcją inline, czyli cały jej kod jest wstawiany w miejsce wywołania. Cała definicja funkcji znajduje się już wewnątrz klasy, więc nie trzeba jej już definiować poza klasą. Funkcjami inline najlepiej jest uczynić funkcje, które mało robią [np. zwracają zawartość składników prywatnych] i są wywoływane często. Dzięki temu czas potrzebny na ich wywołanie będzie krótszy. Acha, żeby nie być gołosłownym, radzę ci teraz wykonać mały eksperyment. Spróbuj w funkcji main odwołać się bezpośrednio do prywatnego składnika ile_zapalek. Zobaczysz, że kompilator nie pozwoli ci przy nim majstrować i już przy kompilacji ostro zaprotestuje. Jedyną możliwością, by poznać ilość zapałek w pudełku, jest użycie "autoryzowanej" metody Pokaz(). Nie ma natomiast żadnego sposobu, by wpisać w obiekcie niepoprawną ilość zapałek. I to jest jedna z wielu zalet programowania obiektowego - ukrywanie danych, które chroni przed wieloma błędami. A jaki jest koszt takiej ochrony? Czy programy obiektowe działają wolniej? Czy zajmują więcej miejsca? Cóż, niektórzy lubią zwalać swoje błędy na programowanie obiektowe, ale prawda jest inna ;-P. Program obiektowy przeważnie zajmuje tyle samo, co jego odpowiednik strukturalny. Każdy obiekt zajmuje tyle, ile zajmują jego dane składowe. Funkcje, jako instrukcje procesora, nie muszą być osobne dla każdego obiektu, więc zajmują miejsce w EXEku jednorazowo. Możesz łatwo sprawdzić rozmiar obiektu operatorem sizeof i przekonać się, że obiekt zawierający dwa składniki typu int zajmuje tyle, ile zajmowałyby łącznie te zmienne "luzem", czyli 8 bajtów.
  14. Struktury Przypuśćmy, że chcesz zrobić sobie komputerową książkę adresową. W tej książce mają się znajdować informacje o każdym twoim kumplu: jego imię, nazwisko, adres i telefon. Jednak adres i na przykład numer telefonu to właściwie różne typy danych, powiązane ze sobą jedynie tym, że opisują jedną osobę. Jednak dużo lepiej by było mieć tego "qmpla" jako całość, a nie rozrzucone po RAMie zmienne, prawda? Właśnie do tego wymyślono struktury. Struktura łączy w sobie kilka mogących się różnić typów danych, związanych ze sobą w jakiś logiczny sposób, oraz co najważniejsze pozwala odwoływać się do nich jako do całości. Przykładowa struktura opisująca naszego kumpla mogłaby wyglądać tak: struct Kumpel { char Imie[20]; char Nazwisko[30]; char Adres[40]; int Telefon; }; Zauważ, że deklaracja struktury kończy się średnikiem! Tak samo jak deklaracja zwykłej zmiennej. Widać wyraźnie, że w strukturze zawarte są cztery zmienne [tak zwane pola]. Jednak zmienne takie nie istnieją jeszcze w pamięci, bo struktura jest tylko definicją nowego typu zmiennej, który zwie się w tym przypadku Kumpel. Konkretną zmienną typu Kumpel trzeba dopiero utworzyć. Na przykład tak: Kumpel gostek; Od dzisiaj gostek jest twoim kumplem ;-). Jak widzisz, struktura rzeczywiście wygląda jak nowy typ danych, bo można z jego pomocą deklarować nowe zmienne. Ale podobnie jak każda inna zmienna tuż po zadeklarowaniu, struktura zawiera przypadkowe dane. Taki gostek nie ma jeszcze imienia, ani nazwiska, ani nawet nigdzie nie mieszka... Trzeba zainicjować strukturę odpowiednimi wartościami podczas deklaracji zmiennej, w podobny sposób jak z tablicą: Kumpel gostek = {"Franek","Łopata","Wygwizdowo, ul.Błotna 4",8425523}; Drugim sposobem jest ustawienie każdego pola struktury z osobna. Ale jak się odwoływać do pól struktury? Służy do tego kropka. Najpierw pisze się nazwę zmiennej typu strukturowego, daje się kropkę i dopiero nazwę pola struktury. No więc wypełnimy strukturę gostek w taki właśnie sposób: Kumpel gostek; gostek.Imie = "Franek"; gostek.Nazwisko = "Łopata"; gostek.Adres = "Wygwizdowo, ul.Błotna 4"; gostek.Telefon = 8425523; Przypomina to trochę wypełnianie formularza. Pola struktury możemy później w programie dowolnie modyfikować, odwołując się do nich przy pomocy kropki, tak jak powyżej. Aby teraz zrobić książkę adresową, trzeba po prostu zmontować tablicę, której elementami będą struktury typu Kumpel. Kumpel kumple[100]; Jak widzisz, struktura jest pełnoprawnym typem danych, a więc można też z niego zbudować tablicę. Jeśli teraz chcesz sprawdzić, kto jest na piątym miejscu tablicy, robisz to tak: cout << kumple[4].Nazwisko; Najpierw wybierasz, który Kumpel [który element tablicy, będący strukturą] ma coś powiedzieć o sobie. Wyrażenie kumple[4] zostaje "zastąpione" strukturą, która jest w tym miejscu tablicy, więc teraz wystarczy odwołać się do jednego z pól tej struktury przy pomocy kropki i nazwy pola. Banał, no nie? ;-) Jaki rozmiar w pamięci ma nasza struktura Kumpel? Pomyśl. Mamy trzy tablice typu char przechowujące tekst, o rozmiarach kolejno 20 bajtów, 30 bajtów i 40 bajtów. Do tego dochodzą cztery bajty na liczbę int przechowującą numer telefonu. Czyli razem powinno być 94 bajty. Czy rzeczywiście tak jest? Możemy to łatwo sprawdzić, posługując się operatorem sizeof. Używa się go podobnie jak funkcji, a zwraca nam on wielkość obiektu wyrażoną w bajtach. Sprawdźmy więc: cout << sizeof(Kumpel); Możemy sprawdzić wielkość typu, lub zmiennej, na jedno wychodzi. I co? Zgadza się? Możliwe że wynik wyszedł nieco inny, różniący się nawet do trzech bajtów. Dlaczego? Niektóre 32-bitowe kompilatory wyrównują pola struktury do 32 bitów. I tak na przykład pierwszy napis składa się z 20 bajtów, czyli 5 paczek po 32 bity. Ale już drugi napis to 30 bajtów, a więc brakuje jeszcze dwóch, by rozmiar był podzielny przez 4 bajty [32 bity]. Kompilator wyrówna więc to pole o te 2 bajty, bo dzięki temu będzie mógł traktować każdą komórkę tablicy jako wartość 32-bitową, a procesor 32-bitowy operuje szybciej na takich liczbach. Na codzień nie będzie ci zbytnio potrzebna ta wiedza, ale jeśli będziesz o tym pamiętać, to cię nie zaskoczy inny rozmiar niż przewidywany ;-). Struktury przydają się do różnych zastosowań. Pierwszym z nich są, jak widzisz, bazy danych. Ale na tym nie koniec. Struktura może też określać budowę nagłówka jakiegoś pliku, na przykład BMP. Struktura może zawierać wskaźniki, nawet takie które pokazują na inne struktury tego samego typu, co pozwala na budowanie różnych konstrukcji programistycznych, takich jak listy, drzewa, kolejki, stosy, i wiele innych. Możesz użyć struktury do przechowywania informacji o elementach mapy w twojej grze, albo o parametrach jakiejś postaci. Zastosowań jest naprawdę bez liku, wystarczy tylko pomyśleć i jakieś sobie znaleźć ;-). W tej lekcji narazie jeszcze nie napiszemy książki adresowej, bo nie znasz funkcji bibliotecznych do operacji na napisach. Omówię je w jednej z kolejnych lekcji i tam też napiszemy sobie taką "bazę danych".
  15. Saurons

    Typ Wyliczeniowy

    Typ wyliczeniowy Żonglowanie milionem cyferek w programie to niezbyt ciekawe zajęcie i nietrudno się w tych wszystkich cyferkach pogubić. Przykładowo masz kilka liczb oznaczających wynik gry. Zero oznacza, że gra się nie odbyła, jedynka że wygrał gracz pierwszy, dwójka że wygrał gracz drugi, no i trójka oznaczająca remis. Cztery liczby to może nie jest jeszcze zbyt dużo, choć i tyle łatwo pomylić będąc już gdzieś głęboko w kodzie. Dużo łatwiej by było, gdyby zamiast cyfr posługiwać się jakimiś ich nazwami, co nie? ;-) Najprostszym wyjściem jest użycie dyrektywy kompilatora #define, która może przyporządkować liczbie nazwę. Robimy więc tak: #define WYNIK_GRY_NIE_BYLO 0 #define WYNIK_WYGRAL_GRACZ_1 1 #define WYNIK_WYGRAL_GRACZ_2 2 #define WYNIK_REMIS 3 Teraz możesz posugiwać się nazwą liczby, zamiast bezpośrednio liczbą. Takie definicje są czymś w rodzaju stałych typu int, ale mają jedną poważną wadę: w debuggerze [specjalny program do wykrywania błędów] nie pokazują się ładne nazwy, tylko dalej nic nie znaczące liczby. Lepiej więc jest używać normalnych stałych. Na przykład tak: const int WYNIK_GRY_NIE_BYLO = 0; const int WYNIK_WYGRAL_GRACZ_1 = 1; const int WYNIK_WYGRAL_GRACZ_2 = 2; const int WYNIK_REMIS = 3; Ten sposób jest lepszy, bo w debuggerze pokaże nam się nie tylko wartość, ale też nazwa zmiennej [a raczej stałej]. Ale to dalej nie jest to, czego byśmy chcieli. Dajmy na to masz funkcję, która sprawdza wynik gry. Powinna ona przyjmować tylko wspomniane wyżej wartości od 0 do 3. Oczywiście wrzucenie tam liczby 20 jest błędem i kompilator powinien nas o tym powiadomić. Co zrobić, żeby tak było? Do tego celu wymyślono właśnie typ wyliczeniowy. Jest on jakby zbiorem kolejnych liczb, z których każda ma swoją etykietkę. Zobacz, jak moglibyśmy z jego pomocą zdefiniować nowy typ, będący naszym wynikiem gry: enum WynikGry = {Nieodbyta=0, Wygral1, Wygral2, Remis}; Jeśli teraz zdefiniujesz zmienną typu WynikGry, to będzie ona mogła przyjmować tylko wartości znajdujące się w tym zbiorze, a więc Nieodbyta, Wygral1, Wygral2, albo Remis. Gdy spróbujesz przypisać takiej zmiennej wartość spoza dopuszczalnego zbioru, na przykład tak: WynikGry wyniczek = 54; to kompilator powinien zaprotestować. Protest ten różnie wygląda w różnych kompilatorach, na przykład kompilator Borland Turbo C++ wyświetla jedynie ostrzeżenie. Typ wyliczeniowy jest traktowany jak stała typu int, więc można przypisywać tak jak poniżej: int inny_wynik = Remis; a nawet używać typu wyliczeniowego w pętli for [i nie tylko], w instrukcjach warunkowych, a także przesyłać go jako parametr do funkcji. Omówienia wymaga jeszcze tylko ten znak = w definicji typu. Normalnie numerowanie etykiet w typie wyliczeniowym zaczyna się od zera, więc właściwie to "=0" nie jest potrzebne. Jednak nie zapomnij, że można zacząć numerację od innej liczby, a nawet zmienić numerację gdzieś w środku zbioru i np. zacząć liczyć od 50 wzwyż :-). Typy wyliczeniowe przydają się wtedy, gdy jakaś wartość może przyjmować tylko kilka różnych stanów, które chcielibyśmy mieć jakoś nazwane. Ta lekcja może nie była zbyt ciekawa ani nawet długa, a zrobiłem ją tylko po to, żeby niczego nie pominąć. Po prostu dobrze jest czasem wiedzieć, że w C++ jest coś takiego jak typ wyliczeniowy ;-J. Następna lekcja będzie już ciekawsza, obiecuję ;-).
×
×
  • Dodaj nową pozycję...