Operaattoreiden uudelleenmäärittely



Samankaltaiset tiedostot
Luokan operaatiot. Osoittimet ja viittaukset luokan olioihin

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

12 Mallit (Templates)

Virtuaalifunktiot ja polymorfismi

Osoitin ja viittaus C++:ssa

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Periytyminen. Luokat ja olio-ohjelmointi

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

Mallit standardi mallikirjasto parametroitu tyyppi

Ohjelman virheet ja poikkeusten käsittely

13 Operaattoreiden ylimäärittelyjä

Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat:

Luokat. Luokat ja olio-ohjelmointi

Olio-ohjelmointi Syntaksikokoelma

ITKP102 Ohjelmointi 1 (6 op)

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

C++11 lambdat: [](){} Matti Rintala

Tietueet. Tietueiden määrittely

C-kielessä taulukko on joukko peräkkäisiä muistipaikkoja, jotka kaikki pystyvät tallettamaan samaa tyyppiä olevaa tietoa.

Ohjelmassa muuttujalla on nimi ja arvo. Kääntäjä ja linkkeri varaavat muistilohkon, jonne muuttujan arvo talletetaan.

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

Osoittimet. Mikä on osoitin?

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

Sisällys. 7. Oliot ja viitteet. Olion luominen. Olio Java-kielessä

Olio-ohjelmointi 2. välikoe HYV5SN

14. oppitunti. Operaattorin ylikuormitus. Osa. Operaattorin ylikuormittaminen

Kääntäjän virheilmoituksia

Merkkijono määritellään kuten muutkin taulukot, mutta tilaa on varattava yksi ylimääräinen paikka lopetusmerkille:

Omat tietotyypit. Mikä on olio?

Ehto- ja toistolauseet

Sisällys. 3. Muuttujat ja operaatiot. Muuttujat ja operaatiot. Muuttujat. Operaatiot. Imperatiivinen laskenta. Muuttujat. Esimerkkejä: Operaattorit.

Java-kielen perusteet

3. Muuttujat ja operaatiot 3.1

7. Oliot ja viitteet 7.1

Ohjelmointi 1 Taulukot ja merkkijonot

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Taulukot. Jukka Harju, Jukka Juslin

815338A Ohjelmointikielten periaatteet Harjoitus 4 vastaukset

T Olio-ohjelmointi Osa 3: Luokka, muodostin ja hajotin, this-osoitin Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

ITKP102 Ohjelmointi 1 (6 op)

Tehtävä 1. TL5302 Olio-ohjelmointi Koe Malliratkaisuja. Tässä sekä a)- että b)-kohdan toimiva ratkaisu:

Ohjelmoinnin perusteet Y Python

Java-kielen perusteet

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

Ohjelmoinnin perusteet Y Python

ITKP102 Ohjelmointi 1 (6 op)

ITKP102 Ohjelmointi 1 (6 op)

Ohjelmoinnin peruskurssi Y1

Sisältö. 2. Taulukot. Yleistä. Yleistä

Sisältö. 22. Taulukot. Yleistä. Yleistä

Ohjelmoinnin perusteet Y Python

Javan perusteita. Janne Käki

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Olio-ohjelmointi Javalla

Sisällys. 1. Omat operaatiot. Yleistä operaatioista. Yleistä operaatioista

INSIDE C++ Ohjelmoijan käsikirja. Ivor Horton WROX PRESS

1. Omat operaatiot 1.1

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

Tietotyypit ja operaattorit

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

ITKP102 Ohjelmointi 1 (6 op), arvosteluraportti

Muuttujien roolit Kiintoarvo cin >> r;

Standardi mallikirjasto

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Ohjelmointi 2. Jussi Pohjolainen. TAMK» Tieto- ja viestintäteknologia , Jussi Pohjolainen TAMPEREEN AMMATTIKORKEAKOULU

Ohjelmoinnin jatkokurssi, kurssikoe

Koottu lause; { ja } -merkkien väliin kirjoitetut lauseet muodostavat lohkon, jonka sisällä lauseet suoritetaan peräkkäin.

Harjoitustyö: virtuaalikone

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

Osa III. Olioiden luominen vapaalle muistialueelle

Tietorakenteet ja algoritmit

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Tietorakenteet ja algoritmit

IDL - proseduurit. ATK tähtitieteessä. IDL - proseduurit

Algoritmit 2. Luento 2 To Timo Männikkö

ATK tähtitieteessä. Osa 3 - IDL proseduurit ja rakenteet. 18. syyskuuta 2014

13. Loogiset operaatiot 13.1

Sisällys. 17. Ohjelmoinnin tekniikkaa. Aritmetiikkaa toisin merkiten. for-lause lyhemmin

Sisällys. 16. Ohjelmoinnin tekniikkaa. Aritmetiikkaa toisin merkiten. Aritmetiikkaa toisin merkiten

Valinnat ja päätökset

Ohjelmoinnin perusteet Y Python

16. Ohjelmoinnin tekniikkaa 16.1

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Ohjelmoinnin perusteet Y Python

Metodien tekeminen Javalla

Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

ITKP102 Ohjelmointi 1 (6 op), arvosteluraportti

Kerta 2. Kerta 2 Kerta 3 Kerta 4 Kerta Toteuta Pythonilla seuraava ohjelma:

Algoritmit 2. Luento 2 Ke Timo Männikkö

Tietorakenteet ja algoritmit

Ohjelmoinnin perusteet Y Python

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä. Tiedonkätkentä. Aksessorit. 4.2

Zeon PDF Driver Trial

7. Näytölle tulostaminen 7.1

Algoritmit 2. Luento 7 Ti Timo Männikkö

Taulukot. Taulukon määrittely ja käyttö. Taulukko metodin parametrina. Taulukon sisällön kopiointi toiseen taulukkoon. Taulukon lajittelu

16. Ohjelmoinnin tekniikkaa 16.1

Transkriptio:

Operaattoreiden uudelleenmäärittely 14 Operaattoreiden uudelleenmäärittely Tässä luvussa käsittelemme, kuinka voit lisätä toiminnallisuutta luokkiisi, jotta ne toimivat enemmän C++:n perustietotyyppien tapaan. Olemme jo nähneet, kuinka luokillamme voi olla jäsenfunktioita, jotka käsittelevät olion jäsenmuuttujia - voimme ajatella tätä lauseella jäsenfunktio käsittelee olioita, joihin sillä on pääsy - mutta voimme tehdä tämän lisäksi paljon muutakin. Operaattoreiden uudelleenmäärittelyn avulla voit toteuttaa C++:n perusoperaattorit hyvin määritellyllä tavalla omille luokillesi. Tässä luvussa käsittelemme seuraavia aiheita: Mitä C++:n operaattoreita voit toteuttaa omille tietotyypeillesi Miten luokillesi toteutetaan funktioita, jotka uudelleenmäärittelevät operaattorit Miten operaattorifunktiot toteutetaan luokan jäseninä ja tavallisina funktioina Milloin sijoitusoperaattori on pakko toteuttaa Miten tyypinmuunnokset toteutetaan operaattorifunktioina Mitä ovat fiksut osoittimet Operaattoreiden toteutus luokille Laatikko-luokkamme suunniteltiin siten, että se soveltuu sovellukselle, joka on pääasiassa kiinnostunut laatikon tilavuuksista. Tällaiselle sovellukselle on tärkeää pystyä vertaamaan laatikoiden tilavuuksia. Luvussa 12 toteutimme Laatikko-luokalle vertaatilavuuksia()-funktion, joka vertasi kahden olion tilavuuksia, mutta eikö olisikin hienoa, jos voisit kirjoittaa: if(ltk1 < Ltk2) // Tee jotain... seuraavanlaisen, varsin kömpelön funktiokutsun sijaan: if(ltk1.vertaatilavuuksia(ltk2) < 0) // Tee jotain... 533

C++ Ohjelmoijan käsikirja! Haluaisit varmastikin laskea yhteen kahden Laatikko-olion tilavuudet lausekkeella Ltk1 + Ltk2 tai jopa valita kolmas laatikko Laatikko-olioiden listasta lausekkeella kuorma1[2]. No, kaikki tämä ja paljon muutakin on mahdollista - sinun tulee vain kirjoittaa koodi, joka toteuttaa haluamasi toiminnon. Tämä voidaan tehdä C++:n operaattoreiden uudelleenmäärittelyn avulla. Operaattoreiden uudelleenmäärittely Operaattoreiden uudelleenmäärittelyn avulla voit käyttää standardioperaattoreita (kuten +, -, *, < ja niin edelleen) omien tyyppiesi mukaisten olioiden kanssa. Tämän voit tehdä kirjoittamalla funktion, joka määrittelee uudelleen tietyn operaattorin, jotta se suorittaa tietyn toiminnon aina, kun sitä käytetään luokkasi olioiden kanssa. Jos esimerkiksi haluat määrätä, miten <-operaattorin tulee toimia Laatikko-olioiden yhteydessä, voit kirjoittaa Laatikko-luokalle jäsenfunktion, joka määrittelee operaattorin toiminnan. Meidän tapauksessamme olemme kiinnostuneita Laatikko-olioiden tilavuuksista - eli voimme kirjoittaa funktion, joka palauttaa true, jos ensimmäisen Laatikko-olion tilavuus on pienempi kuin toisen olion. Tällaisen funktion nimi olisi tässä tapauksessa operator<(). Yleisesti ottaen, tietyn operaattorin uudelleenmäärittelevän funktion nimi koostuu avainsanasta operator, jonka perässä on uudelleenmääriteltävä operaattori. Operaattoreiden, joiden nimi muodostuu kirjaimista, kuten new ja delete, ja avainsanan välillä tulee olla vähintään yksi tyhjä merkki. Muiden operaattoreiden kohdalla tyhjä merkki on vapaaehtoinen. Operaattorit, jotka voidaan uudelleenmääritellä Operaattoreiden uudelleenmäärittelyn avulla et voi keksiä uusia operaattoreita. Et myöskään voi muuttaa operaattoreiden suoritusjärjestystä ja operandien lukumäärää, joten operaattorin uudelleenmääritellyllä versiolla on sama suoritusjärjestys kuin alkuperäisellä operaattorillakin. Kaikkien operaattoreiden suoritusjärjestyksen löydät liitteestä D. Vaikka et voikaan määritellä kaikkia operaattoreita uudelleen, rajoitukset eivät ole kovinkaan suuret. Seuraavassa on operaattorit, joita et voi määritellä uudelleen: Näkyvyysalueoperaattori :: Ehto-operaattori?: Suora jäsenen käsittelyoperaattori. 534 Osoitin jäseneen -operaattori.* sizeof-operaattori sizeof Emme ole vielä tavanneet osoitin jäseneen -operaattoria, mutta käsittelemme sitä luvussa 16. Lisäksi et voi määritellä uudelleen esikääntäjäkomennon symbolia # tai ##-symbolia. Kaikki muu on vapaasti määriteltävissä. Kuten tulemme huomaamaan, luokan tietyn operaattorin uudelleenmäärittelevän funktion ei välttämättä tarvitse olla luokan jäsen - se voi olla tavallinenkin funktio. Näemme esimerkkejä molemmista.

Operaattoreiden uudelleenmäärittely On sanomattakin selvää, että versiosi standardioperaattorista tulisi vastata operaattorin normaalia käyttötilannetta niin pitkälle kuin luokka antaa siihen mahdollisuuden. Ei olisi kovinkaan järkevää määritellä luokalle +-operaattoria uudelleen siten, että se suorittaa operandiolioidensa kertolaskun. Paras tapa ymmärtää operaattorin uudelleenmäärittelyn toiminta, on käydä läpi esimerkki, joten toteutetaan edellä jo käsittelemämme <-operaattori Laatikko-luokalle. Uudelleenmääritellyn operaattorin toteutus Määritelläksemme luokalle operaattorin, kirjoitamme yksinkertaisesti operaattorifunktion. Binäärisellä operaattorilla, joka toteutetaan luokan jäsenenä, on yksi parametri. Palaamme tähän hetken kuluttua. Voimme lisätä seuraavan funktion, joka määrittelee uudelleen <-operaattorin, prototyypin Laatikko-luokan määrittelyyn: class Laatikko public: bool operator<(const Laatikko& altk) const; //Uudelleenm. pienempi kuin ; // Laatikko-luokan loput määrittelyt Koska toteutamme vertailua, paluuarvon tyyppi on bool. Operaattorifunktiotamme operator<() kutsutaan, kun kahta Laatikko-oliota vertaillaan <-operaattorilla. Parametri on <-operaattorin oikeanpuoleinen operandi ja vasemmanpuoleinen vastaa this-osoitinta. Koska funktio ei muuta kumpaakaan operandia, olemme määritelleet parametrin ja funktion const-tyyppisiksi. Jotta näemme miten tämä toimii, oletetaan, että meillä on seuraava if-lause: if(ltk1 < ltk2) cout << ltk1 on pienempi kuin ltk2 << endl; Sulkeiden sisällä olevan testilausekkeen arvo on funktiomme paluuarvo. Se vastaa funktion kutsua ltk1.operator<(ltk2). Itse asiassa, jos haluat, voit kirjoittaa lausekkeen myös tällä tavalla: if(ltk1.operator<(ltk2)) cout << ltk1 on pienempi kuin ltk2 << endl; Kuten huomaat operator<-funktion käyttö tekee koodista luettavampaa. Kun nyt tiedämme, miten lausekkeen ltk1 < ltk2 operandit sidotaan funktion kutsuun, voimme toteuttaa uudelleenmääritellyn funktion varsin helposti. Jäsenfunktion, joka määrittelee uudelleen <-operaattorin, määrittely näkyy seuraavan sivun kaaviossa. 535

C++ Ohjelmoijan käsikirja if( ltk1 < ltk2 ) this-osoittimen osoittama olio Operaattorifunktion parametri bool Laatikko::operator<(const Laatikko& altk) const return this->tilavuus() < altk.tilavuus();! Käytämme viittausparametriä välttääksemme tarpeettoman parametrin kopioinnin funktion kutsun yhteydessä. return-lauseke käyttää jäsenfunktiota tilavuus() this-osoittimella osoitetun Laatikko-olion tilavuuden laskentaan ja vertaa tulosta (tavallisella <-operaattorilla) altk-olion tilavuuteen. Eli boolean-arvo true palautetaan, jos this-osoittimen osoittama Laatikko-olio on tilavuudeltaan pienempi kuin viittauksena välitetty altk-olio - ja false muulloin. Tässä this-osoitinta käytettiin näyttämään yhteys ensimmäiseen operandiin. thisosoitinta ei tarvitse käyttää eksplisiittisesti. Kokeile itse - Uuudelleenmääritellyn <-operaattorin käyttö Kokeillaan Laatikko-olion uudelleenmääriteltyä pienempi kuin -operaattoria. Laatikko-luokan määrittely on otsikkotiedostossa Laatikko.h ja sisältää uudelleenmääritellyn operaattorifunktion operator<() määrittelyn: // Laatikko.h - Laatikko-luokan määrittely #ifndef LAATIKKO_H #define LAATIKKO_H class Laatikko public: //Muodostinfunktio Laatikko(double apituus = 1.0, double asyvyys = 1.0, double akorkeus = 1.0); double tilavuus() const; // Laskee laatikon tilavuuden double luepituus() const; double luesyvyys() const; double luekorkeus() const; inline bool operator<(const Laatikko& altk) const //Uudelleenm. pienempi kuin //Määritellään avoimeksi return tilavuus() < altk.tilavuus(); 536

Operaattoreiden uudelleenmäärittely private: double pituus; double syvyys; double korkeus; ; #endif Huomaa, että olemme määritelleet uudelleenmääritellyn operaattorin avoimeksi funktioksi, koska se on tehokkaampi sellaisena. Otsikkotiedostossa on lisäksi Laatikko-luokan muiden jäsenten prototyypit. Määrittelyt ovat Laatikko.cpp-tiedostossa: // Laatikko.cpp Laatikko-luokan toteutus #include "Laatikko.h" // Muodostinfunktio Laatikko::Laatikko(double apituus, double asyvyys, double akorkeus): pituus(apituus), syvyys(asyvyys), korkeus(akorkeus) // Funktio, joka laskee laatikon tilavuuden double Laatikko::tilavuus() const return pituus * syvyys * korkeus; // luexxx() -funktiot double Laatikko::luePituus() const return pituus; double Laatikko::lueSyvyys() const return syvyys; double Laatikko::lueKorkeus() const return korkeus; Vertailuoperaattorin uudelleenmäärittelyn jälkeen emme enää tarvitse vertaatilavuuksia()- jäsentä, joten olemme poistaneet sen. Testaamme <-operaattoria etsimällä Laatikko-olioiden taulukon suurimman alkion. Tässä on ohjelman koodi: // Esimerkki 14.1 - Uudelleenmääritellyn pienempi kuin -operaattorin käyttö #include <iostream> #include <cstdlib> // Satunnaislukugeneraattoria varten #include <ctime> // time-funktiota varten using namespace std; #include "Laatikko.h" // Funktio, joka generoi satunnaisluvun väliltä 1 - lkm inline int satunnaisluku(int lkm) return 1 + static_cast<int>((lkm*static_cast<long>(rand()))/(rand_max+1)); int main() const int dimraja = 100; srand((unsigned)time(0)); const int ltklkm = 20; Laatikko laatikot[ltklkm]; // Laatikon mittojen yläraja // Alustetaan satunnaislukugeneraattori // Taulukon alkioiden lukumäärä // Laatikko-olioiden taulukko 537

C++ Ohjelmoijan käsikirja for(int i = 0 ; i < ltklkm ; i++) laatikot[i] = Laatikko(satunnaisluku(dimRaja), satunnaisluku(dimraja), satunnaisluku(dimraja)); // Etsitään taulukon suurin laatikko laatikko* psuurin = &laatikot[0]; for(int i = 1 ; i < ltklkm ; i++) if(*psuurin < laatikot[i]) psuurin = &laatikot[i]; cout << endl << "Suurimman laatikon mitat ovat: " << psuurin->luepituus() << " * " << psuurin->luesyvyys() << " * " << psuurin->luekorkeus() << endl; return 0; Kun suoritan ohjelman, se tulostaa: Suurimman laatikon mitat ovat: 78 * 78 * 98 Koska tässä käytetään satunnaislukuja, saat mitä todennäköisimmin erilaisen tuloksen. Kuinka se toimii main()-funktio luo ensin Laatikko-olioiden taulukon. Oletamme aluksi, että taulukon ensimmäinen alkio on suurin ja talletamme sen osoitteen lauseella: laatikko* psuurin = &laatikot[0]; Tämän jälkeen vertaamme osoitteessa psuurin olevaa Laatikko-oliota jokaiseen taulukon seuraavaan alkioon: for(int i = 1 ; i < ltklkm ; i++) if(*psuurin < laatikot[i]) psuurin = &laatikot[i]; if-lauseen vertailussa kutsutaan operator<() funktiotamme. Funktion parametri on laatikot[i] ja this-osoitin osoittaa osoittimen psuurin osoittamaan Laatikko-olioon. Jos laatikot[i]:n tilavuus on suurempi kuin psuurin osoittaman laatikon tilavuus, vertailun tulos on true ja talletamme laatikot[i]:n osoitteen uutena suurimpana Laatikko-oliona. Käytämme tässä osoitinta, koska se on huomattavasti tehokkaampi kuin toinen vaihtoehto: Laatikko suurin = laatikot[0]; for(int i = 1 ; i < ltklkm ; i++) if(suurin < laatikot[i]) suurin = laatikot[i]; // Kopioi taulukon olion 538 Tässä koodissa on ylimääräistä hidastetta verrattuna alkuperäiseen. Joka kerta, kun Laatikkoolio talletetaan silmukassa muuttujaan suurin, laatikot-taulukon olio tulee kopioida suurinolioon jäsen jäseneltä. Se, kuinka kauan tähän kopiointiin menee aikaa, riippuu olion monimutkaisuudesta. Osoitinta käytettäessä ainoastaan osoite kopioidaan.

Operaattoreiden uudelleenmäärittely Kun silmukan suoritus päättyy, osoittimessa psuurin on taulukon suurimman Laatikko-olion osoite, joten voimme tulostaa suurimman olion mitat osoittimen avulla: cout << endl << "Suurimman laatikon mitat on: " << psuurin->luepituus() << " * " << psuurin->luesyvyys() << " * " << psuurin->luekorkeus() << endl; Globaalit operaattorifunktiot Funktio tilavuus() on Laatikko-luokan julkinen jäsen, joten voimme toteuttaa <-operaattorin tavallisena funktiona - globaalina operaattorifunktiona - luokan ulkopuolella. Tällöin funktion määrittely näyttää seuraavalta: inline bool operator<(const Laatikko& Ltk1, const Laatikko& Ltk2) return Ltk1.tilavuus() < Ltk2.tilavuus(); Olemme esitelleet operator<()-funktion avoimeksi funktioksi, koska haluamme, että se käännetään sellaiseksi, jos se vain on mahdollista. Kun operaattori on määritelty tällä tavalla, edellisen esimerkin main()-funktion koodi toimii täsmälleen samaan tapaan. Tätä versiota ei tietenkään tule esitellä const-tyyppiseksi - const-määrettä voidaan soveltaa vain luokan jäsenfunktioihin. Kun määrittelet jäsenfunktion const-tyyppiseksi, kerrot kääntäjälle, että se ei muuta oliota, josta funktiota kutsuttiin - eli const koskee tässä tapauksessa oliota, johon funktio kuuluu. Vaikka operaattorifunktion tulisi päästä käsiksi luokan yksityisiin jäseniin, se voidaan toteuttaa tavallisena funktiona, jos se esitellään luokan ystäväfunktiona. Yleisesti ottaen, jos funktion täytyy päästä käsiksi luokan yksityisiin jäseniin, suositeltavampi tapa on määritellä se luokan jäseneksi. Operaattorin täydellinen toteuttaminen! Operaattorin, kuten <, toteuttaminen luokalle saa aikaan tiettyjä odotuksia. Voit kirjoittaa lausekkeita kuten Ltk1 < Ltk2, mutta entäpä lausekkeet Ltk1 < 25.0 tai 10 < Ltk2? Operaattorifunktiomme operator<() ei pysty käsittelemään kumpaakaan näistä. Kun suunnittelet toteuttavasi uudelleenmääritellyn operaattorin funktiolle, tulee sinun miettiä todennäköiset tilanteet, joissa operaattori tullaan käyttämään. Kun käytämme alkuperäistä <-operaattoria esimerkiksi vertailtaessa float-tyyppistä ja int-tyyppistä arvoa keskenään, kääntäjä muuntaa toisen operandin tyypin samaksi kuin toinen operandi ennen vertailun toteuttamista - ja alkuperäinen <- operaattori on määritelty uudelleen vertaamaan kahta saman tyyppistä tietoa. Yllä kuvattu tilanne on hieman erilainen - meidän tulee määritellä <-operaattori uudelleen siten, että yllä mainittu vertailu on järkevä. 539

C++ Ohjelmoijan käsikirja Voimme varsin helposti tukea edellä mainittuja lausekkeita. Lisätään ensiksi funktio, joka vertailee Laatikko-olion tilavuutta (Laatikko-olio on ensimmäinen operandi) toiseen operandiin, joka on double-tyyppinen. Määrittelemme sen avoimeksi funktioksi luokan ulkopuolelle - ihan vain, jotta näemme miten se voidaan tehdä. Meidän tulee lisätä seuraava prototyyppi Laatikkoolion public-osaan: bool operator<(double aarvo) const; //Vertaa Laatikon tilavuus < double-arvo Laatikko-olio välitetään funktiolle implisiittisellä this-osoittimella ja double-arvo välitetään parametrinä. Tämän funktion toteutus on yhtä helppo kuin ensimmäisen operaattorifunktionkin - sen rungossa on vain yksi lause: // Funktio, joka vertaa Laatikko-oliota vakioon inline bool Laatikko::operator<(double aarvo) const return tilavuus() < aarvo;! Tämän määrittelyn tulee olla yhteneväinen luokan esittelyyn otsikkotiedostossa Laatikko.h. Avointa funktiota ei tule määritellä erillisessä.cpp-tiedostossa, koska sen määrittelyn tulee olla jokaisessa sitä käyttävässä lähdetekstitiedostossa. Sijoittamalla se luokan määrittelyyn varmistetaan, että näin aina on. Lausekkeen 10 < ltk2 käsittely ei ole sen vaikeampaa; se on vain hieman erilainen. Operaattorifunktio, joka on luokan jäsen, käyttää aina this-osoitinta vasempana operandina. Koska vasen operandi on tässä tapauksessa double-tyyppinen, emme voi toteuttaa operaattoria jäsenfunktiona. Jäljelle jää kaksi vaihtoehtoa: funktion toteuttaminen globaalina operaattorifunktiona tai ystäväfunktiona. Koska meidän ei tarvitse päästä käsiksi luokan yksityisiin jäseniin, voimme toteuttaa sen tavallisena funktiona: // Funktio, joka vertaa vakiota Laatikko-olioon inline bool operator<(const double aarvo, const Laatikko& altk) return aarvo < altk.tilavuus(); Nyt meillä on kolme uudelleenmääriteltyä versiota <-operaattorista Laatikko-oliota varten. Eli voimme nyt käsitellä lausekkeen ltk1 < ltk2 lisäksi esimerkiksi lausekkeita ltk1 < 2.5 * ltk2.tilavuus() tai jopa 0.5 * (ltk1.tilavuus() + ltk2.tilavuus()) < ltk3. Kumpikin <- operaattorin operandi voi olla lauseke, jonka arvo on tyyppiä double tai Laatikko-olio. 540

Kokeile itse - Operaattorin < täydellinen uudelleenmäärittely Operaattoreiden uudelleenmäärittely Voimme yhdistää kaiken edellä olleen esimerkiksi. Operaattorifunktion lisäksi lisäämme Laatikko-luokkaan funktion, joka tulostaa olion mitat. Tämä tekee main()-funktion koodista kompaktimman. Poistamme ensin esimerkin 14.1 Laatikko.h-tiedostosta vanhan operator<()- funktion ja lisäämme seuraavat prototyypit luokan määrittelyn julkiseen osaan: bool operator<(const Laatikko& altk) const; bool operator<(const double aarvo) const; // Vertailee Ltk < Ltk // vertailee Ltk < double-arvo Sitten lisäämme seuraavat avointen funktioiden määrittelyt Laatikko.cpp-tiedostoon: // Funktio, joka vertaa Laatikko-olio < Laatikko-olio inline bool Laatikko::operator<(const Laatikko& altk) const return tilavuus() < altk.tilavuus(); // Funktio, joka vertaa Laatikko-olio < double-arvo inline bool Laatikko::operator<(const double aarvo) const return tilavuus() < aarvo; Lopuksi lisäämme globaalin operaattorifunktion määrittelyn Laatikko.h-tiedoston loppuun: // Funktio, joka vertaa double-arvo < Laatikko-olio inline bool operator<(const double aarvo, const Laatikko& altk) return aarvo < altk.tilavuus(); Nämä kolme operaattorifunktion määrittelyä tulee sijoittaa Laatikko-luokan määrittelyn ja #endif-komennon väliin. Olemme sijoittaneet kaikkien operaattorifunktioiden määrittelyt otsikkotiedostoon Laatikko.h, koska ne ovat kaikki avoimia funktioita. Ainoastaan sellaisten jäsenfunktioiden, jotka eivät ole avoimia, määrittelyt tulee kirjoittaa.cpp-tiedostoon. Tästä syystä tämän esimerkin Laatikko.cpp-tiedosto on sama kuin esimerkissä 14.1. Voimme käyttää uusia operaattoreita, kun etsimme taulukon kaikki Laatikko-oliot, joiden tilavuus on tiettyjen rajojen sisällä. Tässä on koodi: // Esimerkki 14.2 - Uudelleenmääriteltyjen pienempi kuin -operaattoreiden testaus #include <iostream> #include <cstdlib> // Satunnaislukugeneraattoria varten #include <ctime> // time-funktiota varten using namespace std; #include "Laatikko.h" // Funktio, joka generoi satunnaisluvun väliltä 1 - lkm inline int satunnaisluku(int lkm) return 1 + static_cast<int>((lkm*static_cast<long>(rand()))/(rand_max+1)); 541

C++ Ohjelmoijan käsikirja // Tulostetaan laatikon mitat void nayta(const Laatikko& altk) cout << endl << altk.luepituus() << " * " << altk.luesyvyys() << " * " << altk.luekorkeus(); int main() const int dimraja = 100; srand((unsigned)time(0)); const int ltklkm = 20; Laatikko laatikot[ltklkm]; // Laatikon mittojen yläraja // Alustetaan satunnaislukugeneraattori // Taulukon alkioiden lukumäärä // Laatikko-olioiden taulukko for(int i = 0 ; i < ltklkm ; i++) laatikot[i] = Laatikko(satunnaisluku(dimRaja), satunnaisluku(dimraja), satunnaisluku(dimraja)); // Etsitään taulukon suurin laatikko Laatikko* psuurin = &laatikot[0]; for(int i = 1 ; i < ltklkm ; i++) if(*psuurin < laatikot[i]) psuurin = &laatikot[i]; cout << endl << "Taulukon suurimman laatikon mitat on:"; nayta(*psuurin); int tilmin = 100000.0; // Alaraja laatikon tilavuudelle int tilmax = 500000.0; // Yläraja laatikon tilavuudelle // Tulostetaan rajojen välissä olevien Laatikko-olioiden mitat cout << endl << endl << "Laatikot, joiden tilavuus on välillä " << tilmin << " ja " << tilmax << " ovat:"; for(int i = 0 ; i < ltklkm ; i++) if(tilmin < laatikot[i] && laatikot[i] < tilmax) nayta(laatikot[i]); cout << endl; return 0; Kun suoritan ohjelman, saan tulostukseksi: Taulukon suurimman laatikon mitat on: 96 * 87 * 93 542 Laatikot, joiden tilavuus on välillä 100000 ja 500000 ovat: 96 * 30 * 92 23 * 100 * 64 39 * 32 * 84 22 * 72 * 96 52 * 57 * 88

Operaattoreiden uudelleenmäärittely Kuinka se toimii Funktio nayta() tulostaa sille välitetyn Laatikko-olion mitat. Se on apufunktio, jota käytämme main()-funktiossa. main()-funktion alkuosassa luomme Laatikko-olioiden taulukon ja etsimme taulukon suurimman tilavuuden omaavan Laatikko-olion, kuten ennenkin. Tässä käytämme alkuperäistä versiota operator<()-funktiosta, jonka määrittelimme Laatikko-luokan jäseneksi. Käytämme kahta uutta operaattorifunktiotamme koodissa, joka tulostaa niiden Laatikko-olioiden mitat, joiden tilavuus on välillä tilmin ja tilmax: for(int i = 0 ; i < ltklkm ; i++) if(tilmin < laatikot[i] && laatikot[i] < tilmax) nayta(laatikot[i]);! if-lauseessa kutsutaan kumpaakin uusista operaattorifunktioista. Alilauseke tilmin < laatikot[i] vastaa funktion operator<(tilmin, laatikot[i]) kutsua ja laatikot[i] < tilmax vastaa jäsenfunktion kutsua laatikot[i].operator < (tilmax). Tulostaaksemme Laatikko-olion mitat, jotka täyttävät ehdot, välitämme sen yksinkertaisesti nayta()-funktiolle. Kaikki vertailuoperaattorit voidaan toteuttaa vastaavalla tavalla kuin olemme tässä toteuttaneet pienempi kuin -operaattorin. Ne eroavat vain pieniltä yksityiskohdiltaan; yleisellä tasolla ne ovat kaikki samanlaisia. Erilaiset operaattorifunktiot Kaikkien niiden operaattoreiden, jotka voidaan määritellä uudelleen, operaattorifunktiot ovat saman mallisia kuin olemme edellä nähneet. Kun operaattori X määritellään uudelleen ja vasen operandi on olio, jolle X määritellään uudelleen, operaattorin uudelleenmäärittelevä jäsenfunktio on muotoa: Paluuarvon_tyyppi operator X(Tyyppi OikeaOperandi); Paluuarvon_tyyppi riippuu siitä, mitä operaattori tekee. Vertailuoperaattoreiden ja loogisten operaattoreiden kohdalla se on yleensä bool (voit myös käyttää tyyppiä int). Operaattoreiden, kuten + ja *, tulee palauttaa jonkinlainen olio, kuten tulemme huomaamaan. Kun toteutat binäärisen operaattorin ei-jäsenfunktiolla, sen muoto on: Paluuarvon_tyyppi operator X(Luokan_tyyppi VasenOperandi, Tyyppi OikeaOperandi); Tässä Luokan_tyyppi on luokka, jolle määrittelet uudelleen operaattorin X. Jos binäärisen operaattorin vasenta operandia, tyyppiä Tyyppi, ei ole määritelty luokan Tyyppi jäsenenä, se tulee määritellä globaalina operaattorifunktiona: Paluuarvon_tyyppi operator X(Tyyppi VasenOperandi, Tyyppi OikeaOperandi); 543

C++ Ohjelmoijan käsikirja Kun toteutat unaarisen operaattorin luokan jäsenenä, se ei tarvitse yleensä parametriä - lisäys- ja vähennysoperaattorit ovat poikkeus tähän, kuten tulemme huomaamaan. Esimerkiksi yleisen operaattorifunktion muoto on: Luokka_tyyppi& operator Op(); Globaaleina operaattorifunktioina toteutetuilla unaarisilla operaattoreilla on yksi parametri. Kuten muiden kuin jäsenfunktioidenkin kohdalla, unaarisen Op-operaattorin operaattorifunktio voidaan toteuttaa seuraavasti: Luokka_tyyppi& operator Op(Luokka_Tyyppi&); Huomaa, että kaikkien operaattorifunktioiden - sekä jäsenfunktioiden että globaalien funktioiden - parametrien lukumäärä on kiinteä. Parametrien määrä määräytyy kyseisen operaattorin operandien lukumäärän mukaan. Emme käy esimerkeillä läpi kaikkien operaattoreiden uudelleenmäärittelyä, koska monet niistä ovat samankaltaisia kuin olemme jo nähneet. Käymme kuitenkin läpi sellaiset operaattorit, joissa on erityisesti käsiteltäviä asioita. Sijoitusoperaattorin uudelleenmäärittely Sijoitusoperaattorilla tarkoitamme =-operaattoria, jotta erotamme sen operaattoreista +=, *= ja niin edelleen. Sijoitusoperaattori kopioi tietyn tyyppisen olion (sijoitusoperaattorin oikealla puolella olevan) toiseen samantyyppiseen olioon (vasemmalla puolella olevaan). Sijoitusoperaattoria kutsutaan, kun kirjoitat: Laatikko ltk1; Laatikko ltk2(10, 10, 10); ltk1 = ltk2; //Kutsutaan sijoitusoperaattoria Jos et itse toteuta luokallesi uudelleenmääriteltyä sijoitusoperaattoria, kääntäjä muodostaa oletusversion operator=(). On mielenkiintoista tutkia, mitä tapahtuu, jos määrittelet hyvin yksinkertaisen luokan - esimerkiksi luokan, jossa on vain yksi jäsenmuuttuja: class Data public: int arvo; ; Riippuen siitä, miten luokkaa käytät, todellisuudessa luokka näyttää seuraavalta: class Data public: int arvo; 544

Operaattoreiden uudelleenmäärittely ; Data() ~Data() Data(const Data& adata) : arvo(adata.arvo) Data& operator=(const Data& adata) arvo = adata.arvo; return *this; // Oletusmuodostinfunktio // Tuhoajafunktio // Kopiomuodostin // Sijoitusoperaattori Sijoitusoperaattorin oletusversio suorittaa yksinkertaisen jäsen jäseneltä -prosessin, samaan tapaan kuin oletuskopiomuodostinkin. Älä sekoita kopiomuodostinta ja sijoitusoperaattoria - ne ovat eri. Kopiomuodostinta kutsutaan, kun luodaan uusi olio, joka alustetaan olemassa olevan saman luokan oliolla tai kun olio välitetään funktiolle arvoparametrinä. Sijoitusoperaattoria kutsutaan, kun sijoitusoperaattorin vasemmalla ja oikealla puolella on samantyyppiset oliot. Laatikko-luokkamme kohdalla oletussijoitusoperaattori toimii oikein, mutta kaikkien sellaisten luokkien kohdalla, joiden jäsenille on varattu muistia dynaamisesti, asiaan tulee paneutua tarkemmin. On suuren virheen paikka ohjelmassasi, jos et tällöin määrittele sijoitusoperaattoria uudelleen. Ongelmat ovat samantyyppisiä kuin kopiomuodostimenkin kohdalla. Oletetaan, että käytämme oletussijoitusoperaattoria olioille, joilla on dynaamisesti varattua muistia. Tuloksena on, että kaksi oliota jakaa samat vapaan muistin oliot, joten jos muutamme toista oliota, toisen olion eheys saattaa rikkoontua. AutoKuorma-luokka on juuri tällainen. Jäsen jäseneltä -kopioinnin tuloksena molemmat oliot jakavat saman listan; tämän ongelmanhan ratkaisimme jo luvussa 13. Ratkaisu oli yksinkertainen: toteutetaan sijoitusoperaattori siten, että olion dynaamiset osat kopioidaan asiallisesti. Tämä ei kuitenkaan pelkästään riitä, kuten tulemme huomaamaan. Itse asiassa, kaikilla luokilla, joilla on ongelmia oletuskopiomuodostimen kohdalla, on ongelmia myös oletussijoitusoperaattorin kohdalla. Jos toteutat näistä toisen, sinun tulee toteuttaa myös toinen - sekä tuhoajafunktio. Sijoitusoperaattorin toteuttaminen Katsotaan, mitä tällaisen funktion tulisi tehdä. Sijoituksessa on kaksi operandia: oikea operandi on kopioitava olio ja vasen operandi on kopioinnin kohde, joka muuttuu oikeanpuoleisen operandin kopioksi. Teemme kopioinnin operaattorifunktiossa, joten mikä on sen paluuarvon tyyppi? Tuleeko meidän palauttaa mitään? Katsotaan, miten funktiota käytetään käytännössä. Sijoitusoperaattorin normaalin toiminnan mukaan voimme kirjoittaa: kuorma1 = kuorma2 = kuorma3; 545

C++ Ohjelmoijan käsikirja Nämä kolme muuttujaa ovat AutoKuorma-typpiset ja teemme muuttujista kuorma1 ja kuorma2 muuttujan kuorma3 kopion. Koska sijoitusoperaattori on oikealta laskettava, tämä vastaa lausetta: kuorma1 = (kuorma2 = kuorma3); Jäsenfunktiolla operator=() kirjoitettuna tämä vastaa lausetta: kuorma1.operator=(kuorma2.operator=(kuorma3)); Tästä näkyy selvästi, että mitä tahansa funktio operator=() palauttaakin, se voi päätyä toisen operator=()-funktion parametriksi. Koska funktion operator=() parametri on viittaus olioon, voimme päätellä, että funktion tulee palauttaa vasemman operandin olio. Lisäksi, jos haluamme välttää turhan kopioinnin, paluuarvon tyypin tulee olla viittaus tähän olioon. Oikeanpuoleisen operandin kopiointi on samanlainen toiminto kuin käytimme kopiomuodostimessakin. Koska nyt tiedämme, mikä paluuarvon tyypin tulee olla, voimme yrittää tehdä =-operaattorin AutoKuorma-luokalle. Tarkastele ensin seuraavaa koodia; käsittelemme sen virheitä hetken kuluttua. AutoKuorma& AutoKuorma::operator=(const AutoKuorma& kuorma) palku = phanta = pnykyinen = 0; if(kuorma.palku == 0) return; Paketti* papu = kuorma.palku; do lisaaltk(papu->pltk); while(papu = papu->pseur); // talletetaan uuden listan osoitteet return *this; // Palautetaan vasen operandi this-osoitin sisältää vasemman operandin osoitteen, joten palauttamalla *this, palautamme tämän olion. Muutoin koodi on sama kuin olion kopiomuodostimessa. Funktio näyttää olevan OK ja toimiikin useimmissa tilanteissa, mutta siinä on kaksi ongelmaa. Ensimmäinen ongelma on vasen operandi. Se on AutoKuorma-olio, joka todennäköisesti sisältää listan. Asettamalla jäsenmuuttujan palku arvoksi 0, jätämme tuuliajolle kaikki tämän AutoKuorma-olion omistamat Paketti-oliot. Meidän tulee ensin poistaa kaikki vasemman operandin omistamat Paketti-oliot: AutoKuorma& AutoKuorma::operator=(const AutoKuorma& kuorma) while(pnykyinen = palku) // Kopioidaan ja testataan onko null palku = palku->pseur; // Siirretään palku osoittamaan seuraavaan olioon delete pnykyinen; // Poistetaan nykyinen olio 546 palku = phanta = pnykyinen = 0; if(kuorma.palku == 0) return;

Operaattoreiden uudelleenmäärittely Paketti* papu = kuorma.palku; do lisaaltk(papu->pltk); while(papu = papu->pseur); return *this; // talletetaan uuden listan osoitteet // Palautetaan vasen operandi Jos vasen operandi sisältää listan, palku ei ole null. while-silmukka tallettaa osoittimen listan ensimmäiseen Paketti-olioon ja tarkistaa onko se null. Jos se ei ole null, silmukan runko suoritetaan, jolloin palku siirretään osoittamaan listan seuraavaan Paketti-olioon ja nykyinen listan alku poistetaan. Näin käymme koko listan läpi ja poistamme yksi kerrallaan kaikki listan oliot, kunnes saavutamme null-osoittimen. Tämä huolehtii vasemmasta operandista, mutta jäljellä on yhä yksi ongelma. Oletetaan, että joku kirjoittaa lauseen: kuorma1 = kuorma1; Tämä ei kylläkään näytä kovinkaan järkevältä sijoitukselta, mutta tällainen tilanne saattaa hyvinkin syntyä monimutkaisemman lausekkeen tuloksena, jolloin tällaisen lopullisen sijoituksen saaminen ei ole niin selvästi näkyvissä. Tällöin, ensimmäisen ongelman ratkaisu poistaa olion listan, jonka jälkeen yritämme kopioida listan, jota ei ole enää olemassa! Meidän täytyy tarkistaa, ovatko molemmat operandit samat. Tämän voimme tehdä seuraavilla muutoksilla: AutoKuorma& AutoKuorma::operator=(const AutoKuorma& kuorma) if(this == &kuorma) // Verrataan operandien osoitteita return *this; // Jos samat, palautetaan 1. operandi while(pnykyinen = palku) // Kopioidaan ja testataan onko null palku = palku->pseur; // Siirretään palku osoittamaan seuraavaan olioon delete pnykyinen; // Poistetaan nykyinen olio palku = phanta = pnykyinen = 0; if(kuorma.palku == 0) return; Paketti* papu = kuorma.palku; do lisaaltk(papu->pltk); while(papu = papu->pseur); return *this; // talletetaan uuden listan osoitteet // Palautetaan vasen operandi Jos molemmat operandit ovat samat, niiden osoitteet ovat samat, joten osoittimen this ja funktiolle operator=() välitetyn parametrin vertailu riittää. Aina, kun teet sijoitusoperaattorin, sinun tulee tarkistaa, ovatko molemmat operandit samat. 547

C++ Ohjelmoijan käsikirja Voimme nyt laajentaa dynaamisten luokkiemme kultaista sääntöä, jota rupesimme kehittämään luvussa 13: Jos luokan funktiot varaavat muistia vapaasta muistista, toteuta luokalle aina kopiomuodostin, sijoitusoperaattori ja tuhoajafunktio. Voit määritellä sijoitusoperaattorin muuhunkin tehtävään kuin olion kopioimiseen. Yleisesti ottaen, luokallasi voi olla useita uudelleenmääriteltyjä sijoitusoperaattoreita. Lisäversioilla on luokan tyypistä poikkeava parametrin tyyppi - joten ne ovat todellisuudessa muunnoksia. Joka tapauksessa paluuarvon tyypin tulee olla vasemman operandin tyyppi. Voit tietysti määritellä uudelleen myös Op=-sijoitusoperaattorit. Monissa tapauksissa saatat haluta erikseen määritellä, että et salli tiettyjä sijoitusoperaatioita luokan olioille. Jos näin on, voit estää ne esittelemällä sijoitusoperaattorin luokan yksityisenä jäsenenä. Kokeile itse - Sijoitusoperaattorin toteuttaminen Rakennetaan pieni käytännön esimerkki, josta näemme, miten luokan oliot toimivat, jos niille on määritelty sijoitusoperaattori tai ei ole. Määrittelemme luokan VirheIlmoitus, joka kuvaa virheilmoitusta ja jolla on kopiomuodostin, sijoitusoperaattori ja tuhoajafunktio: // VirheIlmoitus.h #ifndef VIRHEILMOITUS_H #define VIRHEILMOITUS_H #include <iostream> using namespace std; class VirheIlmoitus public: VirheIlmoitus(const char* pteksti = "Virhe"); // Muodostinfunktio ~VirheIlmoitus(); // Tuhoajafunktio void resetilmoitus(); // Muuta ilmoitusta VirheIlmoitus& operator=(const VirheIlmoitus& Ilmoitus); // Sijoitusoperaattori char* mita() const return pilmoitus; // Tulostetaan ilmoitus private: char* pilmoitus; ; #endif 548 Koska tämä luokka varaa muistia dynaamisesti, sille tulee määritellä kopiomuodostin, sijoitusoperaattori ja tuhoajafunktio. Poistamalla sijoitusoperaattorin luokasta, huomaamme mitä tapahtuu, jos unohdamme toteuttaa sen. Muodostinfunktion, tuhoajafunktion ja muiden jäsenfunktioiden määrittelyt sijoitetaan VirheIlmoitus.cpp-tiedostoon:

Operaattoreiden uudelleenmäärittely // VirheIlmoitus.cpp VirheIlmoitus-luokan toteutus #include <cstring> #include "VirheIlmoitus.h" using namespace std; // Muodostinfunktio VirheIlmoitus::VirheIlmoitus(const char* pteksti) pilmoitus = new char[ strlen(pteksti) + 1 ]; // varataan muisti strcpy(pilmoitus, pteksti); // Kopioidaan // Tuhoajafunktio VirheIlmoitus::~VirheIlmoitus() cout << endl << "Tuhoajafunktiota kutsuttu." << endl; delete[] pilmoitus; // Vapautetaan muisti // Muutetaan viesti void VirheIlmoitus::resetIlmoitus() // Korvataan ilmoitus asteriskeilla for(char* apu = pilmoitus ; *apu!= '\0' ; *(apu++) = '*') ; // Sijoitusoperaattori VirheIlmoitus& VirheIlmoitus::operator=(const VirheIlmoitus& ilmoitus) if(this == &ilmoitus) // Verrataan osoitteita, jos samat return *this; // palautetaan vasen operandi delete[] pilmoitus; // Vapautettaan vasemman muisti pilmoitus = new char[ strlen(ilmoitus.pilmoitus) + 1]; // Kopioidaan oikean operandin merkkijono vasempaan operandiin strcpy(this->pilmoitus, ilmoitus.pilmoitus); return *this; // Palautetaan vasen operandi Käytämme tätä virheilmoitusluokan versiota seuraavanlaisessa ohjelmassa: // Esimerkki 14.3 - sijoitusoperaattorin uudelleenmäärittely #include <iostream> #include <cstring> using namespace std; #include "VirheIlmoitus.h" int main() VirheIlmoitus varoitus("tässä on nyt paha ongelma"); VirheIlmoitus standardi; 549

C++ Ohjelmoijan käsikirja cout << endl << "varoitus sisältää - " << varoitus.mita(); cout << endl << "standardi sisältää - " << standardi.mita(); standardi = varoitus; // Käytetään sijoitusoperaattoria cout << endl << "Varoituksen sijoittamisen jälkeen standardi sisältää - " << standardi.mita(); cout << endl << "Resetoidaan varoitus, ei standardia" << endl; varoitus.resetilmoitus(); // Resetoidaan varoitus cout << endl << "varoitus sisältää nyt - " << varoitus.mita(); cout << endl << "standardi sisältää nyt - " << standardi.mita(); cout << endl; return 0; Kun ohjelma suoritetaan tällaisenaan, se tulostaa: varoitus sisältää - Tässä on nyt paha ongelma standardi sisältää - Virhe Varoituksen sijoittamisen jälkeen standardi sisältää - Tässä on nyt paha ongelma Resetoidaan varoitus, ei standardia varoitus sisältää nyt - ************************* standardi sisältää nyt - Tässä on nyt paha ongelma Tuhoajafunktiota kutsuttu. Tuhoajafunktiota kutsuttu. Kuten huomaat, kaikki toimii niin kuin pitäisikin. Pystymme resetoimaan ensimmäisessä VirheIlmoitus-oliossa olevan ilmoituksen ilman, että toinen, kopioitu olio muuttuu. Poista tai kommentoi nyt sijoitusoperaattorin esittely luokan määrittelystä ja määrittely tiedostosta VirheIlmoitus.cpp. Käännä ohjelma uudelleen, ja se tulostaa nyt: varoitus sisältää - Tässä on nyt paha ongelma standardi sisältää - Virhe Varoituksen sijoittamisen jälkeen standardi sisältää - Tässä on nyt paha ongelma Resetoidaan varoitus, ei standardia varoitus sisältää nyt - ************************* standardi sisältää nyt - ************************* Tuhoajafunktiota kutsuttu. Tuhoajafunktiota kutsuttu. 550 Saatat saada muitakin virheilmoituksia, koska ohjelma yrittää vapauttaa muistin vapaasta muistista kahteen kertaan saman olion kohdalla.

Operaattoreiden uudelleenmäärittely Kuinka se toimii Esimerkin kiinnostava osa on sen erikoinen toiminta ilman sijoitusoperaattoria. main()-funktiossa luomme olion varoitus lauseella: VirheIlmoitus varoitus("tässä on nyt paha ongelma"); Tämä kutsuu VirheIlmoitus-luoka muodostinfunktiota merkkijono parametrinään merkkijono. Seuraavaksi luomme VirheIlmoitus-olion käyttämällä ilmoituksen oletusarvoa: VirheIlmoitus standardi; Kun olemme tulostaneet kummankin olion ilmoituksen, kutsumme VirheIlmoitus-luokan sijoitusoperaattoria lauseella: standardi = varoitus; // Käytetään sijoitusoperaattoria Ohjelman toisella suorituskerralla tässä kutsutaan oletussijoitusoperaattoria, jonka kääntäjä luo automaattisesti, joten molemmat pilmoitus-oliot sisältävät saman osoitteen. Tästä seuraa se, että kun muutamme varoitus-oliota lauseella varoitus.resetilmoitus(); // Resetoidaan varoitus muutammekin molempien olioiden yhteistä merkkijonoa - tulostuksesta näemme selvästi, että sekä varoitus-olion että standardi-olion merkkijonot ovat samat. Ohjelman lopussa kummankin VirheIlmoitus-olion näkyvyysalue päättyy ja niiden tuhoajafunktioita kutsutaan, kuten tulostuksesta näkyy. Tuhoajafunktion ensimmäisellä kutsukerralla vapautetaan pilmoitus-osoittimen osoittama muisti vapaasta muistista. Virheellisessä versiossa, kun toinen tuhoajafunktion kutsu suoritetaan, yritetään vapauttaa sama merkkijono uudelleen, koska molempien VirheIlmoitus-olioiden pilmoitus-osoitin osoittaa samaan osoitteeseen - järjestelmäsi saattaa muodostaa oman virheilmoituksensa. Ohjelman ensimmäisellä suorituskerralla, jolloin käytössä on erikseen toteutettu VirheIlmoitusluokan sijoitusoperaattori, luodaan itsenäinen kopio oikeanpuoleisesta operandista ja standardiolio ei muutu, kun muutamme varoitus-oliota. Kummankin olion pilmoitus-osoitin osoittaa eri merkkijonoihin, joten tuhoajafunktiossa ei tapahdu mitään virheellistä. Aritmeettisten operaattoreiden uudelleenmäärittely Katsotaan seuraavaksi, kuinka voimme uudelleenmääritellä Laatikko-luokallemme lisäysoperaattorin. Tämä on kiinnostava tapaus, sillä lisäys on binäärinen operaatio, jossa luodaan ja palautetaan uusi olio. Uusi olio on operaattorin operandeina olevien Laatikko-olioiden summa (tai miten ikinä operaattorin määrittelemmekin). Mitä haluamme summan tarkoittavan? On eri vaihtoehtoja, mutta koska laatikon päätarkoitus on jonkin säilöminen ja sen tilavuus kiinnostaa meitä eniten, voimme olettaa, että kahden laatikon summa on laatikko, johon mahtuu molemmat summatut laatikot. 551

C++ Ohjelmoijan käsikirja Tältä pohjalta voimme määritellä, että kahden Laatikko-olion summa on Laatikko-olio, johon mahtuu molemmat alkuperäiset laatikot päällekkäin asetettuina. Tämä määritelmä sopii myös ajatukseen, että luokkaamme käytetään paketoinnissa, koska lisäämällä halutun määrän Laatikko-olioita yhteen, tuloksena on Laatikko-olio, johon ne kaikki mahtuvat. Voimme tehdä tämän seuraavalla yksinkertaisella tavalla. Uuden olion pituus-jäsen on suurempi lisättävien olioiden pituus-jäsenistä. Syvyys-jäsen muodostetaan samaan tapaan. korkeus-jäsen on kahden operandin korkeus-jäsenten summa. Tällöin tuloksena olevaan laatikkoon mahtuu molemmat Laatikko-oliot. Muutamme muodostinfunktiota vielä niin, että Laatikko-olion pituus-jäsen on aina suurempi tai yhtä suuri kuin syvyys-jäsen. Seuraava kaavio havainnollistaa tuloksena olevaa Laatikko-oliota: pituus = 20 pituus = 25 syvyys = 15 syvyys = 10 korkeus = 7 ltk1 pituus = 25 korkeus = 14 ltk2 syvyys = 15 Box1 korkeus = 21 Box2 ltk1+ltk2 Koska lisäyksen tulos on uusi Laatikko-olio, lisäyksen toteuttavan funktion tulee palauttaa Laatikko-olio. Jos +-operaattorin uudelleenmäärittelevä funktio toteutetaan jäsenfunktiona, funktion esittely luokan Laatikko määrittelyssä näyttää seuraavalta: Laatikko operator+(const Laatikko& altk) const; //Lisätään kaksi laatikkoa Voimme määritellä parametrin const-tyyppiseksi, koska funktio ei muuta sitä, sekä viittaukseksi, jolloin vältämme oikean operandin tarpeettoman kopioinnin funktiota kutsuttaessa. Voimme määritellä myös funktion const-tyyppiseksi, koska se ei muuta vasenta operandiaan. Jäsenfunktion määrittely näyttäisi seuraavalta: 552 // Funktio, joka laskee yhteen kaksi Laatikko-oliota inline Laatikko Laatikko::operator+(const Laatikko& altk) const // Uudella oliolla on suurempi pituuksista ja syvyyksistä ja korkeuden summa return Laatikko( pituus > altk.pituus? pituus : altk.pituus, syvyys > altk.syvyys? syvyys : altk.syvyys, korkeus + altk.korkeus );

Operaattoreiden uudelleenmäärittely Huomaa erityisesti, että emme luo Laatikko-oliota vapaasta muistista kutsujalle palautuksen yhteydessä. Se olisi erittäin huono tapa toteuttaa funktio, koska muistin vapauttamisen suorittamisesta ei olisi täyttä varmuutta. Osoittimen palauttaminen vaikuttaisi myös muiden operaattoreiden, kuten operator=(), toteutukseen. Nyt funktiossa luodaan paikallinen Laatikkoolio, jonka kopio palautetaan kutsuvalle ohjelmalle. Koska nämä ovat automaattisia muuttujia, muistin hallinta suoritetaan automaattisesti. Kokeile itse - Lisäysoperaattorin uudelleenmäärittely Kokeillaan uutta lisäysoperaattoriamme esimerkin avulla. Lisäämme sen esimerkissä 14.2 käyttämäämme Laatikko-luokkaan. Meidän tulee lisätä uuden operaattorifunktion esittely Laatikko-luokan määrittelyn public-osaan Laatikko.h-tiedostossa: class Laatikko public: //Muodostinfunktio Laatikko(double apituus = 1.0, double asyvyys = 1.0, double akorkeus = 1.0); double tilavuus() const; // Laskee laatikon tilavuuden double luepituus() const; double luesyvyys() const; double luekorkeus() const; bool operator<(const Laatikko& altk) const; // Vertailee Ltk < Ltk bool operator<(const double aarvo) const; // vertailee Ltk < double-arvo Laatikko operator+(const Laatikko& altk) const; //Lisätään kaksi laatikkoa private: double pituus; double syvyys; double korkeus; ; Juuri näkemämme avoimen funktion operator+() määrittely tulee lisätä Laatikko.h-tiedostoon luokan määrittelyn perään. Muutamme Laatikko.cpp-tiedostossa olevaa muodostinfunktiota seuraavasti: Laatikko::Laatikko(double apituus, double asyvyys, double akorkeus) double maxsivu = apituus > asyvyys? apituus : asyvyys; double minsivu = apituus < asyvyys? apituus : asyvyys; pituus = maxsivu > 0.0? maxsivu : 1.0; syvyys = minsivu > 0.0? minsivu : 1.0; korkeus = akorkeus > 0.0? akorkeus : 1.0; Muodostinfunktiota muutettiin, jotta voidaan varmistua, että pituus-jäsen sisältää aina Laatikkoolion pidemmän mitan arvoista pituus ja syvyys. Tämä pitää oliot yhdenmukaisessa suunnassa, vaikka lisäysoperaattorimme tuottaakin näin erittäin pitkiä ja kapeita laatikoita. 553

C++ Ohjelmoijan käsikirja Kokeillaan lisäysoperaattoria yhdessä <-operaattorin kanssa. Etsimme taulukon Laatikkoolioiden parin, jonka tilavuus on pienin, kun ne lisätään toisiinsa. Taulukon kohdalla, jossa on 20 Laatikko-oliota, tämän tarkoittaa 380 parin tarkastamista. Seuraavassa on koodi: // Esimerkki 14.4 - Laatikko-olioiden lisäys #include <iostream> #include <cstdlib> // Satunnaislukugeneraattoria varten #include <ctime> // time-funktiota varten using namespace std; #include "Laatikko.h" // Funktio, joka generoi satunnaisluvun väliltä 1 - lkm inline int satunnaisluku(int lkm) return 1 + static_cast<int>((lkm*static_cast<long>(rand()))/(rand_max+1)); // Tulostetaan laatikon mitat void nayta(const Laatikko& altk) cout << endl << altk.luepituus() << " * " << altk.luesyvyys() << " * " << altk.luekorkeus(); int main() const int dimraja = 100; srand((unsigned)time(0)); const int ltklkm = 20; Laatikko laatikot[ltklkm]; // Laatikon mittojen yläraja // Alustetaan satunnaislukugeneraattori // Taulukon alkioiden lukumäärä // Laatikko-olioiden taulukko for(int i = 0 ; i < ltklkm ; i++) laatikot[i] = Laatikko(satunnaisluku(dimRaja), satunnaisluku(dimraja), satunnaisluku(dimraja)); int ensimmainen = 0; // Parin ensimmäisen laatikon indeksi int toinen = 1; // Parin toisen laatikon indeksi double mintilavuus = (laatikot[ensimmainen] + laatikot[toinen]).tilavuus(); for(int i = 0 ; i < ltklkm - 1 ; i++) for(int j = i + 1 ; j < ltklkm ; j++) if(laatikot[i] + laatikot[j] < mintilavuus) ensimmainen = i; toinen = j; mintilavuus = (laatikot[i] + laatikot[j]).tilavuus(); 554 cout << "Oliot, joiden tilavuus lisäyksen jälkeen on pienin, ovat:"; cout << endl << "laatikot[" << ensimmainen << "] "; nayta(laatikot[ensimmainen]); cout << endl << "laatikot[" << toinen << "] "; nayta(laatikot[toinen]); cout << endl << "Lisäyksen jälkeen tilavuus on " << mintilavuus << endl;

return 0; Suorituksen jälkeen sain tulostuksen: Oliot, joiden tilavuus lisäyksen jälkeen on pienin, ovat: laatikot[2] 31 * 1 * 31 laatikot[13] 23 * 3 * 13 Lisäyksen jälkeen tilavuus on 4092 Operaattoreiden uudelleenmäärittely Ohjelman jokaisella suorituskerralla tulee tavallisesti eri tulos. Kuinka se toimii Talletamme taulukon alkioiden parin, joiden Laatikko-olioiden tilavuus on pienin, kun ne lisätään toisiinsa, indeksit muuttujiin ensimmainen ja toinen. Aluksi sijoitamme taulukon kaksi ensimmäistä laatikkoa pienimmäksi pariksi lauseella: double mintilavuus = (laatikot[ensimmainen] + laatikot[toinen]).tilavuus(); Koska operator+() palauttaa Laatikko-olion, voimme kutsua tilavuus()-funktiota Laatikkooliolle, joka on palautettu summaamalla laatikot[ensimmainen] ja laatikot[toinen]. Sulkeet kahden Laatikko-olion summan ympärillä ovat välttämättömät, koska jäsenen käsittely - operaattori on suoritusjärjestyksessä ennen +-operaattoria. Jos olisimme halunneet tallettaa yhdistetyn Laatikko-olion, olisimme voineet kirjoittaa: Laatikko yhdistetty = laatikot[ensimmainen] + laatikot[toinen]; double mintilavuus = yhdistetty.tilavuus(); Yhdistettynä tilavuudeltaan pienin pari löytyy sisäkkäisillä silmukoilla: for(int i = 0 ; i < ltklkm - 1 ; i++) for(int j = i + 1 ; j < ltklkm ; j++) if(laatikot[i] + laatikot[j] < mintilavuus) ensimmainen = i; toinen = j; mintilavuus = (laatikot[i] + laatikot[j]).tilavuus(); Ulompi silmukka, jota kontrolloi i, käy läpi taulukon alkiot ensimmäisestä toiseksi viimeiseen. Sisempi silmukka, jota kontrolloi j, yhdistää i:n valitseman Laatikko-olion kaikkiin i:tä seuraaviin taulukon olioihin. Jokaisen Laatikko-olioiden parin kohdalla summan tilavuutta verrataan if-lauseessa mintilavuus-muuttujaan uudelleenmääritellyllä operaattorilla <. Tämä lauseke vastaa lauseketta: (laatikot[i].operator+(laatikot[j])).operator<(mintilavuus) operator+() palauttaa Laatikko-olion, jonka operator<()-funktiota kutsutaan. Jos operator<() palauttaa arvon true, nykyisten indeksien ensimmainen ja toinen arvot talletetaan ja Laatikkoolioiden summan tilavuus talletetaan muuttujaan mintilavuus. 555

C++ Ohjelmoijan käsikirja Lopuksi käytämme nayta()-funktiota tulostaessamme löytämämme Laatikko-oliot lauseilla: cout << "Oliot, joiden tilavuus lisäyksen jälkeen on pienin, ovat:"; cout << endl << "laatikot[" << ensimmainen << "] "; nayta(laatikot[ensimmainen]); cout << endl << "laatikot[" << toinen << "] "; nayta(laatikot[toinen]); cout << endl << "Lisäyksen jälkeen tilavuus on " << mintilavuus << endl; Uudelleenmääriteltyä +-operaattoria voidaan tietysti käyttää myös monimutkaisemmissakin lausekkeissa, joissa summataan Laatikko-olioita. Voimme esimerkiksi kirjoittaa: Laatikko ltk4 = ltk1 + ltk2 + ltk3; Tämän tuloksena on Laatikko-olio ltk4, johon mahtuu kolme muuta Laatikko-oliota päällekkäin kasattuna. Olisimme aivan yhtä hyvin voineet toteuttaa lisäysoperaattorin tavallisena funktiona (eli muuna kuin jäsenfunktiona), koska Laatikko-olion mittoihin päästään käsiksi julkisten jäsenfunktioiden kautta. Tällaisen funktion prototyyppi olisi: Laatikko operator+(const Laatikko& altk, const Laatikko& bltk); Jos jäsenmuuttujiin ei päästä käsiksi tällä tavalla, voit silti toteuttaa funktion tavallisena funktiona, joka sitten määritellään Laatikko-luokan ystäväksi. Näistä mainituista vaihtoehdoista ystäväfunktioon perustuva ratkaisu on aina huonoin, joten käytä aina muuta ratkaisutapaa kuin ystäväfunktio, jos se vain on mahdollista. Operaattorifunktiot ovat luokan varsin perustavanlaatuinen ominaisuus, joten minä yleensä toteutan ne jäsenfunktioina - se kuvaa paremmin, että operaatio on tyypin osa. Operaattorin toteutus perustuen toiseen operaattoriin Jos toteutat luokalle lisäysoperaattorin, saat aikaan odotuksen, että myös +=-operaattori on toteutettu luokalle. Käytännössä + ja += -operaattoreilla ei ole mitään yhteistä, mutta jos tarkoituksesi on toteuttaa molemmat, kannattaa huomata, että +-operaattori voidaan toteuttaa taloudellisesti +=-operaattorin avulla. Määritellään ensin funktio, joka toteuttaa +=-operaattorin Laatikko-luokalle. Koska siihen liittyy sijoitus, sen tulee palauttaa viittaus. Käyttämällä samaa logiikkaa kuin käytimme Laatikkoolioiden lisäysoperaattorin yhteydessä, voimme kirjoittaa määrittelyn seuraavasti: // Uudelleenmääritelty += -operaattori inline Laatikko& Laatikko::operator+=(const Laatikko& oikea) pituus = pituus > oikea.pituus? pituus : oikea.pituus; syvyys = syvyys > oikea.syvyys? syvyys : oikea.syvyys; korkeus += oikea.korkeus; return *this; 556 Tämä on varsin suoraviivainen toteutus, joka muuttaa vasemman operandin, eli *this, arvoa lisäämällä siihen oikean operandin. Jälleen tuloksena olevaan olioon mahtuvat alkuperäiset oliot sijoitettuna päällekkäin.

Operaattoreiden uudelleenmäärittely Voimme nyt toteuttaa funktion operator+() käyttämällä funktiota operator+=(): // Funktio, joka laskee yhteen kaksi Laatikko-oliota inline Laatikko Laatikko::operator+(const Laatikko& altk) const return Laatikko(*this) += altk; Tässä lauseke Laatikko(*this) kutsuu kopiomuodostinta luodakseen kopion lisäyksen vasemmasta operandista. Tämän jälkeen kutsutaan operator+=()-funktiota, joka lisää siihen oikean operandin. Voit samaa ideaa käyttäen toteuttaa myös uudelleenmääritellyt versiot operaattoreista -=, *= ja niin edelleen. Samaan tapaan voit uudelleenmääritellä myös vertailuoperaattorit toisen operaattorin pohjalta. Koska olemme jo määritelleet operator<()-funktion, voit uudelleenmääritellä >=-operaattorin sen pohjalta: inline bool Laatikko::operator>=(const Laatikko& altk) const return!(*this<(altk)); Pienempi kuin vastakohta on suurempi tai yhtä suuri kuin, joten voit toteuttaa funktion tällä tavalla. Indeksointioperaattorin uudelleenmäärittely Indeksointioperaattori [] tarjoaa erittäin mielenkiintoisia mahdollisuuksia tietyn tyyppisille luokille. Tämä operaattori on selvästikin tarkoitettu valitsemaan yksi olio oliojoukosta, joka voidaan ajatella taulukoksi - mutta oliot voivat itse asiassa olla missä tahansa säiliötyypissä. Voit uudelleenmääritellä indeksointioperaattorin harvalle taulukolle (jossa suuri osa alkioista on tyhjiä) tai esimerkiksi linkitetylle listalle. Tieto voi olla talletettuna jopa tiedostoon ja voit käyttää indeksointioperaattoria piilottamaan syöttö- ja tulostusoperaatioiden monimutkaisuudet. Koska itse päätät, mitä tapahtuu operaattorifunktion sisällä, voit jopa parantaa standardin indeksointioperaattorin toimintaa - esimerkiksi tarkistamalla, että annettu indeksi on sallittu. Luvun 13 AutoKuorma-luokkamme on esimerkki luokasta, jolle voitaisiin toteuttaa indeksointioperaattori. Jokainen AutoKuorma-olio sisältää järjestetyn joukon olioita. Sen sijaan, että käyttäjän pitäisi itse kirjoittaa koodi, joka kävisi läpi listan Laatikko-oliot, voit mahdollistaa listan läpikäynnin indeksin arvon avulla. Indeksin arvo 0 palauttaa listan ensimmäisen olion, arvo 1 palauttaa toisen olion ja niin edelleen. Indeksointioperaattorin sisäinen rakenne huolehtii listan selaamisesta, jotta haluttu olio löytyy. Mietitään hieman, miten operator[]()-funktion tulisi toimia tässä tapauksessa. Sen tulee hyväksyä indeksin arvo, joka tulkitaan listan sijainniksi ja palauttaa tämän sijaintikohdan Laatikko-olio. Meidän tulee palauttaa olio (ei osoitin), jos haluamme toteuttaa esimerkiksi lausekkeen kuorma[3] normaalin toiminnan, joka viittaa kuorma-olion kuvaaman listan neljänteen olioon. Funktion esittely AutoKuorma-luokassa voisi täten olla: 557