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:
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.
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.)
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;
};
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
Klijent
— nasljeđuje sve varijable i metode bazne klase — u ovom slučaju Osoba
:
Pristup članicama bazne klase ovisi o sljedećem:
Prava pristupa u baznoj klasi:
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 određen je labelom pristupa koju smo iskoristili pri definiciji klase: to može biti public, private i protected:
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, 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.
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 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) {}
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 može biti proizvoljno dugačak. Objekt najizvedenije klase ima dio koji odgovara svakoj njegovoj nadklasi.
U kontrolu kopiranja ulaze:
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. |
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;
};
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;
};
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 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.
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 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
.
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.
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
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.
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.
explicit
, onda je to i generirani konstruktor izvedene klase.
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:
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,
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()
.
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).
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
.
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
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;
}