Programowanie obiektowe Lab.1. Struktury a klasy. Przejście od paradygmatu proceduralnego do obiektowego (c) Krzysztof Urbański 2014, wszelkie prawa zastrzeżone. Materiały te są chronione prawem autorskim. Żaden fragment publikacji nie może być powielany lub rozpowszechniany w żadnej formie i w żaden sposób bez uprzedniego zezwolenia. Wyrażam zgodę na użycie tych materiałów w trakcie realizacji kursów dydaktycznych na Wydziale Elektroniki Mikrosystemów i Fotoniki Politechniki Wrocławskiej w semestrze letnim 2013/2014 r. Zagadnienia do opanowania: " Definicja struktury danych i używanie zmiennych strukturalnych. " Referencje w argumentach funkcji: & . " Dynamiczna alokacja i zwalnianie pamięci w C (malloc, calloc, free). " Przeciążanie funkcji w C++. " Definicja klasy w C++. Sekcje private, protected, public. " Konstruktor i destruktor w klasie C++ - jak definiować, kiedy są wywoływane. W poprzednim semestrze obowiązywał język C, a użycie mikrokontrolerów o niewielkich zasobach pamięciowych wręcz wykluczało możliwość użycia języka C++. Język C zaliczany jest do języków proceduralnych, co w uproszczeniu sprowadza się do programowania aplikacji w taki sposób, aby wyraznie wyodrębnić definicje i deklaracje typów danych (zwykle struktur danych), a zupełnie osobno określić algorytmy przetwarzające te dane (podprocedury lub funkcje). Z zupełnie innym podejściem zapoznamy się w tym semestrze w ramach kursu Programowanie obiektowe. Problem nie będzie rozbijany na dane i algorytmy, zamiast tego będziemy modelować rzeczywistość za pomocą klas i zależności między nimi. Takie możliwości oferuje nam C++, dlatego zakładając projekt należy dodać do niego plik z rozszerzeniem .cpp (a nie *.c) . Może to być np. main.cpp. Spowoduje to użycie kompilatora C++ zamiast C. Początkowo różnice między tymi językami będą prawie niezauważalne, ale pod koniec zajęć pojawi się to coś, czego nie było w języku C, a mianowicie klasy i obiekty. 1 Programowanie obiektowe; � Krzysztof Urbański 2014 Zaczniemy zupełnie nieobiektowo, od definicji struktury danych, jaką jest Student: struct Student { int nralb; char imie[100]; // nieeleganckie rozwiązanie }; int main() { Student s; printf("nralb: %d, imie: %s\n", s.nralb, s.imie); return 0; } Wyniki: nralb: -858993460, imie: `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%ł �! `%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`%`% 1) Ponieważ używamy C++, nie jest konieczne każdorazowe dodawanie słowa kluczowego struct, wystarczy sama nazwa typu strukturalnego (Student). 2) Kompilator C++ pozwala zastąpić wywołania funkcji printf() mniej ryzykownym obiektem std::cout. Znajdz informacje (np. online), jak to zrobić. 3) Podobnie jak w C, zmienna lokalna w C++ nie ma określonej wartości początkowej, więc należy zadbać o jej stan początkowy samodzielnie. Z drugiej strony, zmienne globalne i statyczne są zerowane). 4) Warto przy okazji zdefiniować specjalizowaną funkcję do wyświetlania dowolnego studenta, aby w przyszłości, wraz ze zwiększaniem liczby pól struktury, nie było konieczne modyfikowanie wszystkich miejsc w kodzie, gdzie chcemy Studenta wyświetlić. void Student_ustaw(Student &s, int nralb, char imie[100]) { s.nralb = nralb; strcpy(s.imie, imie); // ryzykowne lepiej używać strncpy } void Student_wyswietl(Student s) { printf("nralb: %d, imie: %s\n", s.nralb, s.imie); } void main() { Student s; Student_ustaw(s, 12345, "Jasiu"); Student_wyswietl(s); } Wyniki: nralb: 12345, imie: Jasiu 2 Programowanie obiektowe; � Krzysztof Urbański 2014 Lepiej, ale nie do końca. Zwróć uwagę na referencję w funkcji Student_ustaw(& ). Jest to trzeci sposób przekazywania argumentów do funkcji. W C używaliśmy sposobu przez wartość oraz przez wskaznik . C++ wprowadza bardzo przydatny mechanizm przez referencję . Używamy go wtedy gdy chcemy, aby funkcja mogła bezpośrednio modyfikować przekazaną do niej zmienną, a nie tylko jej kopię. To, co na pewno będzie wymagało poprawienia, to przekazywanie parametru char imie[100] do funkcji Student_ustaw(& ). Nie jest też dobrym pomysłem użycie Student_wyswietl(Student s) należałoby raczej posłużyć się wskaznikiem albo referencją, zmniejszyłoby to liczbę kopiowanych bajtów i pozwoliłoby zaoszczędzić pamięć stosu oraz przyspieszyłoby działania kodu. Sama struktura Student też nie jest zbyt dobrze zaprojektowana, pole imie zawsze zajmuje 100 bajtów niezależnie od tego, czy długość imienia wynosi 0, 10 czy 99 znaków. Co gorsza, gdybyśmy chcieli użyć imienia dłuższego niż 99 znaków, to nie będzie to możliwe w tak zdefiniowanej strukturze. Lepiej byłoby użyć wskaznika znakowego, zaś pamięć potrzebną do przechowania imienia można alokować dynamicznie, dokładnie takiej wielkości, jaka jest potrzebna, ani mniej, ani więcej. Dynamiczna alokacja pamięci narzuca też na nas obowiązek zwalnianie tej pamięci, gdy już nie będzie potrzebna. Pojawia się zatem kolejna funkcja Student_usun(& ), która o to zadba. struct Student { int nralb; char *imie; }; void Student_inicjalizuj(Student &s) { s.nralb = 0; s.imie = NULL; } void Student_ustaw(Student &s, int nralb, char *imie) { s.nralb = nralb; if(s.imie != NULL) //jeśli student już posiada imię, free(s.imie); //to najpierw się go pozbywamy, s.imie = strdup(imie); //a następnie dynamicznie tworzymy //duplikat (kopię) przekazanego argumentu wywołania funkcji } void Student_wyswietl(Student *s) { printf("nralb: %d, imie: %s\n", s->nralb, s->imie); } void Student_usun(Student *s) { printf("zwalniam pamiec studenta o nralb=%d\n", s->nralb); 3 Programowanie obiektowe; � Krzysztof Urbański 2014 if(s->imie != NULL) free(s->imie); } void main() { Student s; Student_inicjalizuj(s); Student_ustaw(s, 12345, "Jasiu"); Student_wyswietl(&s); Student_usun(&s); } Powyższy kod powinien być zrozumiały, ale na oko widać, że jest nadmiernie rozwlekły. Niepotrzebnie w identyfikatorach kolejnych funkcji został użyty przedrostek Student_. W języku C byłoby to konieczne, aby uniknąć pojawienia się kilku funkcji o identycznych nazwach (np. wyświetl), przeznaczonych do operowania na różnych argumentach. C nie pozwala na istnienie w projekcie kilku definicji funkcji o identycznych nazwach (za wyjątkiem funkcji statycznych, ale to przypadek szczególny) W języku C++, w odróżnieniu od C, istnieje mechanizm przeciążania (przeładowania) funkcji. Nie jest konieczne nazywanie tych funkcji aż tak rozwlekle zamiast Student_inicjalizuj(s); można krócej napisać inicjalizuj(s);. Nawet wtedy, gdy w naszym programie zdefiniujemy inną strukturę (np. struct NapojGazowany {& }), i też zechcemy mieć funkcję inicjalizującą zmienne tego typu, to możemy śmiało zapisać to tak: struct NapojGazowany { char *nazwa; double cena, kalorie; }; void inicjalizuj(NapojGazowany &n) { n.nazwa = NULL; n.cena = b.kalorie = 0.0; } void inicjalizuj(Student &s) { s.nralb = 0; s.imie = NULL; } void main() { Student s; NapojGazowany n; inicjalizuj(s); inicjalizuj(n); & } 4 Programowanie obiektowe; � Krzysztof Urbański 2014 Mamy 2 funkcje o identycznej nazwie inicjalizuj(& ), różniące się jednak typem argumentów. To w zupełności wystarczy, aby kompilator C++ poradził sobie z odróżnianiem tych funkcji. Wyraznie oddzielenie definicji typu danych (struktury danych) od algorytmów, które ich używają (funkcji) niekoniecznie najlepiej odzwierciedla rzeczywistość. Ten sposób opisu działania aplikacji sprawdza się niezle w programach służących wyłącznie do przetwarzania danych, ale trudniej opisywać w ten sposób grę komputerową, np. klasyczną strzelankę 3D. Wracając do wcześniejszego przykładu, wydaje się, że bardziej naturalne byłoby przypisanie studentowi nie tylko cech ilościowych (nr albumu, wiek, imię, nazwisko, oceny& ), ale także zdefiniowanie umiejętności, jakie student powinien posiadać (narodziny, zakuwanie, imprezowanie, zgon). Od strony czysto praktycznej wygodniej by też było, gdy zmienne w programie same potrafiły się samodzielnie inicjalizować (w chwili narodzin) oraz zwalniać pamięć automatycznie wtedy, gdy już nie są nam one więcej potrzebne (gdy nastąpi zgon). Takie możliwości daje nam użycie klasy zamiast struktury. Podstawowa różnica między strukturą a klasą jest taka, że oprócz pól w tej drugiej można definiować także funkcje (w tym przypadku będziemy je nazywać metodami). Tych różnic w przyszłości pojawi się dużo więcej. class Student { public: int nralb; // to jest pole char *imie; // to jest pole void ustaw(int nralb, char *imie) // to jest metoda { this->nralb = nralb; if(this->imie != NULL) free(this->imie); this->imie = strdup(imie); } void wyswietl() { printf("nralb: %d, imie: %s\n", this->nralb, this->imie); } void usun() { printf("zwalniam pamiec studenta o nralb=%d\n", nralb); if(imie != NULL) free(imie); } void inicjalizuj() { nralb = 0; 5 Programowanie obiektowe; � Krzysztof Urbański 2014 imie = NULL; } }; void main() { Student s; s.inicjalizuj(); s.ustaw(9999, "Malgosia"); s.wyswietl(); s.usun(); } Wyniki: nralb: 9999, imie: Malgosia zwalniam pamiec studenta o nralb=9999 Zdecydowanie lepiej! Czy można usprawnić nasz kod jeszcze bardziej? To, co najbardziej się może przydać, to umiejętność automatycznego ustawiania (inicjalizacja) własnych pól i automatyczne zwalnianie pamięci, którą zajęliśmy. Służą do tego konstruktory i destruktor. Każda klasa w C++ może mieć 0 lub więcej konstruktorów i 0 lub 1 destruktor. class Student { public: & Student() // a to jest specjalna metoda: konstruktor { printf("inicjalizacja pol obiektu klasy Student\n"); nralb = 0; imie = NULL; } ~Student() // specjalna metoda: destruktor { printf("zwalniam pamiec studenta o nralb=%d\n", nralb); if(imie != NULL) free(imie); } }; void main() { Student s; s.ustaw(9999, "Malgosia"); s.wyswietl(); } Wyniki: inicjalizacja pol obiektu klasy Student nralb: 9999, imie: Malgosia zwalniam pamiec studenta o nralb=9999 6 Programowanie obiektowe; � Krzysztof Urbański 2014 A gdyby tak jeszcze bardziej ułatwić życie programiście? W klasie możemy zdefiniować dodatkowy konstruktor następującej postaci: class Student { public: & Student(int nralb, char *imie) //to prawie to samo co metoda ustaw(& ) { printf("tworze obiekt klasy Student (%d, %s)\n", nralb, imie); this->nralb = nralb; this->imie = strdup(imie); } }; void main() { Student s(9999, "Malgosia"); //krócej i ładniej się nie da s.wyswietl(); } Wyniki: tworze obiekt klasy Student (9999, Malgosia) nralb: 9999, imie: Malgosia zwalniam pamiec studenta o nralb=9999 c.d.n. 7 Programowanie obiektowe; � Krzysztof Urbański 2014 Zadanie do realizacji 1) Zbadaj zachowanie konstruktorów i destruktora, korzystając z wcześniejszych kodów zródłowych. W którym momencie jest wywoływany każdy z nich? Jak zmienia się działanie kodu, kiedy mamy do czynienia ze zmiennymi lokalnymi lub globalnymi? 2) Mamy dany poniższy fragment kodu. Co zostanie wyświetlone na ekranie? Jaki napis pojawi się jako pierwszy? A jaki jako ostatni? DLACZEGO? Student s(9999, "Malgosia"); void main() { printf("Witamy w funkcji main()!\n"); s.wyswietl(); } 3) Idąc tropem z punktu (2), zbadaj zachowanie aplikacji, kiedy masz do dyspozycji więcej zmiennych obiektowych różnych typów (różnych klas). Co decyduje o kolejności wykonania konstruktorów? Aby zrealizować ten podpunkt, przygotuj kompletną klasę inną niż Student{}, może to być np. class SokOwocowy{} i utwórz kilkoro Studentów oraz kilka SokówOwocowych. 4) Soki owocowe są na tyle zdrowe, że warto nauczyć Studenta, aby pił je (z umiarem) nawet kilka razy w ciągu dnia. Pomyśl, w postaci jakich danych można reprezentować obiekt klasy Studenta z plecakiem wypełnionym obiektami klasy SokOwocowy? Co jeszcze może student nosić w plecaku? Czy są to tylko napoje? Jak najogólniej można zdefiniować klasę, która będzie pasowała do dowolnego elementu wyposażenie studenta? 5) Celem tych zajęć jest zapoznanie się z pojęciem model obiektowy lub programowanie zorientowane obiektowo . Zamodeluj obiektowo wycinek rzeczywistości, jakim jest ekipa dobrze wyposażonych studentów wraz z opiekunami na najbliższym Rajdzie MTR. Użyj języka UML i narysuj wybrany diagram (np. diagram zależności). 8 Programowanie obiektowe; � Krzysztof Urbański 2014