1.22k likes | 1.41k Views
Olio-ohjelmoinnin perusteet luento 5: Rajapinnoista, periytymisen huomioon ottamisesta, operaattoreiden uudelleenmäärittely. Jani Rönkkönen jani.ronkkonen@lut.fi Luennot muokattu Sami Jantusen ja Kari Smolanderin aikaisempien vuosien luennoista. Sisältö. Rajapinnoista Esimerkki
E N D
Olio-ohjelmoinnin perusteetluento 5: Rajapinnoista, periytymisen huomioon ottamisesta, operaattoreiden uudelleenmäärittely Jani Rönkkönen jani.ronkkonen@lut.fi Luennot muokattu Sami Jantusen ja Kari Smolanderin aikaisempien vuosien luennoista
Sisältö • Rajapinnoista • Esimerkki • Abstrakti luokka • Puhdas virtuaalinen funktio • Rajapinnan käytöstä • Komponentteihin jaottelusta • Periytymisen vaikutus olion luontiin ja tuhoamiseen • Muodostimet ja periytyminen • Purkajat ja periytyminen • Perityn luokan eri tyypit • Aliluokan ja kantaluokan suhde • Tyyppimuunnokset • Olioiden sijoitus ja kopiointi • Olioiden kopiointi • Olioiden sijoitus • Puhdasoppinen luokka • Serialisaatio • Operaattoreiden uudelleenmäärittely • Ystäväfunktiot • Ystäväluokat • Yhteenveto
Tarina… • Olipa kerran insinööri, joka työskenteli 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 hän huomasi, 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ä hän päätti soveltaa yhtä olioajattelun perusajatuksista: Tiedon piilottamista Hän loi 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, joka osoittaa itseensä • this –osoitin on aina samaa tyyppiä kun siihen liittyvä osoitinkin aivan kun this olisi määritelty luokassa tyyliin: MyClass *this; 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ä ongelman?
Vielä ongelmia • Okei, nyt 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 MessageReader virtual void messageArrived(CArchive *message) = 0; PipeUser PipeClient *myPipe
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 luoda. • 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 luokkaan 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ää abstraktiksi ja periyttää siitä erirotuisia koiria. Käytettävä abstraktiotaso määräytyy sen mukaan, kuinka hienojakoisesti ohjelman luokat pitää erotella toisistaan
Muistatko viel?Moniperiytyminen -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 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 • Periytymisen vaikutus olion luontiin ja tuhoamiseen • Muodostimet ja periytyminen • Purkajat ja periytyminen • Perityn luokan eri tyypit • Aliluokan ja kantaluokan suhde • Tyyppimuunnokset • Olioiden sijoitus ja kopiointi • Olioiden kopiointi • Olioiden sijoitus • Serialisaatio • Operaattoreiden uudelleenmäärittely • Ystäväfunktiot • Ystäväluokat • Yhteenveto • Puhdasoppinen luokka • Kertaus
Sä muistatko viel? Muodostimen käyttö periytymisen yhteydessä Isäluokan muodostinta kutsutaan aina!* CPoodle.cpp CPoodle::CPoodle(int x, string y) : CDog (x,y) { cout << “Tuli muuten tehtyä puudeli" << endl; } Normaali muodostin Luokkia perittäessä on muodostimien ja purkajien käytössä on paljon huomioitavaa
Periytyminen ja muodostimet • Jokainen aliluokan olio koostuu kantaluokkaosasta (tai osista) sekä aliluokan lisäämistä laajennuksista Aliluokalla on oltava oma muodostimensa. • Mutta miten pitäisi hoitaa kantaluokkien alustus? Mammal int weight giveBirth( ) Land-Mammal int numLegs Dog boolean rabid SheepDog
Periytyminen ja muodostimetVastuut • Aliluokan vastuulla on: • Aliluokan mukanaan tuomien uusien jäsenmuuttujien ja muiden tietorakenteiden alustaminen. • Em. vastuita varten aliluokkiin toteutetaan oma(t) muodostimet/muodostin • Kantaluokan vastuulla on: • Pitää huoli siitä, että aliluokan olion kantaluokkaosa tulee alustetuksi oikein, aivan kun se olisi irrallinen kantaluokan olio • Tämän alustuksen hoitavat aivan normaalit kantaluokan muodostimet
Periytyminen ja muodostimetParametrit? • Miten taataan että kaikki muodostimet saavat tarvitsemansa parametrit? • Päivänselvää aliluokalle. Sitä luodessahan kutsutaan aliluokan itse määrittelemiä muodostimia • Kantaluokan parametrien saannin takaamiseksi C++:n tarjoama ratkaisu on, että aliluokan muodostimen alustuslistassa kutsutaan kantaluokan muodostinta ja välitetään sille tarvittavat parametrit CPoodle.cpp CPoodle::CPoodle(int x, string y) : CDog (x,y) { cout << “Tuli muuten tehtyä puudeli" << endl; }
Entä jos? • Jos aliluokan muodostimen alustuslistassa ei kutsuta mitään kantaluokan muodostinta: • Kääntäjä kutsuu automaattisesti kantaluokan oletusmuodostinta (joka ei siis tarvitse parametreja) • Tällainen ratkaisu ei läheskään aina johda toivottuun tulokseen Muista siis kutsua aliluokan muodostimessa kantaluokan muodostinta itse!
Muodostimien suoritusjärjestys • Huipusta alaspäin • Olio ikäänkuin rakentuu vähitellen laajemmaksi ja laajemmaksi. • Näin taataan se, että aliluokan muodostin 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 muodostimienkin • 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 muodostimia • Ensin kutsutaan aliluokan purkajia ja siitä siirrytään periytymishierakiassa ylöspäin • Näin varmistetaan se, että aliluokan purkajassa voidaan vielä kutsua kantaluokkien toiminnallisuutta
Esimerkki Jotain pahasti pielessä! -Mitä? • Mammal *myMammal; • myMammal = new SheepDog(); • ... • //koodia missä käytetään SheepDog-luokkaa • ... • delete myMammal; Mammal int weight ~Mammal ( ) Land-Mammal int numLegs Dog boolean rabid SheepDog
Esimerkki • Vain kantaluokan purkajaa kutsutaan! Kuinka korjata tilanne? • Mammal *myMammal; • myMammal = new SheepDog(); • ... • //koodia missä käytetään SheepDog-luokkaa • ... • delete myMammal; Mammal int weight ~Mammal ( ) Land-Mammal int numLegs Dog boolean rabid SheepDog
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 • Muodostimet ja periytyminen • Purkajat ja periytyminen • Perityn luokan eri tyypit • Aliluokan ja kantaluokan suhde • Tyyppimuunnokset • Olioiden sijoitus ja kopiointi • Olioiden kopiointi • Olioiden sijoitus • Puhdasoppinen luokka • Serialisaatio • Operaattoreiden uudelleenmäärittely • Ystäväfunktiot • Ystäväluokat • Yhteenveto
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 rajapintaan.
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 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: • kokonaisluvuista 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 oikean tyyppinen olio, palautetaan kyseiseen olioon osoittava aliluokkaosoitin.