previous next Title Contents Index

18. Operaattoreiden kuormitus


Mitä tässä luvussa käsitellään?

* uusien merkityksien antaminen operaattoreille

* automaattinen tyypinmuunnos

* hajottajan hyödyntäminen

Syntaksi:

	<< 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.

18.1 operator+=

Tuote oli sellainen, että siihen lisättiin aina saman tuotteen yhteensä- tiedot:
	  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

Tehtävä 18.162 Useita versioita += operaattorista

Kirjoita em. versiot reaaliluvun ja kokonaisluvun lisäämiseksi tuotteeseen.

18.2 operator<< ja operator>>

Aikaisemmin mainittiin että eräs tietovirtojen merkittävimmistä eduista printf- tyyliseen tulostukseen on se, että tietovirtoihin voi tulostaa myös itse tehtyjä olioita. printf:hän ei ole mahdollista enää itse muuttaa ja lisätä uusia %- formaatteja.

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");
Tätä tietoa hyödyntäen voimme kirjoittaa operaattorista version, jonka ansiosta tulostus
	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_rivi
tapaa.

18.3 Binäärisen operaation kuormitus

Vastaavasti kuin binäärisiä << ja >> - operaattoreita, voidaan myös muitakin binäärisiä operaattoreita kuormittaa. Binääristä operaatiota kuormitettaessa vähintään toisen operandin tulee olla luokka (eli ei skalaari kuten int, double jne.)

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.):

operator\rajoit.cpp - esimerkki binäärisen operaation kuormituksesta

	#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:

operator\rajoit2.cpp - esimerkki operaatioden kuormituksesta

	#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);

18.4 Tyypinmuunnosoperaattori ja sijoitus

Esimerkin rajoit.cpp toinen +- operaattori voitaisiin välttää tekemällä muunnosoperaattori, joka muuttaa cRajoitettu - tyyppisen olion kokonaisluvuksi:
	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.

Tehtävä 18.163 Ajan lisääminen

Aikaisemmin oli esitetty cAika- luokka. Lisää luokkaan += - operaattori ja + - operaattori joiden ansiosta seuraavat sijoitukset olisivat mahdollisia
	cAika a1(10,30),a2(15,55);
	a1 += 50;
	a2 = a1 + 20;

Tehtävä 18.164 Aika rajoitettujen kokonaislukujen avulla

Miten voisit tehdä luokan cAika käyttämällä edellä ollutta cRajoitettu- luokkaa. Tarvitseeko cRajoitettu joitakin muutoksia?

18.5 Hajottajan hyödyntäminen

18.5.1 Tulostusmuotoilun palauttaminen

Oletetaanpa tuoterek.cpp - ohjelmassa että hinnat haluttaisiinkin tulostaa kahdella desimaalilla:
	  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?

ali\StreamPr.h - Luokka tulostustarkkuuden asettamiseksi

	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!

18.5.2 Tiedoston automaattinen sulkeminen

Samaan ideaanhan perustuu myös se, että C++:ssa voidaan tiedosto "jättää sulkematta":
	{
	  ifstream fi(nimi);
	  ... lukeminen ...
	}
eli kun olion fi vaikutusalue lakkaa, kutsutaan tietovirtaolion hajottajaa, joka sulkee tiedoston.

18.6 Ystäväfunktiot

Usein kirjallisuudessa mm. tulostus opetetaan tekemään seuraavasti:
	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).


previous next Title Contents Index