Kompozycja i dziedziczenie¶
Jedną z najbardziej użytecznych cech C++ jest możliwość powtórnego wykorzystania kodu – code reusability. Technika ta polega na korzystaniu z istniejących klas bez zmian w ich kodzie
Istnieją dwa sposoby osiągnięcia tego celu:
Kompozycja – polega na tworzeniu obiektów nowych klas, które zawierają jako składowe klasy już istniejące
Dziedziczenie – polega na stworzeniu nowej klasy jako typu klasy istniejącej i dodaniu do niego nowego kodu, bez modyfikacji kodu już istniejącego
Kompozycja¶
Jeśli obiekt pewnej klasy zawiera obiekty innych klas, to podczas jego inicjalizacji wywoływane są w pierwszej kolejności (zanim wykonane zostaną instrukcje umieszczone w ciele konstruktora ) konstruktory obiektów składowych.
Porządek wywołań konstruktorów obiektów składowych określony jest przez kolejność deklaracji składowych wewnątrz klasy – nie zależy on od listy inicjalizacji.
Implementacja kompozycji¶
enum class EngineType { petrol, diesel, tdi, wenkel };
class Engine
{
EngineType type_;
long id_;
public:
Engine(EngineType engine_type, long id) : type_{engine_type}, id_{id}
{ /*...*/ }
// interfejs klasy Engine
};
class Car
{
Engine engine_;
std::string registration_number_;
public:
Car(EngineType engine_type, long engine_id, const std::string& registration_number)
: Engine{engine_type, engine_id}, registration_number_{registration_number}
{
/*...*/
}
// interfejs klasy Car
};
Dziedziczenie¶
Dziedziczenie – umożliwia tworzenie nowych klas, które przejmują formę i funkcjonalność klas bazowych. Wprowadza relację „is-a-kind-of” między klasami.
Klasy pochodne specjalizują klasy bazowe.
Klasa pochodna dziedziczy wszystkie składowe klasy bazowej:
pola (dane składowe)
metody
Metody klasy pochodnej mają dostęp do publicznych oraz chronionych (protected
) składowych klasy bazowej.
C++ umożliwia następujące rodzaje dziedziczenia:
pojedyncze (jednobazowe)
wielokrotne (wielobazowe)
Definiowanie klas pochodnych¶
Składnia dziedziczenia:
struct Point
{
int x, y;
Point(int x, int y) : x{x}, y{y}
{}
};
class Shape
{
Point coord_;
public:
Shape(int x = 0, int y = 0) : coord_{x, y}
{}
void draw() const;
};
class Circle : public Shape
{
int radius_;
public:
Circle(int x = 0, int y = 0, int radius = 0) : Shape{x, y}, radius_{radius}
{}
}
Dostęp do składowych podczas dziedziczenia¶
class Derived : [public|protected|private] Base
{
};
W przypadku dziedziczenia na sposób private (prywatny) istnieje możliwość wybiórczego udostępnienia wybranych składowych public lub protected z klasy bazowej.
class Base
{
public:
int i;
protected:
int foo();
};
class Derived : private Base
{
public:
Base::i;
protected:
Base::foo;
};
Konstruktor klasy bazowej¶
Konstruktory klasy bazowej są wywoływane w trakcie tworzenia obiektu klasy pochodnej. Konstruktor klasy bazowej może/musi być wywołany na liście inicjalizacyjnej konstruktora klasy pochodnej.
class Shape
{
private:
Point coord_;
public:
Shape(int x = 0, int y = 0) : coord_{x, y}
{}
Shape(Point coord) : coord_{coord}
{}
};
class Line : public Shape
{
private:
Point end_;
public:
Line(const Point& start, const Point& end) : Shape{coord}, end_{coord}
{
}
};
Przesłanianie metod klasy bazowej¶
Jeżeli nie odpowiada nam implementacja metody klasy bazowej możemy ją przesłonić pisząc nową implementację funkcji o takiej samej sygnaturze w klasie pochodnej.
class Shape
{
//...
public:
void draw() const
{
// implementacja rysowania
}
};
class Line : public Shape
{
//...
public:
void draw() const
{
Shape::draw(); // wywołanie metody z Shape
// dodatkowe instrukcje rysujące
}
};
Wykorzystanie obiektów klasy pochodnej¶
Obiekty klasy pochodnej posiadają składowe klasy bazowej i nowe składowe wprowadzone w klasie pochodnej.
Obiekt klasy pochodnej jest obiektem klasy bazowej.
Shape shape{1, 20};
Circle circle{1, 4, 10);
shape = circle; // wycięcie – utrata informacji o promieniu
Shape* psh = &circle; // ok
Shape& rsh = circle; // ok
Korzystając ze wskaźnika lub referencji do klasy bazowej zawasze zamiast obiektu klasy bazowej możemy użyć obiektu klasy pochodnej.
Dziedziczenie konstruktorów¶
Deklaracja using
może być użyta w połączeniu z konstruktorami klasy bazowej. Powoduje to niejawne deklaracje konstruktorów klasy pochodnej, które przyjmują takie same listy paramtrów co konstruktory klasy bazowej. Ich implementacja polegająca na wywołaniu wersji z klasy bazowej jest generowana tylko wtedy, gdy są one rzeczywiście użyte.
class Base
{
public:
explicit Base(int);
void do_something(int);
};
class Derived : public Base
{
public:
using Base::Base; // OK in C++11
// implicit declaration of Derived::Derived(int)
Derived(int, int); // overloaded inherited Base ctor
};
Użycie dziedziczenia konstruktorów w klasach pochodnych, które dodają nowe pola może być ryzykowne:
class Augmented : public Derived
{
public:
using Derived::Derived;
private:
std::string name_;
int value_;
};
Augmented a {10}; // a.name_ == "" - defualt init
// and a.value_ is uninitialized
Polimorfizm¶
Referencje lub wskaźniki do typów bazowych mogą odnosić się do obiektów różnego typu (typów pochodnych). Wywołanie metody dla referencji lub wskaźnika spowoduje zachowanie odpowiednie dla pełnego typu obiektu wywoływanego. Jeśli dzieje się to w czasie działania programu, to nazywa się to późnym wiązaniem lub wiązaniem dynamicznym.
class Client
{
public:
void render(const Shape& shape) const
{
//...
shape.draw();
}
};
//...
Circle c{1, 10, 20};
Rectangle rect{10, 30, 20, 40};
auto line = std::unique_ptr<Shape>{ std::make_unique<Line>(20, 20, 40, 40) };
Client client;
client.render(c);
client.render(rect);
client.render(*line);
Statyczne i dynamiczne wiązanie funkcji¶
Statyczne wiązanie funkcji:
Kompilator podczas kompilacji ustala typ obiektu, dla którego wywoływana jest funkcja i generuje kod wywołujący funkcję należącą do tego typu
Dotyczy również wskaźników i referencji
Dynamiczne wiązanie funkcji:
Kompilator generuje kod, który na podstawie rzeczywistego typu obiektu wywoła właściwą funkcję – decyzja ta podejmowana jest trakcie wykonywania programu
Dynamiczne wiązanie jest dostępne w C++ poprzez użycie słowa kluczowego
virtual
Metody wirtualne¶
Tworzenie metod wirtualnych¶
Metoda wirtualna z klasy bazowej może być nadpisana w klasie pochodnej.
class Shape
{
public:
virtual void draw() const
{
// definicja metody wirtualnej
// rysowanie: Shape
}
};
class Circle : public Shape
{
public:
[virtual] void draw() const override
{
// rysowanie: Circle
}
} ;
Kontrola nadpisywania metod wirtualnych - override
¶
Słowo override
ma specjalne znaczenie w deklaracji klas i powoduje sprawdzenie na etapie kompilacji, czy nadpisywana metoda jest zadeklarowana w taki sam sposób w klasie bazowej.
class Base
{
public:
virtual void f();
virtual void g() const;
virtual void h(char);
void k();
};
class Derived1 : public Base
{
public:
void f(); // overrides Base::f()
void g(); // doesn't override B::g() const
virtual void h(char); // overrides B::h(char)
void k(); // doesn't override
};
class Derived2: public Base
{
public:
void f() override; // OK - overrides Base::f()
void g() override; // error - doesn't override B::g() const
virtual void h(char); // overrides B::h(char)
void k() override; // error - B::k() is not virtual
};
Wywoływanie metody wirtualnej¶
using ShapePtr = std::unique_ptr<Shape>;
void render_shapes(const std::vector<ShapePtr>& shapes)
{
for(const auto& s : shapes)
s->draw();
}
}
auto l1 = ShapePtr{ std::make_unique<Line>(1, 2, 20, 30) };
auto l2 = ShapePtr{ std::make_unique<Line>(20, 10, 15, 50) };
vector<ShapePtr> shapes;
shapes.push_back(ShapePtr{ make_unique<Circle>(10, 20, 30) });
shapes.push_back(move(l1));
shapes.push_back(move(l2));
render(shapes);
Użycie metody z klasy bazowej¶
Możliwe jest wywołanie metody z klasy bazowej wewnątrz metody w klasie pochodnej.
class Line: public Shape
{
public:
virtual void move_to(const Point& new_coord)
{
Point old_coord = coord();
Shape::move_to(new_coord);
end_coord.x += ( coord().x - old_coord.x );
end_coord.y += ( coord().y - old_coord.y );
}
};
Metody czysto wirtualne¶
Metoda czysto wirtualna nie posiada implementacji. Sygnatura metody jest poprzedzona słowem
kluczowym virtual
a sama metoda jest zainicjowana zerem.
class Shape
{
public:
virtual void draw() const = 0; // pure virtual method
};
Definicja klasy jest niekompletna tak długo, jak długo zawiera ona jakąkolwiek metodę czysto wirtualną.
Klasy abstrakcyjne¶
Klasa abstrakcyjna - klasa która nie reprezentuje żadnego konkretnego obiektu. Tworzenie obiektu klasy abstrakcyjnej nie ma sensu. Posiada przynajmniej jedną metodę czysto wirtualną.
Przykład:
// klasa abstrakcyjna
class Shape
{
Point coord_;
public:
Shape(int x = 0, int y = 0) : coord_{x, y}
{
}
Point coordinates() const
{
return coord_;
}
virtual ~Shape() = default;
virtual void draw() const = 0; // metoda czysto wirtualna
};
Klasa abstrakcyjna definiuje atrybuty oraz operacje wspólne dla wszystkich obiektów klas, które będą dziedziczyły po tej klasie bazowej.
Przeciwieństwem klasy abstrakcyjnej jest klasa konkretna. Tworzenie obiektów tej klasy ma sens i jest możliwe.
Abstrakcyjne klasy bazowe¶
Abstrakcyjne klasy bazowe stanowią jeden z podstawowych środków umożliwiających podział rozbudowanych programów na moduły i komponenty. Ich interfejs definiuje kontrakt między obiektem, który dostarcza pewnej usługi i obiektem, który z niej korzysta.
Umożliwiają rozdzielenie interfejsu i implementacji. Jeśli umowa ta zostanie zdefiniowana za pomocą abstrakcyjnej klasy bazowej, strony tej umowy mogą być tworzone i modyfikowane niezależnie od siebie.
Destruktory wirtualne¶
Klasa jest właściwie przygotowana do uczestniczenia w procesie dziedziczenia tylko wtedy, gdy posiada wirtualny destruktor.
Wirtualny destruktor powinien być zdefiniowany nawet wówczas, gdy klasa bazowa nie przewiduje posiadania własnego destruktora. Domyślny destruktor nie jest destruktorem wirtualnym.
class Shape
{
// ...
public:
virtual ~Shape()
{
// implementacja
}
};
// ...
Shape* ptrShape = new Circle(new Coord(1, 2), 10);
delete ptrShape; // teraz bezpieczne
W przypadku trywialnej implentacji destruktora w klasie bazowej możemy go zainicjować
słowem default
:
class Shape
{
// ...
public:
virtual ~Shape() = default;
};
Blokowanie dziedziczenia lub nadpisywania metod - final
¶
Klasy lub implementacje metod wirtualnych mogą być oznaczone jako skończone z wykorzystaniem
słowa final
class NoInheritable final
{
// ...
};
class Derived : public NoInheritable // error - base marked as final
{
// ...
};
class Base
{
public:
virtual void f() const;
};
class Derived1 : public Base
{
public:
virtual void f() const final; // enables additional optimization
};
class Derived2 : public Derived1
{
public:
virtual void f() const; // error - f() attempts to override Derived1::f()
// marked as final
};