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.

Podobne dokumenty