Tajniki Programowania nr 5
Transkrypt
Tajniki Programowania nr 5
Tajniki Programowania nr 5 Serwis www.kodowanie.prv.pl Jakub Laskowski 31 sierpnia 2004 Spis treści 1 Słowo wstępne 2 2 Co nowego na kodowanie.prv.pl 3 3 Projekt PCMath: Liczby 128-bitowe 3.1 Informacje o projekcie PCMath . . . . 3.2 Wprowadzenie . . . . . . . . . . . . . . 3.3 Interfejs klasy . . . . . . . . . . . . . . 3.4 Implementacja . . . . . . . . . . . . . . 3.4.1 Konstruktory i Destruktor . . . 3.4.2 Operator przypisania . . . . . . 3.4.3 Funkcja set . . . . . . . . . . . 3.4.4 Dodawanie i Odejmowanie . . . 3.4.5 Inkrementacja i Dekrementacja 3.4.6 Mnożenie . . . . . . . . . . . . 3.4.7 Bitowa suma, iloczyn i negacja 3.4.8 Bitowe przesunięcia . . . . . . . 3.4.9 Operatory równości . . . . . . . 3.4.10 Operatory relacji <, >, , ¬ . . 3.4.11 Dzielenie i reszta z dzielenia . . 3.4.12 Operator << (wyjścia) . . . . . 3.5 Zakończenie . . . . . . . . . . . . . . . 4 Biblioteka SDL — Zastosowania 4.1 Wstęp . . . . . . . . . . . . . . 4.2 Kod źródłowy . . . . . . . . . . 4.3 Zbiór Mandelbrota . . . . . . . 4.3.1 Co to są fraktale? . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 5 5 6 6 7 7 7 8 9 10 10 11 12 13 14 15 . . . . 15 15 16 21 21 1 SŁOWO WSTĘPNE 4.4 4.5 4.6 2 4.3.2 Liczby zespolone . . . . . . . . . . . . . . . . 4.3.3 Jeszcze trochę teorii . . . . . . . . . . . . . . 4.3.4 Liczby zespolone i C . . . . . . . . . . . . . . 4.3.5 Koniec teorii . . . . . . . . . . . . . . . . . . . 4.3.6 Ulepszanie programu . . . . . . . . . . . . . . Algorytm rysowania linii . . . . . . . . . . . . . . . . 4.4.1 Wprowadzenie . . . . . . . . . . . . . . . . . . 4.4.2 Opis algorytmu . . . . . . . . . . . . . . . . . 4.4.3 Kod . . . . . . . . . . . . . . . . . . . . . . . Wyświetlanie znaków z użyciem biblioteki FreeType2 4.5.1 Wstęp . . . . . . . . . . . . . . . . . . . . . . 4.5.2 Wyświetlanie pojedynczej litery . . . . . . . . 4.5.3 Wyświetlamy dłuższe napisy . . . . . . . . . . Zakończenie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 23 23 24 29 33 33 34 34 38 38 39 45 49 5 Tutorial o C++ — część 4 5.1 ZAKRES WAŻNOŚCI: czyli skołowali, a potem zaginęło ;) . . 5.2 KOMENTARZE: czyli znaczek // się uśmiecha . . . . . . . . 5.3 INSTRUKCJE WARUNKOWE I OPERATORY: czyli już wiem! 5.3.1 Instrukcja if . . . . . . . . . . . . . . . . . . . . . . . . 5.3.2 Instrukcja switch . . . . . . . . . . . . . . . . . . . . . 5.3.3 Operatory . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.4 Ćwiczenia . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 51 53 53 57 59 63 6 Ułamki w systemie binarnym 6.1 Wprowadzenie . . . . . . . . . . . . . . . . . . . . 6.2 Ułamki — sposób tradycyjny . . . . . . . . . . . 6.3 Sprawdzanie wyniku czyli jedziemy z powrotem . 6.4 Coś dla leniwych czyli algorytm Hornera część. 1 . 6.5 Coś dla leniwych czyli algorytm Hornera część. 2 . 6.6 Wyjątki czyli to, co Polacy lubią najbardziej . . . 66 66 66 66 67 67 68 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Słowo wstępne Drogi czytelniku! Oto masz przed sobą a piąty numer e-zinu Tajniki Programowania. Z numerem tym związana jest wielka zmiana — z powodu gigantycznego wzrostu ilości treści przesyłanych z każdym kolejnym numerem zmieniła się jego forma — zamiast ponad 1000-liniowych e-maili każdy z was otrzymał krótki e-mail 2 CO NOWEGO NA KODOWANIE.PRV.PL 3 z załącznikiem w postaci pliku PDF. Myślę, że ta zmiana zostanie przejęta przez wszystkich pozytywnie. W razie jakichkolwiek problemów powstałych z tego powodu, proszę o kontakt e-mailem na adres [email protected]. Z powodu tej znaczącej zmiany formy Tajników wystąpiło duże opóźnienie. W tym miejscu pragnę przeprosić wszystkich, którzy długo musieli oczekiwać na ten numer. Żywię głęboką nadzieję, że korzyści wynikające ze zmiany formy przyćmią powstałe z tego powodu utrudnienia. Kolejną przyczyną opóźnienia jest na prawdę duża ilość artykułów znajdujących się w środku. Napisanie tego wymaga trochę czasu. W końcu, są też wakacje :). W piątym numerze Tajników Programowania można przeczytać o kilku zastosowaniach biblioteki SDL — a ściślej — poznać implementację kilku prostych algorytmów wykorzystujących tą bibliotekę i w kolejnej, czwartej już części Tutoriala zgłębić tajniki języka C++. Ponadto pojawił się artykuł, który docelowo ma zapoczątkować stały dział — Dział projektu PCMath, czyli projektu, nad którym ostatnio pracuję. W dziale tym będą prezentowane wyniki tej pracy, w postaci kodów źródłowych wraz z analizą. Więcej informacji o tym projekcie na początku artykułu. 2 Co nowego na kodowanie.prv.pl Podczas wakacji na stronie kodowanie.prv.pl nie było zbyt wiele nowości — pojawiły się jedynie artykuły z poprzedniego numeru e-zine. Ponadto, nawiązaliśmy ostatnio współpracę z pewny nowo powstającym serwisem internetowym. Na razie wszystko pozostaje owiane tajemnicą, lecz wkrótce ujawnimy bliższe szczegóły współpracy. Zachęcam do odwiedzania serwisu, gdzie zostaną podane bliższe informacje. 3 Projekt PCMath: Liczby 128-bitowe Jakub Laskowski 3.1 Informacje o projekcie PCMath Projekt PCMath to projekt stworzenie w pełni funkcjonalnego kalkulatora – programu matematycznego na komputery PC. Docelowo program ma mieć bardzo szeroką funkcjonalność. W tym zaawansowaną wizualizację, obliczenia symboliczne, własny, prosty język interpretowany i inne. W chwili obecnej projekt jest w bardzo początkowej fazie lecz niektóre rzeczy już działają. Oto krótka lista dostępnej (na razie) funkcjonalności: 3 PROJEKT PCMATH: LICZBY 128-BITOWE 4 • Parser wyrażeń matematycznych. – Operuje na liczbach typu long double. – Działania dodawanie, odejmowania, mnożenie, dzielenie, potęgowanie. – Obsługa 26 zmiennych. – Stałe: pi oraz e. – Bardzo dużo funkcji, w tym trygonometryczne, hiperboliczne, logarytmy, min, max, część całkowita i ułamkowa, liczby losowe, funkcje odwrotne do trygonometrycznych i hiperbolicznych. – Obsługa domyślnego mnożenia. • Wykresy 2D w prostokątnym układzie współrzędnych (w dwóch wersjach). • Wykresy 2D w biegunowym układzie współrzędnych. • Wykresy funkcji 2 zmiennych w prostokątnym układzie współrzędnych (kolory). • Rozwiązywanie prostych równań liniowych. • Wiele funkcji graficznych – Określanie wyświetlanego fragmentu płaszczyzny – Rysowanie punktów – Rysowanie linii – Rysowanie okręgów – Wyświetlanie tekstu – Tworzenie negatywu obrazu – Zapis do pliku BMP. • Generowanie wyrażeń matematycznych w LATEX2e • Obsługa liczb 128-bitowych • Zaczątki interfejsu użytkownika w ncurses. • Prawie gotowa obsługa liczb o długości 1800 cyfr. Program działa pod konsolą a do wyświetlania grafiki używa biblioteki SDL. Oczywiście lista funkcji programu będzie znacznie rozszerzana. 3 PROJEKT PCMATH: LICZBY 128-BITOWE 3.2 5 Wprowadzenie Prędzej czy później każdy programista spotka na swojej drodze problem, do którego rozwiązania zakres, standardowych 32-bitowych lub nawet 64bitowych zmiennych jest zbyt mały. Często wtedy sięga się po rozmaite biblioteki dające możliwość obsługi liczb całkowitych ograniczonych jedynie wielkością dostępnej pamięci. Często jednak jest to wyprawa czołgiem na muchę. Z pomocą może przyjść wtedy przedstawiona tutaj klasa obligująca liczby o długości 16 bajtów. W prosty sposób można tą klasę rozszerzać (np. stosując szablony) by otrzymać liczby o większym zakresie — np. 256 bitowe, 512 bitowe, 1024 bitowe itp. W pewnym momencie algorytm ten będzie mniej wydajny od bibliotek liczb o nieograniczonej długości, jednak w przypadku 128 bitów klasa ta nie ustępuje standardowym intom. Zaprezentowana tutaj klasa jest prawie kompletna — obsługuje podstawowe działania, dla zwiększenia jej możliwości należałoby dodać jeszcze możliwość wykonywania operacji z typami int, dodać operatory uproszczone np. +=, *=, itp. Przy tworzeniu tej klasy korzystałem z książki Uczta Programistów autorstwa Henry’ego S. Warrena, wydawnictwa Helion. Zawiera ona opisy sprytnych i interesujących sztuczek programistycznych. 3.3 Interfejs klasy Na początku dołączamy odpowiednie pliki nagłówkowe (iostream i cstdlib), oraz definiujemy Uint64 jako unsigned long long int. Klasa w sekcji private zawiera definicje dwóch zmiennych typu Uint64 oraz funkcję mulhu - obliczającą starsze słowo iloczynu (szczegóły później). Ponadto klasa implenetuje najczęściej używane operatory: #include <iostream> #include <cstdlib> #define UInt128 Uint128 #define Uint64 unsigned long long int using namespace std; class Uint128 { private: 3 PROJEKT PCMATH: LICZBY 128-BITOWE 6 Uint64 x1, x2; Uint64 mulhu(Uint64 a, Uint64 b); public: Uint128 inline friend operator+(Uint128 a, Uint128 b); Uint128 inline friend operator-(Uint128 a, Uint128 b); Uint128 inline friend operator*(Uint128 a, Uint128 b); Uint128 inline friend operator/(Uint128 a, Uint128 b); Uint128 inline friend operator%(Uint128 a, Uint128 b); Uint128 inline friend operator&(Uint128 a, Uint128 b); Uint128 inline friend operator|(Uint128 a, Uint128 b); Uint128 inline friend operator^(Uint128 a, Uint128 b); Uint128 inline friend operator<<(Uint128 a, int x); Uint128 inline friend operator>>(Uint128 a, int x); Uint128 inline friend &operator++(Uint128 &t); Uint128 inline friend &operator--(Uint128 &a); ostream friend &operator<<(ostream &out, const Uint128 &b); Uint128 inline set(Uint64 a, Uint64 b); bool inline operator=(const Uint128 a); bool inline operator==(const Uint128 b); bool inline friend operator==(const Uint64 int a, const Uint128 b); bool inline operator>=(const Uint128 b); bool inline operator>(const Uint128 b); bool inline operator<=(const Uint128 b); bool inline operator<(const Uint128 b); bool inline operator!=(const Uint128 b); Uint128(); Uint128(const Uint128 &a); ~Uint128(); }; 3.4 3.4.1 Implementacja Konstruktory i Destruktor Konstruktor zwykły przypisuje zmiennym x1 i x2 wartość 0. Konstruktor kopiujący przypisuje odpowiednie wartości zmiennym. Destruktor nie robi nic: Uint128::Uint128() { x1 = 0; x2 = 0; 3 PROJEKT PCMATH: LICZBY 128-BITOWE 7 } Uint128::Uint128(const Uint128 &a) { x1 = a.x1; x2 = a.x2; }; Uint128::~Uint128() { }; 3.4.2 Operator przypisania Operator ten jest analogiczny do konstruktora kopiującego: bool Uint128::operator=(const Uint128 a) { x1 = a.x1; x2 = a.x2; return true; }; 3.4.3 Funkcja set Funkcja ta służy do przypisywania naszej liczbie wartości w postaci dwóch liczby typu Uint64. Klasa w zaprezentowanej tutaj wersji nie pozwala na przypisywanie jej liczb znajdujących się w łańcuchach znaków. Funkcja ta, jest więc jedynym sposobem na przypisanie wartości liczbie. Uint128 Uint128::set(Uint64 a, Uint64 b) { x1 = a; x2 = b; return (*this); } 3.4.4 Dodawanie i Odejmowanie W przypadku dodawania i odejmowania sprawa nie jest jednoznaczna. Wykorzystujemy wszystkie 64 bity naszych zmiennych, a nie mamy dostępu do flagi przeniesienia procesora. Musimy skorzystać więc z dwóch sztuczek, które 3 PROJEKT PCMATH: LICZBY 128-BITOWE 8 odpowiednio w dodawaniu i odejmowaniu przyjmą wartość 1 gdy zajdzie potrzeba przeniesienia lub pożyczenia. Poniższy kod zawiera te sztuczki. Uint128 operator+(Uint128 a, Uint128 b) { Uint128 c; Uint64 d; c.x2 = a.x2 + b.x2; d = ((a.x2 & b.x2)|((a.x2 | b.x2)&(~c.x2)))>>63; c.x1 = a.x1 + b.x1 + d; return c; } Uint128 operator-(Uint128 a, Uint128 b) { Uint128 c; Uint64 d; c.x2 = a.x2 - b.x2; d = ((~a.x2 & b.x2)|((~(a.x2^ b.x2))&(c.x2)))>>63; c.x1 = a.x1 - b.x1 - d; return c; } 3.4.5 Inkrementacja i Dekrementacja Operatory inkrementacja i dekrementacja są jedynie nieznacznie przerobionymi operatorami dodawania i odejmowania: Uint128 &operator++(Uint128 &t) { Uint128 c; Uint64 d; c.x2 = t.x2 + 1; d = ((t.x2 & 1)|((t.x2 | 1)&(~c.x2)))>>63; c.x1 = t.x1 + d; t = c; return t; } Uint128 operator--(Uint128 &a) { Uint128 c; 3 PROJEKT PCMATH: LICZBY 128-BITOWE 9 Uint64 d; c.x2 = a.x2 - 1; d = ((~a.x2 & 1)|((~(a.x2 ^ 1))&(c.x2)))>>63; c.x1 = a.x1 - d; a = c; return c; } 3.4.6 Mnożenie Aby obliczyć wynik mnożenia dwóch liczb 128-bitowych musimy najpierw napisać funkcję która policzy starszą połowę iloczynu liczb 64 bitowych. Nazywa się ona mulhu, przyjmuje jako argumenty dwie liczby Uint64 i zwraca taką liczbę. Funkcja jest kolejną sztuczką z książki uczta programistów. Gdy posiadamy tą funkcję mnożenie jest już proste — stosujemy prosty, szkolny wzór na iloczyn sumy. Niestety nie zawsze iloczyn zmieści się w zakresie naszej liczby. Musimy jakąś część odjąć. Uint64 mulhu(Uint64 u, Uint64 v) { Uint64 u0, v0, w0; Uint64 u1, v1, w1, w2, t; u0 = u & 0xFFFFFFFF; u1 = u >> 32; v0 = v & 0xFFFFFFFF; v1 = v >> 32; w0 = u0*v0; t = u1*v0 + (w0 >>32); w1 = t & 0xFFFFFFFF; w2 = t >> 32; w1 = u0*v1 + w1; return u1*v1 + w2 + (w1 >> 32); } Uint128 operator*(Uint128 a, Uint128 b) { Uint128 c; c.x1 = a.x1*b.x2+a.x2*b.x1+mulhu(a.x2, b.x2); c.x2 = a.x2*b.x2; return c; } 3 PROJEKT PCMATH: LICZBY 128-BITOWE 3.4.7 10 Bitowa suma, iloczyn i negacja Operacja te są bardzo proste. Wykonujemy odpowiednie działania na każdej ze składowych: Uint128 operator&(Uint128 a, Uint128 b) { Uint128 c; c.x1 = a.x1 & b.x1; c.x2 = a.x2 & b.x2; return c; } Uint128 operator|(Uint128 a, Uint128 b) { Uint128 c; c.x1 = a.x1 | b.x1; c.x2 = a.x2 | b.x2; return c; } Uint128 operator^(Uint128 a, Uint128 b) { Uint128 c; c.x1 = a.x1 ^ b.x1; c.x2 = a.x2 ^ b.x2; return c; } 3.4.8 Bitowe przesunięcia Niestety zaprezentowany w książce „Uczta Programistów” sposób wykonywania przesunięć na liczbach podwójnej długości nie działał w przypadku stosowania liczb typu Uint64. Musiałem napisać własne funkcje sprawdzające ilość bitów do przesunięcia i w razie potrzeby ustawiające zmienne na wartość 0. Funkcje wykorzystują instrukcje if, są więc niestety mniej wydajne. Uint128 operator<<(Uint128 a, int x) { Uint128 c; if (x<64) { 3 PROJEKT PCMATH: LICZBY 128-BITOWE 11 c.x1 = (a.x1 << x )|(a.x2 >> (64-x)); c.x2 = a.x2 << x; } else if (x<128) { c.x1 = a.x2 << (x-64); c.x2 = 0; } return c; } Uint128 operator>>(Uint128 a, int x) { Uint128 c; if (x<64) { c.x2 = a.x2 >> x | a.x1 << (64-x); c.x1 = a.x1 >> x; } else if (x<128) { c.x2 = a.x1 >> (x-64); c.x1 = 0; } return c; } 3.4.9 Operatory równości Dwie liczby 128-bitowe są równe wtedy i tylko wtedy gdy obie jej składowe są równe bool Uint128::operator==(const Uint128 b) { if ((x1==b.x1)&&(x2==b.x2)) { return true; } return false; } 3 PROJEKT PCMATH: LICZBY 128-BITOWE 12 bool Uint128::operator!=(const Uint128 b) { if ((x1==b.x1)&&(x2==b.x2)) { return false; } return true; } 3.4.10 Operatory relacji <, >, , ¬ Najpierw porównujemy bardziej znaczące składowe, w następnie miej znaczące. W zależności od wyniku odpowiednich porównań zwracamy true lub false: bool Uint128::operator>=(const Uint128 b) { if (b.x1 > x1) { return false; } else if ((b.x1 == x1)&&(b.x2 > x2)) { return false; } return true; } bool Uint128::operator<=(const Uint128 b) { if (b.x1 < x1) { return false; } else if ((b.x1 == x1)&&(b.x2 < x2)) { return false; } return true; } 3 PROJEKT PCMATH: LICZBY 128-BITOWE 13 bool Uint128::operator<(const Uint128 b) { if (b.x1 > x1) { return true; } else if ((b.x1 == x1)&&(b.x2 > x2)) { return true; } return false; } bool Uint128::operator>(const Uint128 b) { if (b.x1 < x1) { return true; } else if ((b.x1 == x1)&&(b.x2 < x2)) { return true; } return false; } 3.4.11 Dzielenie i reszta z dzielenia Algorytmy te również pochodzą z książki „Uczta Programistów” Opierają się one na operacjach przesunięć i porównać, dlatego do ich działania wymagana jest implementacja przesunięć bitowych na naszych liczbach 128-bitowych: Uint128 operator/(Uint128 a, Uint128 b) { Uint128 c; int i; for (i = 1; i <= 128; i++) { c = (c << 1) | (a >> 127); a = a << 1; if (c >= b) 3 PROJEKT PCMATH: LICZBY 128-BITOWE 14 { c = c - b; ++a; } } c = a; return c; } Uint128 operator%(Uint128 a, Uint128 b) { Uint128 c; int i; for (i = 1; i <= 128; i++) { c = (c << 1) | (a >> 127); a = a << 1; if (c >= b) { c = c - b; ++a; } } return c; } 3.4.12 Operator << (wyjścia) Operator ten wykorzystuje operatory dzielenia i reszty z dzielenie do przekształcenia liczby na postać dziesiętną. Zaprezentowana tutaj funkcja nie jest optymalna (Dzielenie jest wykonywane 2 razy, a prosta przeróbka pozwoli na jego wykonanie raz). Tą optymalizację pozostawiam czytelnikom :) ostream &operator<<(ostream &out, const Uint128 &b) { char t[50]; int i = 0; Uint128 a,c; a = b; Uint128 zero; Uint128 dzies; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 15 dzies.set(0,10); while (a!=zero) { c = a % dzies; a = a / dzies; t[i] = c.x2 + 48; i++; } for (i--;i>=0;i--) out << t[i]; return out; } 3.5 Zakończenie To już koniec klasy liczb 128-bitowych. Dla uzyskania pełnej jej funkcjonalności należało by zaimplementować więcej operatorów, umożliwić wczytywanie liczb ze strumieni a także przypisywanie im łańcuch znaków. Gdy posiadamy tą klasę implementacja tych wyżej wymienionych, dodatkowych funkcji nie powinna być trudna. Pozostawiam to czytelnikom. 4 Biblioteka SDL — Zastosowania Jakub Laskowski 4.1 Wstęp To już jest trzecia część z cyklu artykułów o bibliotece SDL. Nie będę mówił w niej o możliwościach stosowania tej biblioteki — te są bardzo różnorakie i w dużej mierze oczywiste — jak choćby programowanie gier, tylko przestawię implementację kilku prostych algorytmów graficznych z wykorzystanie tej biblioteki. Artykuł ten, mimo iż traktuje o bibliotece SDL, może być bez większych przeszkód przystosowany do wykorzystania dowolnej innej biblioteki graficznej - jest to kwestia zmienienia tylko kilku funkcji. W poprzednich częściach tego artykułu poznaliśmy kilka możliwości biblioteki SDL, które teraz będziemy wykorzystywać. Oto krótka lista poznanych możliwości. • Inicjalizowanie biblioteki • Tworzenie podstawowego Surface 4 BIBLIOTEKA SDL — ZASTOSOWANIA 16 • Rysowanie pojedynczych pikseli • Prosty algorytm rysowania okręgów • Wstawianie bitmap • Obsługa zdarzeń związanych z klawiaturą i myszką • Wyświetlanie bitmap z wykorzystanie pół-przezroczystości i przezroczystości Nie są to oczywiście wszystkie możliwości, które oferuje nam ta biblioteka. Być może w kolejnych częściach artykułu omówię także inne - np. obsługa dźwięku, obsługa joysticka itp. lecz na razie te, które przedstawiłem w poprzednich częściach wystarczą aby móc podać przykłady kilku algorytmów graficznych. 4.2 Kod źródłowy Oto kod źródłowy naszego programu powstałego w poprzednich częściach artykułu: #include "SDL.h" #include <stdio.h> #define pi 3.14159 void SDL_DisplayBitmap(char *file, SDL_Surface *ekran, int x, int y) { SDL_Surface *image; SDL_Rect dest; image = SDL_LoadBMP(file); if ( image == NULL ) { fprintf(stderr, "Nie można wczytać %s: %s\n", file, SDL_GetError()); return; } dest.x dest.y dest.w dest.h = = = = x; y; image->w; image->h; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 17 SDL_BlitSurface(image, NULL, ekran, &dest); SDL_UpdateRects(ekran, 1, &dest); SDL_FreeSurface(image); } void SDL_DisplayBitmapAlpha(char *file, SDL_Surface *ekran, int x, int y, Uint8 alpha) { SDL_Surface *image; SDL_Rect dest; image = SDL_LoadBMP(file); if ( image == NULL ) { fprintf(stderr, "Nie można wczytać %s: %s\n", file, SDL_GetError()); return; } dest.x = x; dest.y = y; dest.w = image->w; dest.h = image->h; SDL_SetAlpha(image, SDL_SRCALPHA, alpha); SDL_BlitSurface(image, NULL, ekran, &dest); SDL_UpdateRects(ekran, 1, &dest); SDL_FreeSurface(image); } void SDL_DisplayBitmapTransparent(char *file, SDL_Surface *ekran, int x, int y, Uint8 r, Uint8 g, Uint8 b) { SDL_Surface *image; SDL_Rect dest; image = SDL_LoadBMP(file); if ( image == NULL ) { fprintf(stderr, "Nie można wczytać %s: %s\n", file, SDL_GetError()); return; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 18 } dest.x = x; dest.y = y; dest.w = image->w; dest.h = image->h; SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format,r,g,b)); SDL_BlitSurface(image, NULL, ekran, &dest); SDL_UpdateRects(ekran, 1, &dest); SDL_FreeSurface(image); } void SDL_PutPixel(SDL_Surface *ekran, int x, int y, Uint8 R, Uint8 G, Uint8 B) { Uint8 *p = (Uint8 *)ekran->pixels + y * ekran->pitch + x * ekran->format->BytesPerPixel; if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = R; p[1] = G; p[2] = B; } else { p[0] = B; p[1] = G; p[2] = R; } SDL_UpdateRect(ekran, x, y, 1, 1); } int main() { SDL_Surface *screen; SDL_Event event; float i; int alp = 1; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { 4 BIBLIOTEKA SDL — ZASTOSOWANIA 19 screen = SDL_SetVideoMode(1024,768,24,SDL_HWSURFACE |SDL_DOUBLEBUF|SDL_FULLSCREEN); for (i=0; i<=2*pi; i+= pi/800) { SDL_PutPixel(screen, 512+cos(i)*200, 384+sin(i)*200, 255, 0, 0); } for (i=0; i<=2*pi; i+= pi/400) { SDL_PutPixel(screen, 512+cos(i)*100, 384+sin(i)*100, 0, 255, 0); } for (i=0; i<=2*pi; i+= pi/200) { SDL_PutPixel(screen, 512+cos(i)*50, 384+sin(i)*50, 0, 0, 255); } SDL_Delay(3000); SDL_DisplayBitmap("tlo.bmp", screen, 10, 10); SDL_DisplayBitmapAlpha("1.bmp", screen, 50, 50, 128); SDL_DisplayBitmapTransparent("2.bmp", screen, 50, 100, 255, 255, 255); for (;;) { SDL_WaitEvent(&event); switch (event.type) { case SDL_KEYDOWN: //Nacisnieto klawisz printf("Naciśnięto klawisz: %s\n", SDL_GetKeyName(event.key.keysym.sym)); if (event.key.keysym.sym==SDLK_ESCAPE) { goto end; } if (event.key.keysym.sym==SDLK_UP) { SDL_DisplayBitmapAlpha("3.bmp", screen, 250, 250, alp++); } break; case SDL_MOUSEMOTION: //Poruszono myszka if (event.motion.state & SDL_BUTTON(1)) 4 BIBLIOTEKA SDL — ZASTOSOWANIA 20 { printf("Ruch z pierwszym przyciskeim myszy\n"); } if (event.motion.state & SDL_BUTTON(2)) { printf("Ruch z drugim przyciskiem myszy\n"); } if (event.motion.state & SDL_BUTTON(3)) { printf("Ruch z trzecim przyciskiem myszy\n"); } SDL_PutPixel(screen, event.motion.x, event.motion.y, 123,23,230); printf("Przesunięto myszkę o %d,%d\n", event.motion.xrel, event.motion.yrel); break; case SDL_MOUSEBUTTONDOWN: //Nacisnieto przycisk myszy printf("Naciśnięto przycisk o numerze %d na " "punkcie o wspólrzędnych %d,%d\n", event.button.button, event.button.x, event.button.y); break; case SDL_QUIT: //Zakonczono program (np. nacisniecie przycisku // zamknij na oknie z programem) goto end; } } end: SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } Z przytoczonego wyżej programu nie wszystkie funkcje będą nam potrzebne — część można spokojnie usunąć. Pozostawienie funkcji main, SDL PutPixel 4 BIBLIOTEKA SDL — ZASTOSOWANIA 21 i ew. SDL DisplayBitmap jak na razie powinno wystarczyć. Wszystkie przykłady podawane w tej części artykułu będą obudowane w odrębne funkcje, tak by funkcja main nie wymagała wielu zmian za każdym razem. 4.3 4.3.1 Zbiór Mandelbrota Co to są fraktale? Za pewne wielu z Was słyszało o fraktalach, być może niektórzy widzieli różne fraktale. Zbiór Mandelbrota jest właśnie fraktalem, jednym z najsłynniejszych. Fraktal (łać. fractus - złamany, cząstkowy) to figura, dla której dwie wielkości matematyczne - wymiar topologiczny i wymiar Hausdorffa - są różne. Dla fraktala mianowicie wymiar Hausdorffa nie jest liczbą całkowitą. Fraktale zostały wprowadzone do matematyki przez francuskiego informatyka i matematyka Benoit Mandelbrota w latach siedemdziesiątych XX wieku. Jako punkt wyjścia posłużyły Mandelbrotowi obserwacje natury: płatków śniegu, zarysu linii brzegowej wyspy, konturu grzbietu górskiego, wirów na powierzchni cieczy. Za jedną z cech charakterystycznych fraktala uważa się samopodobieństwo, to znaczy podobieństwo fraktala do jego części. Dla figur samopodobnych można określić wielkość zwaną wymiarem samopodobieństwa lub wymiarem fraktalnym... Wikipedia, Wolna Encyklopedia Jak informuje definicja, Mandelbrot jest ojcem fraktali. Zbiór uzyskany przez niego powstał z wielokrotnego wykonywania pewnych prostych operacji na podzbiorze zbioru liczb zespolonych. Zanim przejdziemy do generowania graficznej interpretacji tego zbioru poznamy trochę teorii. Niezbędną podstawą są liczby zespolone. 4.3.2 Liczby zespolone Na początku przytoczę definicję liczby zespolonej z wolnej encyklopedii. Liczby zespolone są pewnym rozszerzeniem zbioru liczb rzeczywistych i podobnie jak one tworzą ciało. Rozszerzeniem pojęcia liczb zespolonych są kwaterniony. 4 BIBLIOTEKA SDL — ZASTOSOWANIA 22 Każdą liczbę zespoloną można przedstawić w postaci a + bi, gdzie a oraz b są liczbami rzeczywistymi, natomiast i obiektem o takiej własności, że i2 = −1. Przy tym i nazywamy jednostką urojoną, a częścią rzeczywistą, zaś b częścią urojoną liczby zespolonej a+bi. (. . . ) Dodawanie, odejmowanie i mnożenie liczb zespolonych wykonuje się tak samo jak odpowiednie operacje na wyrażeniach algebraicznych, należy tylko pamiętać o własności i2 = −1. (a + bi) + (c + di) = (a + c) + (b + d)i (1) (a + bi) − (c + di) = (a − c) + (b − d)i (2) (a+bi)(c+di) = ac+(bc+ad)i+bdi2 = (ac−bd)+(bc+ad)i. (3) (. . . ) Wikipedia, Wolna Encyklopedia Ta krótka definicja nie oddaje oczywiście całej istoty liczby zespolonych, jednak daje nam pewien ich obraz. Interpretacją geometryczną liczby zespolonej jest punkt na płaszczyźnie, któremu przypisujemy współrzędne? (a,b). Liczby zespolone są więc traktowane jak uporządkowane pary liczb rzeczywistych, dla których definiowane dodawanie i mnożenie pewnymi wzorami (z grubsza wzory takie jak (1),(2) i (3)). Wróćmy jednak do geometrycznej interpretacji liczby zespolonej, gdyż w przypadku generowania fraktali ona nas będzie najbardziej interesować. Punkt na płaszczyźnie może być przedstawiony w postaci pary licz x, y, ale również w postaci pary dwóch licz: długości promienia wodzącego punktu i kąta zawartego między nim a osią OX. Długość promienia wodzącego dla punktu będącego liczbą zespoloną równy jest wartości bezwzględnej tej liczby. Wartość bezwzględna liczby zespolonej jest uogólnieniem wartości bezwzględnej liczby rzeczywistej i definiuje się ją następująco: √ (4) |a + bi| = a2 + b2 Zaś kąt, ϑ, dla którego: sin ϑ = b |a + bi| (5) cos ϑ = a |a + bi| (6) i: nazywamy argumentem liczby zespolonej a + bi 4 BIBLIOTEKA SDL — ZASTOSOWANIA 23 Korzystając ze wzoru (3) łatwo możemy wyprowadzić wzór na podniesienie liczby zespolonej a+bi do kwadratu: (a + bi)2 = (a + bi)(a + bi) = a2 + 2abi + (bi)2 = (a2 − b2 ) + (2ab)i (7) Mam nadzieję, że ten krótki opis liczb zespolonych jest w miarę zrozumiały, dla każdego, nawet bardzo początkujących i wystarczający by móc zrozumieć ideę zbioru Mandelbrota. 4.3.3 Jeszcze trochę teorii Czym jest więc zbiór Mandelbrota? Może nie będzie to fachowa definicja, ale postaram się to jasno opisać. Każdemu punktowi (x, y), z analizowanego przez nas fragmentu płaszczyzny przypisujemy liczbę zespoloną c = a + bi taką, że a = x i b = y. W pierwszym przebiegu iteracji, liczbie zespolonej z, przypisujemy liczbę c. W każdym kolejnym przebiegu, przypisujemy jej liczbę postaci: zn + c Tak więc kilka pierwszych wyrazów ciągu liczb (dla n = 2) z wyglądałoby następująco: c, c2 + c, (c2 + c)2 + c, [(c2 + c)2 + c]2 + c, ... Jeśli bo nieskończenie wielu przebiegach iteracji, uzyskana liczba z będzie należała do koła o stałym promieniu r(czyli jej wartość bezwzględna będzie mniejsza lub równa r), to liczba zespolona c należy do zbioru Mandelbrota. Uzyskany w ten sposób zbiór możemy przedstawić na płaszczyźnie w postaci fraktala. Jednak taki fraktal składałby się jedynie z dwóch kolorów — jednego określającego, że dana liczba c należy do zbioru Mandelbrota oraz drugiego, stwierdzającego że liczba c do niego nie należy. Mimo iż fraktal taki posiadałby interesujący kształt, nie wyglądałby tak ładnie. Stosuje się więc kolorowanie - otóż każdemu punktowi przypisuje się kolor zależny od tego, po ilu iteracjach „wypadł” poza zbiór. Oczywiście przeprowadzenie nieskończenie wielu iteracji jest niemożliwe, można jedynie uzyskiwać przybliżone obrazy tego zbioru, a ponieważ liczba pikseli jest skończona to i tak dojdziemy do momentu, w którym zwiększanie liczby iteracji nie zmieni nic w naszym fraktalu. 4.3.4 Liczby zespolone i C W standardzie C99 wprowadzono obsługę liczb zespolonych. Większość współczesnych kompilatorów wspiera ten standard, więc powinna też sobie 4 BIBLIOTEKA SDL — ZASTOSOWANIA 24 dobrze radzić z liczbami zespolonymi. W języku C liczba zespolona przechowywana jest jako para dwóch liczb, z których jedna stanowi jej część rzeczywistą, a druga część urojoną. Każda z tych dwóch liczb może być typu float, double bądź long double. Liczbę zespoloną deklarujemy dodając po nazwach tych słów Complex Odpowiednie deklaracje typów zespolonych będą wyglądały więc następująco: float _Complex z1; double _Complex z2; long double _Complex z3; Można też tworzyć analogicznie liczby urojone (Liczbą urojoną nazywamy liczbę zespoloną z zerową częścią rzeczywistą, czyli po prostu ai, gdzie a ∈ R) zamiast Complex po nazwie typu używamy Imaginary . Ponadto w pliku nagłówkowym complex.h zdefiniowane są makrodefinicje complex i imaginary, znaczące odpowiednio: Complex i Imaginary. W nowych programach zaleca się stosowanie tych właśnie makrodefinicji. W pliku nagłówkowym complex.h zdefiniowano też kilka innych, istotnych makroinsturkcji i funkcji dotyczących typów zespolonych: Complex I, Imaginary I, I, oznaczające jednostkę urojoną, funkcje matematyczne: csin(), ccos(), ctan(), casin(), cacos(), catan(), csinh(), ccosh(), ctanh(), casinh(), cacosh(), catanh(), cpow(), csqrt(), cexp(), clog(), conj() (Sprzężenie zespolone), cproj() (Rzutowanie na sferę Riemanna), a także funkcje zwracjące liczby typu double: cabs() (Wartość bezwzględna), carg() (Argument), creal() (Część rzeczywista), cimag(), (Część urojona). Z wymienionych przeze mnie funkcji i definicji, interesować nas będzie jedynie definicja jednostki urojonej oraz funkcja cabs(), która zwraca moduł liczby zespolonej. 4.3.5 Koniec teorii Gdy poznaliśmy już nieco teorii na temat fraktali i liczb zespolonych, możemy przejść do generowania fraktali z użyciem biblioteki SDL. Z przytoczonego wcześniej programu przyda nam się funkcja SDL PutPixel, należy więc skopiować ją do nowego pliku. Wykorzystamy także fragmenty funkcji main, ale nie warto w tym celu kopiować całej funkcji. Tworzymy więc funkcje main w nowym pliku. Przydatna będzie jeszcze jedna funkcja - funkcja rysująca pojedynczy punkt fraktalu. W przykładowym programie nosi ona nazwę punkt i pobiera cztery parametry: 1. Wskaźnik na strukturę SDL Surface - na tej strukturze będzie zaznaczany punkt fraktalu 4 BIBLIOTEKA SDL — ZASTOSOWANIA 25 2. Współrzędną x punktu 3. Współrzędną y punktu 4. Liczbę iteracji, które ma wykonać. void punkt (SDL_Surface *screen, double x, double y, int k) Na początku tej funkcji deklarujemy zmienne. Potrzebne będą dwie zmienne zespolone np. z, c oraz jedna zmienna typu int - jako licznik w pętli wykonującej iterację. double complex z, c; int i; Następnie przypisujemy tym zmiennym wartości: z = 0 + 0 * I; c = x + y * I; Zmiennej c przypisaliśmy współrzędne punktu, natomiast zmiennej z - wartość zero. W naszej pętli zmienna z będzie przyjmowała kolejne wartości ciągu, wartość zmiennej c będzie zaś stała. Następnie tworzymy pętle typu for - która będzie się wykonywała od i = 0, do i mniejszego od k, w każdym przebiegu i zwiększamy o 1: for (i=0; i<k; i++) W każdym przebiegu pętli liczbie z przypisujemy liczbę z 2 + c. Następnie sprawdzamy czy uzyskana w ten sposób liczba z mieści się w okręgu o określonym promieniu (w przykładowym programie - 5). Aby to sprawdzić, sprawdzamy po prostu czy cabs(z) jest większe od 5. Jeśli jest, kończymy pętlę. Jeśli nie, wykonujemy dalej. Tak więc ciało pętli będzie wyglądało następująco: z = z*z + c; if (cabs(z)>5) { break; } Po zakończeniu pętli zaznaczamy piksel o odpowiednim kolorze. W najprostszej wersji przykładowego programu cały fraktal jest w odcieniach szarości. Współrzędne punktu, zależą od wielkości fragmentu płaszczyzny, na którym rysujemy fraktal. W praktyce aby ujrzeć cały fraktal wybieramy ok. 4 BIBLIOTEKA SDL — ZASTOSOWANIA 26 x ∈< −2.5, 2.5 >, y ∈< −1.5, 1.5 >. W przykładowym programie zawęziłem troszkę zakres zmiennej x: x ∈< −2.5, 1.5 > W tym przypadku aby uzyskać współrzędne punktu, x i y mnożymy przez 200, następnie do x dodajemy 500, a do y — 300. W ten sposób fraktal powstanie odbity symetrycznie względem osi OY, ale prosta ta jest jego osią symetrii, nie będzie więc żadnej różnicy. Kolor, jakim oznaczamy punkt zależy w dużej mierze od ilości iteracji, które będziemy wykonywać. Dla 32 przebiegów, aby uzyskać kolor od czarnego, do białego zmienną i mnożymy przez 8 i podajemy dla każdego składowego koloru r, g i b. SDL_PutPixel(screen, x*200+500, y*200+300, i*8, i*8, i*8); To tyle w funkcji punkt - oczywiście można ją jeszcze ulepszać. W całości wygląda ona następująco: void punkt (SDL_Surface *screen, double x, double y, int k) { double complex z, c; int i; z = 0 + 0 * I; c = x + y * I; for (i=0; i<k; i++) { z = z*z + c; if (cabs(z)>5) { break; } SDL_PutPixel(screen, x*200+500, y*200+300, i*8, i*8, i*8); } Teraz możemy przejść już do funkcji main. Na początku należy zadeklarować kilka zmiennych. Zacznijmy od wskaźnika na strukturę SDL Surface, która będzie stanowiła nam Surface podstawowy. Ponadto przydatne będą jeszcze dwie zmienne typu double - np. i, j jako liczniki w pętlach. Deklaracje zmiennych wyglądają następująco: SDL_Surface *screen; double i, j; Następnie inicjujemy bibliotekę SDL, i jeśli wszystko pójdzie dobrze, tworzymy podstawowy Surface: 4 BIBLIOTEKA SDL — ZASTOSOWANIA 27 if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_SWSURFACE|SDL_DOUBLEBUF); Po tych instrukcjach możemy napisać główną pętle programu - to w niej będziemy wywoływać funkcję punkt() dla każdego piksela na ekranie. W tym celu należy zbudować dwie zagnieżdżone pętle typu for. Jedna dla i = -2.5, do i ¡ 1.5, zwiększając o 0.005, druga dla j = -1.5 do 1.5, zwiększając także o 0.005. W każdym przebiegu zagnieżdżonej pętli wywołujemy funkcję punkt z odpowiednimi parametrami (jako liczbę iteracji podajemy 31, nie 32, ponieważ dla 32 w funkcji punkt byłoby wywołane SDL PutPixel z kolorami ustawionymi na 256 - a przyjmują one wartości od 0 do 255): for (i = -2.5; i<1.5; i+=0.005) { for (j = -1.5; j<1.5; j+=0.005) { punkt(screen, i, j, 31); } } Po tej pętli, warto odczekać kilka sekund, aby móc podziwiać efekty pracy :), można też (znając już zdarzenia) odczekać na naciśnięcie dowolnego klawisza. Należy też dopisać fragment kodu, wypisujący informacje, gdy zainicjowanie SDL się nie powiedzie: SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } Całość kodu programu generującego fraktal Mandelbrota wygląda następująco: #include #include #include #include "SDL.h" <stdio.h> <math.h> <complex.h> 4 BIBLIOTEKA SDL — ZASTOSOWANIA 28 void SDL_PutPixel(SDL_Surface *ekran, int x, int y, Uint8 R, Uint8 G, Uint8 B) { Uint8 *p = (Uint8 *)ekran->pixels + y * ekran->pitch + x * ekran->format->BytesPerPixel; if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = R; p[1] = G; p[2] = B; } else { p[0] = B; p[1] = G; p[2] = R; } SDL_UpdateRect(ekran, x, y, 1, 1); } void punkt (SDL_Surface *screen, double x, double y, int k) { double complex z, c; int i; z = 0 + 0 * I; c = x + y * I; for (i=0; i<k; i++) { z = z*z + c; if (cabs(z)>5) { break; } } SDL_PutPixel(screen, x*200+500, y*200+300, i*8, i*8, i*8); } int main() { SDL_Surface *screen; double i, j; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 29 if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_SWSURFACE|SDL_DOUBLEBUF); for (i = -2.5; i<1.5; i+=0.005) { for (j = -1.5; j<1.5; j+=0.005) { punkt(screen, i, j, 31); } } SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } Kompilujemy program pod linuksem poleceniem gcc fraktal.c -o fraktal ‘sdl-config --libs --cflags‘ a następnie uruchamiamy ./fraktal Po kilku sekundach powinniśmy ujrzeć na ekranie wygenerowany zbiór Mandelbrota. 4.3.6 Ulepszanie programu Powyższy kod programu można znacznie jeszcze modyfikować, ulepszać, tworzyć różne wersje. Możemy także nieco go przyspieszyć. Podczas generowania zbioru, widzimy po kolei jak rysują się na ekranie wszystkie punkty. Daje to taką zaletę, że już w trakcie generowania możemy podejrzeć jak fraktal będzie wyglądał, jednak dość znacznie zwiększa czas jego tworzenia. Gdy zechcemy się przyjrzeć temu dokładniej, odkryjemy, że za wyświetlenie punktu na naszym ekranie odpowiada wywołanie SDL UpdateRect w funkcji SDL PutPixel. Odświeża ona wybrany, prostokątny fragment ekranu. 4 BIBLIOTEKA SDL — ZASTOSOWANIA 30 W tym przypadku - tylko pojedynczy piksel. Jak widać, jest więc ona wywoływana aż 480000 razy! A jak wspominałem we wcześniejszych częściach artykułu, są trzy funkcje odpowiadające za odświeżenie. Jedną z nich była funkcja SDL Flip, która odświeżała cały surface. Zakomentujmy więc w funkcji SDL PutPikxel wywołanie SDL UpdateRect, a w funkcji main dodajmy wywołanie SDL Flip(screen) przed SDL Delay. Nie będę na razie przytaczał całego kodu programu. Oto małe porównanie czasu wykonania(funkcja SDL Delay wyłączona oraz 32 iteracje): root@po142:~/prg/sdl# time ./fraktal real 0m54.149s user 0m3.210s sys 0m4.020s root@po142:~/prg/sdl# gcc fraktal.c -o fraktal ‘sdl-config --libs --cflags‘ root@po142:~/prg/sdl# time ./fraktal real 0m2.725s user 0m0.690s sys 0m0.010s root@po142:~/prg/sdl# Jak widać, przyspieszenie wynikające z zamienienia wywołań SDL UpdateRect na jedno wywołanie SDL Flip jest 20-30 krotne! Różnica ta jest na prawdę duża. Należy zwrócić na to uwagę, i w każdym programie, gdy zaznaczamy więcej niż kilka pikseli, zastosować zmienioną wersję SDL PutPixel i następnie odświeżać albo większą grupę pikseli funkcją SDL UpdateRect i SDL UpdateRects, lub odświeżać cały ekran funkcją SDL Flip. Innym ulepszeniem, które możemy wprowadzić jest zastosowanie różnych kolorów. Wymaga to użycia jakiejś palety, określającej jaki kolor przypada danej liczbie wykonanych przebiegów. Można użyć jakiejś stałej palety, można też spróbować wygenerować ją losowo. W przykładowym programie, zastosuję tą drugą wersję. Na początku, po dyrektywach #include, dodajmy trzy zmienne globalne — tablice, w których będziemy przechowywać naszą paletę, oraz makrodefinicję, której używać będziemy zamiast liczby iteracji. Tablice mają ilość elementów równą tej makrodefinicji: #define FRAKT 32 Uint8 red[FRAKT], green[FRAKT], blue[FRAKT]; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 31 Tablice są typu Uint8, czyli zmienna jednobajtowa bez znaku. Następnie w funkcji main po deklaracjach zmiennych dodajmy jeszcze jedną (będzie nam potrzebna jako licznik w pętli generującej paletę): int k; Po deklaracji tej zmiennej, dajemy jedną pętlę typu for, wykonywującą się od k = 0, do k mniejszego od FRAKT−1 (od frakt odejmujemy 1, ponieważ chcemy by w naszym przykładowym programie sam zbiór Mandelbrota miał kolor stały), w każdym przebiegu zwiększając k o 1. W ciele pętli losujemy 3 liczby z zakresu od 0 do 255 i przypisujemy każdą z nich do red[k], green[k], blue[k]. Cała pętla wyglądała będzie więc następująco: for (k = 0; k < FRAKT-1; k++) { red[k]=rand()%256; green[k]=rand()%256; blue[k]=rand()%256; } Następnie, po tej pętli, ustalamy jeszcze kolor dla samego zbioru Mandelbrota. W tym przypadku — biały: red[FRAKT-1]=255; green[FRAKT-1]=255; blue[FRAKT-1]=255; Musimy dokonać jeszcze jednej zmiany w funkcji punkt(). W wywołaniu funkcji SDL PutPixel, zamieńmy i*8, na odpowiednio: red[i], green[i], blue[i]: SDL_PutPixel(screen, x*200+500, y*200+300, red[i], green[i], blue[i]); Jeszcze jedna mała zmiana kosmetyczna, skoro już zdefiniowaliśmy liczbę iteracji to wstawmy tą definicję, zamiast stałej liczbowej w wywołaniu funkcji punkt. Oto cały kod programu, po wykonaniu tych dwóch zmian (optymalizacji i kolorów): #include #include #include #include "SDL.h" <stdio.h> <math.h> <complex.h> #define FRAKT 32 4 BIBLIOTEKA SDL — ZASTOSOWANIA 32 Uint8 red[FRAKT], green[FRAKT], blue[FRAKT]; void SDL_PutPixel(SDL_Surface *ekran, int x, int y, Uint8 R, Uint8 G, Uint8 B) { Uint8 *p = (Uint8 *)ekran->pixels + y * ekran->pitch + x * ekran->format->BytesPerPixel; if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = R; p[1] = G; p[2] = B; } else { p[0] = B; p[1] = G; p[2] = R; } } void punkt (SDL_Surface *screen, double x, double y, int k) { double complex z, c; int i; z = 0 + 0 * I; c = x + y * I; for (i=0; i<k; i++) { z = z*z + c; if (cabs(z)>5) { break; } } SDL_PutPixel(screen, x*200+500, y*200+300, red[i], green[i], blue[i]); } int main() { SDL_Surface *screen; double i, j; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 33 int k; for (k = 0; k < FRAKT-1; k++) { red[k]=rand()%256; green[k]=rand()%256; blue[k]=rand()%256; } red[FRAKT-1]=255; green[FRAKT-1]=255; blue[FRAKT-1]=255; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_SWSURFACE|SDL_DOUBLEBUF); for (i = -2.5; i<1.5; i+=0.005) { for (j = -1.5; j<1.5; j+=0.005) { punkt(screen, i, j, FRAKT-1); } } SDL_Flip(screen); SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } Po skompilowaniu tego programu powinniśmy ujrzeć szybko wygenerowany oraz kolorowy fraktal. 4.4 4.4.1 Algorytm rysowania linii Wprowadzenie Standardowo biblioteka SDL nie daje nam żadnego algorytmu rysowania linii. Algorytm ten nie jest zbytnio skomplikowany, lecz za to jest często bardzo 4 BIBLIOTEKA SDL — ZASTOSOWANIA 34 potrzebny. Przedstawię tutaj jeden z kilku, będzie on myślę dosyć prosty, a za razem szybki i dający dobre efekty. 4.4.2 Opis algorytmu Zaprezentowany tutaj przeze mnie algorytm jest dosyć prosty. Opiera się on na rysowaniu poziomych linii o odpowiedniej długości, które w istocie połączą dwa punkty - (x1 , y1 ) oraz (x2 , y2 ). Na początku musimy uzyskać pewność, że punkt o mniejszej współrzędnej y będzie pierwszy. Porównujemy więc współrzędne obydwu punktów i w razie potrzeby zamieniamy te punkty, jeśli są równe - nie robimy nic. Następnie zwiększamy współrzędną końcowego punktu o 1. Gdybyśmy tego nie zrobili, nasza linia nie kończyłaby się w tym punkcie, a jedynie jeden piksel nad nim. Następnie obliczamy przyrost x: ∆x = x2 − x1 y2 − y1 Dzięki ewentualnej zamianie punktów y2 nigdy nie będzie mniejsze od y1 oraz dzięki zwiększeniu y2 o 1 nie musimy się martwić o sytuacje, w której mianownik byłby zerem. Po tych wstępnych przygotowaniach rozpoczyna się główna pętla algorytmu, w której zwiększając z każdym przebiegiem zmienną y o 1, począwszy od y1 aż do y2 rysujemy poziomą linię od punktu (x11 , y) aż do punktu (x12 , y), gdzie x12 = x11 + ∆x. Dla ułatwienia zaprogramowania algorytmu x12 traktujemy jako osobną zmienną, zwiększaną na początku pętli o ∆x. Zwiększamy na końcu przebiegu o ∆x wartość zmiennej x11 Gdy zmienna x12 jest mniejsza od zmiennej x11 zamieniamy punkty (x11 , y) i (x12 , y) (przed narysowaniem linii poziomej). Rysowanie linii poziomej jest prostą pętlą zaznaczającą poszczególne, kolejne piksele. Jeszcze przed główną pętlą przypisujemy zmiennym x11 i x12 wartość x1 . W powyższym algorytmie, zmienne x1 , x2 , y1 , y2 , y są liczbami całkowitymi, zaś zmienne x11 , x12 , ∆x — liczbami rzeczywistymi 4.4.3 Kod Gdy poznaliśmy już co nieco algorytm rysujący linię, możemy przejść do jego implementacji z użyciem biblioteki SDL. Z kodu źródłowego przytoczonego na początku tego artykułu, kopiujemy funkcję SDL PutPixel do nowego pliku. Tworzymy też funkcję main, oraz funkcję odpowiadającą za rysowanie linii. Funkcja rysująca linię (w przykładowym programie nazywa się linia przyjmuje 8 parametrów. Pierwszy z nich jest wskaźnikiem na strukturę SDL Surface, na której rysowana będzie linia, kolejne 2 parametry są współrzędnymi jednego końca odcinka, kolejne, również dwa są współrzędnymi 4 BIBLIOTEKA SDL — ZASTOSOWANIA 35 drugiego końca tego odcinka. Kolejne 3 parametry zawierają odpowiednie składowe koloru linii: r, g, b. void linia (SDL_Surface *screen, int x1, int y1, int x2, int y2, int r, int g, int b) Następnie deklarujemy odpowiednie zmienne: y, dx, x11, x12. Potrzebny będzie też licznik w pętli rysującej linię poziomą, tak więc dokładamy jeszcze jedną zmienną np. i. Należy pamiętać, że zmienne dx, x11 i x12 deklarujemy jako liczby zmiennoprzecinkowe. float dx, x11, x12; int y; int i; Teraz przechodzimy już do właściwej implementacji algorytmu. Na początku musimy porównać odpowiednie współrzędne i w razie czego zamienić punkty miejscami (należy pamiętać, że musimy zamieniać obydwie współrzędne punktów, zarówno x jak i y. if (y2<y1) { x1=x1+x2; x2=x1-x2; x1=x1-x2; y1=y1+y2; y2=y1-y2; y1=y1-y2; } Dla niektórych (być może wielu) zaprezentowany tutaj sposób zamiany wartości zmiennych może wydać się dziwny, jest to jednak prosta sztuczka, która nie wymaga tworzenia dodatkowej zmiennej tymczasowej. W prosty sposób (na kartce, lub nawet w pamięci) można sprawdzić, iż zamiana taka daje poprawne wyniki. Następnie zwiększamy zmienną y2 o 1, obliczamy dx i przypisujemy wartość x1 zmienny x11 i x12: y2++; dx=(float)(x2-x1)/(float)(y2-y1); x11 = x1; x12 = x1; Następnie w naszym kodzie rozpoczyna się główna pętla. 4 BIBLIOTEKA SDL — ZASTOSOWANIA 36 for(y = y1; y < y2; y++) Na początku każdego przebiegu zwiększamy wartość zmiennej x12 o dx, po czym porównujemy x11 i x12. Jeśli x11 jest większe zamieniamy te zmienne wartościami. Wykorzystano tą samą metodę co w poprzedniej zamianie wartości. x12+=dx; if (x12<x11) { x11=x11+x12; x12=x11-x12; x11=x11-x12; } Po tych instrukcjach rozpoczyna się zagnieżdżona pętla w której rysujemy linię poziomą od punktu (x11, y) do punktu (x12, y). Do zaokrąglania używamy funkcji lround: for (i=lround(x11);i<=lround(x12); i++) { SDL_PutPixel(screen, i, y, r, g, b); } Na końcu przebiegu pętli zwiększamy zmienną x11 o wartość dx: x11+=dx; Kończymy blok pętli i możemy już zakończyć naszą funkcję rysującą linię. Możemy przejść do funkcji main. Ta wygląda dosyć standardowo, inicjalizujemy bibliotekę, po czym dla sprawdzenia wywołujemy kilka razy funkcję linia z różnymi parametrami. Oto cały kod programu: #include "SDL.h" #include <stdio.h> void SDL_PutPixel(SDL_Surface *ekran, int x, int y, Uint8 R, Uint8 G, Uint8 B) { Uint8 *p = (Uint8 *)ekran->pixels + y * ekran->pitch + x * ekran->format->BytesPerPixel; if(SDL_BYTEORDER == SDL_BIG_ENDIAN) { p[0] = R; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 37 p[1] = G; p[2] = B; } else { p[0] = B; p[1] = G; p[2] = R; } SDL_UpdateRect(ekran, x, y, 1, 1); } void linia (SDL_Surface *screen, int x1, int y1, int x2, int y2, int r, int g, int b) { float dx, x11, x12; int y; int i; if (y2<y1) { x1=x1+x2; x2=x1-x2; x1=x1-x2; y1=y1+y2; y2=y1-y2; y1=y1-y2; } y2++; dx=(float)(x2-x1)/(float)(y2-y1); x11 = x1; x12 = x1; for(y = y1; y < y2; y++) { x12+=dx; if (x12<x11) { x11=x11+x12; x12=x11-x12; x11=x11-x12; } for (i=lround(x11);i<=lround(x12); i++) 4 BIBLIOTEKA SDL — ZASTOSOWANIA 38 { SDL_PutPixel(screen, i, y, r, g, b); } x11+=dx; } } int main() { SDL_Surface *screen; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_SWSURFACE|SDL_DOUBLEBUF); linia(screen, 0, 0, 20, 40, 0, 255, 255); linia(screen, 1, 1, 200, 2, 255, 0, 255); linia(screen, 1, 21, 200, 23, 255, 255, 0); linia(screen, 1, 10, 200, 10, 0, 0, 255); linia(screen, 300, 1, 300, 200, 255, 0, 0); linia(screen, 400, 1, 401, 200, 0, 255, 0); linia(screen, 500, 10, 450, 30, 255, 128, 0); linia(screen, 100, 400, 50, 350, 0, 128, 255); SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } 4.5 4.5.1 Wyświetlanie znaków z użyciem biblioteki FreeType2 Wstęp Bardzo często zachodzi potrzeba w wielu programach korzystających z trybu tekstowego po prostu wyświetlania tekstu. Biblioteka SDL nie dostarcza takich funkcji, należy więc je stworzyć samemu. Istnieje w prawdzie wiele różnych bibliotek dla SDL które wykonują takie funkcje. Możliwości ich jednak 4 BIBLIOTEKA SDL — ZASTOSOWANIA 39 często są bardzo niewielkie. Biblioteka FreeType obsługuje wiele formatów czcionek. Daje ponadto możliwość skalowania ich oraz wiele innych funkcji. Nie będę się jednak na tym skupiał — opisanie tej biblioteki mogłoby być dobrym tematem na kolejny cykl artykułów. Być może kiedyś na stronie www.kodowanie.prv.pl taki cykl się ukaże. Podobno w manualu FreeType znajduje się opis wyświetlania liter z użyciem biblioteki SDL. Niestety nie udało mi się odszukać tego kodu. Jednak fragmenty - które widziałem na grupach Usenetowych nie działały poprawnie, ponadto można było je znacznie ulepszyć. Przedstawione tutaj algorytmy, są już wersjami ulepszonymi (dającymi np. możliwość wyświetlania tekstu w kolorze innym niż biały na czarnym tle). Na początku należy ściągnąć i zainstalować bibliotekę FreeType. Bibliotekę tę można ściągnąć z oficjalnej witryny: http://www.freetype.org/index2.html. Biblioteka FreeType podobnie jak sdl dysponuje narzędziem podającym odpowiednie parametry do wywołania kompilatora i linkera. Do polecenia kompilującego nasze programy będziemy musieli dodać jeszcze ‘freetype-config --libs --cflags‘ Tak więc program kompilować będziemy poleceniem: g++ tekst.c -o tekst.o ‘sdl-config --libs --cflags‘ \ ‘freetype-config --libs --cflags‘; Należy także zaopatrzyć się w jakiś plik czcionki (najlepiej TrueType), którą będziemy używać w programach. Oczywiście w bardziej zaawansowanych projektach często wykorzystuje się kilka różnych czcionek. 4.5.2 Wyświetlanie pojedynczej litery Po tych kilku wstępnych informacjach o bibliotece FreeType możemy przejść do konstrukcji pierwszego programu wykorzystującego tą bibliotekę - wyświetli on na ekranie pojedynczą literę. Z przytoczonego na początku artykułu programu nie będą potrzebne żadne funkcje. Tworzymy nowy plik w którym umieszczamy podstawowy kod inicjalizujący bibliotekę: int main() { SDL_Surface *screen; SDL_Event event; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 40 float i; int alp = 1; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_HWSURFACE|SDL_DOUBLEBUF); SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } większość dalszych instrukcji umieszczać będziemy między wywołaniem SDL SetVideoMode a SDL Delay. Na początku musimy jednak dołączyć dodatkowe pliki nagłówkowe oraz zadeklarować kilka zmiennych. Na początku pliku dodajemy: #include <ft2build.h> #include FT_FREETYPE_H W dalszej części, po deklaracji zmiennych używanych przez SDL deklarujemy dwie zmienne potrzebne bibliotece FreeType: FT_Library ft; FT_Face czcionka; Następnie po funkcji SDL SetVideoMode inicjalizujemy bibliotekę FreeType i wybieramy plik czcionki FT_Init_FreeType(&ft); FT_New_Face(ft, "./goodtime.ttf", 0, &czcionka); Następnie tworzymy funkcję, która będzie nam wyświetlała na ekranie jedną literę. Pobiera ona 7 parametrów: wskaźnik na surface, współrzędne prawego górnego rogu litery, rozmiar, strukturę FT Face zawierającą wybraną czcionkę, wskaźnik na strukturę SDL Color (będziemy otrzymywać go z funkcji pomocniczej, o której napiszemy za chwilkę — jest to paleta kolorów) oraz znak, który ma być wyświetlany. Funkcja zwraca typ int. 4 BIBLIOTEKA SDL — ZASTOSOWANIA 41 int litera(SDL_Surface *ekran, int x, int y, int rozmiar, FT_Face czcionka, SDL_Color *tpaleta, char l) Na początku funkcji deklarujemy dwie zmienne: wskaźnik na SDL Surface (będzie służył jako pomocniczy surface) oraz strukturę SDL Rec. SDL_Surface *fontbuf; SDL_Rect cel; Następnie wybieramy rozmiar czcionki, znak oraz renederujemy czcionkę jako bitmapę: FT_Set_Pixel_Sizes(czcionka, 0, rozmiar); FT_Load_Glyph(czcionka, FT_Get_Char_Index(czcionka, l), FT_LOAD_DEFAULT); FT_Render_Glyph(czcionka->glyph, ft_render_mode_normal); Uzyskana bitmapa znajduje się w tablicy czcionka-¿glyph-¿bitmap.buffer, natomiast zmienne czcionka-¿glyph-¿bitmap.pitch, czcionka-¿glyph¿bitmap.rows, zawierają odpowiednio długość jednego wiersza oraz liczbę wierszy. Przydatna będzie funkcja SDL CreateRGBSurfaceFrom. We wspomnianym wcześniej manuali do FreeType używana jest funkcja SDL CreateRGBSurface. Jest ona wolniejsza, gdyż używając jej musimy skopiować ręcznie bitmapę. Funkcja SDL CreateRGBSurfaceFrom nie kopiuje danych lecz używa już istniejących. Funkcja ta pobiera 8 parametrów. Pierwszy z nich do dane pikseli, następne dwa to szerokość i wysokość bitmapy, kolejnym jest liczba bitów na piksel. Piąty parametr to długość wiersza w bajtach. Kolejne 4 parametry to maski dla kanałów czerwonego, zielonego, niebieskiego oraz kanału Alpha. W przykładowym programie funkcję tą wywołujemy z następującymi parametrami: fontbuf = SDL_CreateRGBSurfaceFrom(czcionka->glyph->bitmap.buffer, czcionka->glyph->bitmap.pitch, czcionka->glyph->bitmap.rows, 8, czcionka->glyph->bitmap.pitch, 0, 0, 0, 0); Następnie ustawiamy dla nowo utworzonego Surface paletę. Służy do tego funkcja SDL SetPallete. Przyjmuje ona 5 parametrów. Pierwszy z nich to wskaźnik na strukturę SDL Surface, której dotyczy ustawienie, kolejny parametr przyjmuje dwie flagi: SDL LOGPAL i/lub SDL PHYSPAL. Opowiadają one paletom logicznym (używanych przy kopiowaniu na drugi Surface) i fizycznym (używanych przy wyświetlaniu na urządzeniu). Kolejny parametr 4 BIBLIOTEKA SDL — ZASTOSOWANIA 42 to wskaźnik na strukturę SDL Color. Następne dwa to odpowiednio numer początkowego koloru oraz liczba kolorów. Przy wyświetlaniu liter funkcję tę będziemy wywoływać w sposób następujący: SDL_SetPalette(fontbuf, SDL_LOGPAL|SDL_PHYSPAL, tpaleta, 0, 256); Kolejnym krokiem jest przypisanie odpowiednich wartości polom struktury cel: cel.x=x; cel.y=y+rozmiar-czcionka->glyph->bitmap_top; cel.w=fontbuf->w; cel.h=fontbuf->h; Myślę, że powyższy kod jest oczywisty. Do współrzędnej y dodawany jest rozmiar czcionki (wyrażony jest on w pikselach) a następnie odjęte przesunięcie czcionki względem dolnej krawędzi. Jest to wykonywane po to, aby wszystkie fonty w jednej linii były na równym poziomie. Kolejna linia jest opcjonalna, aczkolwiek przydatna. Ustawia ona kolor czarny jako kolor przezroczysty: SDL_SetColorKey(fontbuf, SDL_SRCCOLORKEY, SDL_MapRGB(fontbuf->format,0,0,0)); Teraz przyszedł czas na właściwe wyświetlenie litery. Wykonujemy funkcję SDL BlitSurface po czym SDL UpdateRects: SDL_BlitSurface(fontbuf, NULL, ekran, &cel); SDL_UpdateRects(ekran, 1, &cel); Na końcu zwalniamy nie potrzebny już Surface fontbuf oraz zwracamy szerokość znaku. W prawdzie na razie nie jest ona do niczego potrzebna, ale gdy będziemy wypisywać teksty dłuższe niż jedna litera - będzie to przydatne. SDL_FreeSurface(fontbuf); return fontbuf->w; To już cała funkcja wyświetlająca literę. Potrzebna będzie jeszcze jedna, wspomniana już wcześniej pomocnicza funkcja przygotowująca odpowiednią paletę kolorów. Funkcja ta pobiera trzy parametry: kolor r, g i b tekstu. Zwraca wskaźnik na strukturę SDL Color: SDL_Color *textcolor(Uint8 r, Uint8 g, Uint8 b) 4 BIBLIOTEKA SDL — ZASTOSOWANIA 43 Struktura SDL Color składa się z 4 pól typu Uint8. 3 określają składowe koloru r, g, b. Czwarte pole jest nieużywane. Na początku deklarujemy wskaźnik na tą strukturę oraz zmienną pomocniczą i służącą jako licznik w pętli. Następnie alokujemy pamięć na 256 struktur i przypisujemy ją do wskaźnika SDL_Color *tpaleta; int i; tpaleta = (SDL_Color *)malloc(sizeof(SDL_Color)*256); Następnie tworzymy pętle, która przypisze odpowiednim składowym wartości. Pętle wykonujemy dla i od 0 do 255 przypisując im wartości r, g, b z parametrów funkcji pomnożone przez i/255. for (i = 0; i < 256; i++) { tpaleta[i].r = r*i/255; tpaleta[i].g = g*i/255; tpaleta[i].b = b*i/255; } Na końcu zwracamy tpaleta: return tpaleta; To już cały niezbędny kod do wyświetlenia litery. Po funkcjach inicjalizujących bibliotekę FreeType dodajemy kilka wywołań funkcji litera dla sprawdzenia naszego kodu. Oto cały program: #include #include #include #include "SDL.h" <stdio.h> <ft2build.h> FT_FREETYPE_H SDL_Color *textcolor(Uint8 r, Uint8 g, Uint8 b) { SDL_Color *tpaleta; int i; tpaleta = (SDL_Color *)malloc(sizeof(SDL_Color)*256); for (i = 0; i < 256; i++) { tpaleta[i].r = r*i/255; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 44 tpaleta[i].g = g*i/255; tpaleta[i].b = b*i/255; } return tpaleta; } int litera(SDL_Surface *ekran, int x, int y, int rozmiar, FT_Face czcionka, SDL_Color *tpaleta, char l) { SDL_Surface *fontbuf; SDL_Rect cel; FT_Set_Pixel_Sizes(czcionka, 0, rozmiar); FT_Load_Glyph(czcionka, FT_Get_Char_Index(czcionka, l), FT_LOAD_DEFAULT); FT_Render_Glyph(czcionka->glyph, ft_render_mode_normal); fontbuf = SDL_CreateRGBSurfaceFrom(czcionka->glyph->bitmap.buffer, czcionka->glyph->bitmap.pitch, czcionka->glyph->bitmap.rows, 8, czcionka->glyph->bitmap.pitch, 0, 0, 0, 0); SDL_SetPalette(fontbuf, SDL_LOGPAL|SDL_PHYSPAL, tpaleta, 0, 256); cel.x=x; cel.y=y+rozmiar-czcionka->glyph->bitmap_top; cel.w=fontbuf->w; cel.h=fontbuf->h; SDL_SetColorKey(fontbuf, SDL_SRCCOLORKEY, SDL_MapRGB(fontbuf->format,0,0,0)); SDL_BlitSurface(fontbuf, NULL, ekran, &cel); SDL_UpdateRects(ekran, 1, &cel); SDL_FreeSurface(fontbuf); return fontbuf->w; } int main() { SDL_Surface *screen; SDL_Event event; FT_Library ft; FT_Face czcionka; float i; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 45 int alp = 1; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_HWSURFACE|SDL_DOUBLEBUF); FT_Init_FreeType(&ft); FT_New_Face(ft, "./goodtime.ttf", 0, &czcionka); litera(screen, 0, 0, 120, czcionka, textcolor(255, 150, 150), ’A’); litera(screen, 100, 100, 12, czcionka, textcolor(255, 150, 150), ’l’); SDL_Delay(5000); SDL_Quit(); return 0; } else { printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } Kompilujemy go wcześniej podanym poleceniem. 4.5.3 Wyświetlamy dłuższe napisy Zaprezentuje tutaj drobne rozszerzenie powyższego kodu źródłowego. Napisany program wyświetla tylko pojedyncze znaki. Jego rozszerzenie, dawało będzie możliwość wyświetlania dłuższych łańcuchów - a także liczb i innych możliwości, które posiada zwykłe, standardowe wyjście. Na początku dołączamy kolejny plik nagłówkowy (będzie potrzebny do zmiennej listy parametrów) stdarg.h: #include <stdarg.h> Następnie tworzymy funkcję podobną do funkcji litera, z tym że zamiast ostatniego parametru podajemy parametr char * format, po którym zaczyna się zmienna lista argumentów. int napis(SDL_Surface *ekran, int x, int y, int rozmiar, FT_Face czcionka, SDL_Color *tpaleta, const char *format, ...) Na początku funkcji deklarujemy dwie zmienne: char *l, *x; va_list arglist; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 46 Następnie alokujemy pamięć na tymczasowy łańcuch znaków, oraz makroinstrukcją va start tworzymy listę parametrów. l = (char *)malloc(sizeof(char)*200); va_start(arglist, format); W kolejnym etapie wywołujemy funkcję vsprintf która przedstawi nam naszą listę argumentów w postaci łańcucha znaków zgodnie z informacjami podanymi w parametrze format: vsprintf(l, format, arglist); Następnie wskaźnikowi m przypisujemy wskaźnik l. Jest to potrzebne, ponieważ będziemy inkrementować l, a zaalokowaną pamięć należy zwolnić. m = l; Dalej w kodzie jest pętla, w której rysujemy wszystkie litery, należy za każdym razem zwiększać współrzędną x o szerokość litery oraz o jakiś ustalony odstęp. Gdy napotkamy na spację, dodajemy dodatkowy odstęp kilku pikseli. while (*l!=’\0’) { x+=litera(ekran, x, y, rozmiar, czcionka, tpaleta, *l)+3; if (*l==’ ’) x+=6; l++; } Na końcu zwalniamy zaalokowaną pamięć. free(m); Dodajmy jeszcze kilka wywołań naszej nowej funkcji dla sprawdzenia jej działania. Oto pełny kod programu: #include #include #include #include #include "SDL.h" <stdio.h> <stdarg.h> <ft2build.h> FT_FREETYPE_H SDL_Color *textcolor(Uint8 r, Uint8 g, Uint8 b) { SDL_Color *tpaleta; int i; 4 BIBLIOTEKA SDL — ZASTOSOWANIA 47 tpaleta = (SDL_Color *)malloc(sizeof(SDL_Color)*256); for (i = 0; i < 256; i++) { tpaleta[i].r = r*i/255; tpaleta[i].g = g*i/255; tpaleta[i].b = b*i/255; } return tpaleta; } int litera(SDL_Surface *ekran, int x, int y, int rozmiar, FT_Face czcionka, SDL_Color *tpaleta, char l) { SDL_Surface *fontbuf; SDL_Rect cel; FT_Set_Pixel_Sizes(czcionka, 0, rozmiar); FT_Load_Glyph(czcionka, FT_Get_Char_Index(czcionka, l), FT_LOAD_DEFAULT); FT_Render_Glyph(czcionka->glyph, ft_render_mode_normal); fontbuf = SDL_CreateRGBSurfaceFrom(czcionka->glyph->bitmap.buffer, czcionka->glyph->bitmap.pitch, czcionka->glyph->bitmap.rows, 8, czcionka->glyph->bitmap.pitch, 0, 0, 0, 0); SDL_SetPalette(fontbuf, SDL_LOGPAL|SDL_PHYSPAL, tpaleta, 0, 256); cel.x=x; cel.y=y+rozmiar-czcionka->glyph->bitmap_top; cel.w=fontbuf->w; cel.h=fontbuf->h; SDL_SetColorKey(fontbuf, SDL_SRCCOLORKEY, SDL_MapRGB(fontbuf->format,0,0,0)); SDL_BlitSurface(fontbuf, NULL, ekran, &cel); SDL_UpdateRects(ekran, 1, &cel); SDL_FreeSurface(fontbuf); return fontbuf->w; } int napis(SDL_Surface *ekran, int x, int y, int rozmiar, FT_Face czcionka, SDL_Color *tpaleta, const char *format, ...) 4 BIBLIOTEKA SDL — ZASTOSOWANIA 48 { char *l, *m; va_list arglist; l = (char *)malloc(sizeof(char)*200); va_start(arglist, format); vsprintf(l, format, arglist); m = l; while (*l!=’\0’) { x+=litera(ekran, x, y, rozmiar, czcionka, tpaleta, *l)+3; if (*l==’ ’) x+=6; l++; } free(m); } int main() { SDL_Surface *screen; SDL_Event event; FT_Library ft; FT_Face czcionka; float i; int alp = 1; if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_EVENTTHREAD)==0) { screen = SDL_SetVideoMode(800,600,24,SDL_HWSURFACE|SDL_DOUBLEBUF); FT_Init_FreeType(&ft); FT_New_Face(ft, "./goodtime.ttf", 0, &czcionka); litera(screen, 0, 0, 120, czcionka, textcolor(255, 150, 150), ’A’); litera(screen, 100, 100, 12, czcionka, textcolor(255, 150, 150), ’l’); napis(screen, 200, 200, 20, czcionka, textcolor(200, 100, 0), "SDL to swietna biblioteka"); napis(screen, 200, 250, 20, czcionka, textcolor(200, 100, 0), "(c) by Jakub Laskowski %d v %.1f", 2004, 1.0); SDL_Delay(5000); SDL_Quit(); return 0; } else { 5 TUTORIAL O C++ — CZĘŚĆ 4 49 printf("Coś poszło nie tak: %s/n",SDL_GetError()); return -1; } } 4.6 Zakończenie W tej części artykułu omówiłem kilka prostych, niemalże podstawowych zagadnień związanych z grafiką. Jak widać biblioteka SDL nie jest pod żadnym względem „upośledzona” i bez problemu można zaimplementować w niej nawet zaawansowane algorytmy. 5 Tutorial o C++ — część 4 Mr. Heh 5.1 ZAKRES WAŻNOŚCI: czyli skołowali, a potem zaginęło ;) Ciekawy tytuł ? Brrr... Ale spoko sprawa jest prosta. Tylko trzeba mieć podejście ;) Kiedy chowasz coś do plecaka, to na pewno [chociaż przez krótki czas...] wiesz, że to się tam znajduje. Kiedy podejdzie do ciebie kolega, to nie ma on pojęcia co masz w plecaku... Wiedziałby [raczej...] gdybyś trzymał to w rękach. Po co to wprowadzenie? Kiedy mówisz kompilatorowi, że będziesz używał zmiennej możesz to zrobić na dwa sposoby: globalnie - czyli z naszej opowieści trzymasz na rękach lokalnie - chowasz do plecaka jak to wygląda podczas programowania? globalnie: int zmienna; int main () { ... } 5 TUTORIAL O C++ — CZĘŚĆ 4 50 Tak jak w naszym drugim programie. Jakie ma to zalety? Nasza funkcja stan() mogła użyć tych zmiennych, bo nasz program ’niósł je na rękach’ i wiedziała, że w ogóle istnieją. No dobrze. Ale czy może być inaczej? No tak. Możesz schować do plecaka! Jak? void funkcja() { int naszaZmiennaSchowana; } O coś nowego! Tak! I co z tego? Schowaliśmy zmienną w miejsce, które zna tylko funkcja(). *Nikt!* nie będzie mógł skorzystać z tej zmiennej, tylko właściciel. Dlatego: void funkcja() { int naszaZmiennaSchowana; } void funkcja2() { naszaZmiennaSchowana = 12345; } mamy błąd!!!!!!!!! zapamiętaj, że kompilator, podobnie jak człowiek nie jasnowidz i nie będzie wiedział co masz w ’plecaku’. Uff! Gratulacje! Dobrnęliśmy do pewnego pułapu! Teraz możemy pobawić się programowaniem! Wykorzystać i utrwalić nasze umiejętności. Abyś nie zmęczył tematu proponuję żebyś wybrał jakieś ćwiczenie, potem [za 4h, następnego dnia] następne... A potem... Wymyśl coś sam! Tak! Własna praktyka własne ćwiczenia są najważniejsze. Jeśli coś nie będzie działać to skorzystaj z moich rad i odstaw sprawę, później na świeżo łatwiej będzie rozwiązać ci problem. Chciałbym poruszyć jeszcze jedną sprawę: sytuację gdy zmienna lokalna [w plecaku] i zmienna globalna [w rękach] mają tą samą nazwę. Otóż dla funkcji to co jest w plecaku jest dużo ważniejsze niż to co w rękach. Ale jeśli chcemy skorzystać tego co w rękach? Pomoże nam operator :: - operator widoczności, to on poinformuje funkcję [nawet main ? to także *zwykła* funkcja] że chcemy skorzystać ze zmiennej w rękach. Użycie jest banalne, 5 TUTORIAL O C++ — CZĘŚĆ 4 51 przed nazwą dodajemy ::, np. ::zmienna. Nie będę opisywał tego szerzej, polecam poeksperymentować! Dobra... miały być ćwiczenia... ale mam coś lepszego! Wyłącz komputer! Idź na spacer, spotkaj się z dziewczyną albo kolegami zapomnij na chwilę o komputerze! A potem wróć! I dalej :) W ramach ćwiczeń przerób poprzednie trzy programy z ćwiczeń, tak aby działały korzystając ze zmiennych lokalnych.Zapraszam także do eksperymentów! 5.2 KOMENTARZE: czyli znaczek // się uśmiecha Co to są te komentarze? To druga najważniejsza rzecz w programowaniu! Zwlekałem żeby ci je pokazać, ale teraz nadszedł już najwyższy czas!!! Kiedy czytasz lekturę i przygotowujesz się do sprawdzianu [jeśli ktoś musi jeszcze chodzić do szkoły...] to zapisujesz ważne wydarzenia, osoby, czasem zaznaczasz coś w książce. Bardzo to dobre, bo ułatwia orientację i sprawia, że wśród setek znaków nie czujesz się zagubiony. Taką samą [!!!] rolę pełnią w programowaniu komentarze. Służą one jedynie i AŻ informowaniu ciebie, bądź kogoś kto będzie kiedyś czytał twój kod. Myślisz sobie, że po co one ci skoro sam się uczysz i nie będziesz nikomu tego pokazywał.Myślisz podobnie jak ja kiedyś. Bardzo, na prawdę bardzo szybko się zdziwisz, bo czasem będziesz chciał wrócić i sprawdzić jak rozwiązałeś ten bądź inny problem. A nawet podczas realizacji czegoś większego [niebawem to cię czeka! :) ] może dojść do tego, że zapomnisz dlaczego tu użyłeś takiej instrukcji albo wywołałeś inną funkcję.Myślę że cię przekonałem, jeśli mi się nie udało, to niebawem sam siebie przekonasz :)Jak zapisywać komentarze? Spokojnie... Powracając do lektury, możesz zaznaczać coś w książce, albo coś dłuższego na karteczce.Po co ci to przypominam? Bo można używać w C++ DWÓCH rodzajów komentarzy: // - zaznaczenie w książce, krótka informacja raczej do charakteryzacji danej instrukcji /* */ - komentarz praktycznie nieograniczony - nasza karteczka, piszemy ile chcemy!Dobrze teraz pokażę ci jak wykorzystać, żeby nie wprowadzać nic nowego, uaktualnię naszą pierwszą argumentową funkcję: /* Oto nasz pierwszy dług komentarz. Jest to komentarz do uaktualnionej, pierwszej funkcji z argumentami. */ void wypisz(float waga) //informacja wymagana dla funkcji to liczba //zmienno-przecinkowa 5 TUTORIAL O C++ — CZĘŚĆ 4 52 { printf("wazy %f\n", waga); /* wywołujemy printf i prosimy o wypisanie naszego argumentu */ }//koniec Trochę przesadziłem tu z komentarzami, jednak chciałem pokazać zasady używania i dużą dowolność.Żebyś nie popełniał błędów [raczej ciężko] wyjaśnię ci parę reguł: /* między tymi znakami może być wszystko! */ //to zmienia w komentarz tylko wszystko do końca linii /* tak też //można zrobić */ To jest raczej wszystko. PAMIĘTAJ TYLKO O ESTETYCE. Bardzo ważne jest także ładne formatowanie komentarzy, a także kodu. Sprzyja to ogólnemu wrażeniu bardzo ułatwia orientację w kodzie.Porównaj to: int brzydkaFunkcja() {printf("Ten kod jest na prawdę brzydki");return 35;} z tym: int ladnaFunkcja() //bez parametrów zwraca liczbe zwierząt w //zagrodzie Jana Lejbina :)) { printf("Ten kod jest ładny i estetyczny"); //wywołanie tekstu informacyjnego return 432; //to jest pożądana wartość } Różnica, prawda? Myślisz że komentarze to niepotrzebne tracenie czasu, ale szybko przekonasz się jak bardzo się mylisz. Nie tylko dlatego co powiedziałem już wcześniej, wiele razy podczas komentarzy wpadałem na różne pomysły, sposoby pokonania błędów. Komentarze jest to taka chwila relaksu dla twojego procesora w głowie. Pisz komentarze bezpośrednio kiedy piszesz kod, ponieważ później nie będziesz miał już tych samych spostrzeżeń i nie będą one tak bardzo dobre jak te podczas pisania kodu. Teraz już ostatnia uwaga i dam ci spokój z komentarzami. Zachowaj umiar. Złoty środek-jest na prawdę najważniejszy. Komentarz przy jasnym wywołaniu printf nie jest potrzebny, tak samo jak przy return 432, więc zachowaj umiar. Poćwicz sobie pisanie komentarzy na wcześniejszych przykładach i zobacz, że o wiele gorzej pisać je teraz, niż razem z kodem Złote zasady dobrego kodera: 5 TUTORIAL O C++ — CZĘŚĆ 4 53 • ładnie sformatowany i przejrzysty kod • dobre nazwy zmiennych i funkcji [obrazowe] • dużo komentarzy, jednak tylko tych potrzebnych [dyskusyjna sprawa...] • między operatorami stosuje spacje [zamiast x=3+4; to x = 3 + 4;] • we wszelkich bardziej złożonych operacjach używa do ograniczania () [co prawda kompilatory znają kolejność działań, ale...] • argumenty do funkcji ładnie wyspacjowane [ nie funkcja(1,2,3) tylko funkcja(1, 2, 3)]- bez przesady!!! [kod to nie obraz picassa, choć, może dla niektórych...] Ładnie sformatuj i opisz komentarzami poprzednie programy :) Zobaczysz jakie to nużące zadanie teraz i może zaczniesz pisać komentarze razem z kodem :) 5.3 5.3.1 INSTRUKCJE WARUNKOWE I OPERATORY: czyli już wiem! Instrukcja if Codziennie w naszym życiu dokonujemy wyborów. Decydujemy w co rano się ubrać, co zjeść na śniadanie, itd. Na razie nasze programy, były takie trochę jałowe, ponieważ nie potrafiły się zdecydować. Robiły wszystko dokładnie tak jak napisaliśmy. W dalszym programowaniu na instrukcje warunkowe, czyli wybory będziesz natykał się częściej niż myślisz. Jest to jedno z podstawowych zagadnień. No dobra, kiedy chcę coś wybrać to zazwyczaj wyobrażam sobie jedno i drugie, i porównuje je. Tak samo jest w C++! if (zmienna1==zmienna2) { //... to coś tam } if - jeśli - uruchamiamy wyobraźnię ( ) - ograniczenie wyobrażanych obiektów zmienna1 - pierwszy obiekt porównania == - w C++ ten operator to zwyczajne = ale używany jest tylko do porównania, nie mylić go z = (da to inny rezultat!) 5 TUTORIAL O C++ — CZĘŚĆ 4 54 zmienna2 - drugi obiekt porównania { } - blok instrukcji, zupełnie jak w funkcjach No dobrze... Powiedzmy że jeśli wagaSlonia równa się wagaDuzejMyszy to robimy jakieś tam instrukcje. A jeśli ten warunek nie zostanie spełniony to co się stanie? Nic. Musimy sami o to zadbać, definiując chęć albo kontynuowania porównywania: else if(zmienna1>zmienna2) { } //w innym przypadku, jeśli... albo zakończyć porównywanie: else { //... } Samo else obsłuży każdy inny warunek. Ta teoria jest dość pusta i sucha, więc pora na przykładowy programik, który powinien rozjaśnić sytuację. slonimysz: #include <stdio.h> int wagaSlonia; int wagaDuzejMyszy; int main() { wagaSlonia=934; //możesz się spróbować wpisać te same wartości wagaDuzejMyszy=45; //dla obu tych zmiennych if (wagaSlonia==wagaDuzejMyszy) //jesli wagaSlonia jest równa wagaDuzejMyszy { //to... printf("Nasz słoń wazy tyle, co nasza duza mysz\n"); } else //w każdym innym przypadku { printf("Niestety nie ważą tyle samo..."); } return 0; } 5 TUTORIAL O C++ — CZĘŚĆ 4 55 Spróbuj teraz, uzbroić nasz program w dwa dodatkowe porównania (else if), które sprawdzą dla nas, czy słoń waży więcej,czy jednak duża mysz. UWAGA!!! Blok else if powinien być przed else.Spróbuj rozwiązać problem samodzielnie, a zaraz zrobimy to razem. Zrobione? Działa? Porównaj z moją wersją: slonimysz+: #include <stdio.h> int wagaSlonia; int wagaDuzejMyszy; int main() { wagaSlonia = 674; wagaDuzejMyszy = 9003; if (wagaSlonia==wagaDuzejMyszy) //jeśli wagaSlonia jest równa wagaDuzejMyszy { printf("Nasz slon wazy tyle, co nasza duza mysz\n"); } else if (wagaSlonia>wagaDuzejMyszy) { printf("Nasz slon jest ciezszy\n"); } else if (wagaSlonia<wagaDuzejMyszy) { printf("Nasza duza mysz jest ciezsza\n"); } //else //czy może być jeszcze jakaś opcja? return 0; } Mam nadzieję, że nawet mimo tego, że twój program wyglądał być może inaczej także działał.Teraz mała zagadka, jak myślisz czy różnią się czymś od siebie te dwa porównania: 1: if (zmienna1==zmienna2) {} else if (zmienna1>zmienna2) {} 2: 5 TUTORIAL O C++ — CZĘŚĆ 4 56 if (zmienna1==zmienna2) {} if (zmienna1>zmienna2) {} Różnią się! W drugim przypadku, nawet kiedy zmienna1==zmienna2 to drugie porównanie może też być prawdziwe. Pierwszy to uniemożliwia. Dlaczego? Else - inaczej, w innym wypadku. Jeśli podczas sprawdzania instrukcji if komputer stwierdzi, że wyrażenie było prawdziwe, czyli np. słoń wazy tyle samo co mysz, dalej nie sprawdza.Natomiast w drugim przypadku nowy blok if, to porównanie od nowa. Pytasz się jak to możliwe, przecież jeśli zmienna1jest równa zmienna2 to przecież nie jest większa... ale co jeśli w bloku if zmienimy ich wartości? No właśnie! Po za tym, są jeszcze inne operatory logiczne, poza ==, ¿, ¡ . Ale o tym za chwilkę. Pokażę ci program z wykorzystaniem tej sztuczki, abyś dobrze ją zapamiętał. slonimysz++: #include <stdio.h> int wagaSlonia; int wagaDuzejMyszy; int main() { wagaDuzejMyszy=wagaSlonia=900; //zastanów się wagaSlonia=900, a //wagaDuzejMyszy=wagaSlonia, czyli 900 if (wagaDuzejMyszy==wagaSlonia) //przy porównaniu kolejność nie ma //znaczenia { printf("Waza tyle samo, ale mysz nagle schudla...\n"); wagaDuzejMyszy=45; //mysz chudnie } else if(wagaSlonia>wagaDuzejMyszy) //ale tu ma duże znaczenie!!!! { printf("Slon wazy wiecej niz duza mysz\n"); } //---------------------printf("Drugie porównanie:\n"); wagaDuzejMyszy=wagaSlonia; //znów ważą tyle samo; if (wagaSlonia==wagaDuzejMyszy) { 5 TUTORIAL O C++ — CZĘŚĆ 4 57 printf("Waza tyle samo, ale mysz nagle schudla...\n"); wagaDuzejMyszy=45; } if (wagaDuzejMyszy<wagaSlonia) //jesli koniecznie chcemy zmienić //kolejność obracamy > na < { printf("Slon wazy wiecej niz duza mysz\n"); } return 0; } 5.3.2 Instrukcja switch Potrafimy juz posługiwać się wariantami. Jednak czy taki sposób jest wygodny, jeśli mam do obsłużenia np. 20 wartości[zmienna1==zmienna2, zmienna1==zmienna3, itd..]? Myśle że niezbyt. Dlatego wymyślono specjalną konstrukcję, której zdaniem jest obsługa możliwych wariantów. Tu raczej ciężko cokolwiek tłumaczyć, przejdźmy więc do rzeczy: switch(zmienna1) { case 1: //... instrukcje1 break; case 234: //... instrukcje2 break; case 5: //... instrukcje3 break; default: //... instrukcje4 break; } to jest po prostu: if (zmienna1==1) { //... instrukcje1 } 5 TUTORIAL O C++ — CZĘŚĆ 4 58 else if(zmienna1==234) { //... instrukcje2 } else if(zmienna1==5) { //... instrukcje3 } else { //... instrukcje4 } Może coś być tutaj nie jasne więc omówię konstrukcję switch/case. switch - od tego zaczynamy podobnie jak if ( ) - tu umieszczamy wyraźnie (np. zmienna1-zmienna2), samą zmienną lub liczbę { } - blok, tak jak przy if albo funkcjach case x: - w przypadku kiedy wyrażenie z nawiasu równa się x to [:], x musi być liczbą break - przerwij, jeśli nie zastosujemy tego, to każde następne instrukcje zostaną wykonane [nawet po innych case] dopóki nie spotka break albo blok się nie skończy default - po prostu obsłużenie każdego innego warunku, tak jak else. Swoja drogą czasem nie stosuje się break w case przed default [ale ciężko to brzmi :P] switch(x) { case 1: //...instrukcje default: //to zostanie wykonane nawet jeśli x równa się 1 break; } I jeszcze jedno - default wcale nie musi być na końcu :) Istnieje jeszcze jedna konstrukcja do decyzji, ale jest raczej rzadko używana (czyżby dlatego, że jest brzydka?) 5 TUTORIAL O C++ — CZĘŚĆ 4 59 ( warunek ) ? jeśli_prawdziwy; : jeśli_fałszywy; w praktyce używany tylko np. do (x==0) ? x++; : x--; if (x==0) x++; else x--; 5.3.3 Operatory Czas chyba przejść do operatorów. Opowiem ci o wszystkich po kolei, a w razie potrzeby poprę krótkim przykładem. W między czasie porozmawiamy o logice [czyli co może być w nawiasach if]. • Operator przypisania. Jest to zwykłe ’=’ ale jest tak bardzo ważny, że chciałbym go wyróżnić :) X = Y To co znajduje się po lewej stronie X zmienia wartość na to co po prawej. Po prawej może być wyrażenie, albo zmienna, liczba • Operatory arytmetyczne. Zwykła matematyka :) operat. + − ∗ / ++ ++ −− −− % − za co odpowiada dodawanie odejmowanie mnożenie dzielenie przykład x = 1 + 1 x = 1 - 1 x = 1 * 1 x = 1 / 1 post-inkrementacja x++ pre-inkrementacja ++x post-dekrementacja x-pre-dekrementacja --x reszta z dzielenia x = 3 % 2 zmiana znaku x = -x uwagi przy dzieleniu liczb całkowitych odrzucana jest reszta x=x+1 x=x+1 x=x-1 x=x-1 jak w matematyce :) Czym różni się pre od post? Otóż zwiększa się albo po (post) albo przed (pre). [Z grubsza chodzi o to, że jest różnica w przypadku użycia y = x++ i y = ++x. Jeśli zmienna x będzie zawierała wartość np. 5 to w pierwszym przypadku w y będzie również 5, natomiast w przypadku drugi - będzie 6. Wynika to z tego, że operator 5 TUTORIAL O C++ — CZĘŚĆ 4 60 pre-inkrementacji/dekrementacji najpierw zwiększa/zmiejsza wartość zmiennej o 1, a następnie zwraca jej wartość, natomiast w przypadku post-inkrementacji i dekrementacji kolejność ta jest odwrotna. Ma to bardzo duże znaczenie i jest często spotykaną przyczyną błędów, nawet w programach pisanych przez profesjonalistów - przyp. red.] • Operatory logiczne i porównania. Czyli matematyczne równania i nierówności :) operat. == > >= < <= != ! za co odpowiada czy równe większe większe bądź równe mniejsze mniejsze bądź równe nie równe negacja && do łączenia wyrażeń || do łączenia wyrażeń przykład-if( ) x==y x>y x>=y x<y x<=y x!=y !x uwagi jeśli x nie jest równe y jeśli x jest logicznie prawdziwy, to wyrażenie daje fałsz, o logice dalej x==y && y==z daje prawdę jeśli oba wyrażenia są prawdziwe x==y || y==z daje prawdę jeśli którekolwiek wyrażenie jest prawdziwe • Operatory bitowe. Służą do operacji na bitach - systemie dwójkowym, który jest rodzimym systemem procesora. Co to oznacza? Dla nas prościej jest napisać 10, a dla procesora 1010 [Jeśli nie wiesz ze 10 = 1010 :) to przeczytaj artykuł o systemie binarnym - przyp. red.] 5 TUTORIAL O C++ — CZĘŚĆ 4 operat. & ˆ | << >> ∼ za co odpowiada iloczyn logiczny (koniunkcja bitowa) 61 przykład(Nie C++) 0111 & 1011 = 0011 uwagi ustawia bit na 1 tylko jeśli oba bity są równe 1 alternatywa wył 0111 ^ 1011 = 1100 ustawia bit na 1 (bitowa różnica tylko jeśli tylko symetryczna) jeden z bitów=1 logiczne „lub” 0111 | 1011 = 1111 ustawia bit na 0 (alternatywa bitowa) tylko gdy oba (suma logiczna) bity równe 0 przesuń w lewo 0111 << 1 = 1110 o daną liczbę miejsc, resztę wypełnia zerami przesuń w prawo 1110 << 1 = 0111 o daną liczbę miejsc, resztę wypełnia zerami negacja ~0111 = 1000 zmienia bity (dopełnienie bitowe) na przeciwne • Uff. Omówiliśmy już prawie wszystkie operatory, zostały nam tylko operatory specyficzne C i C++ () organiczna wyrażenia x*(5+2) () ’wywołuje’ funkcję printf() [] odwołanie do elementu tablicy o tablicach już niebawem... . odwołanie do elementu może o tym kiedyś porozmawiamy struktury, klasy, albo unii −> wskazanie do elementu o wskaźnikach także niebawem, ale o reszcie... struktury, klasy, albo unii & uzyskanie adresu o adresach i wskaźnikach niebawem ∗ wyłuskanie dot. wskaźników sizeof zwraca wielkość w pamięci sizeof(int) zwróci rozmiar zajmowany w pamięci przez typ int, przydatne w tablicach , oddzielanie argumentów funkcja(arg1, arg2) :: widoczność ::zmienna, w wypadku kiedy mamy zmienną globalną i lokalną, to daje nam dostęp do globalnej Tym sposobem przebrnęliśmy przez operatory :) Obiecałem, że porozmawiamy o logice. Lubię dotrzymywać słowa,więc zaczynamy! Wyrażenia logiczna? Co ci to mówi? Mi niewiele :) Ale chodzi o to, że wartością wyrażenia 5 TUTORIAL O C++ — CZĘŚĆ 4 62 może być prawda, albo fałsz.Czyli podsumowując wyrażenie może kłamać, albo mówić prawdę. Dlaczego personifikuję wyrażenie? Ponieważ *każde* wyrażenie w C++, nie tylko logiczne może dać prawdę lub fałsz. Nawet nie mam zamiaru się zajmować operatorami [a więc i wyrażeniami] logicznymi, bo już chyba wszystko omówiłem. Np. coś takiego 1+2. Zwykłe wyrażenie arytmetyczne-dodawanie. Ale co będzie jeśli napiszemy: if (1+2) { //...cokolwiek :) } Instrukcje w if wykonają się! Jeśli uważnie czytałeś tego tutka, wiesz już, że wyrażenie mówi prawdę, kiedy jego wartość czyli wynik jest różny od zera. 1 + 2 = 3, większe od zera, więc prawda. Większość nowych kompilatorów poinformuje się zastrzeżeniem (warning) że warunek zawsze będzie prawdziwy [w przypadku 1+2] Podobnie można używać przypisania: if (x=5+y) { //...cos tam } będzie prawdą jeśli y nie będzie równe −5[5 + (−5) = 0] Uwaga! Takie rozwiązanie nie jest zbyt eleganckie i nie świadczy najlepiej o programiście ;) Ponieważ: • nowe kompilatory będą delikatnie sugerować ci, że może się pomyliłeś [= zamiast ==] • jesteś leniem, bo nie chce ci się napisać nawiasu i wyrażenia logicznego: zamiast if (1+2) to if ( (1+2) != 0 ) Myślę że jest to tak logiczne, że nie trzeba tego logicznie wyjaśniać ;)))) W naszych ćwiczeniach użyjemy czegoś, o czym porozmawiamy dopiero za kilka odcinków. Nie będę nawet tego tłumaczył i zajmował się ty szerzej. Będzie to nowa znajoma, którą poprosimy tylko kilka razy o przysługę, a zaprzyjaźnimy się dopiero za kilka odcinków. scanf("%d", &zmienna); Wymaga nagłówka ¡stdio.h¿, nie jest to problem, bo będziemy używać printf. Powiedziałem, że nie będę tłumaczył, jednak muszę się trochę skusić i dorzucić uwagę: & - ten znaczek koniecznie trzeba,przed nazwą zmiennej. Inaczej nie podziała!!! Ach i to %d, tak jak w printf opisuję to typ tego co chcemy [TUTAJ] wpisać. Więc %f - float, %u - unsigned, itd... 5 TUTORIAL O C++ — CZĘŚĆ 4 5.3.4 63 Ćwiczenia Ups! Miałem się tym teraz nie zajmować :) Dobra dosyć gadania przechodzimy do pisania! [ a raczej ćwiczenia ;) ] Ćwiczenie 1: Pobierz od użytkownika wagę słonia i sprawdź czy nie pomylił się za bardzo [dopuszczalna granica błędu 50], poinformuj stosownymi komunikatami. Ćwiczenie 2: Napisz prostą grę. Wybierz w programie liczbę a następnie spytaj użytkownika o nią [zakres od 1 - 10], sprawdź czy trafił albo czy nie wyszedł poza zakres. Możesz także wylosować liczbę. Jak? - napiszę w rozwiązaniach :) Rozwiązanie 1: #include <stdio.h> int wagaSlonia = 650; int wagaWgUzyt; int mozliwyBlad = 50; int goraBledu = wagaSlonia + mozliwyBlad; //czy za duzo int dolBledu = wagaSlonia - mozliwyBlad; //czy za malo int main() { printf("Witaj! Ten program jest do sprawdzenia twojej intuicji! Jego" "tworca choduje malego slonia ile on wazy?" "Mozesz sie pomylic o +/- %d! Odpowiedz:", mozliwyBlad); //ta sztuczke opisywalem w poprzednich cwiczeniach! scanf("%d", &wagaWgUzyt); //pobieramy wartosc zmiennej int - %d if (wagaWgUzyt == wagaSlonia) { printf("Trafiles idealnie!! %d\n", wagaSlonia); } else if ( (wagaWgUzyt > dolBledu) && (wagaWgUzyt < goraBledu) ) //jesli jest w zakresie bledu { printf("Byles blisko, wiec uznaje odpowiedz! Twoja wartosc: %d" " a slon wazy %d", wagaWgUzyt, wagaSlonia); } else if ( wagaWgUzyt > goraBledu ) //za duzo { 5 TUTORIAL O C++ — CZĘŚĆ 4 64 printf("Za duzo! Powiedziales ze %d a na slon wazy %d", wagaWgUzyt, wagaSlonia); } else if ( wagaWgUzyt < dolBledu ) //za malo { printf("Nasz slon to nie mucha... Ty podales:%d a wazy %d", wagaWgUzyt, wagaSlonia); } else //niemozliwe!! { printf("Niemozliwe!!!"); } return 0; } Rozwiązanie 2: Obiecałem, że napiszę o losowaniu, więc słowa dotrzymuję... Jest to rozwiązanie nie wszędzie działające, więc MOŻE ci niedziałać... Wymagane nagłówki to stdlib.h. Żeby wylosować liczbę potrzeba najpierw uruchomić generator liczb pseudo-losowych:randomize(); Dlaczego pseudo? Ponieważ procesor nie zna czegoś takiego jak ’losowość’ a uzyskane liczby pseudo-losowe są wynikiem różnych algorytmów... Teraz wystarczy wylosować liczbę z podanego zakresu random(100); Od zera 0 do 100. A jak uzyskać liczbę?Po prostu funkcja random zwraca jako swój rezultat liczbę pseudo-losową: liczbaLosowa = random(100); W takim przypadku ćwiczenie będzie miało dwa rozwiązania. Jednak napiszę to w taki sposób, że trzeba będzie wymienić tylko funkcję. Więc najpierw ta funkcja. Wersja a) to wariant nie losowy, a wersja b) pseudo-losowy. a) int dajLiczbe() { return 7; } b) int dajLiczbe() { randomize(); 5 TUTORIAL O C++ — CZĘŚĆ 4 65 return random(10); } Teraz główny program, żeby działał jak należy trzeba dodać jedną z funkcji na jego końcu. #include <stdio.h> #include <stdlib.h> //w wariancie nie losowym niepotrzebne, //ale nie zaszkodzi int szukanaLiczba; int liczbaUzyt; int dajLiczbe(); //aby zadzialalo po main dodaj cialo funkcji int main() { szukanaLiczba = dajLiczbe(); printf("Podaj liczb od 1 do 10, zobaczymy, czy zgadniesz moja! "); scanf("%d", &liczbaUzyt); if (liczbaUzyt == szukanaLiczba) { printf("Zgadles!!!!\n"); } else if ( (liczbaUzyt > szukanaLiczba) && (liczbaUzyt < 11) ) //czy jest wieksza i w zakresie { printf("Za duza...\n"); } else if (liczbaUzyt < szukanaLiczba) //mniejsza { printf("Za mala...\n"); } else { printf("Nie takiej liczby sie spodziewalem, poza zakresem!!!\n"); } printf("Prawidlowa liczba: %d", szukanaLiczba); } 6 UŁAMKI W SYSTEMIE BINARNYM 66 Zachowaj chociaż w myślach to ćwiczenie, ponieważ jeszcze się nim kiedyś zajmiemy (troszkę innym ulepszonym)! 6 Ułamki w systemie binarnym Mix Master 6.1 Wprowadzenie W kolejnej części artykułu na temat systemów liczbowych chciałbym Wam przedstawić sposób zapisu ułamków. Tak samo jak w „naszym codziennym” systemie dziesiętnym, tak samo w innych występują ułamki. Dla komputerów są one równie potrzebne co dla nas. Są dwa główne sposoby na przeliczanie ułamków. 6.2 Ułamki — sposób tradycyjny Ten tytułowy tzw. sposób tradycyjny jest bardzo podobny do tego, który przedstawiłem w pierwszej części artykułu. Chodzi o zamianę liczb całkowitych. Tutaj jest prawie tak samo, są tylko dwie zasadnicze różnice. Pierwsza z nich to inne działanie. Tam używaliśmy dzielenia a teraz posłużymy się działaniem odwrotnym, czyli mnożeniem. Druga sprawa to to, że nie zapisujemy z boku reszty z dzielenia a całość po wymnożeniu. Teraz może się to wydawać niejasne, ale oczywiście najlepiej będzie jeśli wytłumaczę na jakimś przykładzie. Weźmy sobie na początek liczbę 0.25 i zamieńmy ją na liczbę binarną 0.25 * 2 | 0 0.50 * 2 | 1 0.00 Liczbę wymnażamy przez podstawę systemu (w tym przypadku 2) dopóki przez obcinanie całości nie otrzymamy 0. Jest to ogólna zasada, o wyjątkach powiem pod koniec artykułu. Liczbę zapisujemy po przecinku spisując cyfry OD GÓRY! Otrzymaliśmy taki wynik: 0.01 Teraz powinniśmy sprawdzić czy wynik jest poprawny. 6.3 Sprawdzanie wyniku czyli jedziemy z powrotem Pamiętacie zapewne jak wagowo układają się podstawy liczb w systemach liczbowych. Tak dla przypomnienia tylko napisze: 6 UŁAMKI W SYSTEMIE BINARNYM 1 0 0 0 1 1 0 <-- liczba n=6 n=5 n=4 n=3 n=2 n=1 n=0 67 w systemie binarnym czyli w tym wypadku było to: 1 ∗ 26 + 1 ∗ 22 + 1 ∗ 21 Przy ułamkach idziemy dalej i po przecinku wchodzimy w potęgi ujemne: 0 . 2 n=0 n=-1 5 <-- liczba dziesiętna n=-2 gdzie n to wykładnik potęgi. Więc żeby sprawdzić nasze wcześniejsze obliczenia zrobimy tak: Liczba binarna –> 0.01 = 0 ∗ 20 + 0 ∗ 2−1 + 1 ∗ 2−2 = 0.25 <– liczba dziesiętna 6.4 Coś dla leniwych czyli algorytm Hornera część. 1 Drugi sposób, o których chciałbym tu co nieco powiedzieć to algorytm hornera. Jest to dość szybka metoda zamiany systemów liczbowych. Jest to bardzo dobra metoda, za pomocą której możemy od razu zakodować część całkowitą i ułamkową liczby. Pierwszym krokiem jaki musimy podjąć to decyzja ile liczb po przecinku chcemy uzyskać po zakodowaniu. Jeśli się już zdecydujemy to czas wziąść się do pracy. 6.5 Coś dla leniwych czyli algorytm Hornera część. 2 Od razu podam wzór ogólny przydatny dla tego algorytmu, będzie potem łatwiej g o zrozumieć: a ∗ nk (8) Gdzie: a - liczba w systemie dziesiętnym; n - podstawa systemu, w którym chcemy zakodować liczbę k - liczba miejsc po przecinku, jaką chcemy uzyskać po zakodowaniu Weźmy za przykład liczbę 15.12 i zakodujmy ją do systemu piątkowego, najpierw skorzystamy z powyższego wzoru (8). Załóżmy, że chcemy mieć 2 miejsca po przecinku: 6 UŁAMKI W SYSTEMIE BINARNYM 68 15.12 ∗ 52 = 378 Tutaj gdybyśmy w wyniku mnożenia otrzymali ułamek to należy liczbę zaokrąglić do całości Dalej postępujemy tak jak ze wszystkimi liczbami całkowitymi: 378 75 15 3 : : : : 5 5 5 5 | | | | 3 0 0 3 Cyfry spisujemy od dołu: 3003 i wstawiamy przecinek tyle miejsc od tylu ile chcieliśmy uzyskać (w tym przypadku 2) i otrzymujemy liczbę 30,03. Zapewniam, że wynik jest prawidłowy jednak jako (Wasze ulubione) zadanie domowe możecie sprawdzić to. 6.6 Wyjątki czyli to, co Polacy lubią najbardziej Zacznę od przedstawienia przyczyny owych wyjątków. Dobrze wszyscy wiemy, że nie każdą liczbę da się zapisać w postaci dziesiętnej - są to liczby niewymierne, czyli nie mające skończonego rozwinięcia dziesiętnego lub wymierne z okresowym rozwinięciem dziesiętnym. Znanym przykładem może być liczba π równa w przybliżeniu 3.14, ale dokładniej to 3.1415. . . no i tak bez końca. Dokładnie tak samo jest w innych systemach. Niestety liczba skończona w systemie dziesiętnym wcale nie musi być skończona w innym systemie. Od razu może przytoczę jakiś przykład. Liczba 0.2, zakodujemy ją do systemu binarnego według tego, co napisałem wcześniej: 0.2 0.4 0.8 0.6 0.2 * * * * * 2 2 2 2 2 | | | | | 0 0 1 1 0 i tutaj tutaj jak widać „zapętlamy się”. Liczba 0.2 w systemie binarnym jest liczbą z rozwinięciem dziesiętnym okresowym. W takim wypadku należy dojść do liczby miejsc po przecinku jakie chcemy (zależy od wymaganej dokładności) i spisać od góry - będzie to zaokrąglenie a nie dokładnie zakodowana liczba. W tym wypadku otrzymany wynik to 0.0011.