C++ LEKCJA23


LEKCJA 23 - Co nowego w C++? ________________________________________________________________ Z tej lekcji dowiesz się, jakie mechanizmy C++ pozwalają na stosowanie nowoczesnego obiektowego i zdarzeniowego stylu programowania i co programy robią z pamięcią. ________________________________________________________________ W porównaniu z klasycznym C - C++ posiada: * rozszerzony zestaw słów kluczowych (ang. keywords): ** nowe słowa kluczowe C++: class - klasa, delete - skasuj (dynamicznie utworzony obiekt), friend - "zaprzyjaźnione" funkcje z dostępem do danych, inline - wpleciony (funkcje przeniesione w formie rozwiniętej do programu wynikowego), new - utwórz nowy obiekt, operator - przyporządkuj operatorowi nowe działanie, private - dane i funkcje prywatne klasy (obiektu), do których zewnętrzne funkcje nie mają prawa dostępu, protected - dane i funkcje "chronione", dostępne z ograniczeniami, public - dane i funklcje publiczne, dostępne bez ograniczeń, template - szablon, this - ten, pointer wskazujący bieżący obiekt, virtual - funkcja wirtualna, abstrakcyjna, o zmiennym działaniu. * nowe operatory (kilka przykładów już widzieliśmy), np.: << - wyślij do strumienia wyjściowego, >> - pobierz ze strumienia wejściowego. * nowe typy danych: klasy, obiekty, abstrakcyjne typy danych (ang. ADT). * nowe zasady posługiwania się funkcjami: funkcje o zmiennej liczbie argumentów, funkcje "rozwijane" inline, funkcje wirtualne, itp.; Przede wszystkim (i od tego właśnie rozpoczniemy) zobaczymy funkcje o nowych możliwościach. ROZSZERZENIE C - FUNKCJE. Funkcje uzyskują w C++ znacznie więcej możliwości. Przegląd rozpoczniemy od sytuacji często występującej w praktyce programowania - wykorzystywania domyślnych (ang. default) parametrów. FUNKCJE Z DOMYŚLNYMI ARGUMENTAMI. Prototyp funkcji w C++ pozwala na podanie deklaracji domyślnych wartości argumentów funkcji. Jeśli w momencie wywołania funkcji w programie jeden (lub więcej) argument (ów) zostanie pominięte, kompilator wstawi w puste miejsce domyślną wartość argumentu. Aby uzyskać taki efekt, prototyp funkcji powinien zostać zadeklarowany w programie np. tak: void Funkcja(int = 7, float = 1.234); Efekt takiego działania będzie następujący: Wywołanie w programie: Efekt: ________________________________________________________________ Funkcja(99, 5.127); Normalnie: Funkcja(99, 5.127); Funkcja(99); Funkcja(99, 1.234); Funkcja(); Funkcja(7, 1.234); ________________________________________________________________ [!!!] Argumentów może ubywać wyłącznie kolejno. Sytuacja: Funkcja(5.127); //ŹLE Funkcja(99); //DOBRZE jest w C++ niedopuszczalna. Kompilator potraktuje liczbę 5.127 jako pierwszy argument typu int i wystąpi konflikt. [P079.CPP] #include void fun_show(int = 1234, float = 222.00, long = 333L); main() { fun_show(); // Trzy arg. domyslne fun_show(1); // Pierwszy parametr fun_show(11, 2.2); // Dwa parametry fun_show(111, 2.22, 3L); // Trzy parametry return 0; } void fun_show(int X, float Y, long Z) { cout << "\nX = " << X; cout << ", Y = " << Y; cout << ", Z = " << Z; } Uruchom program i przekonaj się, czy wstawianie argumentów domyślnych przebiega poprawnie. W KTÓRYM MIEJSCU UMIESZCZAĆ DEKLARACJE ZMIENNYCH. C++ pozwala deklarować zmienne w dowolnym miejscu, z zastrzeżeniem, że deklaracja zmiennej musi nastąpić przed jej użyciem. Umieszczanie deklaracji zmiennych możliwie blisko miejsca ich użycia znacznie poprawia czytelność (szczególnie dużych "wieloekranowych") programów. Klasyczny sposób deklaracji zmiennych: int x, y, z; ... main() { ... z = x + y + 1; ... } może zostać zastąpiony deklaracją w miejscu zastosowania (w tym np. wewnątrz pętli): main() { ... for ( int i = 1; i <= 10; i++) cout << "Biezace i wynosi: " << i; ... } Należy jednak pamiętać o pewnym ograniczeniu. Zmienne deklarowane poza funkcją main() są traktowane jako zmienne globalne i są widoczne (dostępne) dla wszystkich innych elementów programu. Zmienne deklarowane wewnątrz bloku/funkcji są zmiennymi lokalnymi i mogą "przesłaniać" zmienne globalne. Jeśli wielu zmiennym nadamy te same nazwy-identyfikatory, możemy prześledzić mechanim przesłaniania zmiennych w C++. W przykładzie poniżej zastosowano trzy zmienne o tej samej nazwie "x": [P080.CPP] //Program demonstruje przesłanianie zmiennych #include int x = 1; //Zmienna globalna void daj_x(void); //Prototyp funkcji main() { int x = 22; //Zmienna lokalna funkcji main cout << ::x << " <-- To jest globalny ::x \n"; cout << x << " <-- A to lokalny x \n"; daj_x(); return 0; } void daj_x(void) { cout << "To ja funkcja daj_x(): \n"; cout << ::x << " <-- To jest globalny ::x \n"; cout << x << " <-- A to lokalny x \n"; int x = 333; cout << "A to moja zmienna lokalna - automatyczna ! \n"; cout << x << " <-- tez x "; } Program wydrukuje tekst: 1 <-- To jest globalny ::x 22 <-- A to lokalny x To ja funkcja daj_x(): 1 <-- To jest globalny ::x 1 <-- A to lokalny x A to moja zmienna lokalna - automatyczna ! 333 <-- tez x Zwróć uwagę, że zmienne deklarowane wewnątrz funkcji (tu: main()) nie są widoczne dla innych funkcji (tu: daj_x()). Operator :: (ang. scope) pozwala nam wybierać pomiędzy zmiennymi globalnymi a lokalnymi. TYP WYLICZENIOWY enum JAKO ODRĘBNY TYP ZMIENNYCH. W C++ od momentu zdefiniowania typu wyliczeniowego enum staje się on równoprawnym ze wszystkimi innymi typem danych. Program poniżej demonstruje przykład wykorzystania typu enum w C++. [P081.CPP] # include enum ciuchy { niewymowne = 1, skarpetka, trampek, koszula, marynarka, czapa, peruka, koniec }; main() { ciuchy n; do { cout << "\nNumer ciucha ? --> (1-7, 8 = quit): "; cin >> (int) n; switch (n) { case niewymowne: cout << "niewymowne"; break; case skarpetka: cout << "skarpetka"; break; case trampek: cout << "trampek"; break; case koszula: cout << "koszula"; break; case marynarka: cout << "marynarka"; break; case czapa: cout << "czapa"; break; case peruka: cout << "peruka"; break; case koniec: break; default: cout << "??? Tego chyba nie nosze..."; } } while (n != koniec); return 0; } Zwróć uwagę w programie na forsowanie typu (int) przy pobraniu odpowiedzi-wyboru z klawiatury. Ponieważ w C++ "ciuchy" stanowią nowy (zdefiniowany przed chwilą) typ danych, do utożsamienia ich z typem int niezbędne jest wydanie takiego polecenia przy pobieraniu danych ze strumienia cin >> . W opcjach pracy kompilatora możesz włączyć/wyłączyć opcję "Treat enums as int" (traktuj typ enum jak int) i wtedy pominąć forsowanie typu w programie. JEDNOCZESNE ZASTOSOWANIE DWU KOMPILATORÓW. Jak już wspomnieliśmy wcześniej kompilator C++ składa się w istocie z dwu różnych kompilatorów: * kompilatora C wywoływanego standardowo dla plików *.C, * kompilatora C++ wywoływanego standardowo dla plików *.CPP. Oba kompilatory stosują RÓŻNE metody tworzenia nazw zewnętrznych (ang. external names). Jeśli zatem program zawiera moduł, w którym funkcje zostały przekompilowane w trybie charakterystycznym dla klasycznego C - C++ powinien zostać o tym poinformowany. Dla przykładu, C++ * kategorycznie kontroluje zgodność typów argumentów, * na swój własny użytek dodaje do nazw funkcji przyrostki (ang. suffix) pozwalające na określenie typu parametrów, * pozwala na tworzenie tzw. funkcji polimorficznych (kilka różnych funkcji o tej samej nazwie), itp. Zwykły C tego nie potrafi i nie robi. Dlatego też do wprowadzenia takiego podziału kompetencji należy czasem zastosować deklarację extern "C". Funkcja rand() w programie poniżej generuje liczbę losową. [P081.CPP] #include extern "C" { # include //Prototyp rand() w STDLIB.H } main() { cout << rand(); return 0; } GENERACJA LICZB LOSOWYCH. Kompilatory C++ umożliwoają generację liczb pseudolosowych użytecznych często w obliczeniach statystycznych (np. metoda Monte Carlo) i emulacji "rozmytaj" arytmetyki i logiki (ang.fuzzy math). [!!!] UWAGA - Liczby PSEUDO-Losowe. ________________________________________________________________ Funkcja rand() powoduje uruchomienie generatora liczb pseudolosowych. Jeśli chcesz uzyskać liczbę pseudolosową z zadanego przedziału wartości, najlepiej zastosuj dzielenie modulo: int n = rand % 10; powoduje tzw. normalizację. Reszta z dzielenia przez 10 może być wyłącznie liczbą z przedziału 0...9. Aby przy każdym urichomieniu aplikacji ciąg liczb pseudolosowych rozpoczynał się od innej wartości należy uruchomić generator liczb wcześniej - przed użyciem funkcji rand() - np.: randomize(); ... int n = rand() % 100; ... ________________________________________________________________ W programie przykładowym funkcje z STDLIB.H zostaną skompilowane przez kompilator C. Określenie trybu kompilacji deklaracją extern "C" jest umieszczane zwykle nie wewnątrz programu głównego a w dołączanych plikach nagłówkowych *.H. Jest to możliwość szczególnie przydatne, jeśli dysponujesz bibliotekami funkcji dla C a nie masz chęci, czasu, bądź możliwości przerabiania ich na wersję przystosowaną do wymagań C++. Drugi przykład poniżej zajmuje się sortowaniem krewnych przy pomocy funkcji C qsort(). [P082.CPP] # include # include # include extern "C" int comp(const void*, const void*); main() { int max; for(;;) { cout << "\n Ilu krewnych chcesz posortowac? (1...6): "; cin >> max; if( max > 0 && max < 7) break; cout << "\n Nic z tego..."; } static char* krewni[] = { "Balbina - ciotka", "Zenobiusz - kuzyn", "Kleofas - stryjek", "Ola - kuzynka (ach)", "Waleria - tez niby ciotka", "Halina - stryjenka" }; qsort(krewni, 6, sizeof(char*), comp); for (int n = 0; n < max; n++) cout << "\n" << krewni[n]; return 0; } extern "C" { int comp(const void *x, const void *y) { return strcmp(*(char **)x, *(char **)y); } } Program wykonuje następujące czynności: * deklaruje prototyp funkcji typu C, * deklaruje statyczną tablicę wskaźników do łańcuchów znakowych, * sortuje wskaźniki, * wyświetla posortowane łańcuchy znakowe, * definiuje funkcję comp() - porównaj, * wykorzystuje funkcję biblioteczną C - strcmp() - String Compare do porównania łańcuchów znaków. O PAMIĘCI. Program w C++ dzieli dostępną pamięć na kilka obszarów o określonym z góry przeznaczeniu. Dla zaawansowanego programisty zrozumienie i efektywne wykorzystanie mechanizmów zarządzania pamięcią w C++ może okazać się wiedzą wielce przydatną. Zaczniemy, jak zwykle od "elementarza". CO PROGRAM ROBI Z PAMIĘCIĄ. W klasycznym C najczęściej stosowanymi do zarządzania pamięcią funkcjami są: * malloc() - przyporządkuj pamięć, * farmalloc() - przyporządkuj odległą pamięć, * realloc() - przyporządkuj powtórnie (zmienioną) ilość pamięci, * calloc() - przydziel pamięć i wyzeruj, * free() - zwolnij pamięć. Pamięć dzielona jest w obszarze programu na następujące bloki: ___________________ niskie adresy --> Ngłówek programu I. Adres startowy KOD: Kod programu ___________________ Zmienne statyczne II. DANE: 1. Zainicjowane Zmienne globalne ___________________ Zmienne statyczne III. DANE: 2. Niezainicjowane Zmienne globalne ___________________ STERTA: (heap) W miarę potrzeby IV. rośnie w dół. Tu operują funkcje malloc(), free(). ___________________ POLE NICZYJE: V. ___________________ W miarę potrzeby VI. STOS: (stack) rośnie w górę. wysokie adresy --> ___________________ W obszarze kodu (I.) znajdują się instrukcje. Na stosie przechowywane są: * zmienne lokalne, * argumenty przekazywane funkcji w momencie jej wywołania, * adresy powrotne dla funkcji (RET == CS:IP). Na stercie natomiast przy pomocy funkcji (a jak przekonamy się za chwilę - także operatorów C++) możemy przydzielać pamięć dla różnych obiektów tworzonych w czasie pracy programu (ang. run-time memory allocation) - np. tworzyć bufory dla łańcuchów, tablic, struktur itp.. Zwróć uwagę, że obszar V. - POLE NICZYJE może być w czasie pracy programu stopniowo udostępniany dla stosu (który rozrasta się "w górę"), albo dla sterty (która rozrasta się "w dół"). W przykładowym programie poniżej podano, w którym obszarze pamięci zostanie umieszczony dany element programu. # include int a; // III. int b = 6; // II. main() { char *Dane; ... float lokalna; // VI. ... Dane = malloc(16); // IV. ... } OPERATORY new I delete. Operatory new i delete działają podobnie do pary funkcji malloc() - free(). Pierwszy przyporządkowuje - drugi zwalnia pamięć. Dokładniej rzecz biorąc - operator new może zostać zastosowany wraz ze wskaźnikiem do bloku danych określonego typu: * struktury danych, * tablicy, itp. (wkrótce zastosujemy go także w stosunku do klas i obiektów); - przyporządkowuje pamięć blokowi danych; - przypisuje począkowy adres bloku pamięci wskaźnikowi. - operator delete zwalnia pamięć przyporządkowaną poprzednio blokowi danych, Operatory new i delete mogą współdziałać z danymi wieloma typami danych (wcale nie tylko ze strukturami), jednakże rozpoczniemy do struktury Data zawierającej datę urodzenia mojej córki. [P083.CPP] # include "iostream.h" struct Data { int dzien; int miesiac; int rok; }; void main() { Data *pointer = new Data; /* Dekl. wskaznik do struct typu Data */ /* Przydziel pamiec dla struktury */ pointer -> miesiac = 11; // pole "miesiac" = 11 pointer -> dzien = 3; pointer -> rok = 1979; cout << "\n URODZINY CORKI: "; cout << pointer -> dzien << '.'; cout << pointer -> miesiac << ". "; cout << "co rok ! od " << pointer -> rok << " r."; delete pointer; //Skasuj wskaznik - zwolnij pamiec. } Program tworzy w pamięci (dokł. na stercie) strukturę typu Data bez nazwy. O którą strukturę chodzi i gdzie jej szukać w pamięci wiemy dzięki wskaźnikowi do struktury *pointer. Zapis Data *pointer = new Data; oznacza jednoczesną deklarację i zainicjowanie wskaźnika. TWORZENIE DYNAMICZNYCH TABLIC O ZMIENNEJ WIELKOŚCI. Jeśli mamy dane wyłącznie jednego typu (tu: int), zastosowanie struktury jest właściwie przysłowiowym "strzelaniem z armaty do wróbli". Trójelementowa tablica typu int TAB[3]; zupełnie nam wystarczy. Utworzymy ją jednak nie jako tablicę globalną (bądź statyczną) w obszarze pamięci danych, lecz dynamicznie - na stercie. [P084.CPP] # include "iostream.h" main() { int *pointer = new int[3]; // Przydziel pamiec pointer[0] = 3; // Tabl_bez_nazwy[0] - dzien pointer[1] = 11; // Tabl_bez_nazwy[1] - miesiac pointer[2] = 1979; cout << "Data urodzenia: "; for(int i = 0; i < 3; i++) cout << pointer[i] << '.'; delete pointer; } Uważny Czytelnik doszedł zapewne do wniosku, że skoro tablica tworzona jest dynamicznie w ruchu programu (run-time), to kompilator nie musi znać na etapie kompilacji programu (compile-time) wielkości tablicy! Idąc dalej, program powinien taką techniką tworzyć tablice o takiej wielkośći, jakiej w ruchu zażyczy sobie użytkownik. Spróbujmy zrealizować to praktycznie. [P085.CPP] # include # include # include void main() { for(;;) { cout << "\nPodaj wielkosc tablicy (1...100) --> "; int i, size; cin >> size; /* Na stercie tworzymy dynamiczna tablica: */ int *pointer = new int[size]; /* Wypelniamy tablice liczbami naturalnymi: */ for (i = 0; i < size; i++) pointer[i] = i; cout << "\n TABLICA: \n"; /* Sprawdzamy zawartosc tablicy: */ for (i = 0; i < size; i++) cout << " " << pointer[i]; char k = getch(); if(k == 'a') break; delete pointer; } } Twój dialog z programem powinien wyglądać następująco: Podaj wielkosc tablicy (1...100) --> 20 TABLICA: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Podaj wielkosc tablicy (1...100) --> 100 TABLICA: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 Skoro dynamiczne tablice o zmiennej wielkości "chodzą", możemy wykorzystać to w bardziej interesujący sposób. [P086.CPP] # include # include # include extern "C" { int Fporownaj(const void* x, const void* y) { return (strcmp(*(char **)x, *(char **)y)); } } main() { cout << "Wpisz maksymalna ilosc imion --> "; int ilosc, i; cin >> ilosc; char **pointer = new char *[ilosc]; for (i = 0; i < ilosc; i++) { cout << "Podaj imie Nr: " << i + 1 << "--> "; char *imie = new char[80]; cin >> imie; if (strcmp(imie, "stop") == 0) break; else pointer[i] = new char[strlen(imie)+1]; strcpy(pointer[i], imie); delete imie; } qsort(pointer, i, sizeof(char *), Fporownaj); for (i = 0; i < ilosc; i++) cout << pointer[i] << '\n'; for (i = 0; i < ilosc; i++) delete pointer[i]; delete pointer; return 0; } Tworzymy dynamicznie przy pomocy operatora new bezimienną tablicę składającą się z tablic niższego rzędu (łańcuch znaków to też tablica tyle, że jednowymiarowa - ma tylko długość). Zwróć uwagę, że w C++ wskaźnik do wskaźnika (**pointer) odpowiada konstrukcji "tablica składająca się z tablic". Aby program uczynić bardziej poglądowym spolszczymy nazwy funkcji przy pomocy preprocesora. [P087.CPP] # define Fporown_string strcmp # define Fkopiuj_string strcpy # define Fsortuj qsort # include # include # include extern "C" { int Fporownaj(const void* x, const void* y) { return (Fporown_string(*(char **)x, *(char **)y)); } } main() { cout << "Wpisz maksymalna ilosc imion --> "; int ilosc, i; cin >> ilosc; char **pointer = new char *[ilosc]; for (i = 0; i < ilosc; i++) { cout << "Podaj imie Nr: " << i + 1 << "--> "; char *imie = new char[80]; cin >> imie; if (Fporown_string(imie, "stop") == 0) break; else pointer[i] = new char[strlen(imie)+1]; Fkopiuj_string(pointer[i], imie); delete imie; } /* w tym momencie i == ilosc */ Fsortuj(pointer, i, sizeof(char *), Fporownaj); for (i = 0; i < ilosc; i++) cout << pointer[i] << '\n'; for (i = 0; i < ilosc; i++) delete pointer[i]; delete pointer; return 0; } Wskaźnik może wskazywać dane o różnym stopniu złożoności: zmienną, tablicę, strukturę, obiekt (o czym za chwilę), ale może wskazywać także funkcję. JEŚLI ZABRAKNIE PAMIĘCI - _new_handler. Aby obsługiwać błędną sytuację - brakło pamięci na stercie - potrzebna nam będzie funkcja - tzw. HANDLER. Aby jedna było wiadomo, gdzie szukać handlera, powinniśmy operatorowi new przekazać informację jaka funkcja obsługuje brak pamięci i gdzie jej szukać. Możemy podstawiać na miejsce funkcji stosowanej w programie tę funkcję, która w danym momencie jest nam potrzebna. Jest to praktyka często stosowana w programach obiekktowych, więc przypomnijmy raz jeszcze przykładowy program - tym razem w trochę innym kontekście. Aby wskazać funkcję zastosujemy wskaźnik. . Przypomnijmy deklarację double ( *Funkcja ) (double); [P088.CPP] #include #include #include double Nasza_F(double); //Deklaracja zwyklej funkcji double (*Funkcja)(double); //pointer do funkcji double liczba; //zwyczajna zmienna int wybor; int main(void) { clrscr(); cout << "\nPodaj Liczbe \n"; cin >> Liczba; cout << "CO OBLICZYC ?\n________________\n"; cout<<"1 - Sin \n2 - Cos \n3 - Odwrotnosc 1/X\n"; switch(cin >> wybor) { case 1: Funkcja = sin; break; case 2: Funkcja = cos; break; case 3: Funkcja = Nasza_F; break; } cout << "\n\nWYNIK = " << Funkcja(liczba); return (0); } double Nasza_F(double x) { if (x != 0) x = 1/x; else cout << "???\n"; return x; } Komputer nie jest "z gumy" i nie posiada dowolnie dużej "rozciągliwej" pamięci. Funkcja malloc(), jeśli pamięci zabraknie, zwraca pusty wskaźnik (ang. NULL pointer), co można łatwo przetestować w programie. Jeśli natomiast stosujemy operator new - konsekwentnie - operator new powinien zwracać NULL (i próbować dokonać przypisania pointerowi zero). To też można sprawdzić w programie. W C++ istnieje jednak również inny, przydatny do tych celów mechanizm. C++ dysponuje globalnym wskaźnikiem _new_handler (wskaźnik do funkcji obsługującej operator new, jeśli zabraknie pamięci). Dzięki istnieniu tego (predefiniowanego) wskaźnika możemy przyporządkować "handler" - funkcję obsługującą wyjście przez operator new poza dostępną pamięć. Dopóki nie zażyczymy sobie inaczej, wskaźnik _new_handler == NULL // NULL == 0 i operator new w przypadku niepowodzenia próby przyporządkowania pamięci zwróci wartość NULL inicjując pusty wskaźnik (innymi słowy "wskaźnik do nikąd"). Jeśli jednak _new_handler != NULL to zawartość wskaźnika zostanie przez operator new uznana za adres startowy funkcji obsługi błędnej sytuacji (ang. addres to call). [P089.CPP] # include # include static void Funkcja() { cout << "\nTo ja ... Funkcja - handler \n"; cout << '\a' << " ! BRAK PAMIECI ! "; exit (1); } extern void (*_new_handler)(); long suma; //Automatycznie suma = 0; void main() { _new_handler = Funkcja; //Inicjujemy wskaznik for(;;) { char *pointer = new char[8192]; suma += 8192; cout << "\nMam juz " << suma << " znakow w RAM\n"; if (pointer != 0) cout << "Pointer != NULL"; } } [!!!] SPRAWDŹ - KONIECZNIE! ________________________________________________________________ W programach użytkowych, a szczególnie w tych oferowanych klientom jako produkty o charakterze komercyjnym należy ZAWSZE sprawdzać poprawność wykonania newralgicznych operacji - a szczególnie poprawność zarządzania pamięcią i poprawność operacji dyskowych. Utrata danych, lub nie zauważone i nie wykryte przez program przekłamanie może spowodować przykre skutki. Raz utracone dane mogą okazać sie nie do odzyskania. ________________________________________________________________

Wyszukiwarka

Podobne podstrony:
www livemocha com angielski lekcja audio
jezyk ukrainski lekcja 03
Lekcja sortowanie
lekcja12
Kris Jamsa Wygraj Z C lekcja32
lekcja1 (2)
Lekcja7
ćw oswajające z piłką lekcja dla dzieci
Logo na lekcjach matematyki w szkole podstawowej
C LEKCJA18
lekcja
Kris Jamsa Wygraj Z C lekcja 5
Lekcja algorytmy w geometrii
LEKCJA 1 Uwierz w siebie, możesz wszystko!
Lekcja 7 Trening pamieci to nie wszystko Zadbaj o swoja koncentracje
lekcja6

więcej podobnych podstron