Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, perinnän huomioon ottamisesesta
-
Upload
jana-mooney -
Category
Documents
-
view
25 -
download
0
description
Transcript of Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, perinnän huomioon ottamisesesta
Olio-ohjelmoinnin perusteetluento 5: Rajapinnoista, perinnän huomioon ottamisesesta
Sami JantunenLTY/Tietotekniikan osasto
Sisältö Rajapinnoista
Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta
Perinnän vaikutus olion luontiin ja tuhoamiseen Rakentajat ja perintä Purkajat ja perintä
Perityn luokan eri tyypit Aliluokan ja kantaluokan suhde Tyyppimuunnokset
Olioiden sijoitus ja kopiointi Olioiden kopiointi Olioiden sijoitus
Yhteenveto Puhdasoppinen luokka Kertaus
Tarina… Kauan aikaa sitten
työskentelin erään palvelimen parissa
Palvelimen oli tarkoitus pystyä kommunikoimaan lukuisten erilaisten asiakasohjelmien kanssa
Tarina jatkuu…. Palvelimen ja
asiakasohjelmien väliseksi kommunikointitavaksi valittiin 2-suuntainen putki
Tarina jatkuu…. Pian huomasin, että 2-suuntaisen putken
käyttö ei ollut ihan helppoa 2-suuntaisen liikenteen hallinta vaati
synkronointitaitoja Putkesta “tipoittain” lukeminen tukkeutti
putken Putkia piti tarjota sitä mukaan kun
asiakasohjelmat ottivat palvelimeen yhteyttä Säikeistyksen hallinta
Kaikki asiakasohjelmat eivät olleet tiedossa ja niitä tehtiin muiden henkilöiden voimin.kommunikointimekanismi ei saa olla sen käyttäjälle vaikeaa!
…
Tarina jatkuu…
Niinpä ajattelin soveltaa yhtä olioajattelun perusajatuksista: Tiedon piilottamista
Loin kirjaston, joka piilotti putken monimutkaisuuden (synkronointi, säikeiden hallinta, viestien puskurointi, ym.)
Tarina jatkuu…. Ja sen putken käyttö oli
niin mukavaa… Viis hankalista
hallinnoitiasioista. Riitti kun avaa ja
lähettää….
PipeServer
create()send()disconnect()getNumberOfClients()
PipeClient
open()send()disconnect()
Tarina jatkuu… Entäpä viestin vastaanottaminen?
Olisipa mukavaa kun putki osaisi itse kutsua asiakkaan messageArrived –funktiota kun viesti on saapunut
Ainoa asia mitä asiakkaan tarvitsisi tehdä on toteuttaa messageArrived funktio, mihin määriteltäisiin viestin saapumisesta aiheutuva toimintalogiikka.
Ratkaisu?
Mitä jos kukin putkea käyttävä olio esittelee itsensä ja antaa osoittimen itseensä. Putki voisi sitten jatkossa
vain käyttää osoitinta ja kutsua sen avulla käyttäjäolion messageArrived-funktiota
Taustatietoa Jokaisella oliolla on olemassa
osoitinmuuttuja this, mikä osoittaa itseensä
this –osoitin on aina samaa tyyppiä kun siihen liittyvä osoitinkin aivan kun this olisi määritelty luokassa tyyliin:
MyClass *this;
Lähdetään ratkaisemaan ongelmaa
Oletetaan että putkea käyttävä olio identifioi itsensä kun se avaa putken:
PipeClient _myPipe;
_myPipe.open(this);
Nyt putki tietää sitä käyttävän olion osoitteen.
Ratkaisiko tämä meidän ongelman?
Vielä ongelmia
Okei, nyt sitten tiedetään putkea käyttävän olion osoite. Se ei kuitenkaan riitä
Mistä ihmeestä putkikirjasto tietää minkä tyyppinen annettu osoitin on? Eihän se muuten voi kutsua annettua
oliota
Heureka! Mitäs jos vaadittaisiin, että kaikki putken
käyttäjäluokat periytyvät MessageReader-luokasta Silloinhan tiedettäisiin, että asiakkaat ovat aina myös
tyyppiä MessageReader! putkikirjastoon voitaisiin siis kirjoittaa seuraava
koodipätkä:
//Luokan määrittelyssäMessageReader *_addressOfClient;...
//Putkea avattaessaPipeClient::open(MessageReader *client){
_addressOfClient=client;}...
//jossain päin missä luetaan putkea_addressOfClient->messageArrived();
Mitä taas tuli tehtyä?
Loimme luokan (MessageReader), joka ei itse tee yhtään mitään.
Tämähän on ihan selvä rajapintaluokka!
Ne luokat jotka haluavat tarjota rajapintaluokan määrittelemiä palveluita perivät itsensä rajapintaluokasta
PipeUser
PipeClient *myPipe
MessageReader
virtual void messageArrived(CArchive *message) = 0;
Rajapintaluokista Rajapintaluokat ovat yleensä
abstrakteja luokkia Eivät sisällä mitään muuta kuin rajapinnan
määrittelyjä Ei siis jäsenmuuttujia eikä jäsenfunktioiden
toteutuksia Jossain oliokielissä (kuten Java) tällaisille
puhtaille rajapinnoille on oma syntaksinsa eikä niitä silloin varsinaisesti laskeata edes luokiksi
Abstrakti luokka Mikä hyvänsä luokka, jossa on yksi tai
useampi puhdas virtuaalifunktio, on abstrakti luokka eikä sen tyyppisiä olioita voi käyttää.
Puhdas virtuaalifunktio kertoo luokan käyttäjälle kaksi asiaa: Luokan tyyppistä oliota ei voida luoda vaan
siitä pitää periyttää aliluokkia Jokainen puhdas virtuaalifunktio pitää
korvata uudella funktiolla abstraktista luokasta periytetyssä luokassa
Puhdas virtuaalifunktio
Abstrakti luokka tehdään käyttämällä puhtaita virtuaalifunktioita (pure virtual function) Virtuaalifunktio on puhdas, jos se
alustetaan nollalla, esimerkiksi:virtual void Piirra () = 0;
Puhtaan virtuaalifunktion ohjelmointi Yleensä abstraktissa kantaluokassa olevalle
puhtaalle virtuaalifunktiolle ei kirjoiteta funktion määrittelyä
Koska luokan tyyppisiä olioita ei voida koskaan luoda, niin ei ole mitään syytä ohjelmoida luokkan mitään toiminnallisuuttakaan.
Abstrakti luokka on siitä periytetyille luokille yhteinen käyttörajapinta
On toki mahdollista tehdä kantaluokkaan puhtaalle virtuaalifunktiolle toteutus
Sitä kutsutaan silloin lapsiluokista käsin. Esim. se toiminnallisuus, joka on yhteistä kaikille
lapsille siirretään kantaluokkaan.
Milloin kannattaa käyttää abstrakteja luokkia? Ei yksiselitteistä vastausta Päätös tehtävä sen perusteella onko luokan
abstraktisuudesta jotain hyötyä Esimerkki: Eläin-luokka kannattaa olla abstrakti,
mutta Koira-luokka ei, jotta ohjelmassa voidaan käyttää koira-olioita
Toisaalta: Jos ohjelmassa simuloidaan kenneliä, koira-luokka kannattaa jättää abstraktista ja periyttää siitä erirotuisia koiria.
Käytettävä abstraktiotaso määräytyy sen mukaan, kuinka hienojakoisesti ohjelman luokat pitää erotella toisistaan
Muistatko viel?Moniperintä -käyttökohteita Rajapintojen yhdistäminen.
Halutaan oman luokan toteuttavan useiden eri rajapintojen toiminnallisuus
Luokkien yhdistäminen. Halutaan esimerkiksi käyttää hyväksi muutamaa yleiskäyttöistä
luokkaa oman luokan kehitystyössä. Luokkien koostaminen valmiista ominaisuuskokoelmista.
Esimerkki: Kaikki lainaamiseen liittyvät toiminnot on kirjoitettu Lainattava-
luokkaan. Vastaavasti kaikki tuotteen myymiseen liittyvät aisat ovat
luokassa Myytävät. Voimme luoda KirjastonKirja –luokan perimällä sen Kirja-
kantaluokasta ja maustamalla sen Lainattava-luokasta saaduilla ominaisuuksilla
Voimme yhtä lailla luoda KaupallinenCD-ROM-luokan perimällä sen CD-ROM kantaluokasta ja ottaa käyttöön ominaisuudet Myytävä-luokasta
Rajapintaluokat ja moniperiytyminen Jos abstraktit kantaluokat sisältävät
ainoastaan puhtaita virtuaalifunktioita Moniperiytymisen käytöstä ei aiheudu
yleensä ongelmia. Jos moniperiytymisessä kantaluokat
sen sijaan sisältävät myös rajapintojen toteutuksia ja jäsenmuuttujia Moniperiytyminen aiheuttaa yleensä
enemmän ongelmia kuin ratkaisee.
Rajapinnoista Rajapintojen käyttö ja toteutuksen
kätkentä on yksi ehkä tärkeimmistä ohjelmistotuotannon perusperiaatteista Tästä huolimatta sen tärkeyden
perustelu uraansa aloittelevalle ohjelmistoammattilaiselle on vaikeaa
Merkityksen tajuaa yleensä itsestäänselvyytenä sen jälkeen, kun on osallistunut tekemään niin isoa ohjelmistoa, ettei sen sisäistä toteutusta pysty kerralla hallitsemaan ja ymmärtämään yksikään ihminen.
Komponentteihin jaottelusta Isoissa ohjelmissa
komponenttijako helpottaa huomattavasti kehitystyötä. Yksittäinen ohjelmoijan ei enää
tarvitse jatkuvasti hahmottaa kokonaisuutta
Kehittäjä voi enemmän keskittyä oman komponenttiensa vastuiden toteutukseen.
Missä mennään? Rajapinnoista
Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta
Perinnän vaikutus olion luontiin ja tuhoamiseen Rakentajat ja perintä Purkajat ja perintä
Perityn luokan eri tyypit Aliluokan ja kantaluokan suhde Tyyppimuunnokset
Olioiden sijoitus ja kopiointi Olioiden kopiointi Olioiden sijoitus
Yhteenveto Puhdasoppinen luokka Kertaus
Sä muistatko viel? Rakentajien käyttö perinnän yhteydessä
Isäluokan rakentajaa kutsutaan aina!*
CPoodle.cpp
CPoodle::CPoodle(int x, char y[ ]) : CDog (x,y)
{
cout << “Tuli muuten tehtyä puudeli" << endl;
}
Normaalia rakentaja tavaraa
Luokkia perittäessä on rakentajien ja purkajien käytössä on paljon huomioitavaa
Periytyminen ja rakentajat
Jokainen aliluokan olio koostuu kantaluokkaosasta (tai osista) sekä aliluokan lisäämistä laajennuksista Aliluokalla on oltava oma
rakentajansa. Mutta miten pitäisi hoitaa
kantaluokkien alustus?
Mammal
Land-Mammal
int weight
int numLegs
Dogboolean rabid
giveBirth( )
SheepDog
Periytyminen ja rakentajatVastuut
Aliluokan vastuulla on: Aliluokan mukanaan tuomien
uusien jäsenmuuttujien ja muiden tietorakenteiden alustaminen.
Em. vastuita varten aliluokkiin toteutetaan oma(t) rakentaja(t)
Kantaluokan vastuulla on: Pitää huoli siitä, että aliluokan
olion kantaluokkaosa tulee alustetuksei oikein, aivan kun se olisi irrallinen kantaluokan olio
Tämän alustuksen hoitavat aivan normaalit kantaluokan rakentajat
Periytyminen ja rakentajatParametrit?
Miten taataan että kaikki rakentajat saavat tarvitsemansa parametrit?
Päivänselvää aliluokalle. Sitä luodessahan kutsutaan aliluokan itse määrittelemiä rakentajia
Kantaluokan parametrien saannin takaamiseksi C++:n tarjoama ratkaisu on, että aliluokan rakentajan alustuslistassa kutsutaan kantaluokan rakentajaa ja välitetään sille tarvittavat parametritCPoodle.cpp
CPoodle::CPoodle(int x, char y[ ]) : CDog (x,y)
{
cout << “Tuli muuten tehtyä puudeli" << endl;
}
Entä jos? Jos aliluokan rakentajan alustuslistassa ei
kutsuta mitään kantaluokan rakentajaa: Kääntäjä kutsuu automaattisesti kantaluokan
oletusrakentajaa (joka ei siis tarvitse parametreja)
Tällainen ratkaisu harvemmin johtaa toivottuun tulokseen
Muista siis kutsua aliluokan rakentajassa kantaluokan rakentajaa itse!
Rakentajien suoritusjärjestys
Huipusta alaspäin Olio ikäänkuin rakentuu
vähitellen laajemmaksi ja laajemmaksi.
Näin taataan se, että aliluokan rakentaja voi jo turvallisesti käyttää kantaluokan jäsenfunktioita.
Periytyminen ja purkajat
Alustamisen tapaan myös olion siivoustoimenpiteet vaativat erikoiskohtelua luokan “kerrosrakenteen” vuoksi
Purkajien vastuut jaettu samalla lailla kuin rakentajienkin
Kantaluokan tehtävänä on siivota kantaluokkaolio sellaiseen kuntoon, että se voi rauhassa tuhoutua
Aliluokat puolestaan siivoavat periytymisessä lisätyt laajennusosat tuhoamiskuntoon
Purkajien suoritusjärjestys Purkajia kutsutaan päinvastaisessa
järjestyksessä kuin rakentajia Ensin kutsutaan aliluokan purkajia ja
siitä siirrytään perintähierakiassa ylöspäin
Näin varmistetaan se, että aliluokan purkajassa voidaan vielä kutsua kantaluokkien toiminnallisuutta
Esimerkki
Jotain pahasti pielessä!-Mitä?
Mammal
Land-Mammal
int weight
int numLegs
Dogboolean rabid
giveBirth( )
SheepDog
Mammal *myMammal;myMammal = new SheepDog();
...//koodia missä käytetään SheepDog-luokkaa...
delete myMammal;
Esimerkki
Vain kantaluokka tuhoutuu!
Kuinka korjata tilanne?
Mammal
Land-Mammal
int weight
int numLegs
Dogboolean rabid
giveBirth( )
SheepDog
Mammal *myMammal;myMammal = new SheepDog();
...//koodia missä käytetään SheepDog-luokkaa...
delete myMammal;
Virtuaalipurkaja
Jos luokasta peritään muita luokkia, muista aina määritellä purkaja virtuaaliseksi!
Ei haittaa vaikka purkaja on eri niminen lapsiluokassa.
Missä mennään? Rajapinnoista
Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta
Perinnän vaikutus olion luontiin ja tuhoamiseen Rakentajat ja perintä Purkajat ja perintä
Perityn luokan eri tyypit Aliluokan ja kantaluokan suhde Tyyppimuunnokset
Olioiden sijoitus ja kopiointi Olioiden kopiointi Olioiden sijoitus
Yhteenveto Puhdasoppinen luokka Kertaus
Aliluokan ja kantaluokan suhde
Aliluokka tarjoaa kaikki ne palvelut mitä kantaluokkakin (+ vähän lisää omia ominaisuuksia) Periytymisessähän vaan lisätään
ominaisuuksia Aliluokkaa voi siis käyttää
kantaluokan sijasta missä päin hyvänsä koodia
Aliluokan ja kantaluokan suhde Voidaan siis ajatella, että aliluokan olio on
tyypiltään myös kantaluokan olio! Aliluokan oliot kuuluvat ikään kuin useaan
luokaaan: Aliluokkaan itseensä Kantaluokkaan Kantaluokan kantaluokkaan, jne
Tämä is-a suhde tulisi pitää mielessä aina kun periytymistä käytetään!
Jos aliluokka on muuttunut vastuualueeltaan niin paljon, että se ei enää ole kantaluokan mukainen, periytymistä on ilmeisesti käytetty väärin
Aliluokan ja kantaluokan suhde C++:ssa aliluokan olio kelpaa kaikkialle
minne kantaluokan oliokin. Kantaluokan osoittimen tai viitteen voi
laittaa osoittamaan myös aliluokan olioon:
class Kantaluokka {…};class Aliluokka : public Kantaluokka {…};void funktio (Kantaluokka& kantaolio);
Kantaluokka *k_p =0;Aliluokka aliolio;k_p = &aliolio;funktio(aliolio);
Olion tyypin ajonaikainen tarkastaminen
Kantaluokkaosoittimen päässä olevalle oliolle voi kutsua vain kantaluokan rajapinnassa olevia funktioita Ei auta vaikka osoittimen päässä
todellisuudessa olisikin aliluokan olio. Normaalisti kantaluokan rajapinnan käyttö
onkin aivan riittävää Joskus tulee kuitenkin tarve päästä käsiksi
aliluokan rajanpintaan.
Olion tyypin ajonaikainen tarkastaminen
Jos aliluokan olio on kantaluokkaosoittimen päässä ei aliluokan rajapinta ole siis näkyvissä Ainoa vaihtoehto on luoda uusi osoitin
aliluokkaan ja laittaa se osoittamaan kantaluokkaosoittimen päässä olevaan olioon
Tyyppimuunnokset (type cast) Tyyppimuunnos on operaatio, jota
ohjelmoinnissa tarvitaan, kun käsiteltävä tieto ei ole jotain operaatiota varten oikean tyyppistä
Tyyppimuunnos on terminä hieman harhaanjohtava tyyppiä ei oikeastaan muuteta vaan luodaan
pikemminkin uusi arvo haluttua tyyppiä, joka vastaa vanhaa arvoa
Tyyppimuunnos muistuttaa tässä suhteessa suuresti kopiointia. Erona on vaan se, että uusi ja vanha olio on kopioinnista poiketen eri tyyppiä
C++ tyyppimuunnosoperaattorit
Vanha C-kielinen tyyppimuunnos:(uusiTyyppi)vanhaArvo sulkujen sijainti hieman epälooginen
C++ kielessä mahdollista myös: uusiTyyppi(vanhaArvo)
Ongelmia tyyppimuunnosten kanssa
Tyyppimuunnoksia voidaan käyttää suorittamaan kaikenlaisia muunnoksia. Esim: kokonasiluvuista liukuluvuiksi olio-osoittimista kokonaisluvuiksi
Kaikki tyyppimuunnokset eivät ole järkeviä! Kääntäjä ei tarkista tyyppimuunnosten
järkevyyttä Kääntäjä luottaa täysin ohjelmoijan omaan
harkintaan Tyyppimuunnoksiin jää helposti kirjoitusvirheitä Tyyppimuunnosvirheitä on vaikea löytää
Parannellut tyyppimuunnosoperaattorit
Parannellut tyyppimuunnosoperaattorit ovat: static_cast<uusiTyyppi>(vanhaArvo) const_cast<uusiTyyppi>(vanhaArvo) dynamic_cast<uusiTyyppi>(vanhaArvo) reinterpret_cast<uusiTyyppi>(vanhaArvo)
Yhteensopivia mallien käyttämän syntaksin kanssa (malleista puhutaan myöhemmin)
Kukin operaattoreista on tarkoitettu vain tietynlaisen mielekkään muunnoksen tekemiseen
kääntäjä antaa virheilmoituksen jos niitä yritetään käyttää väärin. Vanhat tavat tehdä tyyppimuunnokset ovat yhteensopivuuden takia edelleen
käytettävissä vältä niiden käyttöä ja suosi uusia operaattoreita
static_cast Suorittaa tyyppimuunnoksia, joiden
mielekkyydestä kääntäjä voi varmistua jo käännösaikana.
Esimerkkejä: muunnokset eri kokonaislukutyyppien välillä muunnokset enum-luettelotyypeistä kokonaisluvuiksi ja
takaisin muunnokset kokonaislukutyyppien ja likulukutyyppien
välillä Käyttöesimerkki. Lasketaan kahden
kokonaisluvun keskiarvo liukulukuna:double ka = (static_cast<double>(i1) + static_cast<double>(i2))/ 2.0;
static_cast static_cast ei suostu suorittamaan sellaisia
muunnoksia, jotka ei ole mielekkäitä. Esimerkki:Paivays* pvmp = new Paivays();int* ip = static_cast<int*>(pvmp); //KÄÄNNÖSVIRHE!
static_cast:ia voidaan käyttää myös osoittimen tyyppimuutokseen muunnoksen mielekkyyttä ei tällaisessa
tapauksessa testata ajon aikana Pitää olla itse varma, että kantaluokkaosoittimen
päässä on varmasti aliluokan olio dynamic_cast:n käytto olisi turvallisempaa! static_cast on nopeampi kuin dynamic_cast
const_cast joskus const-sanan käyttö tuo ongelmia const_cast tarjoaa mahdollisuuden
poistaa const-sanan vaikutuksen voi tehdä vakio-osoittimesta ja –viitteestä ei-
vakio-osoittimen tai –viitteen const_cast-muunnoksen käyttö rikkoo C+
+ “vakiota ei voi muuttaa” periaatetta vastaan. sen käyttö osoittaa että jokin osa ohjelmasta
on suunniteltu huonosti Pyri pikemminkin korjaamaan varsinainen
ongelma kuin käyttämään const_cast:ia
dynamic_cast Muunnos kantaluokkaosoittimesta
aliluokkaosoittimeksi onnistuuu tyyppimuunnoksella:dynamic_cast<Aliluokka*>(kluokkaosoitin)
Muunnoksen toiminta on kaksivaiheinen: Ensin tarkastetaan, että
kantaluokkaosoittimen päässä oleva olio todella on aliluokan olio.
Jos kantaluokkaosoittimen päässä on väärän tyyppinen olio, palautetaan tyhjä osoitin 0.
Jos kantaluokkaosoittimen päässä on okean tyyppinen olio, palautetaan kyseiseen olioon osoittava aliluokkaosoitin.
Kantaluokkaosoittimesta aliluokkaosoittimeksi dynamic_cast –muunnosta voi käyttää
myös olioviitteisiin (siis tuottamaan aliluokkaviitteen)
Ainoa ero osoitinmuunnokseen on se, että jos kantaluokkaviitteen päässä on väärän tyyppinen olio, dynamic_cast hiettää poikkeuksen (std::bad_cast) Miksi näin? Puhumme poikkeuksista lisää seuraavilla
luennoilla!
dynamic_cast esimerkkibool myohassako(Kirja* kp, const Paivays& tanaan)
{
KirjastonKirja* kpp = dynamic_cast<KirjastonKirja*>(kp);
if(kkp != 0)
{ //jos tultiin tänne, kirja on kirjastonkirja
return kkp->onkoMyohassa(tanaan);
}
else
{ //jos tultiin tänne, kirja ei ole kirjastonkirja
return false;
}
}
reinterpret_cast Joskus joudutaan käsittelemään tietoa
tavalla, joka ei ole sen todellisen tyypin mukainen Esim. osoitinta voi joskus joutua käsittelemään
muistiosoitteena (=kokonaislukuna) reinterpret_cast:ia käytetään tiedon
esitystavan muuttamiseen. Muunnoksen lähes ainoa käyttökohde on
muuttaa tieto ensin toisentyyppiseksi ja myöhemmin takaisin.
reinterpret_cast sallitut käyttökohteet:
Osoittimen muunto kokonaisluvuksi, jos kokonaislukutyyppi on niin suuri, että osoitin mahtuu siihen
Kokonaisluvun muuntaminen takaisin osoittimeksi
Osoittimen muunto toisentyyppiseksi osoittimeksi
Viitteen muunto toisentyyppiseksi viitteeksi Funktio-osoittimen muunto toisentyyppiseksi
funktio-osoittimeksi.
reinterpret_cast käyttöesimerkki
void luoKayttoliittyma(KirjastonKirja *kirja1, KirjastonKirja* kirja2)
{
luoNappula(“Kirja1”, reinterpret_cast<unsigned long int>(kirja1));
luoNappula(“Kirja2”, reinterpret_cast<unsigned long int>(kirja2));
}
//tätä funktiota kutsutaan kun nappulaa painetaan
void nappulaaPainettu(unsigned long int luku)
{
KirjastonKirja* kp = reinterpret_cast<KirjastonKirja*>(luku);
cout << “Painettu kirjan “ << kp->annaNimi() << “ nappia.” << endl;
}
Missä mennään? Rajapinnoista
Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta
Perinnän vaikutus olion luontiin ja tuhoamiseen Rakentajat ja perintä Purkajat ja perintä
Perityn luokan eri tyypit Aliluokan ja kantaluokan suhde Tyyppimuunnokset
Olioiden sijoitus ja kopiointi Olioiden kopiointi Olioiden sijoitus
Yhteenveto Puhdasoppinen luokka Kertaus
Olioiden kopiointi Olio-ohjelmoinnissa sijoituksen ja
kopioinnin merkitys ei ole yhtä selvä kuin perinteisessä ohjelmoinnissa
C++:ssa varsinkin kopioinnin merkitys korostuu entisestään, koska kääntäjä itse tarvitsee olioiden kopiointia: välittäessään olioita tavallisina
arvoparametreina palauttaessaan olioita paluuarvoina
Olioiden kopioinnista Kopioidun olion määritelmä:
Uuden ja vanhan olion arvojen tai tilojen täytyy olla samat.
Eri tyyppisiä olioita kopioidaan hyvin eri tavalla Kompleksiluokuolion kopiointiin voi riittää
yksinkertainen muistin kopiointi Merkkijonon kopiointi puolestaan saattaa
vaatia ylimääräistä muistinvarausta ja muita toimenpiteitä
Olioiden kopioinnista Yleensä kääntäjä ei pysty automattisesti
kopioimaan olioita hyväksyttävällä tavalla, vaan luokan tekijän tulisi itse määritellä mitä kaikkea olioita kopioitaessa täytyy tehdä.
Kaikkia olioita ei ole järkevää kopioida (esim. hiissin moottoria ohjaavan olion
kopiointi. kopiointi vaatisi myös fyysisen moottorin kopiointia).
Tulisi olla mahdollista myös estää luokan olioiden kopiointi kokonaan
Erilaiset kopiointitavat Olioiden kopiointitavat jaotellaan
usein seuraavasti: Viitekopiointi Matalakopiointi Syväkopiointi
Voi olla kuitenkin tarve kopioida osa olioista yhdellä tavalla ja toisia osia toisella
Viitekopiointi (Reference copy)
Kaikkein helpoin kopiointitavoista. Ei luoda ollenkaan uutta oliota
vaan uutta oliota kuvastaa viite vanhaan olioon.
Viitekopiointi Käytetään etenkin oliokielissä, missä itse
muuttujat ovat aina vain viitteitä olioihin, jotka puolestaan luodaan dynaamisesti (esim. Java ja Smalltalk)
C++:ssa viitekopiointia käytetään vain, kun erikseen luodaan viitteitä olioiden sijaan.
Viitekopioinnin etu on sen nopeus. “Kopion” luominen ei käytännössä vaadi ollenkaan aikaa Mitään kopioimista ei tarvitse oikeastaan tehdä
Viitekopiointi toimii hyvin niin kauan kun olion arvoa ei muuteta.
Jos olion arvoa muutetaan, arvo kopiossakin muuttuu.
Matalakopiointi (shallow copy) Matalakopioinnissa itse oliosta ja sen
jäsenmuuttujista tehdään kopiot Jos jäsenmuuttujina on viitteitä tai
osoittimia olion ulkopuolisiin tietorakenteisiin, ei näitä tietorakenteita kopoida. matalakopioinnin lopputuloksena
molemmat oliot jakavat samat olioiden ulkopuoliset tietorakenteet.
MatalakopioEsimerkki
1 2 3 4 5
6 7
1 2 3 4 5 6 7
AlkuperainenOlio
Matalakopio
MUISTI:
Alkuperaisenolion ulkoiset tietorakenteet
Matalakopiointi Ohjelmointikielten toteutuksen kannalta
matalakopiointi on selkeä operaatio siinä kopioidaan aina kaikki olion
jäsenmuuttujat eikä mitään muuta Selkeydestä johtuen C++ käyttää
oletusarvoisesti matalakopiointia, jos luokan kirjoittaja ei muuta määrää.
Kopioinnin tuloksena on ainakin päällisin puolin kaksi oliota.
Matalakopiointi Yleensä viitekopiointia käytävissä
oliokielissä on myös jokin tapa matalakopiointiin Esim. Javan jäsenfunktio clone
Eri olioiden jakamat ulkoiset tietorakenteet ovat potentiaalinen ongelma. Muutokset ulkoisissa tietorakenteissa
heijastuu kaikkiin matalakopioituihin olioihin
Syväkopiointi (deep copy) Olion ja sen jäsenmuuttujien lisäksi
kopioidaan myös ne olion tilaan kuuluvat oliot ja tietorakenteet, jotka sijaitsvat olion ulkopuolella.
Olioiden kannalta ehdottomasti paras kopiointitapa Luodaan kopio kaikista olion tilaan kuuluvista
asioista Uusi ja alkuperäinen olio ovat täysin erilliset.
SyväkopioEsimerkki
1 2 3 4 5
6 7 1 2
3
1 2 3
1 2 3 4 5 6 7
Alkuperainenolio
Syväkopio
MUISTI:
Alkuperaisenolion ulkoiset tietorakenteet
Syväkopionulkoiset tietorakenteet
Syväkopio Ongelmat Ohjelmointikielen kannalta syväkopiointi
on ongelmallista Usein kopioitavat oliot sisältävät osoittimia
myös sellaisiin olioihin ja tietorakenteisiin, jotka eivät varsinaisesti ole osa olion tilaa ja joita ei tulisi kopioida.
Esim. Kirjaston kirja sisältää osoittimen kirjastoon, josta ne on lainattu. Kirjan tietojen kopioiminen ei saisi aiheuttaa kirjaston kopiointia!
Syväkopio Ongelmat Syväkopioinnin ongelmien johdosta
useimmat ohjelmointikielet eivät tue automaattisesti syväkopiointia Poikkeuksena Smalltalk, joissa oliolta löytyy
myös palvelu deepCopy Yleensä oliokielissä annetaan ohjelmoijalle
itselleen mahdollisuus kirjoittaa syväkopioinnille toteutus, jota kieli osaa automaatiisesti käyttää C++-kielessä ohjelmoija kirjoittaa luokalle
kopiorakentajan, joka suorittaa kopioinnin ohjelmoijan sopivaksi katsomalla tavalla.
Muistatko viel? Kopiorakentaja (copy constructor)
Saa parametrina viitteen olemassa olevaan saman luokan olioon.
Tehtävänä luoda identtinen kopio parametrina saadusta oliosta
Kääntäjä kutsuu sitä automaattisesti tilanteissa, missä kopion luominen on tarpeen.
Jos kopiorakentaja puuttuu, se luodaan kääntäjän toimesta automaattisesti
Periytyminen ja kopiorakentaja Periytyminen tuo omat lisänsä kopion
luomiseen. Aliluokan olio koostuu useista osista, ja
kantaluokan osilla on jo omat kopiorakentajansa, joilla kopion kantaluokkaosat saadaan alustetuksi.
Aliluokan olion kopioiminen onkin jaettu eri luokkien kesken samoin kuin rakentajat yleensä Aliluokan kopiorakentajan vastuulla on kutsua
kantaluokan kopiorakentajaa ja lisäksi alustaa aliluokan osa olioista kopioksi alkuperäisestä
Kopiorakentaja esimerkkiMjono.h
class Mjono{public:
Mjono(const char* merkit);
//kopiorakentaja Mjono(const Mjono& vanha); virtual ~Mjono();...
private: unsigned long koko_;char* merkit_;
};
Mjono.cpp
Mjono::Mjono(const Mjono& vanha) : koko_(vanha.koko_), merkit_(0)
{if (koko_ != 0){//Varaa tilaa, jos koko ei ole nolla
merkit_ = new char[koko_ + 1];for (unsigned long i = 0; i != koko_; ++i)
{ merkit_[i] = vanha.merkit_[i];} //kopioi merkitmerkit_[koko_] = ‘\0’; //loppumerkki
}}
Pmjono.cpp//olettaa että Paivays-luokalla on kopiorakentajaPaivattyMjono::PaivattyMjono(const PaivattyMjono& vanha) : Mjono(vanha),
paivays_(vanha.paivays_){}
Pmjono.hclass PaivattyMjono : public Mjono{public:
PaivattyMjono(const char* merkit, const Paivays& paivays);
//kopiorakentaja PaivattyMjono(const PaivattyMjono& vanha); virtual ~PaivattyMjono();...
private: Paivays paivays_;
};
Muista!
Jos unohdat aliluokan kopiorakentajassa kutsua kantaluokan kopiorakentajaa Kääntäjä kutsuu kantaluokan
oletusrakentajaa automaattisestiOlio ei kopioidu kunnolla
Kääntäjän luoma oletusarvoinen kopiorakentaja Jos et määrittele luokalle
kopiorakentajaa, kääntäjä luo sen automaattisesti Yksinkertaistaa ohjelmointia Oletusarvoinen kopiorakentaja käyttää
matalakopiointia Useinmiten matalakopiointi ei ole riittävä
jos kopiorakentajan toteutus unohtuu, oliot kopioituvat väärin
Jokaiseen luokkaan tulisi erikseen kirjoittaa kopiorakentaja
Kopioinnin estäminen Kun ei ole mitään järkeä kopioida oliota,
kääntäjän automaattisesta kopiorakentajasta on vain haittaa.
Kopiointi on mahdollista estää määrittelemällä kopiorakentaja privaatiksi. Kun olet itse määrittänyt kopiorakentajan,
kääntäjä ei yritä tuputtaa omaansa Kukaan luokan ulkopuolella ei pääse
kutsumaan kopiorakentajaaOnko asia nyt ratkaistu? Huomaatko ongelman?
Kopioinnin estäminen
privaattiin kopiorakentajaan pääsee käsiksi luokan sisältä tai ystävien kautta Ongelma ratkaistaaan jättämällä
kopiorakentaja ilman toteutustaLinkkeri antaa virheilmoituksen, jos joku yrittää käyttää kopiorakentajaa
Esimerkki
PaivattyMjono pmj(“paivays”, jokupaivays);
//luodaan kopio
Mjono mj(pmj); PaivattyMjono
Mjono
Jotain pielessä! Mitä?
Viipaloituminen (Slicing) Ilmiötä, missä oliota kopioitaessa
kopioidaankin erehdyksessä vain olion kantaluokkaosa kutsutaan viipaloitumiseksiPaivattyMjono pmj(“paivays”, jokupaivays);
Mjono mj(pmj); //luodaan kopio
PaivattyMjono
Mjono Copy of Mjono
PaivattyMjono
Viipaloitumisen kiertäminen C++ kielessä Otetaan mallia muista oliokielistä
toteutetaan kloonaa-funktio ja määritellään se virtuaaliseksi
Viipaloitumista ei tapahdu, sillä kloonaa funktion virtuaalisuus takaa sen, että kutsutaan ensin alimmaista lapsiluokkaa
Viipaloituminen on kuitenkin edelleen vaarana parametrin välityksessä ja paluuarvoissa.
Paras ratkaisu näihin on huolellinen suunnittelu ja ongelmien tiedostaminen.
Yksi tapa estää viipaloitumista on myös se, että kaikki kantaluokat ovat abstrakteja.
Viipaloitumista ei pääse tapahtumaan, sillä pelkkää abstraktia luokkaa ei voi muodostaa
Olioiden sijoittaminen
Olioiden kopioimisen lisäksi on toinenkin tapa saada aikaan kaksi keskenään samanlaista oliota: Sijoittaminen
Sijoittamisen ja kopioinnin ero: kopioinnista luodaan uusi olio, joka
alustetaan vanhan olion perusteella sijoittamisessa muutetaan olemassa
olevan olion arvo vastaamaan toista oliota
Sijoittamiseen liittyviä ongelmia Liittyvät useinmiten vanhan sisällön käsittelyyn
Usein joudutaan vapauttamaan vanhaa muistia ja siivoamaan oliota purkajien tapaan ennen kuin uudet arvot voidaan alustaa olioon.
Mitä jos siivousoperaatio johtaa virhetilanteeseen?
Luultavasti haluttaisiin palauttaa vanhat arvot takaisin oliollePitäisi varmistua siitä, että siivottuja arvoja ei ole vielä heitetty roskiin
On myös olemassa tilanteita, missä ei ole mielekästä sallia sijoitusta.pitää olla mahdollista estää sijoitusoperaatio
C++ sijoitusoperaattori(assignment operator) C++:ssa olioiden sijoittaminen tapahtuu
erityisellä jäsenfunktiolla, jota kutsutaan sijoitusoperaattoriksi
Kun ohjelmassa tehdään kahden olion sijoitus a = b, kyseisellä ohjelmarivillä kutsutaan itse asiassa olion a sijoitusoperaattoria ja annetaan sille viite olioon b parametrina.
Sijoitus aiheuttaa jäsenfunktiokutsun a.operator =(b) Sijoitusoperaattorin tehtävänä on sitten tuhota olion
a vanha arvo ja korvata se olion b arvolla. Se mitä kaikkia operaatioita tähän liittyy, riippuu
täysin kyseessä olevasta luokasta
Sijoitusoperaattorin toteutusEsimerkki
Mjono.hclass Mjono{public:
Mjono& operator =(const Mjono& vanha);...
};Mjono.cpp
Mjono& Mjono::operator =(const Mjono& vanha){
if (this != vanha){//Jos ei sijoiteta itseen
delete[] merkit_; merkit_ = 0; //Vapauta vanhakoko_ = vanha.koko_; //Sijoita kokoif (koko_ != 0){ //Varaa tila, jos koko ei nolla
merkit_ = new char[koko_ + 1];for (unsigned long i = 0; i != koko_; ++i)
{ merkit_[i] = vanha.merkit_[i];} //kopioi merkitmerkit_[koko_] = ‘\0’; //loppumerkki
}return *this;
}
Palauttaa viitteen itseensä mahdollistaa
ketjusijoituksen a=b=c
Sijoitus itseen
Mitä seurauksia seuraavalla koodilla on?
a=a;
Miten ongelman voi ehkäistä?
Ensin lähdetään tyhjentämään sijoitettavan luokan vanhaa arvoa Samalla tuhotaan vahingossa sijoitettava arvo Eli muistialueen alustamaton sisältö kopioidaan itsensä päälle
Tarkastetaan ennen sijoitusoperaatioon ryhtymistä, että kyseessä ei ole sijoitus itseen.
Jätetään sijoitusoperaatio tekemättä jos näin on
Periytyminen ja sijoitusoperaattori
Toimitaan samoin kuin kopiorakentajankin kanssa aliluokka kutsuu kantaluokan
sijoitusoperaattoriaPmjono.h
class Mjono{public:
Mjono& operator =(const Mjono& vanha);...
};
Pmjono.cpp
PaivattyMjono& PaivattyMjono::operator =(const PaivattyMjono& vanha){
if (this != vanha){//Jos ei sijoiteta itseen
Mjono::operator =(vanha); //Kantaluokan sijoitusoperaattori
//Oma sijoitus, oletetaan että Paivays-luokalla on sijoitusoperaattoripaivays_ = vanha.paivays_;
}return *this;
}
Oletus-sijoitusoperaattori Jos luokalla ei ole kirjoitettu
sijoitusoperaattoria, kääntäjä luo sen itse. Oletus sijoitusoperaattori yksinkertaisesti
sijoittaa kaikki olion jäsenet yksi kerrallaan Jos jäseninä on osoittimia, molemmat oliot
tulevat sijoituksen jälkeen osoittamaan samaan paikkaan EI HALUTTUA!
Jokaiseen luokkaan tulisi erikseen kirjoittaa sijoitusoperaattori!
Sijoituksen estäminen
Estetään samalla tavalla kuin kopioiminenkin Määritellään sijoitusoperaattori
privaatiksi Ei anneta sijoitusoperaattorille
toteutusta ollenkaan
Sijoitus ja viipaloituminen
Viipaloituminen on mahdollista jos sijoittaminen tapahtuu kantaluokkaosoittimien tai -viitteiden kautta void sijoita (Mjono& mihin, const Mjono& mista)
{mihin = mista;
}
int main(){
Mjono mj(“Tavallinen”);PaivattyMjono pmj(“Päivätty”, tanaan);NumMjono nmj(“Numeroitu”,12);
//Viipaloituminen funktion sisällä!sijoita (pmj, nmj); sijoita(mj, pmj);
}
Viipaloitumisen välttäminen sijoituksessa
Helpointa olisi tehdä luokkahierarkia, jossa kaikki kantaluokat ovat abstrakteja
Voit myös aina tarkastaa sijoituksen yhteydessä että molemmat oliot ovat varmasti samaa tyyppiä tämä onnistuu typeid-operaattorin
avulla
Sijoitettavien olioiden tyypin tarkastus
#include <typeinfo>Mjono& Mjono::operator =(const Mjono& m){
if (typeid(*this) == typeid(m)) { /*virhetoiminta*/}if (this != &m){//Jos ei sijoiteta itseen
.
.
.}return *this;
}
Missä mennään? Rajapinnoista
Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta
Perinnän vaikutus olion luontiin ja tuhoamiseen Rakentajat ja perintä Purkajat ja perintä
Perityn luokan eri tyypit Aliluokan ja kantaluokan suhde Tyyppimuunnokset
Olioiden sijoitus ja kopiointi Olioiden kopiointi Olioiden sijoitus
Yhteenveto Puhdasoppinen luokka Kertaus
YhteenvetoPuhdasoppinen luokka
Olisi hyvä jos kaikki luokat määrittelisivät seuraavat tärkeät funktiot
Oletusrakentaja (Default constructor) Kopiointirakentaja (Copy constructor) Sijoitusoperaattorin (Assignment operator) Purkajan (Destructor)
Tällainen luokkarakenne tunnetaan puhdasoppisen kanonisen luokan muotona (orthodox canonical class)
Puhdasoppinen luokkaEsimerkki
Mjono.hclass Mjono{public://constructors///////////////////////
Mjono(); //oletusrakentaja Mjono(const Mjono& vanha); //kopiorakentaja Mjono(const char* merkit);
//destructors////////////////////////virtual ~Mjono(); //purkaja
//operators/////////////////////////Mjono& operator =(const Mjono& vanha); //sijoitusoperaattori
//operations////////////////////////unsigned long kerroKoko(){ return koko;}
protected:
private: unsigned long koko_;char* merkit_;
};
Mitä tänään opimme? Rajapintojen käyttö ja toteutuksen kätkentä on yksi ehkä tärkeimmistä
ohjelmistotuotannon perusperiaatteista Rajapinta toteutetaan abstrakteina luokkina Abstrakti luokka luodaan puhtaiden virtuaalifunktioiden avulla Rajapintojen avulla voimme pilkkoa monimutkaiset systeemit pienempiin osiin
Perinnän käyttö pitää ottaa huomioon olioita luodessa ja tuhotessa Rakentajat ja perintä Purkajat ja perintä
Peritty luokka on aina myös kantaluokkansa edustaja. Käytettävissä olevat operaatiot riippuvat siitä minkä tyyppinen osoitin on
kyseessä Opimme muuttamaan osoittimien ja viittausten tyyppiä
Perintä pitää ottaa myös huomioon olioita kopioitaessa ja sijoittaessa Kääntäjä ei voi tietää miten kopioidaan ja sijoitetaan järkevästi Oletuskopiointi ja oletussijoitus menevät helposti pieleen
toteuta kopiorakentaja ja sijoitusoperaattori mielummin itse Puhdasoppinen luokka
oletusrakentaja kopiorakentaja sijoitusoperaattori virtuaalinen purkaja