Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, perinnän huomioon ottamisesesta

96
Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, perinnän huomioon ottamisesesta Sami Jantunen LTY/Tietotekniikan osasto

description

Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, perinnän huomioon ottamisesesta. Sami Jantunen LTY/Tietotekniikan osasto. Sisältö. Rajapinnoista Esimerkki Abstrakti luokka Puhdas virtuaalinen funktio Rajapinnan käytöstä Komponentteihin jaottelusta - PowerPoint PPT Presentation

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.

Ja sitten tarinan kysymys!

Mistä putkikirjasto voi tietää ketä kutsua kun viesti saapuu???

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.

ViitekopiontiEsimerkki

AlkuperainenOlio

Viitekopio

MUISTI:

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