Temat ten można potraktować jako temat dodatkowy, uzupełniający.
Wszystkie treści na stronie ir.migra.pl chronione są prawami autorskimi. Więcej informacji znajdziesz tutaj.
Spis treści
- Podstawowe pojęcia programowania obiektowego
- Tworzenie klas w języku C++
- Tworzenie klas w języku Python
- Cechy programowania obiektowego
- Hermetyzacja / enkapsulacja
- Dziedziczenie
- Polimorfizm
- Metody statyczne
1. Podstawowe pojęcia programowania obiektowego
U podstaw programowania obiektowego leży chęć ściślejszego związania rzeczywistości z jej abstrakcyjną reprezentacją w programie. Cel ten osiągnięto poprzez zdefiniowanie klas, obiektów, właściwości i metod.
Obiekt, zwany też niekiedy instancją klasy (ang. class instance), to konkretyzacja danej klasy, wypełnienie wzorca określonymi danymi.
Przykład 1. Określanie klasy, metod i obiektu
Człowiek jest klasą.
Klasa Człowiek posiada następujące właściwości:
imię
nazwisko
data urodzenia
wzrost
waga
Człowiek potrafi też wykonywać następujące czynności (wykonywać metody):
chodzić
siedzieć
leżeć
pracować
spać
myśleć
mówić
Adam_Abacki jest obiektem klasy Człowiek.
Adam_Abacki posiada cechy (pola) o następujących wartościach:
imię: Adam
nazwisko: Abacki
data urodzenia: 6 czerwca 1996 roku
wzrost: 186 cm
waga: 76 kg
Adam_Abacki potrafi wykonywać te same czynności, co każdy inny obiekt klasy Człowiek, choć oczywiście sposób ich wykonania będzie zależeć od jego indywidualnych cech (np. chodzi szybko, śpi długo).
Ćwiczenie 1. Określamy właściwości metody przykładowej klasy
- Rozważ klasę Samochód.
- Zastanów się, jakie właściwości metody będzie zawierać klasa Samochód.
Pierwszym zastosowaniem programowania obiektowego były symulacje komputerowe, a pierwszym językiem obiektowym – język Simula. Przykładowe języki programowania obiektowego to: C++, C#, Python i Java. Wiele elementów programowania obiektowego znajdziemy także w językach Python, JavaScript, VisualBasic i PHP.
2. Tworzenie klas w języku C++
W języku C++ deklarację klasy zaczynamy od słowa kluczowego class.
W jednej z powszechnie używanych konwencji nazewniczych nazwy klas rozpoczyna się literą „C”. W tym temacie nazwy klas będziemy zapisywać wielką literą (w języku C++ poprzedzając nazwę literą „C”). Nazwy metod będziemy zapisywać również wielkimi literami, a w przypadku nazw wieloczłonowych każdy człon będziemy zaczynać od wielkiej litery, nie stosując spacji.
Deklaracja klasy | class CKlasa
|
Obiektem w językach C++ jest zmienna typu klasy:
nazwa_klasy nazwa_zmiennej;
np.:
CKlasa Obiekt;
Ta sama zasada dotyczy wywoływania metod. Tu również podajemy nazwę obiektu, a po kropce – nazwę metody, uzupełnioną ewentualnie o parametry metody:
Obiekt.Metoda1(parametr);
Definicja metody wygląda dokładnie tak, jak definicja funkcji, z tą tylko różnicą, że nazwę metody poprzedzamy nazwą klasy. Ponadto w treści metody można bezpośrednio, bez podawania nazwy klasy, odwoływać się do pól oraz innych metod tej samej klasy.
void CKlasa::Metoda1(typ_parametru parametr)
{
...
}
typ_funkcji CKlasa::Metoda2(typ_parametru parametr)
{
...
}
Uwaga: Parametry formalne metody powinny nosić inne nazwy niż pola składowe klasy.
Przykład 2. Definicja klasy CProstokat w języku C++
Zadeklarowano klasę CProstokat. Posiada ona pola w i h, przechowujące odpowiednio szerokość i wysokość prostokąta. Dodatkowo klasa posiada metody: Pole(), Obwod(). Metody Pole() i Obwod() zadeklarowano jako funkcje. Pokazano też definicję metody Pole klasy CProstokat – funkcja CProstokat::Pole().
class CProstokat
{
public:
float w, h; // Szerokość i wysokość
float Pole();
float Obwod();
};
// Oblicza pole
float CProstokat::Pole()
{
return w * h;
}
// ... dalsze metody
Ćwiczenie 2. Analizujemy przykładową definicję klasy w języku C++
- Otwórz plik TC4_p2_c2.cpp. Przeanalizuj definicję klasy CProstokat i jej metod.
- Sprawdź, w jaki sposób utworzono obiekt Prostokat należący do tej klasy. Zapoznaj się ze sposobami korzystania z pól i metod klasy.
Ćwiczenie 3. Definiujemy klasę w języku C++
- Uzupełnij program zapisany w pliku TC4_p2_c2.cpp o definicję klasy COkrag, zawierającej te same metody, co klasa CProstokat.
- Zastanów się nad polem niezbędnym do opisania okręgu. Zdefiniuj metody.
- Zapisz program pod tą sama nazwą.
Przedstawione w przykładzie 2. definicje klasy CProstokat mają pewną wadę. Dostęp do pól klasy nie jest niczym ograniczony. Przypadkowo można zatem zmienić wartości, np. wysokości na wartość ujemną, która dla długości boku jest niepoprawna.
Ćwiczenie 4. Modyfikujemy program
- W programie zapisanym w pliku TC4_p2_c2.cpp przed instrukcją:
Prostokat.Pole();
wstaw instrukcję:Prostokat.w := -10;
- Zaobserwuj, jak zmieni się działanie programu po wstawieniu podanej w punkcie 1. instrukcji.
Problemów z brakiem kontroli w dostępie do pól obiektu można uniknąć, ograniczając dostęp do pól.
W języku C++ pola i metody dzielimy na prywatne i publiczne i określamy jako takie, używając słów kluczowych: private
i public.
Dostęp do prywatnych pól i metod jest możliwy jedynie przez inne metody danej klasy. Istnieją również pole i metody chronione (słowo kluczowe protected
).
Pole chronione i metody chronione są dostępne dla metod należących do klasy oraz klas pochodnych (punkt 4.2).
Pola publiczne i metody publiczne są dostępne bez powyższych ograniczeń.
Przykład 3. Deklarowanie pól prywatnych
Pola w i h klasy CProstokat zostały zadeklarowane jako prywatne. Poza klasą dostęp do nich nie będzie możliwy. Dodatkowo, dodano metody umożliwiające nadanie wartości zmiennym prywatnym, sprawdzające, czy nie są używane wartości ujemne.
class CProstokat
{
private: // Pola i metody prywatne
float w,h; // Szerokość i wysokość
public: // Pola i metody publiczne
float Pole();
float Obwod();
void OkreslSzerokosc(float pW);
void OkreslWysokosc(float pW);
};
// ...
void CProstokat::OkreslSzerokosc(float pW)
{
w = (pW >= 0 ? pW : 0);
}
Konstruktor zwykle wykorzystuje się do nadania wartości początkowych polom obiektu, natomiast destruktor – do zwolnienia zasobów przydzielonych dodatkowo obiektowi (np. pamięci operacyjnej). Jeżeli nie określimy konstruktora i destruktora dla klasy, kompilator stworzy je automatycznie.
class CProstokat
{
public:
float w, h; // Szerokość i wysokość
float Pole();
float Obwod();
CProstokat(); // Pierwszy konstruktor
CProstokat(float pW, float pH); // Drugi konstruktor
~CProstokat(); // Destruktor
};
Konstruktor deklarujemy jak inne metody, jednak bez określania typu metody, a jako nazwę podajemy nazwę klasy. Konstruktor może posiadać parametry. Może istnieć kilka konstruktorów, różniących się parametrami.
Destruktor nie posiada ani typu ani parametrów. Jako nazwę destruktora podajemy nazwę klasy, poprzedzoną znakiem tyldy („~”).
Jeżeli w ramach klasy zadeklarujemy istnienie konstruktora i destruktora, musimy je zdefiniować – tak, jak w przypadku każdej innej metody.
Przykład 4. Użycie destruktora
CProstokat::CProstokat()
{
w = h = 0;
}
CProstokat::CProstokat(float pW, float pH)
{
w = pW;
h = pH;
}
CProstokat::~CProstokat()
{
// Destruktor nie wykonuje żadnych operacji
}
Przykład 5. Użycie konstruktorów
int main()
{
CProstokat prostokat1; // Deklaracja "pustego" obiektu klasy CProstokat, pola w i h będą miały wartość 0
CProstokat prostokat2(10, 5); // Deklaracja obiektu klasy CProstokat, pole w będzie miało wartość 10, pole h – wartość 5
cout << prostokat1.w << " " << prostokat1.h << endl;
cout << prostokat2.w << " " << prostokat2.h << endl;
}
Ćwiczenie 5. Tworzymy nowy program wykorzystujący klasy w języku C++
- Umieść klasy CProstokat i COkrag w nowym programie i zadeklaruj wszystkie pola jako prywatne. Zapisz program pod nazwą TC4_c5.
- Zastanów się, jak musisz zmodyfikować definicje klas CProstokat i COkrag, aby w programie korzystającym z tych klas można było nadać obiektom początkowe wymiary oraz odczytać te wartości.
3. Tworzenie klas w języku Python
W języku Python (podobnie jak w języku C++) deklarację klasy zaczynamy od słowa kluczowego class.
W tym temacie nazwy klas będziemy zapisywać wielką literą. Nazwy metod w języku Python będziemy zapisywać małymi literami, a w przypadku nazw wieloczłonowych poszczególne człony nazwy będziemy rozdzielać znakiem podkreślenia.
W języku Python pola obiektu (podobnie jak zmienne lokalne) są tworzone w momencie pierwszego przypisania im wartości i dlatego nie trzeba ich uprzednio deklarować jak w C++.
Deklaracja klasy | class Klasa: |
W języku Python obiektem jest zmienna, do której została przypisana instancja klasy, np. po jej stworzeniu:
obiekt = Klasa()
Ta sama zasada dotyczy wywoływania metod. Tu również podajemy nazwę obiektu, a po kropce – nazwę metody, uzupełnioną ewentualnie o parametry metody:
obiekt.metoda1(parametr)
W języku Python definicja metody znajduje się wewnątrz definicji klasy i zawsze zawiera pierwszy parametr self,
poprzez który można odwoływać się do pól i metod obiektu, dla którego wywołano metodę.
class Klasa:
def metoda1(self, parametr):
...
def metoda2(self, parametr):
...
Przykład 6. Definicja klasy Prostokat w języku Python
Zadeklarowano klasę Prostokat. Posiada ona pola w i h, przechowujące odpowiednio szerokość i wysokość prostokąta. Dodatkowo klasa posiada metody: pole(), obwod(). Metody pole() i obwod() zadeklarowano jako funkcje. Pokazano też definicję metody Pole klasy Prostokat – funkcja pole(self).
class Prostokat:
def __init__(self, pW, pH):
self.w = pW # wysokość i szerokość
self.h = pH
# oblicza pole
def pole(self):
return self.w * self.h
# dalsze metody
...
Ćwiczenie 6. Analizujemy przykładową definicję klasy w języku Python
- Otwórz plik TC4_p3_c6.py.
- Przeanalizuj definicję klasy Prostokat i jej metod. Sprawdź, w jaki sposób utworzono obiekt Prostokat należący do tej klasy. Zapoznaj się ze sposobami korzystania z pól i metod klasy.
Ćwiczenie 7. Definiujemy klasę w języku Python
- Uzupełnij program zapisany w pliku TC4_p3_c6.py o definicję klasy Okrag, zawierającej te same metody, co klasa Prostokat.
- Zastanów się nad polem niezbędnym do opisania okręgu. Zdefiniuj metody.
Przedstawione w przykładzie 2. definicje klasy Prostokat mają pewną wadę. Dostęp do pól klasy nie jest niczym ograniczony. Przypadkowo można zatem zmienić wartości, np. wysokości na wartość ujemną, która dla długości boku jest niepoprawna.
Ćwiczenie 8. Modyfikujemy program
- W programie zapisanym w pliku TC4_p2.py przed instrukcją:
prostokat.pole()
wstaw instrukcjęprostokat.w = -10
- Zaobserwuj, jak zmieni się działanie programu po wstawieniu podanej w punkcie 1. instrukcji..
Problemów z brakiem kontroli w dostępie do pól obiektu można uniknąć, ograniczając dostęp do pól.
W języku Python nie ma możliwości ograniczenia dostępu do pól i metod klas (jak w języku C++), ale przyjmuje się, że nazwy metod, które nie powinny być używane spoza klasy, powinny zaczynać się od dwóch podkreślników.
Klasa może posiadać konstruktor i destruktor. Konstruktor jest wywoływany automatycznie w momencie tworzenie obiektu (np. inicjalizacji zmiennej należącej do klasy), natomiast destruktor – automatycznie w momencie usuwania obiektu. Konstruktor zwykle wykorzystuje się do nadania wartości początkowych polom obiektu, natomiast destruktor – do zwolnienia zasobów przydzielonych dodatkowo obiektowi (np. pamięci operacyjnej). Jeżeli nie określimy konstruktora i destruktora dla klasy, kompilator stworzy je automatycznie.
W języku Python rolę konstruktora spełnia specjalna metoda __init__.
W odróżnieniu od C++, w języku Python można zadeklarować tylko jedną taką metodę.
class Prostokat:
def __init__(self, pW, pH):
self.w = pW
self.h = pH
W języku Python istnieje automatyczne zarządzanie przydzielaniem i zwalnianiem pamięci i tylko w szczególnych przypadkach definiuje się destruktory. Wtedy implementujemy specjalną metodę __del__,
której jedynym argumentem jest self,
czyli obiekt, który jest usuwany.
= Prostokat(0, 0)
prostokat2 = Prostokat(10, 5)
print(prostokat1.w, prostokat1.h)
print(prostokat2.w, prostokat2.h)
Ćwiczenie 9. Tworzymy nowy program wykorzystujący klasy w języku Python
Umieść klasy Prostokat i Okrag w nowym programie. Zastanów się, jak musisz zmodyfikować definicje klas Prostokat i Okrag, aby w programie korzystającym tych klas można było nadać obiektom początkowe wymiary oraz odczytać te wartości.
4. Cechy programowania obiektowego
4.1. Hermetyzacja / enkapsulacja
Programowanie z wykorzystaniem klas i obiektów posiada wiele zalet. Dane połączone są z wykonywanymi na nich operacjami poprzez zgrupowanie pól i metod w klasach. Użycie dyrektywy private pozwala zabezpieczyć dane obiektu przed przypadkowymi lub niepoprawnymi zmianami (mówimy tu o hermetyzacji lub enkapsulacji). Metody różnych klas mogą nosić takie same nazwy, co pozwala na pisanie bardziej intuicyjnego i zrozumiałego kodu.
4.2. Dziedziczenie
Na tym nie kończą się zalety programowania obiektowego. Kolejną wygodną dla programisty cechą jest dziedziczenie (ang. inheritance). Pozwala ono na dostosowywanie istniejących klas do własnych potrzeb poprzez wzbogacanie ich o dodatkowe pola lub metody. Tak utworzona klasa może przy tym korzystać ze wszystkich nieprywatnych pól i metod swojego „przodka”.
Przykład 7. Wyjaśnienie dziedziczenia
W przykładzie 1. zdefiniowaliśmy klasę Człowiek. Uczeń jest „szczególnym przypadkiem” człowieka (ma te same cechy i potrafi wykonywać te same czynności), jednak ma też swoje specyficzne właściwości.
Klasa Uczeń jest oparta na klasie Człowiek.
Klasa Uczeń posiada te same pola, co klasa Człowiek, oraz dodatkowo pola:
szkoła
klasa
lista ocen
…
Obiekt klasy Uczeń potrafi wykonywać te same czynności, co obiekt klasy Człowiek, oraz dodatkowo czynności:
uczyć się
odpowiadać
pisać sprawdziany
…
Ćwiczenie 10. Stosujemy dziedziczenie
Zastanów się, jakie inne klasy można utworzyć na bazie klasy Człowiek. Rozważ przypadek klasy Uczeń liceum ogólnokształcącego. Jaka klasa będzie przodkiem? Jakie dodatkowe metody powinna posiadać nowa klasa?
Ćwiczenie 11. Rysujemy schemat blokowy struktury klas
Narysuj w postaci schematu strukturę klas rozważanych w ćwiczeniu 10. Jakiego rodzaju strukturę otrzymaliśmy?
Dziedziczenie klas powoduje powstanie swego rodzaju drzewa. Pojawiające się w nim klasy tworzą hierarchię klas. Odpowiednie zaprojektowanie struktury klas pozwala na skrócenie kodu – kod wspólny dla kilku klas umieszczamy we wspólnej klasie bazowej. Można to zauważyć, porównując:
- w języku C++ kod programu z przykładu 2. i ćwiczenia 3., w którym pewne metody klas CProstokat i COkrag są identyczne,
- w języku Python kod programu z przykładu 3. i ćwiczenia 7., w którym pewne metody klas Prostokat i Okrag są identyczne.
W języku C++ po nazwie klasy, po dwukropku podajemy nazwę klasy bazowej, poprzedzając ją ewentualnie modyfikatorem widoczności (public
– oznacza, że dziedziczone elementy mają taką samą widoczność, jak w klasie bazowej; z pozostałymi modyfikatorami widoczności można zapoznać się w literaturze dodatkowej).
class CKlasaPochodna : public CKlasaBazowa
{
public:
typ1 PoleDodatkowe1;
typ2 PoleDodatkowe2;
// ...
void MetodaDodatkowa1();
}
W języku Python po nazwie klasy dodajemy nazwę klasy bazowej w nawiasie okrągłym.
class KlasaPochodna(KlasaBazowa):
def dodatkowa_metoda(self):
...
4.3. Polimorfizm
Metoda wirtualna to metoda powiązana z obiektem dynamicznie (w języku C++ kompilator wybiera stosowną metodę na podstawie klasy obiektu).
Metody wirtualne pozwalają na korzystanie z polimorfizmu.
W języku C++ z polimorfizmu korzysta się, deklarując odpowiednie metody jako metody wirtualne. Oznacza się je słowem kluczowym virtual,
które musi znajdować się przed deklaracją typu metody.
W języku Python każda zdefiniowana metoda jest wirtualna.
Wykorzystanie polimorfizmu jest zagadnieniem złożonym, dlatego w ramach tego materiału nie będziemy go omawiać.
4.4. Metody statyczne
Metody w programowaniu obiektowym operują na konkretnych danych, należących do obiektu (pól). Czasami zachodzi jednak potrzeba zdefiniowania metody logicznie przynależącej do danej klasy, natomiast niewykorzystującej konkretnych danych.
Przykład 8. Tworzenie klasy do obsługi dat
Utworzymy klasę CData
do obsługi dat:
class CData
{
public:
int dzien;
int miesiac;
int rok;
CData(int d, int m, int r); // Konstruktor
string DataTekstowo();
int DzienRoku();
static bool CzyRokPrzestepny(int rok);
};
Utworzymy klasę Data
do obsługi dat:
class Data:
def __init__(self, d, m, r):
self.dzien = d
self.miesiac = m
self.rok = r
def data_tekstowo(self):
...
def dzien_roku(self):
...
@staticmethod
def czy_rok_przestepny(rok):
...
Klasa posiada pola dzien, miesiac, rok – do przechowywania elementów daty, konstruktor, tworzący obiekt odpowiadający określonemu rokowi, miesiącowi i dniu, oraz metody:
– DataTekstowo(), zwracającą daną datę w postaci tekstu,
– DzienRoku(), obliczająca i zwracającą, którym dniem roku jest dzień odpowiadający dacie.
Implementacja metod (plik TC4_p5_Daty.cpp) odwołuje się do pól dzien, miesiac i rok. Aby jednak obliczyć numer dnia w roku, musimy sprawdzić, ile dni miał luty tego roku (w latach zwykłych ma 28 dni, w latach przestępnych – 29). Sprawdzenie przestępności roku jest czynnością logicznie powiązaną z datami, natomiast niepowiązaną z konkretnym obiektem (do metody CzyRokPrzestepny() przekazujemy parametr rok). Dlatego też metodę CzyRokPrzestepny() zadeklarujemy jako statyczną.
Metody statyczne wywołujemy, poprzedzając nazwę metody nazwą klasy, np.:
cout << 2000 << " " << CData::CzyRokPrzestepny(2000) << endl;
Klasa posiada pola dzien, miesiac, rok – do przechowywania elementów daty, konstruktor, tworzący obiekt odpowiadający określonemu rokowi, miesiącowi i dniu, oraz metody:
– data_tekstowo(), zwracającą daną datę w postaci tekstu,
– dzień_roku(), obliczająca i zwracającą, którym dniem roku jest dzień odpowiadający dacie.
Implementacja metod (plik TC4_p5_Daty.py) odwołuje się do pól dzien, miesiac i rok. Aby jednak obliczyć numer dnia w roku, musimy sprawdzić, ile dni miał luty tego roku (w latach zwykłych ma 28 dni, w latach przestępnych – 29). Sprawdzenie przestępności roku jest czynnością logicznie powiązaną z datami, natomiast niepowiązaną z konkretnym obiektem (do metody czy_rok_przestepny() przekazujemy parametr rok). Dlatego też metodę czy_rok_przestepny() zadeklarujemy jako statyczną.
Metody statyczne wywołujemy, poprzedzając nazwę metody nazwą klasy, np.:
print(f'rok 2000: przestępny: {Data.czy_rok_przestepny(2000)}')
Zadania
Uwagi:
- Aby wykonywać zadania 1-4, otwórz plik TC4_p5_Daty.cpp lub TC4_p5_Daty.py.
- Jeśli nie ma oznaczenia „C++” lub „Python”, zadanie można zrobić w języku C++ lub w języku Python.
- Uzupełnij klasę CData (C++) lub Data (Python) o metodę obliczającą, ile dni minęło między 1 stycznia 1900 roku a daną datą.
- Uzupełnij klasę CData lub Data (Python) o metodę obliczającą liczbę dni pomiędzy dwiema datami. Skorzystaj z metody utworzonej w zadaniu 1.
- Uzupełnij klasę:
CData o metodyZmienNaJutro(), ZmienNaWczoraj()
przesuwające datę o jeden dzień do przodu lub do tyłu.
Data o metodyzmien_na_jutro(), zmien_na_wczoraj()
przesuwające datę o jeden dzień do przodu lub do tyłu. - Uzupełnij klasę:
CData o metodę umożliwiającą zmianę daty o zadaną liczbę dni. Wykorzystaj metodyZmienNaJutro(), ZmienNaWczoraj().
Data o metodę umożliwiającą zmianę daty o zadaną liczbę dni. Wykorzystaj metodyzmien_na_jutro(), zmien_na_wczoraj().
- Znajdź informacje o przeciążaniu operatorów. Skorzystaj z możliwości przeciążania operatorów do przeciążenia operatora minus (-) w celu obliczenia różnicy między dwiema datami.
- Znajdź informacje na temat funkcji
sprintf()
(i pokrewnych:printf(), fprintf()
), wykorzystanej w implementacji metodyCData::DataTekstowo().
Wymień przykładowe elementy formatowania, z jakich można korzystać w tych funkcjach. - Znajdź informacje na temat użytych w plikach ćwiczeń f-ciągów (ang. f-string), czyli sposobu formatowania danych wyjściowych w języku Python. Spróbuj tak zmienić klasę Data, aby miesiąc i dzień zawsze używały dwóch znaków, np. pierwszy stycznia 2022 jako 01.01.2022.
Dla zainteresowanych