Klasa može proširivati više baznih klasa. U tom slučaju govorimo o višestrukom nasljeđivanju.
Na primjer, u biblioteci SFML
klasa Shape
proširuje dvije bazne klase: apstraknu bazu Drawable
i implementacijsku bazu Transformable
.
class A1 {
public:
A1(){ std::cout << "konstruktor: A1"<< std::endl;}
void f() {}
//...
};
class A2 {
public:
A2(){ std::cout << "konstruktor: A2"<< std::endl;}
//...
};
class B1 : public A1, public A2 {
public:
B1(){ std::cout << "konstruktor: B1"<< std::endl;}
void g(){}
// ...
};
class B2 {
public:
B2(){ std::cout << "konstruktor: B2"<< std::endl;}
void g() {}
// ...
};
class C : public B1, public B2 {
public:
C(){ std::cout << "konstruktor: C"<< std::endl;}
// ...
};
int main()
{
C c;
return 0;
}
Program ispisuje:
konstruktor: A1
konstruktor: A2
konstruktor: B1
konstruktor: B2
konstruktor: C
Struktura objekta izvedene klase:
Stablo nasljeđivanja ima ovaj oblik:
Objekt tipa C
ima sljedeću formu:
C c;
A1 * pa1 = &c;
B2 * pb2 = &c;
c.f(); // o.k.
pa1->f(); // o.k
pb2->f(); // greška
c.g(); // greška
c.B2::g(); // o.k.
template <typename T>
class A{
public:
A(T x): p(new T(x)) {}
A(const A& a) : p(new T(a.get())) {
std::cout << "A-CCtor: T = " << typeid(T).name()
<< ", val = " << a.get() << std::endl;
}
A& operator=(const A& a);
virtual ~A(){
delete p;
std::cout << "A-Dtor: T = " << typeid(T).name()<< std::endl;
}
T get() const {return *p; }
void set(T x) { *p = x; }
private:
T * p;
};
template <typename T>
A<T>& A<T>::operator=(const A& a)
{
if( a.p == p ) return *this;
if( p != 0) delete p;
p = new T(*a.p);
std::cout << "A-op=: T = " << typeid(T).name()<< ", val = " << a.get() << std::endl;
return *this;
}
class C : public A<int>, public A<double>
{
public:
C() : A<int>(0), A<double>(0) {}
C(int x, double y) : A<int>(x), A<double>(y) {}
// Poziv funkcijama get i set je dvosmislen.
// Stoga trebamo nove verzije tih metoda.
double get_d() { return A<double>::get(); }
int get_i() { return A<int>::get(); }
void set_d(double y) { A<double>::set(y); }
void set_i(int y) { A<int>::set(y); }
};
int main() {
A<int> a(2);
C c; // defaultni konstruktor
c.set_i(12);
c.set_d(16.0);
A<int> b(c); // o.k. b se "izrezuje" iz c
A<double> * p_a = &c; // o.k. c dohvaćamo kroz
// A<double> sučelje
p_a->set(19.0);
cout << p_a->get() << endl; // o.k. Vidi samo funkciju
// get it A<double> klase
cout << c.get() << endl; // Greška! Dvosmislen poziv.
// get postoji u obje bazne klase
cout << c.A<double>::get() << endl; // o.k.
// Eksplicitno zadavanje bazne klase
// pri pozivu funkcije get.
C cc(c); // Sintetizirani CCtor. Zove redom bazne CCtore
C d;
d = cc; // Sintetizirani OP. Zove redom bazne OP
// ....
return 0;
}
Višestruko nasljeđivanje prirodno koristimo kada imamo objekt koji implementira više različitih sučelja.
Neke baze mijenjaju ponašanje izvedene klase, bez mijenjanja same izvedene klase:
Kada nasljeđivanje nije virtualno (ključna riječ virtual
nije navedena u derivacijskoj listi)
onda u instanci klase svakoj baznoj klasi odgovara jedan podobjekt tipa bazne klase.
struct X{
int x;
};
struct A : X {
A(int x_) { x= x_; }
};
struct B : X {
B(int x_) { x= 2*x_; }
};
struct C : A, B {
C(int x_, int y_) : A(x_), B(y_) {}
};
dobivamo:
C c(1,2);
c.x = 3; // greška
Klasa koja sadrži dva podobjekta istog tipa ima reduciranu funkcionalnost budući da se iz dosega izvedene klase ne može pozvati funkcija iz bazne klase čije je objekt više puta uključen; Svaki takav poziv je dvosmislen.
Kada je u derivacijskoj listi bazna klasa X
deklarirana virtual
(virtualna baza)
onda će svaka klasa izvedena iz nje sadržavati samo jedan podobjekt tipa X
bez obzira koliko
se puta u stablu nasljeđivanja tip X
javlja kao virtualna baza.
Prethodni primjer s virtualnim nasljeđivanjem daje:
struct X{
int x;
};
struct A : virtual X {
A(int x_) { x= x_; }
};
struct B : virtual X {
B(int x_) { x= 2*x_; }
};
struct C : A, B {
C(int x_, int y_) : A(x_), B(y_) {}
};
int main(){
C c(1,2);
c.x = 3; // o.k. samo je jedan x
Svako virtualno pojavljivanje klase X
u stablu nasljeđivanja odgovara jednom
podobjektu, dok svako drugo, nevirtualno, pojavljivanje iste klase X
rezultira zasebnim podobjektom
tipa X
. Na primjer,
class X { /* ... */ };
class A : public virtual X { /* ... */ };
class B : public virtual X { /* ... */ };
class C : public X { /* ... */ };
class D : public A, public B, public C { /* ... */ };
ima dijagram podobjekata ovog oblika:
Primjer:
class V { public: virtual int f(){ return 2; } int x; };
class W { public: int g(); int y; };
class B : public virtual V, public W
{
public:
int f(){ return 5; } int x;
int g(); int y;
};
class C : public virtual V, public W { };
class D : public B, public C { void h(); };
void D::h()
{
x++; // OK: B::x skriva V::x
f(); // OK: B::f() skriva V::f()
y++; // greška: B::y ili C::W::y ?
g(); // greška: B::g() and C::W::g() ?
}
int main()
{
D d;
V * pv = &d;
std::cout << pv->f() << std::endl; // 2 ili 5 ?
return 0;
}
Ako virtualna bazna klasa mora biti konstruirana, onda imamo problem: kod regularnih baza dužnost je izvedene klase da pozove konstruktore svih svojih baznih klasa, ali to nije primijenjivo na virtualne baze jer bismo tada dobili više podobjekta koji odgovaraju višestrukom pojavljivanju virtualne baze u stablu nasljeđivanja.
Problem konstrukcije virtualne baze riješava se sljedećim pravilom: Konstruktor virtualne baze u svojoj inicijalizacijskoj listi poziva konstruktor najizvedenije klase u stablu nasljeđivanja. Ilustrirajmo to sljedećim primjerom:
Primjer:
class A{
int x;
public:
A(int _x) : x(_x) {cout << "A("<<_x<<")"<<endl;}
int get_x(){ return x; }
// ...
};
class B : public virtual A{
int y;
public:
B(int _y) : A(10 * _y), y(_y) {cout << "B("<<_y<<")"<<endl;}
int get_y(){ return y; }
};
class C : public virtual A{
int y;
public:
C(int _y) : A(_y*_y), y(_y) {cout << "C("<<_y<<")"<<endl;}
int get_y(){ return y; }
};
class D : public B, public C{
public:
D(int _x, int _y) : A(_x), B(_y), C(_y) {cout << "D("<<_x<<","<<_y<<")"<<endl;}
};
Dijagram klasa je sljedeći:
D
, kao najizvedenija, poziva konstruktor svoje virtualne baze.
B
i C
također moraju u svojim konstruktorima inicijalizirati
A
kao svoju direktnu bazu, ali prevodilac te inicijalizacije ignorira.
Izvršimo li sljedeći program:
int main()
{
D d(2,6);
cout << d.get_x() << endl;
cout << d.C::get_y() << endl;
return 0;
}
dobivamo ispis:
A(2)
B(6)
C(6)
D(2,6)
2
6
Vidimo da se prvo konstruira virtualna baza, a zatim ostali dijelovi objekta, prema redosljedu pojavljivanja u derivacijskoj listi. To je općenito pravilo:
Jednostruko nasljeđivanje je na određen način prirodno:
class Base{
int x;
};
class Derived : public Base{
int y;
}
Derived * pDerived = new Derived();
Base * pBase = pDerived;
Objekt klase Derived
u memoriji ima oblik:
Pokazivači pBase
i pDerived:
pokazuju na istu memorijsku lokaciju.
Ako su klase polimorfne onda se instanca klase povećava za pokazivač na tabelu virtualnih funkcija.
class Base{
public:
virtual void set(int x_){ x = x_; }
protected:
int x;
};
class Derived : public Base{
public:
void set(int x_) { x= x_*x_; }
int y;
};
a objekti u memoriji
Svaki objekt sada ima pokazivač na virtualnu tabelu. Novi element u virtualnoj tabeli, koji do sada nismo spominjali, je pokazivač na typeinfo objekt. To je još jedna tabela pridružena klasi koja sadrži informacije o klasi za RTTI sustav. Pokazivač na nju se smješta u tabelu virtualnih funkcija.
Višestruko nasljeđivanje je složenija.
class BaseA{
public:
virtual void set(int x_){ x = x_; }
int x;
};
class BaseB{
public:
virtual int get(){ return y; }
int y;
};
class Derived : public BaseA, public BaseB{
public:
int z;
virtual void square(){ z = z*z; }
};
Derived * pDerived = new Derived();
BaseA * pBaseA = pDerived;
BaseB * pBaseB = pDerived;
U ovoj situaciji objekt klase Derived u memoriji i pripadna tabela virtualnih funkcija imaju sljedeći oblik:
Kao i do sada pokazivači pDerived
i pBaseA
drže istu adresu i pokazuju na početak objekta.
S druge strane, pokazivač pBaseB
(BaseB je desna baza u derivacijskoj listi) ima različitu adresu od one u pDerived
— on pokazuje na podobjekt BaseB
u objektu tipa Derived
.
this
pokazivača pri pozivu virtualnih funkcija
kroz pokazivač na desnu baznu klasu. Iz pokazivača na podobjekt desne bazne klase potrebno je dobiti pokazivač na početak cijelog objekta.
Tu korekciju ne može napraviti prevodilac za vrijeme kompilacije jer je dinamički tip pokazivača poznat tek
za vrijeme izvršavanja programa. Stoga virtualna tabela može pored pokazivača na virtualne funkcije sadržavati i pomake (offsets)
potreban da se dođe do početka virtualne tabele (odnosno vtb pridružene lijevoj bazi).
Ponekad imamo klasu koja nam nudi funkcionalnost koja nam je potrebna, ali nema odgovarajuće sučelje. U tom slučajukoristimo oblikovni obrazac Adapter.
Struktura obrasca je sljedeća:
Target
i koristi sučelje klase Target
;
Adaptee
, ali ona nema traženo sučelje.
Adapter
koja:
Target
i stoga ima sučelje klase Target
;
Adaptee
koju koristi za implementaciju svog sučelja.
Umjesto privatnog nasljeđivanja možemo uvijek koristiti agregaciju:
Imamo klasu Graphics
koja koristi SFML biblioteku za iscrtavanje ravninske triangulacije.
class Graphics{
public:
Graphics(SFMLGrid * pGrid, std::string const & fileName);
void run();
private:
sf::RenderWindow mWindow;
SFMLGrid * mGrid;
void processEvents();
void update();
void render();
};
Triangulaciju pamti klasa SFMLGrid
u obliku sf::VertexArray
objekta koji sadrži sve stranice triangulacije.
Objekti te klase se znaju iscrtati.
class SFMLGrid : public sf::Drawable, public sf::Transformable{
public:
SFMLGrid();
virtual void readFromFile(std::string const & fileName);
private:
sf::VertexArray mVA;
void draw(sf::RenderTarget & target, sf::RenderStates states) const override;
};
Imamo klasu GmshGrid
koja može pročitati mrežu generiranu pomoću gmsh alata za generiranje
triangulacije.
class GmshGrid{
public:
using Triangle = std::tuple<std::size_t, std::size_t, std::size_t>;
GmshGrid();
void read(const std::string & fileName);
// ...
private:
std::vector<double> mVertexXcoor;
std::vector<double> mVertexYcoor;
std::vector<Triangle> mElement;
};
Ova klasa koristi drugačije sučelje, drugačiju strukturu podataka za pamćenje mreže i ne zna se iscrtavati.
Konstruiramo klasu Adapter
koja adaptira GmshGrid
na sučelje a SFMLGrid
klase:
class Adapter: public SFMLGrid, private GmshGrid{
public:
Adapter();
void readFromFile(std::string const & fileName);
private:
// ...
};
Klasa Adapter
prerađuje metodu readFromFile()
koristeći GmshGrid::read()
te radi svu potrebnu adaptaciju.