Programowanie obiektowe w C++¶
Wprowadzenie do programowania obiektowego¶
Cechy podejścia obiektowego
Wszystko jest obiektem. Można wyobrazić sobie obiekt jako wymyślną zmienną - taką, która nie tylko przechowuje dane, ale może także realizować żądania, czyli wykonywać pewne operacje. Dowolne pojęcia ze świata rozwiązywanego problemu można reprezentować za pomocą obiektu.
Program jest zbiorem obiektów, które poprzez wysyłanie komunikatów mówią sobie nawzajem, co robić. „Wysyłanie komunikatu” to inaczej zażądanie wykonania przez niego pewnej operacji. Można myśleć o komunikacie jako o wywołaniu funkcji przynależnej do konkretnego obiektu.
Każdy obiekt posiada własną pamięć, na którą składają się inne obiekty. Nowy obiekt tworzymy, łącząc w jeden pakiet grupę już istniejących obiektów. Budujemy w ten sposób złożone programy, ukrywając jednocześnie ich złożoność za prostą obiektów.
Każdy obiekt posiada swój typ. Każdy obiekt jest instancją pewnej klasy, gdzie klasa jest synonimem typu.
Wszystkie obiekty danego typu mogą otrzymywać te same komunikaty.
Programowanie obiektowe polega na zidentyfikowaniu co/kto składa się na opisywany system, a dopiero w dalszej kolejności jak wykonać poszczególne zadania. Zadaniem programisty jest stworzenie klas - szablonów według których zbudowane będą konkretne obiekty czyli instancje.
Obiekty i klasy¶
Wszystkie obiekty mają:
Zestaw atrybutów (danych) dla nich charakterystycznych
Zestaw zachowań, które wykorzystują te dane
Obiekt ma stan, zachowanie i tożsamość.
Obiekty o podobnych właściwościach i zachowaniach tworzą wspólną klasę.
Konkretny obiekt danej klasy nazywa się instancją klasy.
Klasa może zawierać:
Pola (atrybuty) opisujące właściwości. Polami są zmienne reprezentujące typy proste lub inne obiekty. Wartości pól obiektu opisują stan obiektu.
Metody (funkcje składowe) implementujące zachowania. Zestaw dostępnych dla klienta (innego obiektu) metod definiuje interfejs obiektu.
Typy zagnieżdżone.
Abstrakcja¶
Abstrakcja - uproszczony opis rozpatrywanego problemu, polegający na ograniczeniu zakresu cech obiektu wyłącznie do cech kluczowych, niezależnych od implementacji.
Cele stosowania abstrakcji:
Ułatwienie rozwiązania problemu
Zwiększenie jego ogólności (tym samym stosowalności)
Abstrakcyjny typ danych - zestaw operacji z ich implementacją.
Etapy tworzenia abstrakcyjnego typu danych:
Wybór opisowej nazwy dla typu.
Sporządzenie listy operacji. * Abstrakcyjny typ danych jest identyfikowany przez czynności, w których uczestniczy. * Definicja operacji inicjalizacji (konstruktory) oraz czyszczenia (destruktory).
Zaprojektowanie interfejsu typu. * Interfejs typu powinien upraszczać jego poprawne zastosowania i utrudniać zastosowanie niepoprawne.
Implementacja typu - nie wolno dopuścić aby wpływała na interfejs.
Enkapsulacja¶
Enkapsulacja (kapsułkowanie, hermetyzacja lub inaczej ukrywanie informacji) polega na:
ukrywaniu pewnych danych składowych lub metod obiektów danej klasy tak, aby były one (i ich modyfikacja) dostępne tylko metodom wewnętrznym danej klasy,
zastosowaniu funkcji (metod) w celu ochrony danych przed omyłkową bądź nieupoważnioną modyfikacją.
Enkapsulacja tworzy dwa odrębne obszary zainteresowania (uwagi) - segment kodu, który używa i segment kodu, który nie używa nazw pól danych. Ma ogromne znaczenie z punktu widzenia czytelności kodów i niezależności poszczególnych komponentów kodu.
Zalety hermetyzacji:
Umożliwia lepszą kontrolę - dostęp do danych składowych jest możliwy tylko przy pomocy wybranych metod publicznych.
Umożliwia zmiany - jakakolwiek zmiana wewnętrznej (prywatnej) reprezentacji obiektów nie pociąga za sobą konieczności zmiany kodu aplikacji.
Tworzenie klasy¶
Klasa umożliwia tworzenie typów użytkownika.
Dane i metody są zgrupowane razem w obrębie klasy. Publiczne metody opisują zachowanie obiektu i są wykorzystywane przez klientów. Prywatne pola definiują stan wewnętrzny obiektu i nie powinny być dostępne dla klientów.
class BankAccount
{
public:
void withdraw(double amount);
void deposit(double amount);
private:
double balance_ = 0.0;
long number_ = -1;
std::string owner_ = "unknown";
};
„Zmienne” należące do obiektu danej klasy określają stan obiektu. Metody klasy mają dostęp do składowych klasy.
Składowe obiektu mogą być inicjowane w miejscu deklaracji wewnątrz klasy. Jeśli pole nie zostanie zainicjowane to w przypadku typów prostych przyjmuje wartość przypadkową lub w przypadku typów użytkownika przyjmuje wartość domyślną.
Implementacja metody może zostać zrealizowana wewnątrz klasy lub poza nią:
class BankAccount
{
private:
double balance_ = 0.0;
// ...
public:
void deposit(double amount)
{
assert(amount > 0.0);
balance_ += amount;
}
void withdraw(double amount);
};
void BankAccount::withdraw(double amount)
{
assert(amount > 0.0);
balance_ -= amount;
}
Publiczne metody (funkcje składowe) definiują interfejs obiektu (klasy). Wywoływane są na rzecz konkretnego obiektu.
int main()
{
BankAccount my_account;
my_account.deposit(100.0);
my_account.withdraw(50.0);
}
Modyfikatory dostępu¶
Modyfikatory dostępu definiują poziom dostępności do składowych klasy. Słowa kluczowe określające dostęp do składowych klasy mogą pojawiać się w jej ciele dowolną liczbę razy i w dowolnej kolejności.
Deklaracja |
Zakres dostępności |
---|---|
public |
Nielimitowany dostęp z poziomu kodu |
private |
Dostęp możliwy tylko w obrębie klasy lub funkcji i klas zaprzyjaźnionych |
protected |
Dostęp możliwy w obrębie klasy klas pochodnych lub funkcji i klas zaprzyjaźnionych |
Klasa ma wszystkie składowe domyślnie prywatne.
Struktura ma wszystkie składowe domyślnie publiczne.
Tworzenie obiektów¶
Tworzenie obiektów na stosie¶
void bank_app()
{
BankAccount my_account;
my_account.deposit(200.0);
}
Tworzenie obiektów na stercie¶
std::unique_ptr<BankAccount> your_account = std::make_unique<BankAccount>();
(*your_account).deposit(100.0);
your_account->withdraw(40.0);
store_in_database(std::move(your_account));
assert(your_account.get() == nullptr);
Wskaźnik this
¶
W obrębie metody wskaźnik this
jest wskaźnikiem do obiektu, dla którego wywołano metodę.
class BankAccount
{
double balance_ = 0.0;
public:
void deposit(double amount);
//…
};
void BankAccount::deposit(double amount)
{
this->balance_ += amount;
}
Metody const
¶
Jeśli sygnatura metody jest zakończona modyfikatorem const
oznacza to, że jej implementacja
nie zmienia stanu wewnętrznego obiektu:
class BankAccount
{
double balance_ = 0.0;
// ...
public:
double balance() const
{
balance_ -= 1.0; // błąd kompilacji
return balance_;
}
};
W rezultacie utworzenia metody jako metody const
wewnątrz tej metody wskaźnik this
otrzymuje również modyfikator const
. W przypadku powyżej typem wskaźnika do obiektu na rzecz którego została wywołana metoda jest const BankAccount*
.
Cykl życia obiektów¶
Obiekty lokalne - czas życia zależny od bloku kodu.
//...
{
BankAccount ba;
ba.deposit(100.0);
} // moment zniszczenia obiektu ba
Obiekty kontrolowane przez inteligentne wskaźniki (obiekty na stercie):
jeśli prawo własności do obiektu jest wyłączne (używany jest
std::unique_ptr
) obiekt jest niszczony w tym samym momencie co wskaźnik, który nim zarządzajeśli prawo własności do obiektu jest współdzielone, obiekt jest niszczony, kiedy ostatni wskaźnik typu
std::shared_ptr
odnoszący się do obiektu przestanie istnieć (lub zostanie zresetowany)
//...
{
std::unique_ptr<BankAccount> account {};
// ...
{
account = get_account_from_db(1);
account->deposit(100.0);
}
} // moment zniszczenia account i wskazywanego przez niego obiektu
Konstruktory¶
Konstruktory służą do poprawnej inicjalizacji instancji klasy
Nazwa konstruktora jest taka sama jak nazwa klasy
Nie zwraca żadnej wartości
Konstruktor domyślny¶
Konstruktor domyślny - nie pobiera żadnych argumentów
class BankAccount
{
public:
BankAccount(); // jawny konstruktor domyślny
//...
};
BankAccount::BankAccount()
{
cout << "Tworzenie nowego konta!" << endl;
}
Kiedy klasa nie posiada żadnego jawnego konstruktora kompilator automatycznie generuje automatyczny konstruktor domyślny.
Cechy automatycznego konstruktora domyślnego:
Publicznie dostępny
Nazwa taka sama jak nazwa klasy
Brak zwracanej wartości
Nie pobiera żadnych argumentów
Inicjuje każdą składową, która posiada konstruktor domyślny - składowe bez konstruktorów domyślnych nie są inicjalizowane
Gdy konstruktor domyślny generowany przez kompilator nie jest odpowiedni można go zastąpić własnym.
Przeładowanie konstruktorów¶
Przeładowane konstruktory wewnątrz klasy posiadają odmienne sygnatury.
class BankAccount
{
public:
BankAccount();
BankAccount(double balance, long number, const std::string& owner);
};
BankAccount::BankAccount()
{
balance_ = 0.0;
number_ = -1;
owner_ = "brak";
}
BankAccount::BankAccount(double balance, long number, const std::string& owner)
{
balance_ = balance;
number_ = number;
owner_ = owner;
}
Lista inicjalizacji atrybutów¶
Konstruktor może inicjować składowe obiektu zanim wykonana zostanie jakakolwiek instrukcja. Służy do tego lista inicjalizacji. Jest to jedyny sposób inicjalizacji stałych pól obiektu.
class BankAccount
{
public:
//...
BankAccount(double balance, long number, const std::string& owner);
private:
double balance_;
const long number_;
string owner_;
};
BankAccount::BankAccount(double balance, long number, const std::string& owner)
: balance{balance}, number_{n}, owner_{o}
{
}
Delegowanie konstruktorów¶
Konstruktor może wywoływać inne konstruktory tej samej klasy. Odbywa się to z wykorzystaniem listy inicjalizacji.
class BankAccount
{
public:
BankAccount(double balance, long number, const std::string& owner);
BankAccount(long number, const std::string& owner);
private:
double balance_;
long number_;
string owner_;
};
BankAccount::BankAccount(double balance, long number, const std::string& owner)
: balance{balance}, number_{n}, owner_{o}
{
// a
}
BankAccount::BankAccount(long number, const std::string& owner)
: BankAccount{0.0, number, owner}
{
// b (wywołane po a)
}
Ważne
Obiekt jest uznany za poprawnie skonstruowany, gdy pierwszy konstruktor wywołany zakończy się bez wyjątku (=> wywołany będzie destruktor obiektu).
Stałe składowe klasy (pola const
)¶
Stałe pola - pola z modyfikatorem const
:
Inicjalizacja w kodzie - wartość musi być zainicjowana na liście inicjalizacyjnej konstruktora
Wartość musi być znana w momencie tworzenia obiektu - w trakcie pracy programu (run-time)
class BankAccount
{
public:
BankAccount(double balance, long number, const std::string& owner);
BankAccount(long number, const std::string& owner);
private:
double balance_;
const long number_;
string owner_;
};
BankAccount::BankAccount(double balance, long number, const std::string& owner)
: balance{balance}, number_{n}, owner_{o}
{
++number_; // błąd kompilacji
}
Statyczne składowe klasy¶
Zmienna, która jest częścią klasy (a nie konkretnego obiektu), jest statyczną daną składową.
Funkcja, która potrzebuje dostępu do danych klasy, a nie do danych składowych konkretnego obiektu, jest nazywana statyczną funkcją składową.
Statyczne dane składowe przechowują informacje wspólne dla wszystkich obiektów danej klasy
należą do klasy a nie do konkretnego obiektu
są inicjalizowane przed utworzeniem jakiejkolwiek instancji
są współdzielone przez wszystkie instancje danej klasy
Statyczne metody mają dostęp jedynie do statycznych pól danej klasy.
Składowe statyczne - zarówno funkcje jaki i dane składowe - muszą zostać zdefiniowane.
public class BankAccount
{
private:
static double interest_; // deklaracja statycznego pola klasy
public:
static double get_interest_rate(); // deklaracja metody statycznej
static void set_interest_rate(double interest_rate);
};
double BankAccount::interest_ = 0.0; // definicja statycznego pola klasy
// definicja metody statycznej
void BankAccount::set_interest_rate(double interest_rate)
{
interest_ = interest_rate;
}
double BankAccount::get_interest_rate()
{
return interest_;
}
Metoda statyczna nie ma wskaźnika this
.
Metody statyczne można wywoływać nawet, gdy nie istnieją instancje klasy, używając
składni NazwaKlasy::metoda_statyczne()
.
Metody statyczne można również wywoływać przy pomocy istniejących już obiektów.
int main()
{
BankAccount::set_interest_rate(0.07);
cout << "Wysokość oprocentowania kont: "
<< BankAccount::get_interest_rate() << endl;
// ...
BankAccount ba{132423, "Jan Kowalski"};
ba.set_interest_rate(0.05);
cout << "Zmiana oprocentowania kont: "
<< ba.get_interest_rate() << endl;
}
Funkcje oraz klasy zaprzyjaźnione¶
Funkcje zaprzyjaźnione¶
Funkcja zaprzyjaźniona - funkcja, która nie jest składową klasy, ale ma dostęp do składowych prywatnych tej klasy. Deklaracja przyjaźni odbywa się za pomocą słowa kluczowego friend
.
Możliwe są dwa rodzaje deklaracji przyjaźni funkcji z klasą:
Funkcja niezależna
foo()
jest zaprzyjaźniona z klasąA
class A { //... friend void foo(); };
Metoda
foo()
z klasyB
jest zaprzyjaźniona z klasąA
class A { //... friend void B::foo(); };
Klasy zaprzyjaźnione¶
W przypadku przyjaźni klasy A
z klasą B
wszystkie metody klasy B
mają dostęp do prywatnych składowych klasy A
.
class A
{
//...
friend class B;
};