Programowanie obiektowe; © Krzysztof Urbański 2014
1
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 wyraźnie 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.
Programowanie obiektowe; © Krzysztof Urbański 2014
2
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. Znajdź 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
Programowanie obiektowe; © Krzysztof Urbański 2014
3
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 wskaźnik”. 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ę wskaźnikiem 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ć wskaźnika 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);
Programowanie obiektowe; © Krzysztof Urbański 2014
4
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);
…
}
Programowanie obiektowe; © Krzysztof Urbański 2014
5
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.
Wyraźnie 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ę nieźle 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;
Programowanie obiektowe; © Krzysztof Urbański 2014
6
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
Programowanie obiektowe; © Krzysztof Urbański 2014
7
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.
Programowanie obiektowe; © Krzysztof Urbański 2014
8
Zadanie do realizacji
1) Zbadaj zachowanie konstruktorów i destruktora, korzystając z wcześniejszych kodów
źró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).