Periyttämisen alkeet¶
Attention
Tämän kierroksen varsinaisena aiheena on graafisten käyttöliittymien toteuttaminen. Qt sisältää kuitenkin paljon luokkia, jotka on periytetty toisista luokista. Tämän vuoksi perehdymme ennen Qt:n aloittamista vielä hyvin pintapuolisesti C++:n periytymismekanismiin. Tällä opintojaksolla käytät periytymistä vain tilanteissa, joissa sinulle kerrotaan, mitä pitää periyttää ja mistä. Seuraavilla opintojaksoilla (Ohjelmointi 3: Tekniikat sekä Ohjelmistojen suunnittelu) opetellaan suunnittelemaan periytettyjä luokkia itsekin.
Periyttäminen (inheritance) ohjelmointikielissä on mekanismi, jonka avulla olemassa olevasta luokasta (kantaluokka) voidaan muodostaa uusia luokkia (aliluokkia, periytettyjä luokkia), joilla on samat perusominaisuudet kuin kantaluokalla mutta myös joitakin lisäominaisuuksia. Tämä on erittäin epämuodollinen ja epätäsmällinen määritelmä periyttämiselle, mutta siitä voidaan lähteä liikkeelle.
Tutkitaan yksinkertaista esimerkkiä, jossa on nyt toteutettuna kaksi
luokkaa ja pääohjelma. Tässä esimerkissä kaikki on kirjoitettu samaan
tiedostoon, jotta ohjelmakoodilistaus ei pitkity include
-riveillä
ja olennaisen löytää selvemmin. Edellisen kierroksen opeilla osaisit
toki toteuttaa tämän hienommin siten, että molemmilla luokilla olisi
omat esittely- ja toteutustiedostonsa ja pääohjelma olisi omassa
tiedostossaan.
#include <iostream>
#include <string>
using namespace std;
//--------------------------------------------------------
class Kulkuneuvo {
public:
Kulkuneuvo(double nopeus, string const& vari);
double hae_nopeus() const;
void aseta_nopeus(double nopeus);
string hae_vari() const;
void raportoi_matka(double aika) const;
private:
double nopeus_;
string vari_;
};
Kulkuneuvo::Kulkuneuvo(double nopeus, string const& vari):
nopeus_(nopeus), vari_(vari) {
}
double Kulkuneuvo::hae_nopeus() const {
return nopeus_;
}
void Kulkuneuvo::aseta_nopeus(double nopeus) {
nopeus_ = nopeus;
}
string Kulkuneuvo::hae_vari() const {
return vari_;
}
void Kulkuneuvo::raportoi_matka(double aika) const {
cout << hae_vari() << " kulkuneuvo, jonka nopeus on "
<< hae_nopeus() << ", liikkuu "
<< aika << " sekunnissa "
<< hae_nopeus() * aika / 3600.0 * 1000.0 << " m"
<< endl;
}
//--------------------------------------------------------
class Auto: public Kulkuneuvo {
public:
Auto(double nopeus, string const& vari, string const& reknum);
string hae_rekisterinumero() const;
void raportoi_tunnusmerkit() const;
private:
string rekisterinumero_;
};
Auto::Auto(double nopeus, string const& vari, string const& reknum):
Kulkuneuvo(nopeus, vari), rekisterinumero_(reknum) {
}
string Auto::hae_rekisterinumero() const {
return rekisterinumero_;
}
void Auto::raportoi_tunnusmerkit() const {
cout << "Poliisi etsii autoa: vari " << hae_vari()
<< ", rekisterinumero " << hae_rekisterinumero()
<< endl;
}
//--------------------------------------------------------
int main() {
Kulkuneuvo kneuvo(20.0, "punainen"); // 20 km/h
kneuvo.raportoi_matka(1.0); // 1.0 sekunnissa
kneuvo.aseta_nopeus(50.0); // 50.0 km/h
kneuvo.raportoi_matka(2.5); // 2.5 sekunnissa
Auto subaru(75.0, "siniharmaa", "ABC-123");
subaru.raportoi_matka(1.0);
subaru.aseta_nopeus(120.0);
subaru.raportoi_matka(2.5);
subaru.raportoi_tunnusmerkit();
}
Esimerkissä Kulkuneuvo
-luokasta on periytetty Auto
-luokka.
Tämä tarkoittaa loogisesti sitä, että kaikki autot ovat kulkuneuvoja,
mutta kaikki kulkuneuvot eivät ole autoja. Autoilla on siis samat
ominaisuudet kuin kulkuneuvoilla, mutta sen lisäksi joitakin
lisäominaisuuksia, jotka erottavat ne kulkuneuvoista.
Tai jos saman haluaa ilmaista hiukan toisin: autoille voi tehdä samoja asioita kuin kulkuneuvoille, mutta sen lisäksi niille voi tehdä jotakin vain autoille tyypillisiä asioita.
Tällainen nk. is-a -suhde saadaan C++:ssa aikaan periyttämällä aliluokka kantaluokasta käyttämällä luokan määrittelyssä syntaksia
class PeriytettyOmaLuokka : public Kantaluokka { ... };
Käyttämällä public
-periytymistä kaikista kantaluokan
public
-rajapinnassa olevista
metodeista tulee automaattisesti aliluokan public
-metodeja.
Konkreettisesti tämä tarkoittaa sitä, että aliluokan olioihin voidaan
kohdistaa kaikki ne metodit, jotka on määritelty kantaluokan
julkisessa rajapinnassa.
Aliluokan omista metodeissa ei kuitenkaan päästä suoraan
käsittelemään kantaluokan private
-osassa olevia jäsenmuuttujia,
vaan kaikki niihin kohdistuvat operaatiot on suoritettava kantaluokan
julkisia metodeja kutsumalla.
Kannattaa myös panna merkille syntaksi, jonka avulla aliluokan rakentajan alustuslistassa saadaan alustettua kantaluokalta perityt jäsenmuuttujat:
Auto::Auto(double nopeus, string const& vari, string const& reknum):
Kulkuneuvo(nopeus, vari), rekisterinumero_(reknum) {
}
Tässä siis kutsutaan kantaluokan rakentajaa aliluokan alustuslistassa. Kantaluokan rakentajan kutsun pitää olla alustuslistassa ensimmäisenä.
Periyttäminen edellä esitetyssä perusmuodossaan on käyttökelpoinen, jos on olemassa valmis luokka, joka melkein vastaa tarvetta mutta jonka julkisesta rajapinnasta puuttuu muutamia operaatioita.
Voidaan esimerkiksi kuvitella, että C++:n valmiista
string
-luokasta haluttaisiin versio, jonka julkisessa rajapinnassa
on operaatio onko_palindromi
:
#include <iostream>
#include <string>
using namespace std;
class OmaString: public string {
public:
OmaString();
OmaString(string const& alkuarvo);
bool onko_palindromi() const;
private:
// Ei välttämättä mitään täällä
};
OmaString::OmaString(): string("") {
}
OmaString::OmaString(string const& alkuarvo): string(alkuarvo) {
}
bool OmaString::onko_palindromi() const {
string::size_type vasen = 0;
while ( vasen < length() / 2 ) {
if ( at(vasen) != at(length() - vasen - 1) ) {
return false;
}
++vasen;
}
return true;
}
int main() {
OmaString s1;
OmaString s2( string("abcba") );
OmaString s3(s2);
cout << s1.onko_palindromi() << endl;
cout << s2.onko_palindromi() << endl;
cout << s3.onko_palindromi() << endl;
s1.append("ab");
s2.append("z");
s3.append("z");
cout << s1.onko_palindromi() << endl;
cout << s2.onko_palindromi() << endl;
cout << s3.onko_palindromi() << endl;
}
Valitettavasti monimutkaisemmista kantaluokista periyttäminen ei ole aivan noin suoraviivaista, mutta ideaa tämä havainnollistaa hyvin.
Koska kaikki aliluokan oliot kuuluvat myös kantaluokkaan, niitä voi periaatteessa käyttää kaikkialla, missä voisi käyttää kantaluokan oliota. Käytännössä C++ ei kuitenkaan ole noin joustava. Aliluokan oliota voi kuitenkin käyttää parametrina funktiolle, jonka muodollisen parametrin tyyppi on viite tai osoite kantaluokan olioon.
void prosessoi_kulkuneuvo(Kulkuneuvo const& kn) {
kn.raportoi_matka(15.0);
}
int main() {
Kulkuneuvo kneuvo(20.0, "punainen");
Auto subaru(75.0, "siniharmaa", "ABC-123");
prosessoi_kulkuneuvo(kneuvo);
prosessoi_kulkuneuvo(subaru);
}
Pohjimmiltaan tämäkin on mekanismi, jolla staattisesti tyypitetyssä kielessä yritetään saavuttaa dynaamisen tyypityksen iloja.
Yleisiä huomioita periyttämisestä¶
Periyttäminen on mielenkiintoinen mekanismi, joten sen käyttö houkuttaa pelkästään uutuudenviehätyksen vuoksi. Tämä johtaa helposti hölmöihin ratkaisuihin. Käytä periyttämistä säästeliäästi ja vain tilanteissa, joissa voit perustella itsellesi sen olevan oikeasti hyvä ratkaisu.
Hyvä nyrkkisääntö on seuraava:
Periytä kantaluokasta X aliluokka Y, vain jos pystyt perustelemaan itsellesi, että jokainen Y on myös X.
Siis auto on perusteltua periyttää kulkuneuvosta, koska jokainen auto
on myös kulkuneuvo. Autoa ei kuitenkaan ole järkevää periyttää
polttomoottorista, koska auto ei ole polttomoottori. Tässä on kyseessä
nk. has-a -suhde: autossa on polttomoottori. Toteutusmekanismin
pitäisi siis olla se, että Auto
-luokan private
-osassa on
Polttomoottori
-tyyppinen jäsenmuuttuja.
Edellinen esimerkki oli niin ilmiselvä, että melkein naurattaa. Tosiasia on kuitenkin se, että aloitteleva luokkien suunnittelija lankeaa vastaavaan ansaan vähänkin juonikkaammassa tilanteessa, vaikka silloinkin kyseessä saattaa tarkemmin analysoituna olla tismalleen vastaava is-a vs. has-a -tilanne.
Tässä monisteessa esitetyt esimerkit periyttämisestä ovat vain pinnallinen kosketus kaikkiin mahdollisiin periyttämismekanismeihin, joita C++ tarjoaa. Jokaisella pitäisi nyt kuitenkin olla perusidea siitä, mitä periyttämisellä tarkoitetaan.