Kurs C++ #5
Kurs C++ #5
|========== #05 ==========|
+-------------------------+
| K U R S C + + |
+-------------------------+
f u n k c j e
...czyli autor objaśnia działanie czarnych skrzynek.
Do tej pory nasze programy składały się z deklaracji tylko jednej funkcji - funkcji main(). Gdy potrzebowaliśmy coś zrobić, np. porównać dwa łańcuchy, wywoływaliśmy funkcje biblioteczne C++, które ktoś już wcześniej za nas napisał. W tym rozdziale nauczymy się tworzyć własne funkcje, które będziemy mogli wykorzystać w przyszłości - w programowaniu często "idzie się na łatwiznę", wykorzystując wcześniej napisane i przetestowane funkcje. Dlatego warto dzielić program na funkcje. Poza tym przy rozbudowanych programach, nie mając funkcji, musielibyśmy wielokrotnie przepisywać te same, często kilkusetliniowe instrukcje. Wtedy najlepiej stworzyć funkcję, która będzie wykonywała tę czynność, a później jedynie ją wywołać, podobnie jak funkcję biblioteczną. W razie błędu łatwiej poprawić deklarację funkcji niż przekopywać się przez tysiące linii kodu w przeróżnych miejscach programu.
Jak stworzyć funkcję?
Aby stworzyć funkcję, potrzebujemy jej prototypu i deklaracji. Schemat prototypu funkcji wygląda tak:
typ_zwracany nazwa_funkcji([parametry]);
Element w nawiasach można pominąć (stąd nawias kwadratowy). Typem zwracanym może być dowolny typ prosty, złożony, wskaźnik (również jako tablica dynamiczna; już chyba mówiłem, że w zasadzie nie ma różnicy między wskaźnikiem i tablicą dynamiczną, stąd zawsze mówiąc wskaźnik, mam na myśli również tablicę dynamiczną), klasa itp. Nie można zwracać jedynie tablic statycznych. Na szczęście tablica dynamiczna może zostać zinterpretowana jako tablica statyczna. Jeżeli nie ma potrzeby zwracania wartości przez funkcję, możemy zamiast nazwy typu wpisać słowo void, co oznacza, że funkcja nie zwraca wartości - jest odpowiednikiem procedury w Pascalu, podprogramu w BASICu czy podprocedury w FORTRANie.
Tak wygląda prototyp funkcji. Teraz tę funkcję zadeklarujemy. Deklaracja wygląda jak rozszerzenie prototypu:
typ_zwracany nazwa_funkcji([parametry])
{
// kod funkcji
} // bez średnika
Skoro deklaracja wygląda podobnie, to czy nie można pominąć prototypu funkcji? W zasadzie tak, ale wtedy deklaracja musiałaby się znajdować przed pierwszym wywołaniem funkcji. Jednak przyjęło się już, że w pliku źródłowym pierwsza zadeklarowana jest funkcja main(), wobec tego na początku podajemy tylko prototypy, a deklaracje funkcji znajdują się poniżej deklaracji funkcji main(). Prototypy są również bardzo przydatne przy podziale programu na kilka plików - wtedy wystarczy zapisać prototyp funkcji w pliku nagłówkowym, a następnie jedynie dołączać ten plik do modułu, w którym jest on potrzebny. W identyczny sposób działają funkcje biblioteczne C++: plikach nagłówkowych znajdują się jedynie prototypy funkcji. Jest to możliwe dlatego, że kompilator kompiluje każdy plik osobno, a następnie skompilowane moduły są łączone - i dopiero wtedy potrzebna jest deklaracja funkcji, więc może się ona znajdować w zupełnie innym miejscu. Kompilatorowi potrzebny jest jedynie prototyp, aby mógł sprawdzić, czy została podana odpowiednia ilość parametrów odpowiednich typów. Nawet nazwy przekazywanych parametrów w prototypie i deklaracji mogą być różne. Ba, w prototypie w ogóle można pominąć nazwy parametrów, a wpisywać same typy, np. void funkcja(int, float).
W kodzie funkcji wpisujemy działania, jakie funkcja ma wykonać. Możemy tu zrobić wszystko to, co w kodzie funkcji main(), czyli: deklarować zmienne, definiować typy, wywoływać inne funkcje itp. Jednak nie deklarujcie zmiennej o nazwie _result - C++ nazywa tak ukrytą zmienną, która jest przekazywana jako wynik procedury przy wywołaniu instrukcji return i może to prowadzić do niewyjaśnionych błędów.
Najprostszą funkcją, jaką możemy napisać, to funkcja, która nie zwraca żadnej wartości oraz nie przyjmuje parametrów, ale tworzenie takich funkcji nie ma najmniejszego sensu. Dlatego my zaczniemy od funkcji z jednym parametrem i zwracającą wartość:
// prototyp
double Szescian(double x);
// deklaracja
double Szescian(double x)
{
return x*x*x;
}
Jest to chyba najprostsza funkcja z jednym parametrem. Listę parametrów wpisujemy po nazwie funkcji w nawiasach. Jeżeli funkcja nie przyjmuje parametrów, to w nawiasach nie wpisujemy nic - ale same nawiasy MUSZĄ być zapisane. Parametru funkcji możemy używać tak, jak zwykłej zmiennej. Na końcu funkcji musi znajdować się instrukcja return, a po niej zwracana wartość. W przypadku funkcji nie zwracającej wartości (void) po return wstawiamy średnik. W tym wypadku return w ogóle można pominąć, jednak lepiej ją pisać, bo w przyszłości możemy ponieść konsekwencje naszego lenistwa. :) Funkcja wykonuje się od swojego początku do instrukcji return lub gdy dojdzie do nawiasu klamrowego kończącego swoją deklarację.
Wywołanie funkcji wygląda tak:
double Szescian(double x);
int main()
{
... // instrukcje
// wywołanie funkcji
double z = Szescian(5); ----+ skok do kodu funkcji i wykonanie go
|
... // dalsze instrukcje <--|----+
return 0; | |
} | |
| |
double Szescian(double x) <-----+ |
{ |
... // kod funkcji |
return x*x*x; -------------------+ powrót z funkcji do instrukcji
następującej po jej wywołaniu
}
Starałem się przedstawić sposób wywołania funkcji w sposób pseudograficzny - mam nadzieję, że zrozumiecie, o co tu chodzi. W identyczny sposób wywoływane są inne funkcje, np. strcpy() itp. Funkcja często jest porównywana do czarnej skrzynki - przekazujemy jej dane i pobieramy wyniki, ale nie obchodzi nas, co i jak ta funkcja przetwarza.
Gdy funkcja zwraca wartość, możemy tę wartość przypisywać innym zmiennym bądź wyświetlać na ekranie za pomocą obiektu cout. Funkcja jako parametry może przyjmować typy proste, wskaźniki, enumeratory, struktury, aliasy typów itp. Te same typy mogą być zwracane przez funkcję. Jedynym ograniczeniem jest niemożność pobierania i zwracania tablic statycznych. To znaczy można, ale tylko poprzez wskaźniki (a pamiętacie chyba, że dla kompilatora wskaźnik = tablica). To samo ze zwracaniem wartości - tablice zwracamy tylko przez wskaźnik!
Zmienne lokalne
Jak już mówiłem, w funkcji możemy deklarować zmienne; są to tzw. zmienne lokalne. Dlatego lokalne, ponieważ istnieją tylko podczas wykonywania danego bloku instrukcji. Można deklarować również zmienne globalne - deklarujemy je przed pierwszym użyciem, poza wszelkimi blokami i funkcjami. Najczęściej deklaruje się zmienne globalne przed funkcją main() i prototypami innych funkcji, zaraz po dyrektywach #include. W zasadzie nie jest istotne dla programu, gdzie je zadeklarujemy - można to zrobić również przed funkcją, w której są wykorzystywane po raz pierwszy. Zmienne globalne istnieją przez cały czas wykonania programu. Zmienne globalne nie są jednak dobrym rozwiązaniem. W programowaniu bardzo ważna jest tzw. hermetyzacja danych, czyli maksymalne możliwe ograniczenie dostępu do danych. Zmienna globalna może być wykorzystywana i zmieniana w każdej funkcji, co może prowadzić do przypadkowej jej zmiany. Dobry programista stara się nie wykorzystywać zmiennych globalnych, gdy nie jest to absolutnie konieczne. Choć początkowo mogą one wydać się niezwykle proste i niewymagające dużo pracy.
Trzeba też uważać na zwracanie wskaźników przez funkcję. Ostrzegam was już tutaj, bo to może sprawić wiele błędów. Przy zwracaniu przez wartość, zwracana jest kopia zmiennej zwracanej. Ale przy zwracaniu wskaźników zwracana jest kopia adresu zmiennej - adres wciąż wskazuje na ten sam fragment pamięci, a on jest już zwolniony, bo zmienna, na którą wskazywał, nie istnieje. To samo dzieje się przy zwracaniu referencji (co to jest referencja - w następnym rozdziale). Nie ma natomiast problemu ze zwracaniem wskaźnika, któremu ręcznie została przydzielona pamięć w funkcji - ale wtedy musimy również ręcznie ją zwolnić. Ten temat zostanie omówiony szerzej w następnej lekcji.
Wskaźniki i referencje jako parametry
Podział programu na funkcje znacząco ułatwia programowanie - kod jest bardziej zrozumiały, krótszy i o wiele łatwiej poprawić w nim błędy. Ponadto możemy napisać swój własny, przetestowany zestaw funkcji i używać go w wielu naszych programach. Możemy również wykorzystywać funkcje napisane przez innych programistów, a także publikować własne biblioteki funkcji (najlepiej solidnie udokumentowane za pomocą komentarzy, aby inni nie mieli problemów z ich używaniem).
Wszystko proste i intuicyjne, prawda? Ale gdy będziemy chcieli napisać funkcję, która zmienia wartość zmiennej podanej poprzez parametr, to C++ zrobi nam psikusa. Popatrzmy na przykład:
edycja1.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
void kwadrat(float x);
int main(int argc, char *argv[])
{
float licz = 10.5;
cout << "Przed wywołaniem funkcji:\nlicz = " << licz << endl;
kwadrat(licz); // wywołanie funkcji
cout << "Po wywołaniu funkcji:\nlicz = " << licz << endl;
system("PAUSE");
return EXIT_SUCCESS;
}
void kwadrat(float x)
{
x = x*x;
cout << "Podczas wykonania funkcji:\nlicz = " << x << endl;
return;
}
Jak widać, mimo tego, że podnieśliśmy liczbę do kwadratu, to po wykonaniu funkcji zmienna licz nie zmienia swojej wartości. Dzieje się tak dlatego, że przy wywołaniu funkcji tworzone są kopie parametrów i to właśnie one są zmieniane. Ma to na celu zwiększenie bezpieczeństwa danych - zmiennej licz nie ma prawa nic się stać. Zmienialiśmy jedynie wartość jej kopii. A co, jeśli jednak chcemy zmieniać w ten sposób wartości funkcji? Sposoby są dwa, a jeden ma swoje początki w języku C. Najpierw zajmiemy się tym pierwszym sposobem. Aby za jego pomocą zmienić wartość zmiennej podanej jako parametr, należy jako parametr podać typ wskaźnikowy - nie int, tylko int*. W ten sposób do funkcji zostanie przekazany adres zmiennej, którą chcemy zmienić. Jedyną różnicą w wywołaniu funkcji jest podanie nie samej zmiennej, a jej adresu (operator adresu &).
Drugi sposób został wprowadzony dopiero w C++. Jest to przekazanie przez referencję. Na początek - co to jest referencja? Teoretycznie jest to zmienna o takim samym adresie, jak inna zmienna tego samego typu - tzw. inna instancja zmiennej. W praktyce można porównać ją do wskaźnika o stałym adresie, którego nie można zmienić. Co ważne, odwołujemy się do niej w zwykły sposób, bez żadnych dereferencji i innych zabiegów. Referencja tworzona jako zmienna w programie MUSI zostać zainicjalizowana inną zmienną - nie można stworzyć sobie zerowej referencji i w przyszłości przydzielić jej pamięci. Referencja do zmiennej jest ściśle i dożywotnio powiązana z daną zmienną. Na szczęście podczas przekazywania jako parametr nie potrzeba żadnych inicjalizacji. Deklarowanie referencji jest bardzo podobne do deklaracji wskaźnika, różnicą jest operator: we wskaźnikach jest to operator *, natomiast w referencjach jest to operator &. Oto dwa przykłady funkcji, korzystających z parametrów referencyjnych i wskaźnikowych:
void wczytaj_int(int *x);
void wczytaj_float(float &x);
void wczytaj_int(int *x)
{
cout << "Podaj liczbe calkowita: ";
cin >> *x;
}
void wczytaj_float(float &x)
{
cout << "Podaj liczbe rzeczywista: ";
cin >> x;
}
Możemy tutaj porównać działanie obu metod. A która z nich jest lepsza? To już jest kwestia sporna. Niektórzy mówią, że przekazanie przez referencję jest o wiele wygodniejsze i prostsze. Inni twierdzą, że przekazanie przez wskaźnik pokazuje wyraźnie programiście, że dana zmienna będzie w tej funkcji edytowana, czego nie można poznać po referencji (chyba, że zobaczymy prototyp funkcji). Ja przychylam się do pierwszego zdania - referencje są o wiele wygodniejsze i prostsze.
Przekazywanie przez wskaźnik bądź referencję jest przydatne nie tylko w sytuacji, gdy daną zmienną będziemy trwale zmieniać. Przydaje się również przy przekazywaniu jako parametrów dużych struktur czy klas. Podczas przekazywania przez wartość (standardowo) cała struktura jest kopiowana do zmiennej tymczasowej, co zajmuje dużo czasu i pamięci. W tym wypadku lepiej przekazać zmienną przez wskaźnik czy referencję. Niestety, trzeba uważać, aby przez przypadek nie zmienić w funkcji wartości przekazywanej zmiennej. Jeżeli funkcja ma nie zmieniać wartości parametru, warto zadeklarować parametr (bądź parametrów) ze słowem const. Ma ono to samo zastosowanie, co przy deklaracji zmiennych - nie pozwala na modyfikację zmiennej lub parametru funkcji.
Przykładowy program
Było dużo teorii, teraz trochę praktyki. Przy okazji utrwalimy wiadomości o strukturach. Napiszemy program funkcjonujący jako prosta książka telefoniczna. Oto on:
książka.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
struct DANE
{
char imie[16];
char nazwisko[26];
char telefon[13];
};
DANE *lista;
int MAX;
// prototypy funkcji
bool dodaj(int &nr);
bool usun(int nr, int &aktualny);
void wyswietl(int aktualny);
void menu();
int main(int argc, char *argv[])
{
cout << "Ksiazka telefoniczna\n2006 by .:ArchiE:." << endl;
cout << "Podaj max. liczbe wpisow w ksiazce: ";
cin >> MAX;
lista = new DANE[MAX];
cout << endl;
char ch;
int num, aktualny = 0; // aktualny => wolne miejsce w tablicy rekordów
menu();
do
{
cin >> ch;
switch (ch)
{
case 'n':
if (!dodaj(aktualny))
cout << "\nNie ma wiecej miejsca w ksiazce!" << endl;
menu();
break;
case 's':
wyswietl(aktualny);
menu();
break;
case 'd':
cout << "\nPodaj numer wpisu do skasowania: ";
cin >> num;
if (!usun(num-1, aktualny))
cout << "\nPodano nieprawidlowy numer!" << endl;
menu();
break;
case 'q':
break;
default:
cout << "\nNieznana operacja!" << endl;
menu();
continue;
}
}
while (ch != 'q');
delete[] lista;
system("PAUSE");
return EXIT_SUCCESS;
}
// definicje funkcji
bool dodaj(int &nr)
{
if (nr >= MAX)
return false;
cout << "\nImie:\t\t";
cin >> lista[nr].imie;
cout << "\nNazwisko:\t";
cin >> lista[nr].nazwisko;
cout << "\nNumer tel.:\t";
cin >> lista[nr++].telefon;
return true;
}
bool usun(int nr, int &aktualny)
{
if (nr >= MAX || nr < 0)
return false;
for (int i = nr; i < MAX; i++)
lista[i] = lista[i+1]; // przesuń "w dół" wszystkie rekordy
aktualny--;
return true;
}
void wyswietl(int aktualny)
{
cout << "\n~~~~~~~~~~~~~~~~~~~~\n";
for (int i = 0; i < aktualny; i++)
cout << i+1 << ":" << lista[i].imie << " "
<< lista[i].nazwisko << ", tel: " << lista[i].telefon << endl;
cout << "\n~~~~~~~~~~~~~~~~~~~~\n";
}
void menu()
{
cout << "\nn > nowa pozycja"
"\ns > przeglad numerow"
"\nd > usun pozycje"
"\n\nq > wyjscie"
"\n\nTwoj wybor: ";
}
W tym programie wszystkie elementy są już znane, a działanie programu jest proste - chyba nie muszę tłumaczyć jego działania. Jak widać, użycie funkcji jest bardzo proste i wiele ułatwia. Czasami nawet nie można napisać programu bez funkcji - jest to po prostu niemożliwe.
Rekurencja
Jest to jedna z rzeczy, których nie jesteśmy w stanie zrobić bez podziału programu na funkcje. Rekurencja to pojęcie, które mogło być poznane na lekcjach matematyki w temacie o ciągach liczbowych. Funkcja rekurencyjna w pewnym momencie wywołuje samą siebie. Brzmi głupio? Ale wielokrotnie pomaga znaleźć proste, eleganckie i skuteczne rozwiązanie problemu. Rekurencję możemy zaobserwować, gdy ustawimy na przeciwko siebie dwa lusterka albo skierujemy kamerę na monitor, który wyświetla jej obraz. Wtedy kamera pokazuje obraz, który skamerowała wcześniej, i to w nieskończoność. Oczywiście, już słowo "nieskończoność" powinna wzbudzić naszą niepewność - funkcja, która nie ma końca?... Dlatego zawsze musimy wyraźnie określić warunek jej zakończenia. Jednym z najprostszych przykładów jest funkcja silnia(). Wprawdzie można ją stworzyć w tzw. wersji iteracyjnej, czyli za pomocą pętli, jednak w zapisie rekurencyjnym jest o wiele bardziej oczywista i zrozumiała; my sami wykorzystujemy rekurencję częściej, niż myślimy. Oto definicja funkcji silnia():
unsigned long long silnia(int n) // typ zwracany musi być maksymalnie duży
{
if (n == 0)
return 1;
else
return n * silnia(n-1);
}
Jak widzimy, w pewnym momencie funkcja wywołuje sama siebie, przekazując jej parametr zmniejszony o 1. I funkcja "zagłębia się" w swoje wywołania, dopóki parametr nie wyniesie 0. Rekurencja jest bardzo wygodna, jednak niesie ze sobą szereg pułapek, z których najczęściej zdarzają się: nieskończone wywołania rekurencyjne (w przypadku nieprawidłowego warunku zakończenia wywołań), brak pamięci (funkcja przed wywołaniem swojej kopii zapisuje w pamięci wszystkie swoje zmienne lokalne) oraz czas wykonania. Ten ostatni przypadek może wydać się banalny, ale spróbujcie wykonać na swoich komputerach ten program:
fibbonacci.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
unsigned long long fibbonacci(unsigned short);
int main()
{
int ile_liczb;
cout << "Podaj ilosc liczb z ciagu: ";
cin >> ile_liczb;
cout << endl << endl;
for (int i = 0; i < ile_liczb; i++)
cout << i << ":\t" << fibbonacci(i) << endl;
system("pause");
return EXIT_SUCCESS;
}
unsigned long long fibbonacci(unsigned short val)
{
switch (val)
{
case 0:
return 0;
case 1:
return 1;
default:
return fibbonacci(val-2) + fibbonacci(val-1);
}
}
Program ten wypisuje podaną ilość liczb z ciągu Fibbonacciego, który charakteryzuje się tym, że pierwsze dwa jego wyrazy wynoszą 1, a każdy następny jest sumą dwóch poprzednich. Jeżeli nie podaliście wartości większej niż 20, to nie zauważycie problemu. Ale spróbujcie podać wartość, powiedzmy, 200. Idźcie spać, a gdy się obudzicie, to MOŻE program dojedzie do 150. wyrazu ciągu...
Parametry domyślne
Gdy stworzymy funkcję, to przy każdym jej wywołaniu musimy podawać wartości wszystkich jej parametrów. Ale czasami może być tak, że zazwyczaj funkcję wywołujemy z parametrem 100, ale czasami musimy podać jej wartość 102. Czy to oznacza, że za każdym razem musimy jej podawać ten parametr? A gdy okaże się, że jednak zazwyczaj będzie to 99? W takim wypadku trzeba by poprawić wszystkie wywołania funkcji - co jest najkrótszą drogą do błędu. I tu idą nam z pomocą parametry domyślne.
Parametry domyślne definiujemy TYLKO w prototypie funkcji. Robimy to w prosty sposób:
// prototyp
void jakastam_funkcja(int param1, double param2 = 10);
// deklaracja
void jakastam_funkcja(int param1, double param2)
{
// ciało funkcji
}
Gdy funkcja zawiera parametr domyślny, nie musimy podawać w wywołaniu jego wartości - będzie ona miała wtedy wartość podaną w prototypie. Jeśli natomiast będziemy chcieli podać inną wartość, to wtedy ją podajemy. Mam tylko kilka drobnych uwag dotyczących parametrów domyślnych:
* Parametr z wartością domyślną musi znajdować się na końcu listy parametrów
* Funkcja może przyjmować kilka parametrów domyślnych, ale między nimi nie może znajdować się zwykły parametr
* Nadawanie wartości parametrom przebiega według kolejności ich deklaracji, więc nie można ominąć parametru domyślnego, a podać wartość dla parametru następującego po nim
void zla_funkcja(int par1, char par2 = 'K', double par3); // niepoprawne - za parametrami domyślnymi nie może stać inny parametr
void dobra_funkcja(int par1, double par2, char par3 = 'J', bool par4 = false); // prawidłowa deklaracja
// wywołania:
dobra_funkcja(1, 5.9, 'y', true);
dobra_funkcja(3, 6.753); // wywołane z parametrami domyślnymi
dobra funkcja(6, 0.04, 'v'); // wywołane tylko z jednym parametrem domyślnym
dobra funkcja(0, 23.456, false); // żle - próba ominięcia pierwszego parametru domyślnego
Jak zwykle przykład wyjaśnił chyba najwięcej. Parametry domyślne są bardzo wygodne i wiele ułatwiają. Co nie znaczy, że w każdej funkcji należy je deklarować: róbmy to tylko wtedy, gdy rzeczywiście jest to przydatne, bo przez nadgorliwość często można zrobić więcej złego niż dobrego.
Przeciążanie funkcji
Często zdarza się, że dla parametrów różnego typu musiy wykonać tę samą, lub podobną, długą operację. Wtedy wypadałoby stworzyć kilka funkcji, różnych dla różnych typów. C++ ułatwia takie operacje poprzez możliwość tzw. przeciążania funkcji. Polega to na tym, że możemy stworzyć kilka funkcji o tej samej nazwie, ale różnych parametrach. Działa to podobnie, jak przy funkcjach przeciążonych w Delphi (Object Pascalu), jednak tam musimy zaznaczyć, że funkcja jest przeciążana, dyrektywą override. W C++ nie musimy tego robić - kompilator sam zauważy, że funkcja ma kilka wersji, i wybierze z nich odpowiednią. Jedno jest ważne: parametry muszą być różne. Uwaga! - też przy parametrach domyślnych. Oto przykład:
void funkcja(int a, float b = 2);
void funkcja(int x); // błąd - wywołanie może dotyczyć tej, jak i poprzedniej funkcji
void funkcja(int c, float d); // błąd - powód ten sam
void funkcja(int f, float u, char q = 'l'); // patrz wyżej
Mechanizm przeciążania funkcji pojawił się w C++. Jest bardzo przydatny, jednak czasami może nie wystarczyć. W tym celu możemy użyć mechanizmów szablonów - nowość w technice programowania, przekraczający możliwości programowania obiektowego. Wprawdzie w planie ogólnym mojego kursu nie ma miejsca na szablony, ale nie jest to temat trudny, więc być może pojawi się. Ale to jeszcze odległa przyszłość - najpierw musimy poznać programowanie obiektowe. A i na to trzeba nieco poczekać...
To tyle, jeśli chodzi o podstawowe wiadomości na temat funkcji w C++. Oczywicie, temat nie został omówiony dogłębnie - w miarę poznawania języka poznawać będziemy również bardziej zaawansowane wiadomości o funkcjach. A na razie zapraszam do lekcji szóstej - w niej o zasięgu zmiennych i przestrzeniach nazw. Piszcie na mój e-mail w razie jakichkolwiek wątpliwości czy pytań. Do... przeczytania za miesiąc. :)
autor("ArchiE","archie007@wp.pl")
Wyszukiwarka
Podobne podstrony:
k cplk cpl2k cpl?k cplk cpl1k cpl?k cpl?k cplk cplr08 cpl t (3)t p cplk cpl0k cpl0k cplk cpl1k cplawięcej podobnych podstron