background image

Programowanie obiektowe; © Krzysztof Urbański 2014 

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.  

background image

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. 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 

background image

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  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); 

background image

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); 
… 

background image

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. 

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; 

background image

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 

background image

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. 

background image

Programowanie obiektowe; © Krzysztof Urbański 2014 

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).