Oliot ja luokat

 

Oliot ja luokat

Tervetuloa tämän oppaan ehdottomasti tärkeimmän osion pariin. Kuten ehkä oletkin jo huomannut, rakastan suuresti käytännön tilanteiden pohtimista C++:n avulla. Ja niitä on tulossa vielä paljon lisää, joten ei auta kuin tottua. Nyt mennään taas: Koska vartiointifirma Pamppu ja sateenvarjo oy:lle tekemäsi ohjelma toimi loistavasti ja moitteettomasti, päättävät he tilata sinulta toisenkin ohjelman. Ohjelman pitäisi ylläpitää heidän asiakasluetteloaan. Jokaisella asiakkaalla on tietoina nimi, osoite, sovittu kuukausimaksu jne..

Kuinka tällainen tietomäärä pitäisi järjestää? No, hommahan on helppo. Tehdään taulukko nimi[ASIAKKAIDEN_LKM], maksu[ASIAKKAIDEN_LKM] jne.. Näin se onnistuisi ihan perusominaisuuksien avulla. Mutta mites suu pannaan, kun noin puolet asiakkaista haluaa yövartioinnin ja toinen puoli ei? Kaiken kukkuraksi yövartijoiden määrä vaihtelee. Yksi ratkaisu on tehdä taulukko yovartijat[ASIAKKAIDEN_LKM] ja olla välittämättä siitä, että osa alkioista on tyhjiä ja turhia. Jos ei vielä hymy hyydy, lisäämme soppaan sen, että ohjelman pitäisi pystyä lisäämään ja poistamaan asiakkaita lennossa. Ohjelmaa pyörittää vanha 386-kone, jossa on vähän muistia ja tehoa. Niinpä muistia tai koneen prosessoriaikaa ei saisi hukata ollenkaan.

Kuulen jo kuinka huudatte edellisen esimerkin ikeen alla: "Me haluamme olioita ja luokkia! Tahtoo!" Hyvä on, nyt niitä tulee. Pientä jaarittelua kuitenkin vielä.

Lapsikin huomaa, että edellisen esimerkkiongelman ratkaisu taulukoilla ontuu. Asiakas olisi parempi pitää omana kokonaisuutenaan, ei vain yhtenä indeksinä moniin taulukoihin. Pelkillä taulukoilla ja isolla kasalla purkkavirititelmiä edellisen tapauksen voi kyllä hoitaa muistia ja tehoa haaskaamatta, mutta tulos on sellainen ohjelmanvänkyrä, jota kukaan ei vapaaehtoisesti kyllä ala enää kehittämään. Niinpä käsittelemmekin nyt luokat (class) ja oliot (object). Tämä on nyt vihdoinkin sitä C++:aa itseään. C++ on oliopohjainen kieli, joten ilman olioita ei pitkälle pötki. Toki C++:lla voi ohjelmoida ilmankin olioita, mutta harvemmin syytä sellaiseen on..

Ajatellaan nyt näin päin, että kaikkia asiakkaan tietoja ei laiteta taulukkoon, vaan ne säilötään asiakkaaseen, ja sitten tehdään asiakkaista taulukko. Me tarvitsemme asiakasolioita. Koska kaikki asiakkaat ovat samanlaisia, sitä yövahtijuttua lukuunottamatta, ne ovat samoja olioita. Niillä on samat ominaisuudet, mutta eri tiedot. Samanlaiset oliot kuuluvat samaan luokkaan. Nyt otamme ja määrittelemme luokan Asiakas.

class Asiakas
{
public:
  char* nimi;
  char* osoite;
  int kuukausiMaksu;
};  // huomaa ; -merkki lohkon lopussa!!!

Ensin on varattu sanan class. Sitten seuraa luokan nimi. Sitten alkaa lohko, jossa luokka määritellään. public tarkoittaa, että alla olevat muuttujat ovat kaikkien käytössä. TSM. Sen jälkeen homma on tuttua, määrittelemme vain Asiakkaaseen liittyvät muuttujat. Lohko loppuu ja sitten tulee taas penteleen tärkeä kohta, nimittäin puolipiste lohkon lopussa. Muista se! Lohkojen loppuun ei muuten tule C++:ssa puolipistettä, mutta luokan määrittely on (mielestäni typerä) poikkeus.

Kun haluamme luoda asiakkaan, käy se näin (kunhan luokka on ensin määritelty):

Asiakas erkinKumikorjaamo;

Luomme olion erkinKumikorjaamo. Asiakas on itseasiassa kuin mikä tahansa muuttujatyyppi. Voi tehdä Asiakas-taulukoita ja muiden luokkien jäsenenä voi olla Asiakas-olioita. Palataksemme esimerkkiin, teemme näin:

Asiakas asiakkaat[ASIAKKAIDEN_LKM];

Luomme siis taulukon asiakkaat, joka sisältää ASIAKKAIDEN_LKM (vakio) kappaletta Asiakas-luokan olioita. Olion sisältämiä muuttujia käytetään piste-operaattorin avulla. Siis olionNimi.muuttuja.

Asiakas kalevinKukkakauppa;
kalevinKukkakauppa.kuukausiMaksu = 150; // Kalevi maksaa 150 mk kuukaudessa

On tärkeätä hahmottaa luokan ja olion ero. Luokka on kuin muotti. Se kertoo mitä ominaisuuksia olioilla on. Olio on taas yksi sillä muotilla tehty piparkakku. Sillä on luokan ominaisuudet, siis muuttujat, mutta niissä omat arvonsa.

Tämä ei kuitenkaan ratkaise esimerkkimme kahta ongelmaa, yövahtiasian eroavaisuutta ja asiakkaiden alati muuttuvaa lukumäärää. Näihin asioihin tarvitaan kuitenkin osoittimia, dynaamista muistinhallintaa ja perintää. Ennen niitä sinun pitää kuitenkin oppia luokkien perusteet, joten niitä ensin.

Luokka on muutakin kuin pelkkä kasa muuttujia. Luokkaan saa nimittäin jäsenfunktioita eli metodeja. Niiden kautta luokan olioita käytetään. Seuraa lastenmielinen esimerkkiohjelma:

#include<iostream.h>

class Veturi
{
public:
  int nopeus;
  void Puhu();
};

void Veturi::Puhu()
{
  cout  << "Tuut! Tuut! Hurjastelen " << nopeus << " km/h nopeutta!";
}

int main()
{
  Veturi villeVeturi;
  villeVeturi.nopeus = 50;
  villeVeturi.Puhu();
  
  return EXIT_SUCCESS;
}

Olio on tiedot ja toiminnallisuus

Keksitkö mitään muuta tapaa toteuttaa Veturia matkiva ohjelma? On nimittäin olemassa toinenkin vaihtoehto... Etkö? Erittäin hyvä, koska se toinen vaihtoehto on väärä ja tuo yllä esitetty oikea. Mietipäs millä tavalla alla oleva muunnelma Veturista on huonompi:

#include<iostream.h>

class Veturi
{
public:
  int nopeus;
};

void Puhu(Veturi veturi)
{
  cout  << "Tuut! Tuut! Hurjastelen " << veturi.nopeus << " km/h nopeutta!";
}

int main()
{
  Veturi villeVeturi;
  villeVeturi.nopeus = 50;
  Puhu(villeVeturi);
  
  return EXIT_SUCCESS;
}

Varmasti mieleesi tulee ainakin, että jälkimmäinen vaihtoehto on epälooginen. Kun vertaamme puhumista, on villeVeturi.Puhu() erittäin looginen, sen voisi kääntää selkokieliseksi käskyksi: "Ville Veturi! Puhu!". Puhu(villeVeturi) taas voisi vastata suomen kielen lausetta: "Suorita puheoperaatio Ville Veturilla". Kuulostaa vähän KELA:n lomakkeen täyttöohjeilta...

Kuten huomaat, kun ohjelman metodit (jäsenfunktiot) sijoitetaan samaan luokkaan ohjelman tietojen kanssa, saavutetaan kaksi etua: ohjelma vaikuttaa loogiselta eli on sellainen kuin terveellä maalaisjärjellä sen kuvittelisi olevan, ja sen lisäksi ohjelman lähdekoodi on selkeämpää. Minusta ainakin veturin antaminen Puhu()-funktioille on hankalaa verrattuna luokan sisällä toteutettuun puhumiseen, jossa vastaavaa hankaluutta ei tarvitse tehdä - ollaanhan jo valmiiksi oikean olion "sisällä" (tuolle "sisällä olemiselle" on myös teknisempi määritelmä, mihin palaamme myöhemmin). Ja nytpä olemmekin tunkeutuneet C++-ohjelmoinnin, ja olio-ohjelmoinnin yleensä, kaikkein pyhimpään. Olio-ohjelmointi on sitä, että tiedot (muuttujat) ja toiminnallisuus (metodit) ovat yhdessä. Kaikki mitä tämän jälkeen kerron ei ole lähellekään yhtä tärkeää kuin tuo edellinen havainto. 

OLIO = TIEDOT + TOIMINNALLISUUS

Haluaisin vielä lisätä, että tiedon ja toiminnallisuuden yhdistäminen ei ole pelkästään loogista, vaan johtaa parempiin ohjelmiin. Kun luokkaan on paketoitu sekä tiedot että toiminnot, syntyy "musta laatikko". Luokan käyttäjän ei tarvitse välittää siitä miten luokka itseasiassa toimii, kunhan se toimii oikein. Ohjelmoidessa voi hyvinkin tuntua, että pään sisällä on menossa italialaisen talonyhtiön kokous, siis kaaos. Juuri tätä varten olio-ohjelmoinnissa koitetaan piilottaa kaikki turha, joka muuten kuluttaisi ohjelmoijan kallista mutta vähäistä (?) aivokapasiteettia.

Tiedon kapselointi

Tiedon kapselointi. Armottomana ihmisenä iskin sinua suoraan päin kasvoja yhdellä hyvin tärkeällä oliopohjaisen ajattelun termillä. Vielä uudestaan: tiedon kapselointi. Idea on, että jokaiseen käsitteeseen liittyvät tiedot on kapseloitu siihen käsitteeseen (olioon), eivätkä muut pääse niitä sorkkimaan. Tiedon kapselointi! Edellisessä esimerkissä tietoa ei oltu kapseloitu, vaan se roikkui villin vapaana kuin Kemppaisen kolli kesähousuissaan.

Jos mietit edellista esimerkkiä, niin huomaat että kuka tahansa pääsee sorkkimaan luokan muuttujia. Mitä kapselointia ja suojaamista se on muka olevinaan? Nyt konkari vetää takataskustaan taika-avaimen, nimittäin varatun sanan private. private on publicin kaveri. Kun public määrittelee julkisia jäsenmuuttujia ja funktioita, niin private määrittelee yksityisiä. Julkiset muodostavat olion käyttöliittymän, tavan jolla muut ohjelman osat kommunikoivat olion kanssa. Yksityiset osat muodostavat luokan sisäisen toiminnan. Ne ovat muuttujia ja funktioita, joita luokka tarvitsee omaan sisäiseen toimintaansa ja joita ulkopuoliset eivät pääse sotkemaan. Yksityiseksi määriteltyä tietoa voivat käyttää vain luokan omat metodit, jäsenmuuttujat. Luokan jäsenet ovat oletuksena yksityisiä.

Esittelemme nyt lisää höyrykoneiden kauhuja.

#include<iostream.h>

const int MAX_NOPEUS = 180;

class Veturi
{
public:
  void AsetaNopeus(int n);
  void Kiihdyta();
  void Jarruta();
  int LueMittari(); // nk. saantifunktio
private:
  int nopeus;
};

void Veturi::AsetaNopeus(int n)
{ 
  nopeus = n;
}

void Veturi::Kiihdyta()
{
  nopeus += 20;
  if (nopeus > MAX_NOPEUS) nopeus = MAX_NOPEUS;
}

void Veturi::Jarruta()
{
  nopeus -= 20;
  if (nopeus < 0) nopeus = 0;
}

int Veturi::LueMittari()
{
  return nopeus;
}

int main()
{
  Veturi villeVeturi;
  villeVeturi.AsetaNopeus(0);

  villeVeturi.Kiihdyta();
  villeVeturi.Kiihdyta();
  cout << "Nopeutta " << villeVeturi.LueMittari() << " km/h!" << endl;

  villeVeturi.Jarruta();
  cout << "Nopeutta " << villeVeturi.LueMittari() << " km/h!" << endl;

  // villeVeturi.nopeus = 50;
  
  return EXIT_SUCCESS;
}

Veturi osaa muuttaa nopeuttaan ja ilmoittaa sen. Nopeus asetetaan nollaksi olion luonnin yhteydessä. Ensin veturi kiihdytetään 40 km/h nopeuteen ja nopeus tulostetaan. Sitten jarrutetaan ja nopeus ilmoitetaan uudestaan. Huomaa viimeisellä rivillä oleva viittaus yksityiseen muuttujaan. Jos sitä ei olisi kommentoitu, se ei olisi mennyt kääntäjästä läpi. Yksityinen tieto ei ole ulkopuolisten käytettävissä.

Nopeus on varma muuttuja. Jarruta() ja Kaasuta() metodit valvovat tehokkaasti nopeuden käyttöä. Jos pelkäät, että muuttujien käyttö erillisten funktioiden kautta hidastaa ohjelmaa, niin höpsön pöpsön. Hyvä kääntäjä optimoi saantifunktiot olemattomiin. Huomaa kohta: kääntäjä optimoi. Samalla kääntäjä tarkistaa tietotyypit. Eli kääntäjä tekee enemmän työtä, mutta lopputulos (konekielinen ohjelma) on sama. Metodin AsetaNopeus(int) avulla nopeuden voi kuitenkin sotkea pahanpäiväisesti. Ihannetilanne olisi semmoinen, jossa luokan käyttäjä ei pystyisi sotkemaan ollenkaan luokan jäsenmuuttujien arvoja. Kun kirjoittaa vähän pitempiä ja monimutkaisempia ohjelmia, jaksaa tällaistä ominaisuutta kyllä arvostaa... Niinpä pitäisi olla joku kikka, jolla nopeus voitaisiin alustaa olion luonnin yhteydessä ja AsetaNopeus(int) metodi tulisi tarpeettomaksi... näen horisontissa häämöttävän aasinsillan...

 

Muodostin- ja tuhoajafunktiot

Usein oliota luodessa täytyy tehdä jotain toimia, kuten varata muistia luokan tiedoille. Ei kannata jättää luokan käyttäjän vastuulle kutsua homman hoitavaa funktiota, vaan käyttää luokkaan liittyvää muodostinfunktiota (constructor). Kun luokka luodaan, luo kääntäjä sille automaattisesti muodostinfunktion - joka ei tee mitään. Jos haluamme laittaa jotain toimia luokan tyyppisen olion luonnin yhteyteen, voimme korvata muodostinfuktion omallamme. Muodostinfunktion nimi on luokan nimi ja sillä ei ole paluuarvoa. Muodostinfunktio pitää tietenkin määritellä julkiseksi.

#include<iostream.h>

class Auto
{
public:
  Auto();	// muodostin
};

Auto::Auto()
{
  cout << "Muodostinta kutsuttu";
}

int main()
{
  Auto lada;
  
  return EXIT_SUCCESS;
}

Muodostinfunktiolle voi myös välittää parametrejä ja siitä voi kirjoittaa monta erilaista versiota eri parametreillä - polymorfismi siis toimii tässäkin suhteessa. Muodostinfunktion parina toimii tuhoajafunktio (destructor). Sitä kutsutaan kun olio tuhotaan. Sen määrittely on muuten vastaava, mutta funktion nimi alkaa ~ (tilde, mato)-merkillä. Jos määrittelet oman muodostinfunktion, niin kääntäjä jättää oman tyhjän muodostimensa määrittelemättä. Siis jos määrittelet yhden int-parametrin ottavan muodostimen, ei luokkaa voida enää luoda ilman parametrejä, koska tyhjä parametriton muodostin jää määrittelemättä.

Rakennamme nyt luokan, jota voidaan käyttää hyvin yksinkertaiseen tiedon kryptaamiseen, siis salaamiseen. Siinä on taulukollinen unsigned char muuttujia. Taulukko varataan dynaamisesti (dynaamisesta varauksesta kerron tarkemmin sitten myöhemmin), sen koko annetaan oliota luodessa. Jos kokoa ei anneta, käytetään oletuskokoa. Tietenkin varattu muisti vapautetaan kun olio lakkaa olemasta. Itse kryptausosa on mukana lähinnä aidon tunnelman saavuttamiseksi.

const int OLETUS_KOKO = 256;

class KryptattuTaulukko
{
public:
  KryptattuTaulukko();
  KryptattuTaulukko(int koko);
  ~KryptattuTaulukko();

  void Koodaa();

  unsigned char *taulukko;
  int taulukonKoko;
};

KryptattuTaulukko::KryptattuTaulukko()
{
  taulukko = new unsigned char[OLETUS_KOKO]; // varataan muisti
  taulukonKoko = OLETUS_KOKO;
}

KryptattuTaulukko::KryptattuTaulukko(int koko)
{
  taulukko = new unsigned char[koko]; // varataan muisti
  taulukonKoko = koko;
}

KryptattuTaulukko::~KryptattuTaulukko()
{
  delete [] taulukko; // vapautetaan muisti
}

void KryptattuTaulukko::Koodaa()
{
  for (int i=0; i < taulukonKoko; i++)
    taulukko[i] = 255 ^ taulukko[i]; // pyöräytetään bitit ympäri
}

Pari puutetta vielä yllä olevan ohjelman rakenteessa on, mutta ne eivät ole niin oleellisiä - vielä.

Muodostinfunktiossa muuttujien alustus voidaan hoitaa myös itse funktion rungon ulkopuolella, tähän tyyliin:

KryptattuTaulukko::KryptattuTaulukko() : taulukonKoko(OLETUS_KOKO)
{
	taulukko = new unsigned char[OLETUS_KOKO];
}

Siis muodostinfunktion nimen jälkeen kaksoispiste, sitten muuttujien alustus. Muuttujan arvo annetaan suluissa, muuttujan nimen perässä. Tämä tapa on suositeltavampi kuin muodostinfunktion rungossa muuttujien alustaminen. Yllä oleva funktio on karsea esimerkki muodostinfunktiosta. Koska muodostinfunktio ei palauta arvoa, se ei voi viestiä onnistumisesta mitenkään. Ja koska se varaa muistia, voi ongelmiakin tulla. Muodostinfunktiossa ei saa tehdä mitään, mikä voi epäonnistua (muistin varaus, tiedostojen avaus jne...). Joten luokan kehittely jatkuu, kunhan vaan vedämme operaattorit ensin käyttöön.

 

Operaattoreiden ylikuormittaminen

Mikäli et ole vielä tutustunut String-luokkaan, niin kertoilenpa siitä tässä aluksi. Merkkijonojen ja String-luokan käyttö on tarkemmin esitelty standardikirjastojen yhteydessä. String luokka on kuin yhdistetty luokka ja taulukko. Toisaalta sillä on muodostin- ja tuhoajafunktiot, mutta toisaalta sitä voi käyttää indeksointioperaattorilla kuin tavallista taulukkoa. Siis String-luokan olioita voi käyttää näin: merkkijono[3]. Tämä palauttaa merkkijonon neljännen merkin. Vaikka String onkin kääntäjän mukana tuleva, ei siihen kuitenkaan liity mitään maagisia, kuolevaisille saavuttamattomia temppuja. Pelkästään operaattoreiden ylikuormittamista, mikä kyllä tavallisille kuolevaisille voi vaikuttaa mystisten voimien työltä - mutta ei C++-velholle!

Kun kääntäjän pitää käsitellä +-operaattori kahden int-luvun yhteydessä, ei se ole mikään ongelma. Kai nyt jokainen pari lukua osaa yhteen ynnätä. Mutta entä jos kääntäjää käsketään ynnäämään kaksi itse ohjelmoidun Tyontekija-luokan oliota? Kääntäjä ei tunne luokan ominaisuuksia, joten ainoa heti mieleen tuleva ratkaisu olisi laskea olioiden muistiosoitteet yhteen. Ratkaisu onkin mahdollinen, mutta järjeton. Muistiosoitteella ei ole mitään tekemistä luokan toiminnan kanssa ja tulos osoittaisi mihin sattuu. Parempi vaihtoehto olisi vaikka laskea yhteen työntekijöiden palkat. Näin älykästä toimintaa emme kuitenkaan C++-kääntäjältä voi odottaa, vaan meidän pitää se itse ohjelmoida.

Ensimmäinen ratkaisu olisi tehdä Tyontekija-luokkaan metodi Lisaa(Tyontekija&). Lisays tehtaisiin nain:

Tyontekija jamppa;
Tyontekija jomppa;
palkatYhteensa = jomppa.Lisaa(jamppa);

Tuo ei kuitenkaan ole hirveän havainnollinen tapa. Me nimittäin voimme ohjelmoida Tyontekija-luokkaan +-operaattorin, siis ylikuormittaa +-operaattorin. C++ antaa mahdollisuuden ylikuormittaa seuraavat operaattorit:

+  -  *  /  %  ^  &  |  ~  !  =  <  >  +=  -=  *=  /=  %=  ^=  &=  |=  
<<  >>  >>=  <<=  ==  !=  <=  >=  &&  ||  ++  --  ->*  ,  ->  []  ()  
new  delete

Ylikuormitettu operaattori on funktio, joka saa operaattorista riippuvat parametrit. Se siis toimii kuin Lisays(Tyontekija&) -funktio toimisi aikaisemmassa esimerkissä. Operaattorifunktiota voidaan kuitenkin kutsua niin kuin operaatoreita käytetään. Funktio määritellään operator-sanalla. Sen perään kirjoitetaan haluttu operaattori, siis vaikka operator+. Jotta operaattorit ++olio ja olio++ voitaisiin erottaa toisistaan, määritellään jälkimmäisempi (postfix-muoto)

int operator(int) {}

Siis se saa int-tyyppisen haamuparametrin, parametrin joka ei sisällä mitään. Tässäpä sitä on sitten ylikuormitusta kullekin.

#include <iostream.h>

class Tyontekija
{
public:
    void AsetaPalkka(int p) { palkka = p; }
    int AnnaPalkka() { return palkka; }
    int operator+(Tyontekija& toinen) { return palkka + toinen.AnnaPalkka(); }

private:    
    int palkka;
};

int main()
{
    Tyontekija jamppa; jamppa.AsetaPalkka(8000);
    Tyontekija jomppa; jomppa.AsetaPalkka(12000);


    cout << "Kaveruksille pitää maksaa yhteensä " << jomppa + jamppa << endl;
    
    return EXIT_SUCCESS;
}

Nyt voimme kirjoittaa kryptatusta taulukosta hienon luokan. Sitä voi käyttää kuin taulukkoa, mutta se on kuitenkin luokka - siis sijoituksien oikeellisuus voidaan tarkistaa, muutamia hienouksia mainitakseni.


#include 

const int OLETUS_KOKO = 256;

class KryptattuTaulukko
{
public:
	KryptattuTaulukko(int koko = OLETUS_KOKO); // oletusparametri
	~KryptattuTaulukko();
	
	int Alusta(); // todellinen muodostinfunktio
	void Koodaa();
	
	int AnnaKoko() { return taulukonKoko; }
	int OnkoKoodattu() { return onKoodattu; }
	int OnkoAlustettu() { return onAlustettu; }
	
	int& operator[](unsigned kohta); // voidaan sijoittaa kuin taulukkoon
	int operator[](unsigned kohta) const; // voidaan lukea kuin taulukkoa
	void operator=(const int* arvot);// voidaan sijoittaa taulukollinen arvoja
	
private:
	int onAlustettu;
	int onKoodattu;
	int *taulukko;
	int taulukonKoko;
        int virhe; // virheelliset viittaukset ohjataan tänne
};

KryptattuTaulukko::KryptattuTaulukko(int koko) : onAlustettu(0), onKoodattu(0),
						taulukonKoko(koko), virhe(0)
{
	// tyhjä muodostimen runko
}

int KryptattuTaulukko::Alusta()
{
	taulukko = new int[taulukonKoko];
	
	if (taulukko == 0) return 0;
	
	onAlustettu = 1;
	return 1;
}

KryptattuTaulukko::~KryptattuTaulukko()
{
	if (onAlustettu) delete [] taulukko;
}

int& KryptattuTaulukko::operator[](unsigned kohta) 
{
	if (onAlustettu) {
		if (kohta < taulukonKoko) return taulukko[kohta]; // laillinen sijoitus
		else return virhe; // ylivuoto
	}
	else return virhe; // vakion nolla palauttaminen ei olisi mahdollista, koska se ei ole olemassa kuin metodissa
}

int KryptattuTaulukko::operator[](unsigned kohta) const // unsigned estää laittomat negatiiviset indeksit
{
	if (onAlustettu) {
		if (kohta < taulukonKoko) return taulukko[kohta];
		else return 0; 
	}
	else return 0; // ei tarvita viitattavaa kohdetta, joten nolla käy
}

void KryptattuTaulukko::operator=(const int* arvot)
{
	// vaarallista, const int* arvot eivät välttämättä osoita taulukonKoko kokoiseen muistialueeseen...
	if (onAlustettu) for (int a = 0; a < taulukonKoko; a++) taulukko[a] = arvot[a];

}

void KryptattuTaulukko::Koodaa()
{
	for (int i=0; i < taulukonKoko; i++)
		taulukko[i] = 255 ^ taulukko[i]; // pyöräytetään bitit ympäri
	
	if (onKoodattu) onKoodattu = 0; // vaihdetaan koodauksen tila
	else onKoodattu = 1;
}



int main()
{
	int alkupTaulu[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
	KryptattuTaulukko taulu(10);
	
	taulu[5] = 5; // ei tee mitään, koska ei alustettu
	
	if (!taulu.Alusta()) return EXIT_FAILURE; // taulukon luonti epäonnistui
	taulu = alkupTaulu; // kopioidaan alkupTaulu taulu:n
	
	taulu[-1] = 5; // kumpikin laittomia sijoituksia, eivät tee mitään
	taulu[100] = 5;
	
	taulu.Koodaa();
	cout << "Koodattu taulu: ";
	for (int a = 0; a < 10; a++) cout << taulu[a] << " ";
	taulu.Koodaa();
	
	cout << endl << "Koodaamaton taulu: ";
	for (int b = 0; b < 10; b++) cout << taulu[b] << " ";
	cout << endl << endl;
	
	cout << "Ensimmäinen alkio on nyt " << taulu[0] << endl;
	taulu[0] = 7;
	cout << "Sijoituksen jälkeen se on " << taulu[0] << endl << endl;
	
	cout << "Tilamuuttujat: koodaus:" <

 

coutin todellinen luonne

Itse olen joskus kummastellut: "C++:ssä on olemassa muuttujia, funktioita, luokkia ja olioita. Mikä ihmeen vetkutin se cout sitten on?" Vaikka ihan ensimmäisestä ohjelmasta asti olet käyttänyt ciniä ja coutia, niin silti niiden olemus lienee hieman epäselvä.

cin ja cout ovat olioita. Ne ovat iostream.h:ssa (tai sen alaisessa otsikkotiedostossa) valmiiksi määriteltyjä olioita. Niistä löytyy uudelleenmääriteltynä << tai >> operaattori. Siksi siis kun käskemme coutia tulostamaan - siis kutsumme sen jäsenfunktiota - ei toimenpide vaikuta miltään funktiokutsulta, vaikka se sitä onkin. Sellaisia peijooneita ne operaattorit ovat, varsinkin uudelleenmääritellyt. << -operaattorin käyttäminen kutsuu todellisuudessa operator<<(...) -funktiota, jonka me alla olevassa esimerkissä esiinmerkitsemme:

#include <iostream.h>

int main()
{
	int luku = 5;
	
	cout << "Ikäni on " << luku << endl;
	cout.operator<<("Ikäni on ").operator<<(luku).operator<<(endl);
	
	return EXIT_SUCCESS;
}

Esimerkin kahden tulostuslauseen tulostama teksti on sama, käytämme funktiokutsun mitä muotoa tahansa. Monien peräkkäisten funktiokutsujen ketjuttaminen onnistuu sen vuoksi, että operator<<(...) palauttaa viittauksen coutiin. Siis alku:

cout.operator<<("Ikäni on ") 

... muuttuu funktiokutsun jälkeen lausekkeeksi...

cout

.. ja sen perään voi lykätä uutta funktiokutsua ihan rauhassa. Kuten ehkä arvasitkin, nämä pohdinnot ovat kuin hyvä ranskanleipä: ne toimivat myös toisesta päästä pureskeltuna - siis sama idea pätee ciniin, operaattorin nuolet ovat vain toiseen suuntaan.

 

Osoitin jäsenfunktioon

Piti sitten tällekin asialle oma kappale kyhätä. No, nyt on komia paragraafi, niin pitää sitten jotakin yrittää tähän rustata.

void (Luokka::*pFunktio)(int, char);

Voi kehveli, tuohon se nyt lipsahti. Mahtaako sitä enää mitään sanoa? Siis * merkkaamaan osoitinta, luokka ja näkyvyysoperaattori (siis :: ) alkuun. Ja asteriski eli * ennen funktion nimeä, ei koko rimpsun alkuun, koska luokalla ei olla osoittelemassa mihinkään.

Takaisin