* automaattinen tyypinmuunnos
* hajottajan hyödyntäminen
<< luokan ulkopuolella: ostream &operator<<(ostream &os,const cLuokka &olio); >> luokan ulkopuolella: istream &operator>>(istream &is,cLuokka &olio); += luokassa: cLuokka &operator+=(const cLuokka &olio2); + luokan ulkopuolella: cLuokka3 operator+(const cLuo1 &o1, const cLuo2 &o2); + luokassa: cLuokka3 operator+(const cLuo2 &o2); = luokassa: cLuokka &operator=(const cLuo2 &o2); muunnos intiksi luokassa: operator int();Ohjelmassa tuoterek.cpp oli esimerkki siitä, miten C++:ssa voidaan lisämääritellä (eli kuormittaa, overload) myös operaattoreita.
tuotteet[i] += tuote; yhteensa += tuote;Sama voitaisiin tehdä kutsulla
tuotteet[i].ynnaa(tuote); yhteensa.ynnaa(tuote);Osittain on makuasia kummalla tavalla lisääminen tehdään. += - operaattorin käyttö on tullut mahdolliseksi, koska luokassa cTuote on esitelty miten operaattorin on käyttäydyttävä jos luokan olioon halutaan lisätä toinen saman luokan olio:
cTuote &operator+=(const cTuote &tuote) { ynnaa(tuote); return *this; }Operaattori on laitettu palauttamaan saman luokan olio, koska joskus voidaan haluta C:mäistä ketjusijoitusta:
tuote3 = tuote2 += tuote1;Toisaalta ketjusijoitus ei ole aina selvin mahdollinen tapa tehdä asioita, samahan voitaisiin tehdä
tuote2 += tuote1; tuote3 = tuote2;joten myös operaattori += voitaisiin suhteellisen hyvällä omalla tunnolla määritellä myös (luokassa cTuote)
void operator+=(const cTuote &tuote) { ynnaa(tuote); }Oikeasti operaattoria kutsuttaisiin:
tuote2.operator+=(tuote1); // vrt: tuote2.ynnaa(tuote1);mutta operaattori käsitellään erikoistapauksena ja em. kutsu voidaan kirjoittaa lyhyemmässä muodossa
tuote2 += tuote1;Huomattakoon että samasta operaattorista voi olla useita eri versiota riippuen siitä mitä lisätään. Eli voisi olla esimeriksi:
tuote2 += 2000.0; // lisätään reaaliluku hintaan; tuote2 += 5; // lisätään kokonaisluku kappalemäärään
Kuormitettaessa esimeriksi << - operaattoria, pitäisi oikeastaan muuttaa sitä tietovirta- luokkaa, johon ollaan tulostamassa.
cout << "Kissa\n"; // tarkoittaa periaateessa: cout.operator<<("Kissa\n");Onneksi operaattoreista on myös 2- operandiset versiot. Eli
operator<<(cout,"Kissa\n");
cout << tuotteet[i] << "\n";todella tulostaa tietorakenteen i:n tuotteen.
ostream &operator<<(ostream &os,const cTuote &tuote) { return tuote.tulosta(os); }Koska tuotteen tulostaminen on sinänsä jo valmis, pitää meidän vain kutsua tuotteen tulosta- metodia operaattorin määrittelyssä. Operaattorin PITÄÄ palauttaa vastaava tietovirta, jotta ketjutulostus:
cout << tuotteet[i] << "\n";olisi mahdollinen. Tässähän oli itse asiassa kyseessä kutsu
operator<<(operator<<(cout,tuotteet[i]),"\n");Itse tuote on esitelty vakioviitteeksi operaattorin << parametrilistassa. Viitteeksi, koska on turha siirrellä kokonaisia olioita ja vakioviitteeksi koska tulostuksen aikana tuotetta ei tietenkään muuteta!
Aivan vastaavasti esitellään >> - operaattori:
istream &operator>>(istream &is,cTuote &tuote) { tuote.lue(is); return is; }Ohjelman tuoterek.cpp lukusilmukka voisi tämän ansiosta olla jopa muodossa
cTuote tuote; while ( f >> tuote ) ynnaa(tuote);Toisaalta aikaisemmin todettiin että tässä on vaara sotkea rivien järjestys joten parempana voidaan pitää alunperin esitettyä
lue_rivi käsittele_rivitapaa.
Olkoon meillä vaikkapa luokka, joka kuvaa hyvin usein vastaan tulevaa tilannetta, jossa käytössä onkin hyvin rajoitettu kokonaislukualue (esim. kellon minuutit [0,59[, tunnit [0,24[ jne.):
#include <iostream.h> class cRajoitettu { int arvo; int raja; public: int aseta(int aarvo) { arvo = aarvo; int yli = arvo/raja; arvo %= raja; return yli; } cRajoitettu(int aarvo, int araja) { raja = araja; if ( raja < 1 ) raja = 1; aseta(aarvo); } int Arvo() const { return arvo; } int Raja() const { return raja; } }; ostream &operator<<(ostream &os, const cRajoitettu &r) {return os << r.Arvo(); } cRajoitettu operator+(const cRajoitettu &r1, int i) { return cRajoitettu(r1.Arvo()+i,r1.Raja()); } int operator+(int i,const cRajoitettu &r2) { return i+r2.Arvo(); } int main(void) { cRajoitettu m1(55,60),m2(20,60); int i; cout << m1 << " " << m2 << "\n"; m2 = m1 + 20; cout << m1 << " " << m2 << "\n"; i = 20 + m2; cout << i << "\n"; return 0; }Tässä ensimmäinen + - operaattori voitaisiin tehdä myös luokan metodiksi:
#include <iostream.h> class cRajoitettu { ... cRajoitettu operator+(int i) { return cRajoitettu(arvo+i,raja); } };Tämä näyttää yksi- parametriselta operaattorilta, mutta on tosiasiassa binäärinen, nimittäin vasempana operandina on *this.
m2 = m1.operator+(20);
class cRajoitettu { ... operator int() { return arvo; } };Tällöin yhteenlaskussa
i = 20 + m2;m2 ensin muuttuu kokonaisluvuksi ja sitten normaali kokonaislukujen yhteenlasku huolehtii lopusta. Muoto
m1 = 20 + m2;saataisiin toimimaan määrittelemällä sijoitusoperaattori:
class cRajoitettu { ... cRajoitettu &operator=(int i) { aseta(i); return *this; } };Tällöin sijoituslauseen oikea puoli lasketaan kokonaislukuna ja sitten tämä kokonaisluku sijoitetaan em. sijoitusoperaattorilla. Sijoitusoperaattorin tulee palauttaa sijoituksen kohteena olevan luokan tyyppinen tulos, jotta ketjusijoitus
m1 = m2 = 20;olisi mahdollinen.
cAika a1(10,30),a2(15,55); a1 += 50; a2 = a1 + 20;
ostream &tulosta(ostream &os) const { os.precision(2); os.setf(ios::left | ios::showpoint | ios::fixed ); os << setw(20) << nimike << " " << setiosflags(ios::right) << setw(10) << hinta << " " << setw(4) << kpl; return os; } ... ostream &operator<<(ostream &os,const cTuote &tuote) { return tuote.tulosta(os); }Nyt tietysti voisi olla ohjelmoijalle yllätys, jos hän kutsuisi
cTuote tuote("Volvo|23700|1"); const double pi = 3.14159265; cout.precision(6); cout << pi << '\n'; cout << tuote << '\n'; cout << pi << '\n';ja jälkimmäinen piin arvo tulostuisikin vain kahdella desimaalilla. Tämän vuoksi pitäisi alkuperäiset tulostusasetukset palauttaa, mikäli niitä muutetaan.
ostream &tulosta(ostream &os) const { long oldf = os.setf(ios::left | ios::showpoint | ios::fixed ); int olddes = os.precision(2); ... tulostusta ... os.precision(olddes); os.flags(oldf); return os; }Mutta kuka tämän jaksaa tehdä kerrasta toiseen?
Entäpä jos teemmekin luokan cStreamPre (stream precision):
ostream &tulosta(ostream &os) const { cStreamPre pre(os,2); ... tulostusta ... return os; }ja nyt piikin tulee taas pyydetyllä 6:lla desimaalilla molemmilla kerroilla! Miten?
class cStreamPre { ostream &os; long oldf; int oldp; public: cStreamPre(ostream &aos=cout,int npre=1,long flags=0) : os(aos) { oldf = os.setf(ios::showpoint | ios::fixed | flags); oldp = os.precision(npre); } ~cStreamPre() { os.flags(oldf); os.precision(oldp); } };Luokan muodostaja tallettaa olion attribuutteihin tulostuksen muotoilun ennen kuin muotoiluja on muutettu. Kun syntyneen olion vaikutusalue lakkaa, eli poistutaan siitä ohjelmalohkosta jossa olio on syntynyt (auto), kutsutaan olion hajottajaa, joka palauttaa alkuperäiset arvot. Nyt ohjelmoijan tarvitsee vain antaa olion syntyä automaattisesti
cStreamPre pre(os,2);lohkon alussa. Ja tämähän saadaan vähemmällä kirjoittamisella kuin tarkkuuden muuttaminen normaalisti:
os.precision(2); os.setf(ios::left | ios::showpoint | ios::fixed );Huomattakoon, että "temppu" toimii vaikka kesken tulostuksen poistuttaisiin ylimääräisellä return--lauseella. Samoin "temppu" toimii, vaikka tulostuksen sisällä kutsuttaisiin toista samalla tavalla toteutettua tulostusta!
{ ifstream fi(nimi); ... lukeminen ... }eli kun olion fi vaikutusalue lakkaa, kutsutaan tietovirtaolion hajottajaa, joka sulkee tiedoston.
class cTuote { ... friend ostream &operator<<(ostream &os,const cTuote &tuote); } ostream &operator<<(ostream &os,const cTuote &tuote) { ... viitataan suoraan luokan suojattuihin attribuutteihin }Funktion tai luokan esitteleminen ystäväksi tarkoittaa sitä, että funktio tai luokka pääsee suoraan käsiksi olion attribuutteihin. Tämä ei yleensä kuitenkaan ole kovin suotavaa. Jopa itse kielen tekijäkin on myöntänyt että ystäväfunktiot (friend) ovat tarpeettomia. Mehän vältimme ystäväfunktion seuraavalla tekniikalla:
class cTuote { ... ostream &tulosta(ostream &os) const { ... } } ostream &operator<<(ostream &os,const cTuote &tuote) { return tuote.tulosta(os); }emmekä joutuneet edes kirjoittamaan yhtään enempää koodia. Toinen tapa välttää ystäväfunktioita on saantimetodien käyttäminen:
class cRajoitettu { ... int Arvo() const { return arvo; } }; ostream &operator<<(ostream &os, const cRajoitettu &r) {return os << r.Arvo(); }Tässäkään tapauksessa ei ole kenellekään luovutettu pääsyä muuttamaan suojattuja attribuutteja.
Vastaavasti ystävyyttä esitetään kirjallisuudessa muidenkin operaattoreiden kuormittamisessa käytettäväksi, mutta tottakai voimme em. tekniikoilla välttää ystäväfunktioiden käytön.
Siis pyrimme jatkossa välttämään ystäväfunktioita (jopa enemmän kuin goto- lausetta).