07 Programowanie obiektowe klasy, obiekty, ochrona danych

background image

Lekcja 7: Programowanie obiektowe - klasy, obiekty, ochrona
danych.

Wstęp

Umiecie już pisać programy strukturalne (przynajmniej mamy taką nadzieję). Prawdopodobnie słyszeliście też o obiektach - może
jeszcze nie w naszym podręczniku, ale gdzieś na pewno. Aby w pełni skorzystać z możliwości programowania wizualno-obiektowego
w BCB, musicie poznać choćby podstawy programowania zorientowanego obiektowo, będącego istotą programowania w C++. To
przede wszystkim bezpośrednim wsparciem dla takiego programowania różni się język C++ od "zwykłego", czyli strukturalnego języka
C.

Oprócz zwykłej dawki teorii zamieściliśmy więc w tej lekcji przykład obiektowej analizy problemu. Natomiast nie będzie tu już
kalkulatora. Zamiast niego - zamieszczamy przykład przydatnej klasy, która realizuje rotacyjny bufor na liczby o wysokiej wydajności.

Obiekty i klasy

W trakcie dotychczasowego kursu programowania poznaliście (mamy taką nadzieję) podstawy programowania strukturalnego. Jest to
historycznie najstarsza metodologia programowania. Mimo tego jest do tej pory znana i często stosowana, szczególnie wszędzie tam,
gdzie nie mamy dostępu do nowoczesnych języków obiektowych lub gdzie należy szybko rozwiązać niezbyt skomplikowany problem.

Ta lekcja poświęcona będzie innemu podejściu do programowania, tzn.

programowaniu zorientowanemu obiektowo. Aby pokazać

różnicę pomiędzy obiektowym a strukturalnym podejściem do programowania, spróbujmy jeszcze raz sprecyzować, czym
charakteryzuje się programowanie strukturalne. W tego typu podejściu do programowania najważniejszy jest algorytm, natomiast
sposób zapisu i pamiętania danych, na jakich operuje, jest drugorzędny.

Wady programowania strukturalnego

Analizując problem strukturalnie, zawsze układamy pewną kolejność działań, operacji wykonywanych przez komputer. Efektem tego
jest powstawanie funkcji. Każdy z algorytmów wymaga pewnych danych wejściowych, które przekształca, produkując dane
wyjściowe. Tak więc wybór czy też opracowanie algorytmu determinuje, jakich danych będzie potrzebował i jak mają być one
zapamiętywane. W ten sposób dostajemy listę parametrów i zmiennych przekazywanych do procedury.

Ż

eby nie być gołosłownym, przeanalizujmy proces powstawania prostego programiku rysującego np. prostokąt na ekranie komputera i

dokonującego na nim podstawowych przekształceń, tzn. obrotu i przesunięcia. Celem jest otrzymanie rysunku i jego późniejsze
przekształcenia. Najpierw zastanówmy się, jak rysuje się prostokąt. Wystarczy, że narysujemy cztery odcinki łączące jego wierzchołki.
Potrzebujemy więc zapamiętać położenia czterech punktów stanowiących wierzchołki prostokąta. Oczywiście nie jest to jedyny sposób
opisu, jaki możemy zastosować, lecz wydaje się, że jest najbardziej naturalny. Jeśli chcemy przesunąć prostokąt o wektor (x,y),
wystarczy że do odpowiednich współrzędnych dodamy współrzędne wektora. Inaczej z obrotem - aby go dokonać, musimy znać
ś

rodek i kąt, o jaki należy prostokąt obrócić. W tym przypadku wygodniej byłoby pamiętać prostokąt jako położenie jego środka,

długość przekątnej oraz dwa kąty: pomiędzy przekątnymi i pomiędzy jedną z nich a osią układu współrzędnych. W wyniku
przedstawionej analizy otrzymujemy kilka procedur oraz strukturę danych, w jakiej pamiętamy prostokąt. Przykładowo, mogłoby to
wyglądać tak:

struct

SPr1

{

int

x1, y1, x2, y2, x3, y3, x4, y4;

};

struct

SPr2

{

int

xs, ys;

double

dl_p;

double

kat_p, kat_ox;

};

void

rysuj(SPr1 p);

SPr1 przesun (SPr1 p,

int

x,

int

y);

SPr2 obroc(SPr2 p,

double

kat);

background image

I jeszcze dwie procedury pomocnicze, wykorzystywane przez procedurę obracającą, a służące do przekształcenia opisu opartego na
czterech punktach w opis oparty na środku, przekątnej i dwu kątach - i na odwrót:

SPr2 wierzcholki_na_katy(SPr1 p);
SPr1 katy_na_wierzcholki(SPr2 p);

Dobrym zwyczajem w programowaniu jest przewidywanie sytuacji błędnych i reagowanie na nie, tak więc każdą z wymienionych
procedur należałoby poszerzyć o sprawdzanie błędów (np. czy dane cztery punkty rzeczywiście tworzą prostokąt, czy długość
przekątnej nie jest ujemna, itp ...).

Jeżeli uważnie przeczytaliście powyższy tekst, zauważyliście zapewne, że teraz stosunkowo proste zadanie narysowania prostokąta, po
analizie i rozpisaniu na procedury, nie jest już takie przejrzyste. Musimy pamiętać, która procedura potrzebuje jakich danych, mamy
dwa niezależne opisy prostokąta i musimy dbać o synchronizację pomiędzy nimi; przy tym zadania wykonywane przez procedury
częściowo się pokrywają (kontrola poprawności danych wejściowych powinna znaleźć się w każdej procedurze) itd. Jeśli chcielibyśmy
dalej rozbudowywać ten program o nowe figury, bardzo szybko dostaniemy olbrzymią liczbę procedur i zmiennych w luźny sposób
powiązanych ze sobą.

Zaprezentowane powyżej podejście do problemu nie jest do końca typowe dla człowieka. Na ogół jak myślimy o prostokącie, to
myślimy o nim jako o całości. Prostokąt jest figurą geometryczną składającą się z czterech boków parami równoległych, o wszystkich
kątach prostych. Prostokąt można opisać na kilka sposobów, można narysować, przemieścić, obrócić, zetrzeć. W sposób naturalny
traktujemy prostokąt jako obiekt. Podobnie traktujemy np. samochód - blaszane pudełko na kółkach, określone kolorem, marką,
kształtem, które przemieszcza się, skręca, ma pewne osiągi itd... Powstaje więc pytanie - dlaczego takiego właśnie postrzegania świata
nie przenieść do programowania? Odpowiedzią na nie jest właśnie

programowanie zorientowane obiektowo.

Co to jest obiekt i czym jest klasa

Skoro mówimy o programowaniu zorientowanym obiektowo, potrzebna jest jakaś definicja obiektu. Podczas tego kursu

obiektem

będziemy nazywali dane i algorytmy na nich operujące. Zatem w powyższym przykładzie w skład obiektu nazywanego prostokątem
będą wchodzić nie tylko dane stanowiące jego opis, ale i wszystkie procedury służące zarówno do zmiany czy przekształcania tych
danych, czy też do wykonywania odpowiednich działań żądanych przez użytkownika (np. narysowania obiektu na ekranie). W dalszym
opisie dane będziemy często nazywali

polami lub właściwościami obiektu, natomiast algorytmy zapisane w postaci funkcji lub

procedur -

metodami.

Na początek możecie sobie wyobrazić obiekty jako trochę bardziej złożone rekordy, które poznaliście już wcześniej. Oprócz pól, do
których już jesteśmy przyzwyczajeni, dokładamy jeszcze metody, definiowane w sposób zbliżony do definiowania funkcji.

rekordy składają się z pól
obiekty składaj
ą się z pól i metod działających na tych polach

I podobnie jak definiowaliśmy

typ rekordowy

, by opisać strukturę rekordu, możemy teraz zdefiniować

typ obiektowy, by opisać

strukturę obiektu. Przy czym zamiast pojęcia typu obiektowego w C++ używa się pojęcia

klasa (klasa obiektów).

Zamiast mówić

typ obiektowy możemy mówić po prostu: klasa.

Definicja klasy jest podobna do definicji typu rekordowego (struktur), musi tylko uwzględniać metody. W opisie typu rekordowego
używaliśmy słowa kluczowego

struct. W opisie typu obiektowego (klasy) będziemy używać słowa kluczowego class.

W przypadku najprostszych klas, w których nie jest potrzebne definiowanie widoczności poszczególnych pól czy też metod,
możemy pozostać przy słowie

struct lub używać go zamiennie ze słowem class, lecz lepiej od razu przyzwyczajać się do dobrego

standardu używania słowa

class, którego wymaga prawdziwe programowanie obiektowe.

Załóżmy, że chcemy zdefiniowac klasę będącą bardzo prostą komputerową reprezentację diody. Przyjmijmy, że dioda, traktowana jako
ogólne pojęcie, będzie określona przez dwie właściwości (dwa pola):

ma jakiś kolor (pole typu string)

jest zapalona albo nie (pole typu bool)

Na polach tych chcemy móc wykonywać następujące operacje (metody):

"zapalenie diody"

"zgaszenie" diody

obejrzenie jej stanu: czy świeci i w jakim kolorze.

background image

Naszą klasę opisującą pojęcie diody nazwiemy CDioda - zaczynając jej nazwę od dużego C, by od razu było widać, że jest to nazwa
klasy. To nieobowiązująca, ale wygodna konwencja, analogiczna do zapisu struktur - typów rekordowych, których nazwy zaczynaliśmy
od dużego S (lub T).

Możemy więc już zaproponować definicję klasy CDioda:

class

CDioda {

public

:

string kolor;

bool

zapalona;


void

zapal() {

zapalona =

true

;

}

void

zgas() {

zapalona =

false

;

}

void

pokaz() {

if

(zapalona)

cout <<

"Swieci w kolorze "

<< kolor << endl;

else

cout <<

"Nie swieci\n"

;

}
};

W ten sposób pokazaliśmy Wam, jak można zdefiniować nowy typ obiektowy - klasę. Lecz typ obiektowy, czyli klasa obiektów, albo
krócej klasa, to jeszcze nie obiekt - pamiętajcie o tym. Definicja klasy jest opisem wspólnych metod i pól, specyficznych dla
wszystkich obiektów tej klasy, bez podawania ich konkretnych wartości. Przykładowo klasa obiektów znana pod pojęciem człowiek
definiuje, iż każdy człowiek ma kolor oczu. Nie jest powiedziane jaki - wiadomo tylko, że ma. Natomiast doprecyzowanie (określenie,
jaki to jest kolor) - odbywa się w odniesieniu do konkretnego obiektu - takiego lub innego człowieka. Podobnie klasa CDioda
definiuje, że każdy obiekt tej klasy (dioda) ma jakiś kolor. A jaki - to już zależy od konkretnej diody.

Tutaj mamy więc podobną sytuację jak w przypadku rekordów. Najpierw definiowaliśmy typ rekordowy (strukturę), a dopiero potem
rekordy - zmienne danego typu. I dopiero z tych zmiennych mogliśmy zacząć korzystać. Podobnie wygląda sytuacja w przypadku
obiektów. Żeby można było różne obiekty - diody wykorzystać w naszym programie, musimy najpierw zdefiniować klasę CDioda, a
potem utworzyć jakieś zmienne klasy CDioda (zmienne typu obiektowego CDioda).

Zobaczcie to na przykładzie:

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

using

namespace

std;

5.

6.

/* Definicja typu obiektowego - klasy CDioda*/

7.

class

CDioda {

8.

public

:

9.

// definicje pól o nazwach kolor i zapalona

10.

string kolor;

11.

bool

zapalona;

12.

// definicja metody zapal

13.

void

zapal() {

14.

zapalona =

true

;

15.

}

16.

// definicja metody zgas

17.

void

zgas() {

18.

zapalona =

false

;

19.

}

20.

// definicja metody pokaz

21.

void

pokaz() {

22.

if

(zapalona)

23.

cout <<

"Swieci w kolorze "

<< kolor << endl;

24.

else

25.

cout <<

"Nie swieci\n"

;

26.

}

background image

27.

};

28.

29.

int

main(

int

argc,

char

*argv[])

30.

{

31.

cout <<

"Diody ..."

<< endl;

32.

33.

// utworzenie dwóch obiektów typu CDdioda, o nazwach d1 i d2

34.

CDioda d1, d2;

35.

36.

// ustawienie wartości ich pól

37.

d1.kolor =

"zielony"

;

38.

d2.kolor =

"czerwony"

;

39.

d1.zapalona =

false

;

40.

d2.zapalona =

false

;

41.

42.

// a teraz główna pętla progamu - będzie prosić użytkownika

43.

// o podanie działania - i po każdym poleceniu wyświetlać

44.

// stan diod

45.

char

zn;

46.

do

{

47.

cout <<

"Stan diod:\n"

;

48.

d1.pokaz();

49.

d2.pokaz();

50.

cout <<

"\nCo chcesz zrobic?\n1 - zapal diode 1\n"

;

51.

cout <<

"2 - zgas diode 1\n"

;

52.

cout <<

"3 - zapal diode 2\n"

;

53.

cout <<

"4 - zgas diode 2\n"

;

54.

cout <<

"0 - zakoncz program\n"

;

55.

cin >> zn;

56.

switch

(zn) {

57.

case

'1'

: d1.zapal();

break

;

58.

case

'2'

: d1.zgas();

break

;

59.

case

'3'

: d2.zapal();

break

;

60.

case

'4'

: d2.zgas();

break

;

61.

};

62.

}

while

(zn !=

'0'

);

63.

64.

return

0;

65.

}

No tak ... uważny czytelnik może stwierdzić - tyle pisania, tyle teorii, tyle hałasu, a jedyny zysk to fakt, że zamiast pisać

pokaz(d1)

piszemy

d1.pokaz()

. I będzie miał rację - jak na razie ... przynajmniej dopóki nie pokażemy możliwości ochrony pól i metod w

obiekcie. A prawdziwą potęgę programowania obiekowego zobaczycie dopiero w następnej lekcji - jak omówimy zupełnie nowe
pojęcia - dziedziczenie i polimorfizm.

Trochę składni

Po tym prostym przykładzie z diodą możemy już podać uproszczoną, ogólną postać deklaracji klasy:

class nazwa_klasy
{
public: // co znaczy public - w następnym segmencie
// najpierw definiujemy pola różnych typów

typ_pola nazwa_pola, ...nazwa_pola;
...
// potem metody (z parametrami lub bez) operujące na tych polach

typ_zwracany nazwa_metody(lista_parametrów);
...
// potem znowu mogą być inne pola

typ_pola nazwa_pola, ...nazwa_pola;
...
// i inne metody

typ_zwracany nazwa_metody(lista_parametrów);

background image

// i tak dalej...
};

Występującym tutaj słowem kluczowym

public na razie się nie przejmujcie - tylko przyjmijcie, że być musi. Co ono znaczy, i dlaczego

być musi - wyjaśnimy w dalszej części tej lekcji.

W C++, mimo że możliwa jest jednoczesna definicja i deklaracja klasy (tak zrobiliśmy w przykładzie wprowadzającym - diody) -
zwykle postępuje się inaczej. Każdą klasę rozbija się na dwa pliki:

plik z deklaracją (a nie definicją) klasy

plik zawierający implementację klasy, czyli definicje jej metod.

Klasę deklaruje się w pliku nagłówka (*.h), natomiast definicje metod wchodzących w jej skład umieszcza w implementacji (*.cpp).
Tak więc klasa CDioda po poprawkach powinna wyglądać następująco: najpierw tworzymy nowy moduł, następnie w jego pliku
nagłówkowym umieszczamy deklarację klasy:

1.

#ifndef cdiodaH

2.

#define cdiodaH

3.

4.

#include <string>

5.

using

namespace

std;

6.

7.

/* Deklaracja klasy CDioda*/

8.

class

CDioda {

9.

public

:

10.

// pole pamiętające stan diody

11.

bool

zapalona;

12.

// kolor diody

13.

string kolor;

14.

// informacja, że dioda ma metodę zapal

15.

void

zapal();

16.

// informacja, że dioda ma metodę zgas

17.

void

zgas();

18.

// informacja, że dioda ma metodę pokaz

19.

void

pokaz();

20.

};

21.

22.

#endif

W pliku z implementacją podajemy natomiast treść (definicję) metod - to tam piszemy, co należy zrobić. Kierujemy się przy tym
zasadami takimi samymi, jak w przypadku tworzenia klasycznych funkcji, z dwoma istotnymi różnicami:

1. Nazwa metody podczas implementacji składa się z dwóch części. Implementacje (definicje) kolejnych metod tworzą tzw.

wnętrze obiektu, bo należą w całości do obiektów danej klasy. W implementacji metody konieczne jest więc zastosowanie
desygnatora (oznacznika), określającego, do jakiej klasy należy dana metoda. Za desygnatorem umieszcza się podwójny
dwukropek, a dopiero po nim nazwę metody:

typ_zwracany nazwa_klasy::nazwa_metody(lista_parametrów)
{
...
};

2. Pisząc ciało (treść) funkcji będącej metodą jakiejś klasy, możemy odwoływać się do pól tej klasy bezpośrednio, nie podając

nazwy klasy, do której dane pole należy - bo przecież wewnątrz klasy wszystkie pola są znane i bezpośrednio dostępne (to tak
jak my - na polecenie "rusz swoją ręką" - wiemy, która ręka jest nasza). Inaczej mówiąc - wewnątrz metody wszystkie pola klasy
można traktować jak zdefiniowane zmienne lokalne.

Przyjrzyjmy się więc przykładowej implementacji:

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

#include "cdioda.h"

5.

background image

6.

using

namespace

std;

7.

8.

// definicje metod

9.

void

CDioda::zapal()

10.

{

11.

zapalona =

true

;

12.

}

13.

14.

void

CDioda::zgas()

15.

{

16.

zapalona =

false

;

17.

}

18.

19.

void

CDioda::pokaz()

20.

{

21.

if

(zapalona)

22.

cout <<

"Swieci w kolorze "

<< kolor << endl;

23.

else

24.

cout <<

"Nie swieci\n"

;

25.

}

Jak widać, pisząc metodę

zapal poprzedzamy jej nazwę nazwą klasy, natomiast do pola zapalona odwołujemy się bezpośrednio, już

nie umieszczając nazwy obiektu, do którego dane pole należy.

Jeśli już mamy przygotowaną klasę, możemy zacząć z niej korzystać. Zmienne typu obiektowego możemy tworzyć podobnie do
zmiennych złożonych (typu rekordowego). Możemy odwoływać się do ich pól (nadawać im wartości, drukować itp.) i wywoływać ich
metody, zgodnie ze składnią:

nazwa_klasy obiekt_1, obiekt_2, ... , obiekt_n;

obiekt_1.nazwa_pola = wartość;

cout << obiekt_n.nazwa_pola;

obiekt_1.nazwa_metody();

Kontynuując nasz przykład wprowadzający, po umieszczeniu deklaracji i definicji klasy w niezależnym module, otrzymamy program
postaci:

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

#include "cdioda.h"

5.

6.

using

namespace

std;

7.

8.

int

main(

int

argc,

char

*argv[])

9.

{

10.

cout <<

"Diody ..."

<< endl;

11.

12.

// utworzenie obiektów typu CDioda

13.

CDioda d1, d2;

14.

15.

// ustawienie wartości ich pól

16.

d1.kolor =

"zielony"

;

17.

d2.kolor =

"czerwony"

;

18.

d1.zapalona =

false

;

19.

d2.zapalona =

false

;

20.

21.

// główna pętla progamu - będzie prosić użytkownika

22.

// o podanie działania - i po każdym poleceniu wyświetlać

23.

// stan diod

24.

char

zn;

25.

do

{

26.

cout <<

"Stan diod:\n"

;

27.

d1.pokaz();

28.

d2.pokaz();

29.

cout <<

"\nCo chcesz zrobic?\n1 - zapal diode 1\n"

;

background image

30.

cout <<

"2 - zgas diode 1\n"

;

31.

cout <<

"3 - zapal diode 2\n"

;

32.

cout <<

"4 - zgas diode 2\n"

;

33.

cout <<

"0 - zakoncz program\n"

;

34.

cin >> zn;

35.

switch

(zn) {

36.

case

'1'

: d1.zapal();

break

;

37.

case

'2'

: d1.zgas();

break

;

38.

case

'3'

: d2.zapal();

break

;

39.

case

'4'

: d2.zgas();

break

;

40.

};

41.

}

while

(zn !=

'0'

);

42.

43.

return

0;

44.

}

Obiekty dynamiczne

W przykładzie z diodą wykorzystywaliśmy obiekty statyczne (nazywane dotychczas po prostu obiektami). Można też tworzyć obiekty
dynamiczne, i odwoływać się potem do nich przez wskaźnik:

nazwa_klasy *obiekt_1, *obiekt_2, ... , *obiekt_n;

obiekt_1 = new nazwa_klasy();
...

obiekt_1->nazwa_pola = wartość;

cout << obiekt_i->nazwa_pola;

obiekt_1->nazwa_metody();
...

delete obiekt_1;

Jeśli miałyby być wykorzystywane obiekty dynamiczne, zmienia się jedynie postać programu głównego:

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

#include "cdioda.h"

5.

6.

using

namespace

std;

7.

8.

int

main(

int

argc,

char

*argv[])

9.

{

10.

cout <<

"Diody ..."

<< endl;

11.

12.

// utworzenie obiektów typu dioda

13.

CDioda *d1, *d2;

14.

d1 =

new

CDioda();

15.

d2 =

new

CDioda();

16.

17.

// ustawienie wartości ich pól

18.

d1->kolor =

"zielony"

;

19.

d2->kolor =

"czerwony"

;

20.

d1->zapalona =

false

;

21.

d2->zapalona =

false

;

22.

23.

// główna pętla progamu - będzie prosić użytkownika

24.

// o podanie działania - i po każdym poleceniu wyświetlać

25.

// stan diód

26.

char

zn;

27.

do

{

28.

cout <<

"Stan diod:\n"

;

29.

d1->pokaz();

30.

d2->pokaz();

31.

cout <<

"\nCo chcesz zrobic?\n1 - zapal diode 1\n"

;

32.

cout <<

"2 - zgas diode 1\n"

;

background image

33.

cout <<

"3 - zapal diode 2\n"

;

34.

cout <<

"4 - zgas diode 2\n"

;

35.

cout <<

"0 - zakoncz program\n"

;

36.

cin >> zn;

37.

switch

(zn) {

38.

case

'1'

: d1->zapal();

break

;

39.

case

'2'

: d1->zgas();

break

;

40.

case

'3'

: d2->zapal();

break

;

41.

case

'4'

: d2->zgas();

break

;

42.

};

43.

}

while

(zn !=

'0'

);

44.

45.

delete

d1;

46.

delete

d2;

47.

48.

return

0;

49.

}

Termometr

W ten oto sposób poznaliśmy podstawy podstaw programowania obiektowego ;). Spróbujmy więc utrwalić tę wiedzę tworząc jeszcze
jedną klasę - klasę termometru, który będzie pamiętał podaną mu temperaturę (w stopniach Celsjusza), oraz potrafił ją wyświetlić w
skali Celsjusza, Kelwina i Fahrenheita.

Nagłówek:

1.

#ifndef CTERMOMETR_H

2.

#define CTERMOMETR_H

3.

4.

class

CTermometr {

5.

public

:

6.

// pole określające stan termometru - temperaturę

7.

double

Temperatura;

8.

// metoda, która podaje temperaturę w skali Celsjusza

9.

double

dajCelsjusz();

10.

// metoda, która podaje temperaturę w skali Kelwina

11.

double

dajKelwin();

12.

// metoda, która podaje temperaturę w skali Fahrenheita

13.

double

dajFahrenheit();

14.

};

15.

16.

#endif

Implementacja:

1.

#include <cstdlib>

2.

3.

#include "ctermometr.h"

4.

5.

double

CTermometr::dajCelsjusz()

6.

{

7.

return

Temperatura;

8.

}

9.

10.

double

CTermometr::dajKelwin()

11.

{

12.

return

273.15 + Temperatura;

13.

}

14.

15.

double

CTermometr::dajFahrenheit()

16.

{

17.

return

32.0 + 9.0 * Temperatura / 5.0;

18.

}

background image

Jak wykorzystać tę klasę w programie? To już zadanie dla Was, drodzy studenci. Napiszcie program wykorzystujący tę klasę w ramach
własnych ćwiczeń.

Prostokąt

Na zakończenie tego segmentu wróćmy do wprowadzenia - opisu prostokąta. Teraz, już częściowo w obiektowy sposób, moglibyśmy
zaproponować inne rozwiązanie. Nie byłby to zestaw luźno powiązanych struktur danych i funkcji, lecz jeden obiekt, który mógły
wyglądać w następujący sposób:

Nagłówek:

1.

class

CProstokat

2.

{

3.

public

:

4.

// to są pola:

5.

int

x1, y1, x2, y2, x3, y3, x4, y4;

6.

7.

// a to metody, a ściślej tylko ich deklaracje (nagłówki):

8.

void

rysuj();

9.

void

przesun(

int

x,

int

y);

10.

void

obroc(

double

kat);

11.

// metoda zwracająca współrzędną x wybranego wierzchołka

12.

int

x_wspolrzedna(

int

nr_wierzcholek);

13.

// metoda zwracająca współrzędną y wybranego wierzchołka

14.

int

y_wspolrzedna(

int

nr_wierzcholek);

15.

// metoda zwracająca wsp. x środka prostokąta:

16.

int

x_srodek();

17.

// metoda zwracająca wsp. y środka prostokąta:

18.

int

y_srodek();

19.

// metoda zwracająca długość przekątnych

20.

double

dl_przekatna();

21.

// metoda zwracająca kąt pomiędzy przekątnymi

22.

double

kat_przekatna();

23.

// metoda zwracająca kąt między przekątną a osią x

24.

void

kat_os();

25.

};

A to schemat implementacji:

void

CProstokat::rysuj()

{
...
};

void

CProstokat::przesun(

int

x,

int

y)

{
...
};

void

CProstokat::obroc(

double

kat)

{
...
}

int

CProstokat::x_wspolrzedna(

int

nr_wierzcholek)

{
...
}

int

CProstokat::y_wspolrzedna(

int

nr_wierzcholek)

{
...
}

int

CProstokat::x_srodek()

background image

{
...
}

int

CProstokat::y_srodek()

{
...
}

double

CProstokat::dl_przekatna()

{
...
}

double

CProstokat::kat_przekatna()

{
...
}

void

kat_os()

{
...
}

Na koniec

Zapamiętajcie, że:

Klasa i obiekt to nie to samo. Klasa to typ obiektowy, a nie konkretny obiekt.

Mylenie klasy z obiektem jest częstym błędem początkujących adeptów programowania. Na szczęście tego typu błędy prawie zawsze
wyłapie kompilator, i zwróci Wam na nie uwagę.

Zamiast pojęcia:

obiekty danej klasy

spotyka się też pojęcie:

instancje danej klasy

. Czasem też:

egzemplarze danej klasy

. Brzmi

mądrzej, ale znaczy to samo.

Obiektowa analiza problemu

Zanim zaczniemy pokazywać coraz dokładniej konstrukcję klasy - przeczytajcie poniższy wywód ... potraktujcie go częściowo jako
uzasadnienie, dlaczego

warto uczyć się programowania obiektowego.

Z punktu widzenia użytkownika obiekt stanowi nierozerwalną i spójną całość - i my musimy to zapewnić. Przy obiektowym podejściu
do programowania najważniejszy jest zawsze etap analizy problemu. W tym przypadku nigdy nie powinniście zaczynać projektu od
opracowania algorytmu, jak to ma miejsce podczas programowania strukturalnego. Na początku należy odpowiedzieć sobie na pytanie,
co stanowi przedmiot naszego zainteresowania. Programując obiektowo, musimy najpierw "wykryć" obiekty w problemie. Trywialny
przykład - jeśli będziemy mieli napisać program rysujący na ekranie koła, prostokąty i trójkąty - sprawę analizy mamy już za sobą: na
pierwszy rzut oka widać, że stworzymy trzy obiekty odpowiadające poszczególnym figurom i, ewentualnie, obiekt odpowiadający
ekranowi. Zajmiemy się więc przykładem bardziej zaawansowanej analizy obiektowej.

Przykład

Załóżmy, że postawiono przed nami zadanie stworzenia programu odczytującego z czujników temperaturę i rysującego jej wykres na
ekranie. To zadanie nie jest już tak proste, jak poprzednie. Jedną z metod analizy obiektowej problemu jest wyszukanie w jego opisie
rzeczowników i zastanowienie się, które z nich powinny mieć odbicie w programie w postaci klas obiektów. Jeśli analizę taką
zastosować do omawianego przypadku, wyglądałoby to następująco:

Zadanie: Napisać program odczytujący z czujników temperaturę i rysujący jej wykres na ekranie.

Program.

Czy program może być obiektem ? - jak najbardziej. Co więcej, we wszystkich nowych językach wspierających programowanie
zorientowane obiektowo program zawsze jest obiektem. W BCB jest klasa nazywająca się

TApplication - ona właśnie

background image

reprezentuje program jako całość. Niemniej jednak nie jest to teraz na tyle istotne, by zamieszczać dokładny opis implementacji
tej klasy w BCB. Na razie zadowolimy się prostą informacją - ktoś już wykonał całą pracę i BCB zawiera klasę reprezentującą
programy - my nic nie musimy dodatkowo z tym robić.

Czujnik.

W tym przypadku czujnik stanowi rzeczywiste urządzenie istniejące fizycznie, więc naturalnym wydaje się stworzenie jego
odpowiednika w postaci obiektu. Czujnik jest opisany przez wiele danych (odczytana temperatura, masa, wymiary zewnętrzne,
zakres pomiarowy, dokładność, typ, itd...), z których przy implementacji obiektu go opisującego musimy wybrać te, które
wydają się nam potrzebne. Nasza propozycja pól klasy opisującej czujnik: aktualna temperatura (dlaczego, chyba każdy
rozumie), zakres pomiarowy (dwie liczby, będą nam przydatne do ustalenia maksymalnej i minimalnej wartości na wykresie) i
dokładność (znając ją, możemy określić przedział, w którym temperatura znajduje się na pewno). Podobnie postępujemy
wybierając metody. Stawiamy sobie pytanie: co czujnik może zrobić? Zmierzyć temperaturę - więc mamy metodę "pomiar".
Ponadto, jako metody musimy także zaimplementować metody umożliwiające komunikację z rzeczywistym urządzeniem -
czujnikiem.

Temperatura.

Czy temperatura powinna być obiektem ? - to już jest kwestią dyskusyjną. Zauważcie, że już mamy pole opisujące aktualną
temperaturę w obiekcie reprezentującym czujnik. Ponadto, temperatura jest tak naprawdę tylko i wyłącznie liczbą - więc
bezsensowne wydaje się tworzenie obiektu ją reprezentującego. Na tym etapie odrzucamy więc temperaturę z listy obiektów do
utworzenia.

Wykres.

Zastanówmy się, czym jest wykres - w naszym przypadku będzie to przedstawienie serii danych (liczb odpowiadających
temperaturze w danym czasie) na płaszczyźnie zdefiniowanej przez osie (pozioma - czas, pionowa - wartość temperatury).
Brzmi to nieco skomplikowanie. Ale przecież nic nie stoi na przeszkodzie, abyśmy do powyższego zdania także zastosowali
analizę obiektową.

A więc: wykres będzie klasą obiektów zawierających następujące pola: osie (sztuk dwie), listę serii danych (sztuk nie wiadomo
ile), wymiary fragmentu ekranu, na którym będzie narysowany (odpowiada płaszczyźnie z pierwszego zdania).

Metody wykresu:

rysuj

- będzie rysował się na ekranie,

dodaj_serie

,

usun_serie

- metody umożliwiające dodanie i

usunięcie serii danych. Dodatkowo, potrzebne będą pomocnicze procedury skalujące dane, stawiające punkty itd ..., lecz osoba
korzystająca z wykresu nie musi ich znać - więc ich wybór pozostawmy na później.

Tworząc klasę wykres wprowadziliśmy nowe pojęcia:

osi i serii danych, tak więc musimy także opracować odpowiadające im

klasy obiektów. Ponadto pojawiło się pojęcie czasu - dane muszą być odczytywane w określonych odstępach czasowych, więc
potrzebny jest nam

zegarek taktujący cały program.

Oś.

W naszym przypadku oś będzie linią charakteryzującą się kierunkiem (pozioma lub pionowa), wartością maksymalną i
minimalną oraz podziałką. Dane te stanowią pola klasy definiującej oś. Metoda będzie jedna -

rysuj

.

Seria danych.

Serię danych w naszym przypadku stanowią kolejne pomiary temperatury. Czyli popełniliśmy błąd odrzucając w
punkcie 3 temperaturę. Tworzymy więc klasę

seria_danych zawierającą kolejne wartości odczytanej temperatury wraz

z czasem pomiaru. Pola obiektu: tablica [n,2] zawierająca w pierwszej kolumnie czas, a w drugiej wartość pomiaru oraz
liczba pamiętanych pomiarów n. Metody -

dodaj_pomiar

.

Zegar.

Zegar będzie obiektem taktującym cały program. To, czy będziemy go potrzebować, czy nie, zależy od dostępnego
sprzętu. Są dwie możliwości: albo zegar jest wbudowany w czujnik lub kartę łączącą go z komputerem - wtedy w
naturalny sposób zegar wchodzi w skład klasy czujnik, lub fizycznie nie istnieje, więc powinniśmy zaimplementować
go jako oddzielny typ obiektowy. Zakładamy, że występuje sytuacja druga, więc implementujemy zegar jako klasę o
dwu polach: czas taktu i aktualny czas. Udostępniamy także jedną metodę:

tik

, wywoływaną po każdym upłynięciu

czasu taktu.

Ekran.

background image

Wracamy do pierwszego zdania - ekran. To, czy ekran powinien być typem obiektowym, zależy od tego, jak zaimplementujemy
wykres. Zakładamy, że wszystkie procedury obsługi ekranu zostaną zawarte w klasie obiektów wykres i rezygnujemy z typu
obiektowego ekran.

Skoro zdefiniowaliśmy odpowiednie klasy obiektów występujące w naszym programie, pozostaje nam jeszcze zdefiniować wzajemne
powiązania między nimi. Działanie programu powinno być następujące: co określony czas odczytywana jest temperatura i uaktualniany
wykres. Przenosząc to na powiązania pomiędzy naszymi obiektami:

1. Zegar stwierdza, że upłynął zadany czas, więc każe czujnikom odczytać temperaturę.
2. Czujniki kolejno odczytują temperaturę, wynik przekazując odpowiednim seriom danych.
3. Każda seria danych zapamiętuje nową wartość i nakazuje wykresowi się przerysować.
4. Wykres sprawdza i w razie potrzeby przerysowuje osie, następnie odświeża serie danych.

Bardziej algorytmicznie:

1. Metoda

tik

zegara wywołuje kolejno procedurę pomiar wszystkich czujników, przekazując jako parametr czas.

2. Metoda

pomiar

czujnika odczytuje wartość temperatury i wywołuje procedurę

dodaj_pomiar.

powiązaną z danym

czujnikiem serii danych, przekazując jako parametry czas i wartość temperatury.

3. Metoda

dodaj_pomiar

serii danych zapisuje wartość w odpowiednim polu tablicy i wywołuje procedurę

rysuj

wykresu.

4. Metoda

rysuj

wykresu najpierw sprawdza, czy zakresy obu osi są prawidłowe. Jeżeli nie są (np. oś czasu kończy się za

wcześnie), zmienia zakres osi i wywołuje ich metody

rysuj

. Następnie rysuje kolejno każdą serię danych, pobierając z niej

kolejne punkty i umieszczając je na ekranie.

W ten sposób dostaliśmy szkic programu obiektowego wykonującego postawione przed nami zadanie. Teraz pozostaje implementacja
poszczególnych klas obiektów. Oczywiście nie jest to kompletne rozwiązanie tego problemu. W rzeczywistości klasy wchodzące w
skład programu będą znacznie bardziej skomplikowane. Mamy nadzieję jednak, że ten uproszczony opis pomógł Wam zrozumieć, na
czym polega programowanie obiektowe.

Uwagi ogólne

Na koniec tego punktu kilka uwag bardziej ogólnych:

To, co przedstawione zostało powyżej, nazywamy programowaniem obiektowym, a nie programowaniem zorientowanym

obiektowo.

Programowanie obiektowe polega na wykorzystaniu obiektów w programie, programowanie zorientowane obiektowo

to znacznie więcej - powoduje, że program dopiero w trakcie działania "orientuje się" według obiektów, co powinien zrobić -
wykorzystując pewne specyficzne własności obiektów zwane polimorfizmem i dziedziczeniem. Programowaniu zorientowanemu
obiektowo poświęcona jest następna lekcja.

Zwróćcie także uwagę na jeszcze jedną cechę programowania obiektowego. Otóż jest ono bardzo przydatne wszędzie tam, gdzie

nad programem pracuje więcej niż jeden programista. Wtedy każdy z nich ma do opracowania jeden obiekt, który z innymi
komunikuje się tylko i wyłącznie w ściśle określony sposób. Więc zamiast zastanawiać się nad dużym problemem, ma do rozwiązania
mały problemik - czyli może pracować z dużą dozą niezależności. Trochę przypomina to składanie budowli z klocków - obiekt jest
właśnie takim klockiem. Niestety, nie ma róży bez kolców - w przypadku małych programów styl obiektowy wydłuża powstanie
programu, ułatwiając w zamian jego późniejszą modyfikację.

Pisanie każdego niebanalnego programu powinno składać się z cyklicznie następujących po sobie faz:

projektowania,

kodowania, testowania, projektowania, kodowania, testowania itd... aż uzyskamy zadowalający efekt. W powyższym
przykładzie następnym krokiem byłaby implementacja poszczególnych obiektów. W jej trakcie na pewno okaże się, że
popełniliśmy jakieś błędy w definicjach klas obiektów lub powiązań między nimi. Wtedy cofamy się do projektu i modyfikujemy
go. Podobnie, część błędów wyjdzie także w trakcie testowania. Można więc postawić pytanie - po co przywiązywać aż taką wagę
do projektu? Aby zaoszczędzić sobie niepotrzebnej pracy. Jeśli pominiecie analizę problemu i rozpoczniecie pisać program,
najprawdopodobniej po jakimś czasie będziecie musieli zaczynać od początku lub prawie od początku. Dogłębna analiza nigdy
nie wyeliminuje wszystkich błędnych założeń, ale pozwoli uniknąć błędów poważnych. Odnosząc to do przykładu - prawie na
pewno zmieni się zawartość poszczególnych typów obiektów, ale mało prawdopodobne jest, aby któryś z nich stał się
niepotrzebny.

Ochrona danych w klasach

Część prywatna, część publiczna

background image

Do tej pory nie napisaliśmy o jednej istotnej własności programowania obiektowego. Podczas opisu modułów wspomnieliśmy o
definiowaniu zmiennych i procedur lokalnych, które nie są widoczne dla programu głównego (patrz

lekcja 6

). Dość szybko okazało się,

ż

e ta właściwość jest bardzo przydatna w praktyce - tak więc wprowadzono ją w rozszerzonej formie do programowania obiektowego.

Właściwość tę nazywamy

ukrywaniem pól lub metod. W praktyce oznacza to, że daje się programiście możliwość zdefiniowania

obszaru widoczności danej zmiennej lub procedury poprzez odpowiednie jej zdefiniowanie. Standard programowania obiektowego (o
ile można o czymś takim mówić) przewiduje występowanie trzech typów ochrony zmiennych.

Pola i metody mogą być publiczne (ang.

public), co oznacza, że są one dostępne z dowolnego miejsca w programie.

Jeśli chcemy uniemożliwić jakikolwiek dostęp do zmiennej lub metody spoza implementacji metod wchodzących w jej skład,
deklarujemy ją jako prywatną (ang.

private). W C++ jest to zachowanie domyślnie (jeśli nie zadeklarujemy inaczej, wszystkie

pola i metody będą prywatne).

Trzecim typem są zmienne chronione (ang.

protected) - dostępne podczas implementacji danego obiektu i wszystkich

obiektów pochodnych.

Konstrukcja klasy w dialekcie C++, na którym oparty jest BCB, jest szersza - udostępnia aż cztery rodzaje ochrony danych:

public (publiczna) - dane z tej sekcji są zawsze dostępne.
private (prywatna) - wszystkie pola i metody umieszczone w tym bloku są dostępne dla danej klasy i tylko dla niej.

protected (chroniona) - pola i metody z tej sekcji dostępne są w implementacji danej klasy i wszystkich klas pochodnych.
published (publikowana) - sekcja ta jest podobna do sekcji publicznej, lecz kompilator w generuje także dla danych tu
umieszczonych informację o typie potrzebną do automatycznego przekierowywania odwołań do danych tam
umieszczonych. Nie będziemy się tym tutaj dokładniej zajmować - nie jest to częścią standardu C++.

W tym punkcie zajmiemy się dokładniej sekcją publiczną i prywatną, do części chronionej wrócimy później. Wygląda to nieco
zagmatwanie, więc może prosty przykład objaśni na początek, jak pokazywać, co jest prywatne, a co publiczne:

class

CMojObiekt

{

private

:

int

zmienna_prywatna;

void

metoda_prywatna();

int

inna_metoda_prywatna();

public

:

int

zmienna_publiczna;

void

metoda_publiczna();

int

inna_metoda_publiczna();

}

Podobnie jak dla programowania strukturalnego powstał jednolity styl zapisu algorytmu w postaci graficznej, dla programowania
obiektowego również utworzono specjalny język opisu klas obiektów i ich wzajemnych powiązań. Nazywa się on

UML (ang.

Unified Modelling Language

). Korzystając z niego możemy przedstawić obiekty jako prostokąty, każdy z nich ma wydzieloną

część, w kórej zapisuje się zmienne (nazywane argumentami bądź polami) oraz drugą przeznaczoną na procedury i funkcje
(nazywane też operacjami bądź metodami). To, czy dane pole lub metoda jest prywatna czy publiczna, definiuje się stawiając
znak

+ dla części publicznej, znak - dla prywatnej oraz znak # dla części chronionej. Zgodnie więc z zapisem UML nasz typ

obiektowy wyglądałby następująco:

W ten oto sposób zadeklarowaliśmy klasę obiektu do niczego nieprzydatnego ;). Widzicie, że ma ona część prywatną i część publiczną,
a każda z nich zawiera jedną zmienną i dwie metody. Teraz, zgodnie z tym, co napisaliśmy powyżej, do danych i metod umieszczonych
w części publicznej możemy odwoływać się bez żadnych ograniczeń w dowolnym punkcie programu. Inaczej jest ze zmiennymi i
metodami prywatnymi - do nich można odwołać się jedynie z "wnętrza" klasy. Przypominamy, że przez wnętrze klasy rozumiemy
implementację (jak kto woli - definicję) metod wchodzących w jej skład.

void

CMojObiekt::metoda_prywatna()

background image

{
zmienna_publiczna = 1;
metoda_publiczna();
zmienna_prywatna = 1;
inna_metoda_prywatna();
};

void

CMojObiekt::metoda_publiczna()

{
zmienna_publiczna = 1;
inna_metoda_publiczna();
zmienna_prywatna = 1;
metoda_prywatna();
};

Obie powyżej zdefiniowane metody należą do wnętrza klasy CMojObiekt, dlatego też w każdej z metod możemy odwoływać się do
wszystkich metod i pól zdefiniowanych w całej tej klasie (i publicznych, i tych prywatnych)- żadna ochrona zmiennych ani metod
prywatnych nie występuje. Inaczej jest już w przypadku korzystania z obiektów w głównym pliku programu. Jeśli zdefiniujemy jakiś
obiekt, np. o nazwie

mo

, jak pokazano poniżej:

CMojObiekt mo;

i gdzieś dalej w programie będziemy wywoływać jego metody czy też odwoływać się do pól w nim zawartych, to możemy to zrobić
jedynie w odniesieniu do pól i metod z części publicznej:

...
mo.zmienna_publiczna = 1;

// prawidłowo

mo.metoda_publiczna();

// prawidłowo

mo.zmienna_prywatna = 1;

// źle - ta zmienna jest pod ochroną, tak się nie da

mo.metoda_prywatna();

// źle - ta metoda jest pod ochroną, tak się nie da

...

Jak widać na powyższym przykładzie, poza obiektem możecie odwoływać się jedynie do zmiennych publicznych, natomiast
bezpośredni dostęp do zmiennych prywatnych nie jest Wam dany.

Wcześniej wspominaliśmy, że

zmienne prywatne są bardzo przydatne, może więc teraz uściślimy, dlaczego jest to taki

genialny wynalazek. Ukrywanie zmiennych stosujemy z paru powodów:

Ukrywamy w ten sposób przed programistą wykorzystującym nasz obiekt nieistotne dla niego szczegóły implementacji.
Czy do tego aby jeździć samochodem wymagana jest wiedza o budowie komór spalania czy aktualnym składzie
mieszanki paliwowej? Nie. Podczas pisania programów zwykle częściej korzysta się z obiektów napisanych przez innych
niż tworzy się własne. Więc - aby nie utrudniać sobie nawzajem życia,

wszystkie dane i metody, które nie są niezbędne

do korzystania z danego obiektu, ukrywa się przed użytkownikiem. Jeśli odniesiemy to do przykładu
wprowadzającego do obiektowej analizy problemu, możecie zauważyć, że wszystkie pola i metody obiektów
zaproponowanych przez nas są publiczne. Natomiast np. obiekt implementujący zegar będzie wymagał dodatkowych,
niewymienionych pól i metod, przykładowo odczytujących czas ze sprzętowego zegara umieszczonego w komputerze.
Jednak ich znajomość czy w ogóle świadomość ich istnienia nie wpływa w żaden sposób na korzystanie z obiektu - więc
zadeklarujemy je jako prywatne na etapie implementacji.

Czasem deklarujemy jako zmienne prywatne także pola, do których użytkownik powinien mieć dostęp (np. długość boku
w kwadracie). W tym przypadku naszym celem jest kontrolowany dostęp do zmiennej. Zamiast dać użytkownikowi
możliwość bezpośredniego zapisywania i odczytywania danego pola, udostępniamy mu odpowiednie procedury
wykonujące te działania, zwyczajowo nazywane

getNazwaZmiennej i setNazwaZmiennej (lub po polsku daj i zmien).

Kontrola dostępu daje nam możliwość sprawdzenia poprawności przekazywanej wartości, co przy konsekwentnym
stosowaniu zapewnia nam, że parametry obiektu są zawsze prawidłowe (mieszczą się w zbiorze parametrów
prawidłowych, np. długość boku kwadratu jest nieujemna).

Kontrola dostępu do zmiennej może także posłużyć do wykonania określonych czynności przed lub po zmianie
zawartości pola. Przykładowo, metoda zmieniająca kolor kwadratu może oprócz zmiany wartości pola kolor także
narysować figurę w nowym kolorze.

Ortodoksyjne podejście do programowania obiektowego (jak np. w języku SmallTalk czy Java) wymaga wręcz, aby

wszystkie

pola obiektu były prywatne, a dostęp do nich był jedynie poprzez metody. Jest to oczywiście przesadą, jednakże nim
zadeklarujecie jakąś zmienną jako publiczną, głęboko to przemyślcie.

background image

Przykład zastosowania

Wykorzystajmy możliwość kontroli zawartości pól do przykładu klasy termometru z

pierwszego

segmentu poprzedniej lekcji.

Zauważmy, że nie każda wartość temperatury jest możliwa do przyjęcia. Z fizyki wiadomo, że temperatury poniżej -273,15 st.
Celsjusza nie istnieją - nie ma temperatury niższej niż absolutne 0. Więc zabezpieczmy naszą klasę przed wprowadzeniem do niej
niedopuszczalnej wartości temperatury. W tym celu musimy przenieść pole temperatura do części prywatnej klasy - w ten sposób nie
będzie można do niego odwoływać się bezpośrednio z programu głównego, i co za tym idzie - nie będzie można podać nowej wartości
bez jej sprawdzenia. Musimy też dodać funkcję umożliwiającą ustawienie nowej temperatury.

Nasza klasa będzie wyglądać więc następująco:

Nagłówek:

1.

#ifndef CTERMOMETR_H

2.

#define CTERMOMETR_H

3.

4.

class

CTermometr {

5.

public

:

6.

// pobranie temperatury w skali Celsjusza

7.

double

dajCelsjusz();

8.

// pobranie temperatury w skali Kelwina

9.

double

dajKelwin();

10.

// pobranie temperatury w skali Fahrenheita

11.

double

dajFahrenheit();

12.

13.

// zmiana wartości temperatury wskazywanej przez termometr

14.

void

zmienTemperature(

double

_v);

15.

16.

private

:

17.

// stan termometru - temperatura, tym razem jako zmienna chroniona

18.

double

Temperatura;

19.

20.

};

21.

22.

#endif

Implementacja:

1.

#include <cstdlib>

2.

#include <iostream>

3.

4.

#include "ctermometr.h"

5.

6.

using

namespace

std;

7.

8.

double

CTermometr::dajCelsjusz()

9.

{

10.

return

Temperatura;

11.

}

12.

13.

double

CTermometr::dajKelwin()

14.

{

15.

return

273.15 + Temperatura;

16.

}

17.

18.

double

CTermometr::dajFahrenheit()

19.

{

20.

return

32.0 + 9.0 * Temperatura / 5.0;

21.

}

22.

23.

void

CTermometr::zmienTemperature(

double

_v)

24.

{

25.

if

(_v < -273.15) {

26.

// komunikat o błędnej wartości parametru

27.

cout <<

"Podano zla wartosc temperatury. "

;

background image

28.

cout <<

"Stara wartosc pozostaje niezmieniona\n"

;

29.

// kończymy działanie funkcji bez wprowadzania zmian

30.

return

;

31.

}

32.

33.

// w przeciwnym wypadku zapamiętujemy nową wartość temperatury

34.

Temperatura = _v;

35.

};

Napiszemy też krótki program, który korzysta z tej klasy.

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

#include "ctermometr.h"

5.

6.

using

namespace

std;

7.

8.

int

main(

int

argc,

char

*argv[])

9.

{

10.

cout <<

"Test termometru"

<< endl;

11.

12.

CTermometr t;

13.

14.

// ustawiamy początkową temperaturę

15.

t.zmienTemperature(20.0);

16.

17.

// i pozwalamy użytkownikowi "pobawić się" termometrem

18.

char

zn;

19.

double

tv;

20.

do

{

21.

cout <<

"Termometr pokazuje "

<< t.dajCelsjusz() <<

" st. C\n"

;

22.

cout <<

"\nCo chcesz zrobic?\n1 - zmien temp. 1\n"

;

23.

cout <<

"2 - pokaz w skali Kelwina\n"

;

24.

cout <<

"3 - pokaz w skali Fahrenheita\n"

;

25.

cout <<

"0 - zakoncz program\n"

;

26.

cin >> zn;

27.

switch

(zn) {

28.

case

'1'

:

29.

cout <<

"Nowa temperatura: "

;

30.

cin >> tv;

31.

t.zmienTemperature(tv);

32.

break

;

33.

case

'2'

:

34.

cout <<

"W skali Kelwina: "

<< t.dajKelwin() << endl;

35.

break

;

36.

case

'3'

:

37.

cout <<

"W skali Fahrenheita: "

<< t.dajFahrenheit() << endl;

38.

break

;

39.

};

40.

}

while

(zn !=

'0'

);

41.

42.

43.

return

0;

44.

}

Zabezpieczyliśmy się więc przed wprowadzaniem do klasy termometr nieprawidłowych wartości temperatury. Lecz - paradoksalnie -
jeszcze nie mamy pewności, że termometr zawsze pokazuje prawidłową wartość. Dlaczego? Spróbujcie uruchomić powyższy program
z usuniętą linijką:

t.zmienTemperature(20.0);

Wtedy za pierwszym razem termometr wyświetli nam wartość przypadkową, i może się zdarzyć, że będzie to temperatura spoza
dopuszczalnego zakresu... Jak więc się przed tym zabezpieczyć? Poprzez inicjowanie wartości pól w konstruktorze - czym za chwilę.

background image

Naszej klasie termometru brakuje jeszcze sporo do doskonałości, więc mamy dla Was teraz ćwiczenie - zmieńcie tę klasę tak, by
termometr pokazywał zawsze temperaturę zgodnie z wybraną skalą. Jeśli użytkownik chce mieć podaną temperaturę w skali
Kelwina - niech tak będzie, przynajmniej dopóki nie zmieni sposobu wyświetlania na skalę Celsjusza albo Fahrenheita.

Konstruktor i destruktor

W przypadku tworzenia własnych typów obiektowych, ważną rzeczą jest możliwość inicjacji wszystkich jej pól domyślnymi
wartościami. Do tego celu w programowaniu obiektowym służą specjalne metody określane jako

konstruktory. Korzystając z nich

można przypisać domyślne wartości wszystkim polom, utworzyć zmienne dynamiczne wchodzące w skład klasy, czy też dokonać
wszystkich innych niezbędnych w trakcie tworzenia obiektu działań. Ich zachowanie może zmieniać się w zależności od parametrów
przekazywanych metodzie konstruktora, np. mogą posłużyć do wykonania kopii obiektu przekazanego jako wzorzec. A skoro mamy
możliwość zainicjowania obiektu, powinniśmy mieć także możliwość posprzątania po sobie ... Czyli każda klasa może zawierać dwa
typy specjalnych metod:

Konstruktor: metoda, w której umieszczamy kod inicjujący obiekt i wykonujemy wszelkie działania niezbędne do
prawidłowego funkcjonowania obiektu. Konstruktor jest metodą, która nic nie zwraca, i

nazywa się tak samo jak klasa.

Następnie, podobnie jak w zwykłej funkcji, można podać listę parametrów. Ponieważ C++ umożliwia przeciążanie funkcji
(możliwe jest utworzenie dwóch funkcji o takiej samej nazwie lecz innej liście parametrów), możliwe jest zadeklarowanie
kilku różnych konstruktorów w danej klasie.

Obiekty zawsze są tworzone przy wykorzystaniu konstruktora. Nawet jeśli nie wywoła się go jawnie, w C++ zostanie
wywołany w sposób niejawny. I analogicznie - jeśli nie zostanie w klasie zdefiniowany żaden konstruktor, kompilator
wygeneruje sam jego domyślną wersję - która nic nie robi.

Destruktor: metoda, która jest wywoływana w trakcie niszczenia obiektu. Destruktor jest zawsze bezparametrowy i nie zwraca
wartości, zawsze

nazywa się tak jak klasa, poprzedzona znakiem tyldy ~ .

W C++ nie istnieje formalny wymóg definicji konstruktorów czy destruktorów dla każdego typu obiektu, jednak - dobry styl wymaga,
aby konstruktor zawsze był, choćby w celu przypisania zmiennym wartości domyślnych. Tak więc - wszystkie nasze dotychczas
napisane klasy nie były stworzone w dobrym stylu ...

Rodzaje konstruktorów, sposób ich tworzenia oraz wywoływania, nie są w C++ trywialną kwestią. Nie jest też trywialna zasada
działania. Lecz ponieważ nie jest to podręcznik poświęcony niuansom C++, nie będziemy omawiać tego zagadnienia bardzo
szczegółowo, zainteresowanych natomiast odsyłamy do literatury. Tutaj natomiast pokażemy działanie konstruktora na przykładzie
klasy termometru omówionej w poprzednim segmencie.

Po pierwsze - chcielibyśmy, by obiekty tworzone były zawsze z prawidłową wartością temperatury, i zakładamy, że ma być to
temperatura pokojowa. Dlatego też potrzebujemy domyślnego, bezparametrowego konstruktora.

Po drugie - chcielibyśmy mieć możliwość podania wartości początkowej temperatury w momencie tworzenia obiektu. Do tego celu
wykorzystamy konstruktor z jednym parametrem, którym będzie temperatura.

Po trzecie - chcielibyśmy zapewnić możliwość kopiowania termometru, wraz z pokazywaną przez niego temperaturą, więc
potrzebujemy konstruktora kopiującego.

Do kompletu dodamy jeszcze destruktor - nie będzie on wykorzystywany w tej klasie (będzie pusty).

Modyfikujemy więc deklarację klasy następująco - zwróćcie uwagę na nazwy konstruktorów i destruktora:

...

class

CTermometr {

public

:

// Domyślny, pusty konstruktor

CTermometr();

// konstruktor z parametrem - jest nim temperatura

CTermometr(

double

_t);

// konstruktor kopiujący z parametrem - jest nim nazwa obiektu kopiowanego

CTermometr(

const

CTermometr& s);

// destruktor

~CTermometr();

// pobranie temperatury w st. Celsjusza

double

dajCelsjusz();

background image

...

Dodajemy implementację konstruktorów i destruktora:

...

using

namespace

std;


CTermometr::CTermometr()
{

// ustalamy, że wartość temperatury będzie równa na początku

// 20 st. Celsjusza.

Temperatura = 20.0;
}

CTermometr::CTermometr(

double

_t)

{

// jeśli użytkownik podał wartość początkowej temperatury

// sprawdzimy, czy jest to wartość prawidłowa

if

(_t >= -273.15)

// jeśli tak - przyjmujemy tę temperaturę jako początkową

Temperatura = _t;

else

// jeśli nie - zakładamy, że będzie to 20 st. Celsjusza

Temperatura = 20.0;
}

CTermometr::CTermometr(

const

CTermometr& s)

{

// ponieważ kopiujemy istniejący termometr o nazwie s, to na pewno temperatura

// w nim ustawiona jest prawidłowa - nie musimy nic sprawdzać

Temperatura = s.Temperatura;
}

CTermometr::~CTermometr()
{

// destruktor w naszym przypadku nic nie będzie robił

}

double

CTermometr::dajCelsjusz()

{

return

Temperatura;

}
...

Teraz kolejne deklaracje i polecenia będą powodowały wywołanie odpowiednich wersji konstruktora:

...

// termometr zainicjowany domyślną temperaturą

CTermometr t;

// termometr, który po utworzeniu pokazuje 100 st. C

CTermometr t2(100);

// kopia termometru t2

CTermometr t3(t2);
...

I jeszcze wersja dla dynamicznego tworzenia obiektów:

...

// termometr zainicjowany domyślną temperaturą

CTermometr *t =

new

CTermometr();

// termometr, który po utworzeniu pokazuje 100 st. C

CTermometr *t2 =

new

CTermometr(100);

// kopia termometru t2

CTermometr *t3 =

new

CTermometr(*t2);

background image

...

Przykład - bufor danych

Na koniec tej lekcji zamieścimy przykład klasy, która będzie realizowała bufor na n ostatnich liczb.

W pewnym sensie - funkcjonalnie - jest to odpowiednik jednowymiarowej tablicy, tyle że znacznie od niej wygodniejszy. Po pierwsze,
zupełnie swobodnie, i w dowolnym momencie, możemy zmieniać rozmiar naszego bufora. Po drugie - zawsze ostatnio wstawiony do
niego element będzie pamiętany jako ten o indeksie 0, a wszystkie pozostałe przesuną się o jedno pole dalej (zerowy zostanie jedynką,
jedynka dwójką, itd..., a ostatni zostanie "wyrzucony" z bufora).

Zacznijmy od nagłówka klasy (plik cbufor.h):

1.

#ifndef cbuforH

2.

#define cbuforH

3.

4.

#include <string>

5.

6.

/* klasa bufora danych o zmiennej długości, działającego rotacyjnie

7.

8.

Służy do pamiętania n ostatnich wartości */

9.

class

CBufor {

10.

public

:

11.

/* Domyślny konstruktor - brak parametrów, przypisuje wartości domyślne */

12.

CBufor();

13.

/* konstruktor kopiujący */

14.

CBufor(

const

CBufor& src);

15.

/* Destruktor */

16.

virtual

~CBufor();

17.

18.

/* Umieszczenie nowej wartości na początku bufora */

19.

void

dodaj(

double

_v);

20.

21.

/* Pobranie wartości z bufora opóźnionej o k kroków */

22.

double

wartosc(

int

_k);

23.

24.

/* resetowanie bufora - ustawienie we wszystkich próbkach wartości równej

25.

podanej jako parametr */

26.

void

czysc(

double

_v);

27.

28.

/* Wyświetlenie bufora na ekranie */

29.

void

pokaz();

30.

31.

/* ustawienie długości bufora */

32.

void

zmienRozmiar(

int

_n);

33.

/* pobranie długości bufora */

34.

int

dajRozmiar();

35.

36.

protected

:

37.

38.

// Wstaźnik do tablicy będącej buforem

39.

double

*FDane;

40.

// Rozmiar bufora

41.

int

FRozmiar;

42.

// indeks aktualnej pozycji w buforze

43.

int

FPozycja;

44.

};

45.

46.

#endif

Implementacja klasy:

1.

#include <iostream>

background image

2.

3.

#include "cbufor.h"

4.

5.

using

namespace

std;

6.

7.

// domyślny konstruktor - pusty

8.

CBufor::CBufor()

9.

{

10.

// inicjowanie pól

11.

FDane = NULL;

12.

FRozmiar = 0;

13.

// ustawienie domyślnego rozmiaru

14.

zmienRozmiar(2);

15.

}

16.

17.

// konstruktor kopiujący

18.

CBufor::CBufor(

const

CBufor& src)

19.

{

20.

// inicjowanie pól

21.

FDane = NULL;

22.

FRozmiar = 0;

23.

24.

// zmiana rozmiaru

25.

zmienRozmiar(src.FRozmiar);

26.

// kopiowanie danych

27.

for

(

int

i = 0; i < FRozmiar; ++i)

28.

FDane[i] = src.FDane[i];

29.

}

30.

31.

CBufor::~CBufor()

32.

{

33.

// jak usuwany jest obiekt - musimy też zwolnić należącą

34.

// do niego pamięć wykorzystywaną przez tablicę na dane

35.

if

(FDane)

36.

delete

[] FDane;

37.

}

38.

39.

void

CBufor::dodaj(

double

_v)

40.

{

41.

// tutaj zobaczycie fragment kodu "profesjonalnego" ;)

42.

++FPozycja %= FRozmiar;

43.

FDane[FPozycja] = _v;

44.

45.

/* A oznacza on dokładnie coś takiego:

46.

FPozycja++;

47.

if (FPozycja >= FRozmiar) // jak dojdziemy do końca - zaczynamy

48.

// od początku

49.

FPozycja = FPozycja - FRozmiar

50.

FDane[FPozycja] = _v; */

51.

}

52.

53.

double

CBufor::wartosc(

int

_k)

54.

{

55.

// i znów kod "profesjonalny"

56.

return

FDane[(FRozmiar + FPozycja - _k ) % FRozmiar];

57.

58.

/* A oznacza on następujące działania:

59.

k - ma oznaczać wartość umieszczoną w tablicy k kroków wcześniej

60.

czyli FPozycja - k (jeśli k jest mniejsze lub równe FPozycja ) lub

61.

FPozycja + FRozmiar - k ( w przeciwnym wypadku )

62.

int idx;

63.

if (k <= FPozycja)

64.

idx = FPozycja - _k;

65.

else

66.

idx = FPozycja + FRozmiar - _k;

67.

68.

return FDane[idx]; */

69.

}

background image

70.

71.

void

CBufor::czysc(

double

_v)

72.

{

73.

// kasowanie dotychczasowej zawartości bufora - i ustawienie pozycji na jego początek

74.

FPozycja = 0;

75.

for

(

int

i = 0; i < FRozmiar; i++)

76.

FDane[i] = _v;

77.

}

78.

79.

void

CBufor::zmienRozmiar(

int

_n)

80.

{

81.

// sprawdźmy na początek czy użytkownik podał prawidłowy i inny niż dotychczas rozmiar

82.

if

(_n <= 0 || _n == FRozmiar)

83.

// jeśli nie - to go nie posłuchamy - i nic nie zrobimy

84.

return

;

85.

86.

// Tworzymy nową tablicę na dane

87.

double

*ptr =

new

double

[_n];

88.

// wypełniamy zerami

89.

for

(

int

i = 0; i < _n; i++)

90.

ptr[i] = 0.0;

91.

92.

// jeśli mamy w buforze jakieś dane

93.

if

(FDane) {

94.

// to je skopiujemy

95.

int

kk;

96.

if

(_n < FRozmiar)

97.

kk = _n;

98.

else

99.

kk = FRozmiar;

100.

for

(

int

i = 0; i < kk; i++)

101.

ptr[_n-i-1] = wartosc(i);

102.

103.

// kasujemy starą tablicę

104.

delete

[] FDane;

105.

}

106.

107.

// nowo utworzona tablica staje się aktualną

108.

FDane = ptr;

109.

FRozmiar = _n;

110.

FPozycja = FRozmiar - 1;

111.

112.

}

113.

114.

void

CBufor::pokaz()

115.

{

116.

cout <<

"[ "

<< wartosc(0);

117.

for

(

int

i = 1; i < FRozmiar; i++)

118.

cout <<

", "

<< wartosc(i);

119.

cout <<

" ]\n"

;

120.

}

Na koniec sam program. Przyjrzyjcie się mu uważnie ...po raz pierwszy bowiem możecie wyraźnie zauważyć potęgę programowania
obiektowego. Klasę

CBufor wystarczy napisać raz - i raz na to poświęcić czas. A potem ... a potem, w każdym Waszym programie

można korzystać z jej funkcjonalności bardzo prosto dostępnej. Zobaczcie - mamy rotacyjną tablicę, której możemy dynamicznie
zmieniać rozmiar, i której nawet nie musimy kasować!

1.

#include <iostream>

2.

#include <cstdlib>

3.

4.

#include "cbufor.h"

5.

6.

using

namespace

std;

7.

background image

8.

int

main(

int

argc,

char

*argv[])

9.

{

10.

// utwórzmy obiekt klasy CBufor

11.

CBufor b;

12.

13.

cout <<

"Bufor rotacyjny o zmiennym rozmiarze"

<< endl;

14.

15.

// będziemy użytkownika prosili o wprowadzanie poleceń, podawanie kolejnych

16.

// liczb, lub o zmianę rozmiaru bufora.

17.

// Po każdej zmianie będziemy wyświetlać bufor na ekranie

18.

char

zn;

19.

int

ti;

20.

double

tv;

21.

do

{

22.

// wyświetlenie zawartości bufora

23.

b.pokaz();

24.

// możliwe dla użytkownika operacje

25.

cout <<

"Co chcesz zrobic?\n"

;

26.

cout <<

"r - zmiana rozmiaru\n"

;

27.

cout <<

"w - wstaw nowa wartosc\n"

;

28.

cout <<

"k - koniec\n"

;

29.

cin >> zn;

30.

switch

(zn) {

31.

case

'r'

:

case

'R'

:

32.

cout <<

"Nowy rozmiar: "

;

33.

cin >> ti;

34.

b.zmienRozmiar(ti);

35.

break

;

36.

case

'w'

:

case

'W'

:

37.

cout <<

"Podaj nowa wartosc: "

;

38.

cin >> tv;

39.

b.dodaj(tv);

40.

break

;

41.

}

42.

}

while

(zn !=

'k'

);

43.

44.

return

0;

45.

}

Zadania

Zadań szukaj w wersji on-line podręcznika


Wyszukiwarka

Podobne podstrony:
1177 PROGRAM-ostat, WSPOL, WSPOL ochrona osób mienia obiektów
programowanie obiektowe 07, c c++, c#
programowanie obiektowe 07
piasecki,podstawy programowania, Definicja klasy, tworzenie obiektów
Programowanie obiektowe(ćw) 1
Zadanie projekt przychodnia lekarska, Programowanie obiektowe
Programowanie obiektowe w PHP4 i PHP5 11 2005
Programowanie Obiektowe ZadTest Nieznany
Egzamin Programowanie Obiektowe Głowacki, Programowanie Obiektowe
Jezyk C Efektywne programowanie obiektowe cpefpo
Programowanie Obiektowe Ćwiczenia 5
Programowanie obiektowe(cw) 2 i Nieznany
programowanie obiektowe 05, c c++, c#
Intuicyjne podstawy programowania obiektowego0
Programowanie obiektowe, CPP program, 1
wyklad5.cpp, JAVA jest językiem programowania obiektowego
projekt01, wisisz, wydzial informatyki, studia zaoczne inzynierskie, programowanie obiektowe, projek
przeciazanie metod i operatorow, Programowanie obiektowe

więcej podobnych podstron