Nasljeđivanje/proširivanje
Nove klase možemo definirati polazeći od već postojećih metodom nasljeđivanja (proširivanja). Kada klasu B definiramo proširivanjem klase A tada u osnovi klasa B:
-
nasljeđuje varijable i metode klase A,
-
prerađuje neke metode iz klase A, dajući im novu implementaciju,
-
dodaje svoje varijable i metode na one nasljeđene iz klase A.
Gornje tvrdnje treba korigirati u odnosu na prava pristupa jer je mehanizam nasljeđivanja ovisan o pravima pristupa pa je i samo nasljeđivanje kvalificirano labelama public, protected ili private.
Kada klasa B proširuje klasu A, onda govorimo da je klasa A nadklasa ili bazna klasa, a klasa B potklasa ili izvedena klasa.
Jednostavno proširivanje klase: Primjer
Pođimo od klase Osoba
koja drži podatke o osobi (ime, prezime, ID):
class Osoba{
public:
Osoba(std::string ime, std::string prezime, int id = 0) :
mime(ime), mprezime(prezime), mid(id) {}
// Postavi (novi) id
void setId(int id){ mid = id; }
void print(std::ostream & out) const;
virtual ~Osoba(){}
private:
std::string mime;
std::string mprezime;
int mid;
};
(Novi element ovdje je virtualni destruktor kojeg ćemo objasniti kasnije.)
Klijent
Proširujući klasu Osoba
dolazimo do klase Klijent
koja ima dodatno polje: msaldo
.
Nasljeđivanje klasa, odnosno
konstrukcija klase proširivanjem omogućava nam da iskoristimo već postojeći kod
u klasi Osoba
:
class Klijent : public Osoba {
public:
Klijent(std::string ime, std::string prezime,
int id = 0, double saldo = 0.0) :
Osoba(ime, prezime, id), msaldo(saldo) {}
void setSaldo(double saldo){ msaldo = saldo;}
void print(std::ostream & out) const;
private:
double msaldo;
};
Sintaksa
Kod nasljeđivanja koristimo sljedeću sintaksu:
class ime_klase : oznaka_pristupa ime_bazne_klase
gdje oznaka pristupa može biti public, private i
protected. U gornjem kodu klasa Klijent
proširuje klasu Osoba
pa imamo:
class Klijent : public Osoba
-
Podklasa — u ovom slučaju
Klijent
— nasljeđuje sve varijable i metode bazne klase — u ovom slučajuOsoba
:
Ciljevi nasljeđivanja
-
Konzistentnost među klasama koje modeliraju slične i međusobno zavisne koncepte.
-
Iskorištavanje već postojećeg koda (code reuse);
Nasljeđivanje i prava pristupa
Pristup članicama bazne klase ovisi o sljedećem:
-
Pravima pristupa u baznoj klasi;
-
Tipu nasljeđivanja.
Prava pristupa u baznoj klasi:
-
Varijable i metode označene public vidljive su izvan klase te su kao takve vidljive i u proširenoj klasi.
-
U proširenoj klasi nisu vidljive privatne varijable i metode bazne klase.
-
protected članovi nisu vidljivi izvan klase ali su vidljivi u proširenoj klasi. Članovi deklarirani protected imaju i ovo svojstvo:
Objekt izvedene klase može dohvatiti protected član svoje bazne klase
samo na objektu kroz koji je izveden. To je u suprotnosti s pristupom privatnim članovima
klase: kod unutar klase X
može pristupiti privatnoj varijabli klase X
na bilo kojoj instanci klase X
.
Tip nasljeđivanja
Tip nasljeđivanja određen je labelom pristupa koju smo iskoristili pri definiciji klase: to može biti public, private i protected:
-
Kod javnog (public) nasljeđivanja varijable i metode iz bazne klase čuvaju svoju razinu pristupa u izvedenoj klasi. Javne članice iz bazne klase ostaju javne u izvedenoj klasi i zaštićene članice iz bazne klase ostaju zaštićene u izvedenoj klasi. (Privatne bazne članice nisu vidljive u izvedenoj klasi).
-
Kod zaštićenog (protected) nasljeđivanja javne i zaštićene članice iz bazne klase postaju zaštićene u izvedenoj klasi. (Privatne bazne članice nisu vidljive u izvedenoj klasi).
-
Kod privatnog (private) nasljeđivanja javne i zaštićene članice iz bazne klase postaju privatne u izvedenoj klasi. (Privatne bazne članice nisu vidljive u izvedenoj klasi).
Sljedeća tabela sumira gornja pravila:
Prava pristupa članicama u bazi | |||
---|---|---|---|
Nasljeđivanje |
public |
protected |
private |
public |
public |
protected |
nedostupno |
protected |
protected |
protected |
nedostupno |
private |
private |
private |
nedostupno |
Napomena: Ako labela koja određuje tip nasljeđivanja nije eksplicitno dana, onda se podrazumijeva da je nasljeđivanje privatno ako je klasa deklarirana pomoću ključne riječi class, odnosno javno ako je deklarirana sa struct. Na primjer,
class A{ /* ... */ };
class B : A {/* ... */ }; // privatno nasljeđivanje
struct C : A {/* ... */ }; // javno nasljeđivanje
Javno i privatno nasljeđivanje
Javno, zaštićeno i privatno nasljeđivanje predstavljaju tri različita koncepta koji se upotrebljavaju u različitim situacijama. Kao prvo, zaštićeno nasljeđivanje se rijetko koristi, tako da ga možemo slobodno zanemariti.
-
Javno nasljeđivanje se najčešće koristi jer u njemu proširena klasa nasljeđuje sučelje bazne klase. Sve javne članice ( = sučelje) bazne klase ostaju javne u izvedenoj klasi, tako da izvedena klasa prema van izgleda kao bazna klasa s nekim dodacima. Tip izvedene klase jeste tip bazne klase (plus nešto više); u našem slučaju Klijent je Osoba (osoba s računom je još uvijek osoba). Dakle, javno nasljeđivanje modelira vrlo blisku povezanost dvije klasa u kojoj izvedena klasa osigurava proširenje funkcionalnosti bazne klase, odnosno prošireni tip predstavlja specijalizaciju baznog tipa. Javno nasljeđivanje treba koristiti samo u takvim situacijama.
-
Privatno nasljeđivanje je nešto sasvim drugo. Javno sučelje bazne klase postaje ovdje privatnim dijelom izvedene klase, a ne njegovim sučeljem. Prema tome izvedeni tip nije ujedno i bazni tip, on ne dijeli njegovo sučelje. Kako privatne članice služe implementaciji klase možemo zaključiti da privatno nasljeđivanje treba koristiti kada klasu želimo implementirati koristeći funkcionalnost bazne klase.
Konstrukcija objekta izvedene klase
Svaki objekt izvedene klase sastoji se od dva dijela: podobjekta bazne klase i podobjekta proširene klase.
Na primjer u slučaju klase Klijent
imamo ovu sliku:
Zadatak konstruktora je inicijalizirati oba dijela klase. Prvo se dešava inicijalizacija baznog dijela, a zatim inicijalizacija varijabli u izvedenoj klasi, redom kako su deklarirane u klasi.
Konstruktor izvedene klase
Konstruktor izvedene klase mora uvijek prvo pozvati konstruktore svih svojih baznih klasa u inicijalizacijskoj listi, u poretku u kojem su bazne klase deklarirane.
// konstruktor bazne klase
Osoba(std::string ime, std::string prezime, int id = 0) :
mime(ime), mprezime(prezime), mid(id) {}
// konstruktor izvedene klase
Klijent(std::string ime, std::string prezime, int id = 0, double saldo = 0.0) :
Osoba(ime, prezime, id), msaldo(saldo) {}
-
Ako izvedena klasa nema konstruktora prevodilac će sintetizirati defaultni konstruktor koji prvo poziva defaultni konstruktor bazne klase.
Napomena
|
Ako se u defaultnom konstruktoru izvedene klase ispusti konstruktor bazne klase iz inicijalizacijske liste, onda će prevodilac ubaciti poziv defaultnom konstruktoru bazne klase. |
class A{
public:
A() : mx(0) {}
private:
int mx;
};
class B : public A{
public:
B() : my(0) {} // prevodilac ubacuje poziv defaultnom
private: // konstruktoru od A
int my;
};
Lanac nasljeđivanja
Lanac nasljeđivanja može biti proizvoljno dugačak. Objekt najizvedenije klase ima dio koji odgovara svakoj njegovoj nadklasi.
Nasljeđivanje i kontrola kopiranja
U kontrolu kopiranja ulaze:
-
konstruktor kopije (CC), kopiranjem i premještanjem
-
operator pridruživanja (OP), kopiranjem i premještanjem
-
destruktor.
Ukoliko ne implementiramo CC ili OP prevodilac će to učiniti za nas. To vrijedi i za proširenu klasu bez obzira da li bazne klase koriste vlastite ili sintetizirane članove. Ako jednu od te dvije funkcije (CC ili OP) ne implementiramo prevodilac će sintetizirati defaultnu verziju koja prvo poziva odgovarajuću funkciju u baznim klasama, a zatim vrši defaultnu inicijalizaciju ili pridruživanje članova proširene klase.
Kada implementiramo CCtor ili OP u proširenoj klasi, onda te funkcije moraju pozvati odgovarajuće funkcije iz bazne klase koje će izvršiti kopiranje baznog dijela objekta. CCtor to čini analogno kao drugi konstruktori, pozivanjem baznog/baznih CCtora u inicijalizacijskoj listi. OP mora pozvati bazni OP eksplicitno pomoću operatora dosega.
Napomena
|
Bazna se klasa sama brine o svome kopiranju. |
Primjer - Osoba
U klasi Osoba
je sintetizirana kontrola kopiranja posve zadovoljavajuća.
Sve tražene funkcije dobivamo na ovaj način:
class Osoba{
public:
Osoba(std::string ime, std::string prezime, int id = 0) :
mime(ime), mprezime(prezime), mid(id) {}
// Tražimo defaultnu kontrolu kopiranja
Osoba(Osoba const & ) = default;
Osoba(Osoba && ) = default;
Osoba & operator=(Osoba const &) = default;
Osoba & operator=(Osoba &&) = default;
// Postavi (novi) id
void setId(int id){ mid = id; }
void print(std::ostream & out) const;
virtual ~Osoba(){}
private:
std::string mime;
std::string mprezime;
int mid;
};
Primjer - Klijent
U klasi Klijent
dodajemo kontrolu kopiranja da bismo pokazali implementaciju.
Defaultna kontrola kopiranja bi i ovdje bila dovoljna.
class Klijent : public Osoba {
public:
Klijent(std::string ime, std::string prezime, int id = 0, double saldo = 0.0) :
Osoba(ime, prezime, id), msaldo(saldo) {}
Klijent(Klijent const &);
Klijent(Klijent &&) noexcept;
Klijent & operator=(Klijent const &);
Klijent & operator=(Klijent &&) noexcept;
void setSaldo(double saldo){ msaldo = saldo;}
void print(std::ostream & out) const;
private:
double msaldo;
};
Primjer - implementacija
Konstruktori kopije prvo pozivaju (u inicijalizacijskoj listi) konstruktor kopije bazne klase koji kopira bazni dio objekta. Zatim vrše kopiranje članica iz izvedenog dijela objekta.
// CCtor
Klijent::Klijent(Klijent const & k) : Osoba(k), msaldo(k.msaldo){}
// M-Ctor
Klijent::Klijent(Klijent && k) noexcept : Osoba(std::move(k)), msaldo(k.msaldo) {}
Operatori pridruživanja moraju eksplicitno pozvati operator pridruživanja bazne klase i zatim izvršiti pridruživanje izvedenog dijela klase.
// OP
Klijent & Klijent::operator=(Klijent const & k){
if(this != &k){
Osoba::operator=(k); // operator pridruživanja bazne klase
msaldo = k.msaldo;
}
return *this;
}
// M-OP
Klijent & Klijent::operator=(Klijent && k) noexcept{
if(this != &k){
Osoba::operator=(std::move(k)); // operator pridruživanja bazne klase
msaldo = k.msaldo; // move nije potreban
}
return *this;
}
Destruktor
Destruktor u proširenoj klasi nikada, za razliku od konstruktora i OP, nije dužan brinuti o destrukciji baznog dijela klase, već samo o dijelu deklariranom u proširenoj klasi. Stoga u destruktoru nikada ne pozivamo eksplicitno destruktor bazne klase. Prevodilac pri destrukciji objekta poziva redom destruktore (implementirane+sintetizirane) počeviši od onog u najvišoj proširenoj klasi pa prema najnižoj baznoj klasi. Objekt se, dakle, destruira u obrnutom redosljedu od onog u kojem je konstruiran.
S druge strane, ako je klasa namjenjena da bude bazna klasa, onda u njoj treba deklarirati virtualan destruktor, koji najčešće neće raditi ništa, ali je esencijalno da je prisutan.
Sintetizirana kontrola kopiranja u izvedenoj klasi
-
Za izvedenu klasu vrijedi isto pravilo kao i za baznu: ako nemamo CCtor ili OP u izvedenoj klasi, a postoji potreba za njim, prevodilac će sintetizirati jedan.
-
Sintetizirani CCtor ili OP poziva odgovarajuću operaciju na baznoj klasi i zatim kopira članice izvedene klase.
-
Prevodilac može sintetizirani CCtor ili OP kao izbrisan, na primjer, ako bazna klasa nema odgovarajuću operaciju.
-
Prisutnost destruktora u baznoj klasi sprečava sintetiziranje operacija premještanjem. Tada operacije premještanjem neće biti sintetizirane niti u izvedenoj klasi. (Sjetimo se da prevodilac sintetizira konstruktor i OP premještanjem samo ako klasa ne definira niti jedan član kontrole kopiranja.)
Doseg i nasljeđivanje
Znamo da svaka klasa predstavlja doseg pa stoga to vrijedi i za proširenu klasu. Pri tome vrijedi sljedeće pravilo:
Napomena
|
Doseg proširene klase je ugnježden u dosegu bazne klase. |
-
Kada prevodilac traži definiciju nekog imena koje se koristi u proširenoj klasi on će definiciju prvo potražiti u proširenoj klasi, a zatim u baznoj klasi. Tek nakon toga traži u okružujućem dosegu.
-
Zbog ugnježdenja dosega javlja se mogućnost skrivanja članica bazne klase pomoću članica istog imena izvedene klase.
Prerađivanje funkcija
Kada u izvedenoj klasi definiramo metodu iste signature (imena te broja i tipa argumenata) kakvu ima funkcija iz bazne klase onda kažemo da smo funkciju iz bazne klase preradili. Zbog pravila dosega na objektu izvedene klase uvijek će biti pozvana metoda iz izvedene klase (prerađena metoda), a ne ona iz bazne klase. Kako dvije metode imaju posve istu signaturu ukupan efekt je taj da je promijenjena implementacija metode iz bazne klase.
Na primjer, metoda
void print(std::ostream & out) const;
iz klase Osoba
prerađena je u klasi Klijent
.
Primjer.
Nasljeđivanje i prerađivanje funkcija.
#include <iostream>
using namespace std;
struct A
{
void f() { cout << "A.f()" << endl;}
void g() { cout << "A.g()" << endl;}
void f(int x) { cout << "A.f(int)" << endl;}
};
struct B : public A
{
void f(){ cout << "B.f()" << endl;}
};
int main()
{
A a; // objekt tipa A
A *pa = &a; // pokazivač na objekt tipa A
A &ra = a; // referenca na objekt tipa A
B b; // objekt tipa B
B *pb = &b; // pokazivač na objekt tipa B
B &rb = b; // referenca na objekt tipa B
a.f(); // A::f()
pa->f(); // A::f()
ra.f(); // A::f()
b.f(); // B::f()
pb->f(); // B::f()
rb.f(); // B::f()
b.g(); // A::g()
pb->g(); // A::g()
rb.g(); // A::g()
b.f(3); // Greška pri kompilaciji
pb->f(3); // Klasa B nema funkciju tipa
rb.f(3); // void f(int)
return 0;
}
Kada prevodilac traži funkciju danog imena potraga završava čim je ime nađeno. Tek tada ide provjera
da li se funkcija može pozvati sa zadanim argumentima. Ako ne može, potraga za funkcijom se ne nastavlja u
okružujućem dosegu. Posljedica: f()
u izvedenoj klasi skriva f(int)
u baznoj klasi.
Operator dosega
Iako je funkcija void A::f(int)
skrivena u proširenoj klasi ona se u njoj može dohvatiti
pomoću operatora dosega ::
Tako bi u prethodnoj main
funkciji sljedeći kod bio ispravan:
b.A::f(3); // Dohvat pomoću operatora dosega
pb->A::f(3); // Dohvat pomoću operatora dosega
rb.A::f(3); // Dohvat pomoću operatora dosega
Using deklaracija
1. U nekim drugim situacijama mi zaista želimo preraditi samo jednu iz skupa preopterećenih funkcija.
U tim slučajevima dužni smo sve preopterećene funkcije dovesti u lokalni doseg. To je upravo ono što radi
deklaracija using. U deklaraciji using
daje se samo ime funkcije (bez argumenata)
s efektom da se sve preopterećene funkcije tog imena dovode u lokalni doseg. Tada je samo potrebno
preopteretiti onu funkciju kojoj želimo promijeniti implementaciju.
2. Kod privatnog i zaštićenog nasljeđivanja prava pristupa elementima bazne klase postaju restriktivnija no što su to u baznoj klasi. Pojedinoj metodi ili varijabli iz bazne klase možemo vratiti njena originalna prava pristupa u proširenoj klasi pomoću using deklaracije. Potrebno je samo using deklaraciju staviti u odgovarajuću sekciju klase. Pri tome se prava pristupa ne mogu povećati iznad onih koje varijabla/metoda ima u baznoj klasi.
class A{
public:
A(int size_) : size(size_) {}
int n() const {return size;}
// ....
private:
int size;
// ...
};
class B : private A{
public:
B(int size) : A(size) {}
using A::n; // Dovođenje funkcije n() u javnu sekciju klase B
};
Using deklaracija djeluje na ime, tako da ako imamo više preopterećenih funkcija imena n
, sve će one
pomoću using deklaracije biti dovedene u javnu sekciju klase B
.
Napomena. Izraz
using namespace std;
je using direktiva kojom sva imena iz imenika std postaju vidljiva od mjesta direktive do kraja dosega.
Nasljeđivanje konstruktora, C++11
Ako se using
deklaracija primijeni na konstruktor bazne klase onda prevodilac sintetizira
jedan konstruktor izvedene klase za svaki konstruktor bazne klase. Sintetizirani konstruktor izvedene klase uzima
iste argumente kao odgovarajući konstruktor bazne klase i poziva konstruktor bazne klase s tim argumentima.
class Base{
public:
Base() : mx(0) {}
Base(int x) : mx(x) {}
int mx;
};
class Derived : public Base {
public:
using Base::Base; // Nasljeđivanje konstruktora
int my;
};
Prevodilac sintetizira dva konstruktora u javnoj sekciji izvedene klase:
Derived() : Base(), my(0) {}
Derived(int x) : Base(x), my(0) {}
Ako izvedena klasa ima dodatne varijable članice one su inicijalizirane dodijeljenim vrijednostima.
-
using
deklaracija primijenjena na bazni konstruktor ne može promijeniti prava pristupa koja ima konstruktor bazne klase. -
Ako je bazni konstruktor
explicit
, onda je to i generirani konstruktor izvedene klase. -
Izvedena klasa može pored nasljeđenih konstruktora definirati i svoje konstruktore. Ako neki od nasljeđenih konstruktora uzima iste argumente kao konstruktor definiran u izvedenoj klasi, on se tada ne nasljeđuje.
-
Ako bazni konstruktor ima defaultne parametre u izvedenoj klasi će biti definiran niz odgovarajućih konstruktora.
Konverzija izvedenog tipa u bazni tip
Reference i pokazivači
Svaki prošireni tip koji je nastao javnim nasljeđivanjem nekog baznog tipa dijeli sa svojim baznim tipom isto javno sučelje pa se može koristiti kao bazni tip u sljedećem smislu:
-
Referenca ili pokazivač baznog tipa mogu se inicijalizirati objektom (ili adresom objekta) izvedenog tipa.
-
Objekt baznog tipa može se inicijalizirati objektom izvedenog tipa.
Klijent a("Igor", "Herceg", 1, 1000.0);
Klijent b("Z.", "Elvis", 2, -1000.0);
Osoba * pa = &a; // Pokazivač na baznu klasu uzima adresu objekta proširene klase
Osoba & rb = a; // Referenca na baznu klasu inicijalizirana objektom proširene klase
pa = &b; // o.k.
To nam pravilo omogućava pisanje vrlo generalnog koda. Na primjer, funkcija koja prima
referencu (ili pokazivač) na tip Osoba
može uzeti kao argument tipa
Klijent
. Na primjer,
Funkcije koje uzimaju pokazivač ili referencu na baznu klasu
Pretpostavimo da imamo definiranu sljedeću funkciju:
void f(Osoba & rv, int id)
{
rv.setId(id);
}
Tada je legalan poziv
f(a, 13); // a je Klijent
ali, ako referenca (ili pokazivač) tipa bazne klase referira (pokazuje) na objekt proširene klase,
onda se kroz nju (njega) može dohvatiti samo bazni dio proširene klase.
To znači da unutar funkcije f()
ne možemo dohvatiti setSaldo()
.
Statički i dinamički tip varijable (reference ili pokazivača)
Budući da reference i pokazivači nekog tipa mogu referirati (pokazivati) na objekte svih proširenih tipova korisno je razlikovati statički i dinamički tip varijable (reference ili pokazivača).
-
Statički tip je onaj s kojim je varijabla deklarirana i on je poznat pri kompilaciji.
-
Dinamički tip reference ili pokazivačke varijable je tip objekta na koji varijabla referira ili pokazuje. Taj tip nije poznat pri kompilaciji i može biti svaki tip koji proširuje statički tip.
U prethodnom primjeru, u funkciji f
statički tip varijable rv
je referenca na Osoba
.
Dinamički tip varijable rv
je
bio Klijent
koji proširuje (osnovni)
statički tip Osoba
.
Objekti - nasuprot referencama i pokazivačima
-
Kada se referenca (ili pokazivač) na tip bazne klase inicijalizira objektom proširene klase (ili adresom proširene klase) onda sam objekt proširene klase ne doživljava nikakvu promjenu; jedino bazna referenca (pokazivač) referira (pokazuje) na bazni dio objekta proširene klase.
-
Kada se objekt bazne klase inicijalizira objektom proširene klase, onda dolazi do kopiranja onog dijela proširene klase koji pripada baznoj klasi u objekt bazne klase. Kažemo da dolazi do izrezivanja (slicing) dijela objekta proširene klase.
Na primjer, u slučaju da smo funkciju f
definirali na sljedeći način:
void f(Osoba rv, int id)
{
rv.setId(id);
}
tada bi poziv
f(a); // a je Klijent
ostao legalan, ali bi ponašanje koda bilo bitno drugačije.
Kada funkcija f
dobije objekt proširene klase, umjesto objekta bazne klase,
ona zove konstruktor kopije koji inicijalizira formalni argument funkcije izrezujući (slicing) bazni dio argumenta.
Isto proces se dešava kod pridruživanja objekta proširene klase objektu bazne klase, samo što se tamo poziva operator pridruživanja.
Osoba a;
Klijent b;
a = b; // izrezivanje podobjekta
Nasljeđivanje i statički članovi klase
Kada imamo hijerarhiju klasa izvedenu iz bazne klase koja ima statičku varijablu, tada je ta varijabla zajednička čitavoj hijerarhiji klasa. Možemo ju dohvatiti kroz objekt bilo kojeg tipa koji proširuje baznu klasu ili pomoću operatora dosega, polazeći od bilo koje klase iz lanca nasljeđivanja:
class A{
public:
static int y;
static int f() {return y; }
};
int A::y = 0; // Definicija statičke varijable
class B : public A {
public:
int x;
};
int main()
{
B b; // Objekti svih klasa izvedenih iz A
A a; // dijele istu statičku varijablu.
b.y = 3;
std::cout << B::f() << std::endl; // ispisuje 3
std::cout << A::f() << std::endl; // ispisuje 3
a.y = 4;
std::cout << b.f() << std::endl;// ispisuje 4
std::cout << a.f() << std::endl;// ispisuje 4
return 1;
}