May 6, 2024, Monday, 126

Przykładowe zadania do Laboratorium 3

From MJanik

Jump to: navigation, search

Zadanie 3 - Klasy c.d., funkcje zaprzyjaźnione, domyślne wartości, przeładowanie nazw, przesyłanie do funkcji argumentow bedacych obiektami


Zadanie przykładowe zawiera elementy poruszane na wykładzie, wiec warto przejrzeć slajdy przed przystąpieniem do pisania programu.

Contents

Wstęp

Krotki opis pomysłu: Tworzymy oprogramowanie dla firmy. Firma ma pracowników zajmujących standardowo jedno z trzech stanowisk: informatyk, menadżer albo sprzątaczka. Firma posiada również flotę służbowych samochodów, nieprzyporządkowanych do pracowników. Chcielibyśmy posiadać "bazę danych" zarówno pracowników i samochodów, oraz posiadać łatwą możliwość obliczenia kosztów utrzymania wszystkiego.


1. Krok pierwszy: tworzenie klas, obiekty typu "string", wskaźnik "this", pola prywatne i publiczne

Tworzymy potrzebne klasy, umożliwiające dodawanie i wypisywanie zarówno samochodów jak i pracowników.

Wskaźnik *this

- Mówi: "teraz odwołasz sie do składowej >TEJ< klasy" (w której jesteś)

- Jego używanie nie jest obligatoryjne (przydaje sie, jeśli z jakiegoś powodu chcemy posiadać takie same nazwy dla argumentów funkcji jak i składników klasy)

Obiekty string

Zamiast tablicy char-ów stosujemy klasę C++: string. Znacznie ułatwia życie, więc korzystanie z niej jest wskazane. Potrzebne dodanie biblioteki <string>:

 #include <string>

Umożliwia np. łatwe przyrównywanie stringów: string a = "aaa"; string b = a;

Pola prywatne i publiczne:

prywatne: private

Jest dostępne tylko dla funkcji składowych klasy (i funkcji zaprzyjaźnionych z daną klasą). Czasem chcemy ukryć informacje, by nie były dostępne "na zewnątrz" (jest to powszechna praktyka przy tworzeniu dużych programów). Jeśli nie wyszczególnimy etykiety, to wszystkie składniki klasy będą domyślnie prywatne.

publiczne: public

Publiczne składniki mogą być używane zarówno we wnętrzu klasy, jak również spoza jej zakresu.

Potrzebne klasy:

Klasa Pracownik:

pola prywatne:

string imie;
string nazwisko;  
int wiek;
double pensja;
string zawod;

metody publiczne:

void zapisz(string i, string n, int w, double p, string z);
void wypisz();

Klasa samochod:

pola prywatne:

 rodzaj marka;
 double spalanie_na_kilometr;
 double km;

metody publiczne:

 void zapisz(rodzaj marka, double spalanie_na_kilometr, double km); //uwaga, te same nazwy! żeby moc przyporządkować odpowiednie wartości - użycie wskaźnika this! "this->km = km;" - przyporządkuje do <składnika klasy km> wartość <argumentu funkcji km>)
 void wypisz();

By przetestować działanie programu, tworzymy w funkcji main() tablice pracowników i samochodów, oraz tworzymy np. 3 samochody i 3 pracowników. Wypisujemy ich na ekran.


2. Krok drugi: enum

Typy wyliczeniowe: enum. Jest to osobny typ dla wybranego zestawu stałych całkowitych.

Przydaje sie często - np. mamy ograniczana liczbę stanowisk, na jakich może być zatrudniony pracownik (w naszym przypadku: 3). Ale zapamiętywanie każdego zawodu jako "string" nie dość, ze zajmuje niepotrzebna bardzo dużo miejsca, to jeszcze sprzyja generowaniu problemów (np. jeden programista wpisze "Sprzataczka" drugi "sprzataczka", a trzeci, mający brata pracującego przy sprzątaniu wpisze "sprzatacz". A wtedy zaczynają sie problemy z porównywaniem składnika "zawod" z jakimś konkretnym słowem... Wiec myślimy: skoro mamy trzy zawody, to przypiszmy im numery. Informayk - 1, sprzątaczka - 2, manager - 3. I po problemie. Jednak zapamiętywanie, która cyferka odpowiada jakiemu zawodowi może również generować problemy, chociaż innego rodzaju - w dużych programach mamy tysiące linijek kodu. Wyobraźmy sobie, ze musimy dodać nowa funkcje. A kto będzie pamiętał po tygodniu, czy 3 to była sprzątaczka, czy manager...

Używamy wiec typu wyliczeniowego enum:

Przykład użycia: enum zawod{ informatyk=1, sprzataczka, manager};

W ten sposób, jeśli zadeklarujemy taki typ globalnie, to za każdym razem, jeśli napiszemy w kodzie "manager" to program będzie wiedział, ze chodzi o 3. A jeśli programista próbował by napisać np. "Manager" to od razu kompilator zgłosi błąd. Oczywiście powodów użycia typów wyliczeniowych możemy wymyślać dużo więcej.

Zamiast deklarować typu wyliczeniowego globalnie, możemy zrobić to również wewnątrz klasy. Takie postępowanie jest bardzo naturalne - niektóre wyliczenia wiążą sie tylko i wyłącznie z jedną, konkretną klasą. Wtedy możemy zadeklarować:

class Pracownik{
 public:
   enum zawod{ informatyk=1, sprzataczka, menadzer};
    ...
 }

Wtedy wewnątrz składników klasy używamy zwyczajnie slow "informatyk" tak, jakby był to dowolny int.

Na zewnątrz klasy musimy jednak określić, z jakiego zakresu słowo "informatyk" ma pochodzić: Pracownik::informatyk.

Wydawało by sie, ze łatwiej zapamiętać cyfrę 1, niż złożoną konstrukcję "Pracownik::informatyk". Ale to niekoniecznie prawda - jeśli przeglądamy kod jakiś czas po napisaniu programu i widzimy w nim masę 1, 2 i 3 - to niewiele jesteśmy w stanie z niego przeczytać. Jeśli natomiast mamy w takich miejscach "Pracownik::informatyk" kod staje sie dla nas dużo bardziej czytelny i przejrzysty - jesteśmy w stanie od reki odpowiedzieć na pytanie, czego dokładnie dotyczy dana linijka.

Zadanie:

- zmodyfikować markę samochodu tak, by była globalnym typem wyliczeniowym (przyjmijmy, ze firma kupuje jedynie ople, mazdy i toyoty).

- zmodyfikować zawód pracownika tak, by był typem wyliczeniowym (informatyk, sprzataczka, manager) w zakresie klasy Pracownik (oczywiście publicznym).

Wskazówka:

Zamiast: void zapisz(string i, string n, int w, double p, string z); Będziemy mieli: void zapisz(string i, string n, int w, double p, zawod z);

Zamiast: flota[0].zapisz("Opel",7,200); Będziemy mieli: flota[0].zapisz(Opel,7,200);

Zamiast: personel[0].zapisz("Jan","Kowalski",43,1000.0,"informatyk"); Będziemy mieli: personel[0].zapisz("Jan","Kowalski",43,1000.0,Pracownik::informatyk);

Sprawdzić czy działa!


3. Krok trzeci: funkcje zaprzyjaźnione

Funkcje zaprzyjaźnione - funkcje, które mimo, że nie są składnikami klasy maja dostęp do wszystkich (nawet prywatnych i chronionych) składników klasy.

To nie funkcja ma twierdzić, ze przyjaźni sie z klasa, ale sama klasa daje jej dostęp do swoich prywatnych i chronionych składników.

- Funkcja zaprzyjaźniona może być przyjacielem więcej niż jednej klasy!

- Jeśli klasa deklaruje przyjaźń ze wszystkimi funkcjami innej klasy, możemy mieć klasę zaprzyjaźnioną.

- słowo kluczowe: friend

Dodajmy funkcję zaprzyjaznioną z oboma klasami:

friend double suma_wydatkow(Pracownik* pracownicy, int n, Samochod* samochody, int m);

Przyjmuje tablicę pracowników, ilość pracowników, tablicę samochodów, ilość samochodów.

Podlicza cale wydatki firmy: suma pensji pracownikow + wydatki na benzyne (suma spalanie_na_kilometr * km * cena_benzyny) oraz zwraca ja poprzez return.

Cena benzyny może byc zmienną globalna programu:

double cena_benzyny = 5.0;

Jako ze zarówno pensja, jak i cechy samochodu sa prywatne, normalna, niezaprzyjazniona funkcja nie mogła by sie do nich dostać. Jeśli jednak klasy zadeklarują przyjazń z taką funkcją, to bedzie mogla ona bez przeszkód wyciagnąć ich prywatne składniki: w kodzie takiej funkcji będziemy mogli użyć np.: pracownicy[i].pensja

4. Krok czwarty: Przesyłanie do funkcji argumentow bedacych obiektami: Przesyłanie przez wartość i przesyłanie przez referencje

Przesyłanie przez wartosc

- funkcja pracuje na kopii

Jeśli klasa jest spora, zużywamy dużo zasobów na utworzenie kopii. Lecz czasem własnie tego chcemy: wyobraźmy sobie, ze firma ma 5 konkretnych Opli i dokupiła nowy, szósty.

Stworz funkcje Samochod SkopiujSamochod(Samochod a) która zwraca obiekt klasy Samochod, identyczny jak jego kopia z jednym wyjątkiem: chcemy, by liczba kilometrów była wyzerowana.


Przesyłanie przez referencje

- funkcja pracuje na oryginale

Wyobraźmy sobie, ze chcemy zmienic ktoryś z parametrów samochodu. Żeby to zrobic odświerzamy cały wpis.

Stworz klase void ZmienSamochod(Samochod &a, rodzaj m, double s, double k) która poprawia obiekt a, przypisując mu nowe wartosci: marki - m, spalanie_na_kilometr - s oraz km - k Obiekt nie musi byc zwracany na koniec, gdyz pracujemy bezpośrednio na obiekcie podanym jako argument.


5. Krok piąty: Domyslne wartosci argumentow funkcji

To jest niesamowicie proste i przydatne. Powiedzmy, ze tworząc (zapisując) samochod, zazwyczaj nasza firma kupuje samochody spalajace 7 l/km, a gdy są nowe, to ich przebieg wynosi 0.

Modyfikujemy:

 void zapisz(rodzaj marka, double spalanie_na_kilometr, double km)

Dodając "=<jakaś liczba>" przy argumentach funkcji

 void zapisz(rodzaj marka, double spalanie_na_kilometr=7, double km=0)

Wtedy w funkcji main jesli tworzymy samochod, mozemy napisać:

zapisz(opel, 7, 0);

albo

zapisz(opel, 7); - wpisalismy jeden argument mniej

albo

zapisz(opel); - wpisalismy dwa argumenty mniej

albo tez zmienic argumenty:

zapisz(opel, 6, 5000);


Oczywiście, nie muszą być to liczby, tak samo zadziala to z stringiem czy innym typem. Wtedy pamiętamy: najmniej istotne parametry trafiają na koniec, gdyż potem możemy pomijać je tylko od końca - nie moglibyśmy wpisać zapisz(opel, 0) chcąc wyzerować składnik km - bo skąd nasz program ma wiedzieć, że nie chodzi nam o spalanie_na_kilometr? Zawsze będzie przypisywał parametry od początku: pierwszy - pierwszemu, drugi - drugiemu, itd.

Bardzo często i powszechnie stosowane. Zapamiętać!


6. Krok szósty: Przeładowanie nazw funkcji.

- nadanie jej wielu znaczeń, gdyż istnieje kilka różnych funkcji o tej samej nazwie.

Wyobraźmy sobie, ze zamiast "rodzaju" void ZmienSamochod(Samochod &a, rodzaj m, double s=7, double k=0) jednak chcielibyśmy mieć możliwość podania stringa (np. gdy użytkownik podaje nazwę z klawiatury, a nie dzieje sie to wewnątrz kodu programu): void ZmienSamochod(Samochod &a, string m, double s=7, double k=0).

Jako, że obie funkcje robią dokładnie to samo, chcielibyśmy zachować rownież taką samą nazwę. W C++ można tak zrobic - kompilator sam odkryje, czy podawany przez nas obiekt to integer (enum) czy tez string i bazujac na tej informacji wywoła odpowiednią funkcję.

Dla naszego programu zakładamy, że jedynie te trzy marki samochódow sa dostępne. Chcielibyśmy, żeby funkcja void ZmienSamochod(Samochod &a, string m, double s=7, double k=0) sprawdzała, czy string m jest == "Opel" albo "opel" i na tej podstawie ustawiała markę samochodu na opel (z enum!). Podobnie dla pozostalych marek. a pozostale wartosci przypozadkowala tak samo jak w poprzedniej funkcji.

Na koniec:

Podzielić nasz program na oddzielne klasy: główną (main.cpp), dla pracownika (pracownik.cpp, pracownik.h) oraz dla samochodu (samochod.cpp, samochod.h) - uwaga, pewne modyfikacje mogą być konieczne!

Informacje dodatkowe: Zaslanianie nazw - Mozna, ale raczej utrudnia zycie, wiec unikamy.