Kirjautuminen

Haku

Tehtävät

Oppaat: C++-ohjelmointi: Osa 9 - Luokkien perusteet

  1. Osa 1 - Johdanto
  2. Osa 2 - Vakiot, muuttujat ja perustietotyypit
  3. Osa 3 - Ehdot, silmukat ja poikkeukset
  4. Osa 4 - Rakenteet, taulukot ja merkkijonot
  5. Osa 5 - Funktiot
  6. Osa 6 - Esittelyt, määrittelyt ja elinajat
  7. Osa 7 - Viittaukset, osoittimet ja dynaaminen muisti
  8. Osa 8 - Mallit
  9. Osa 9 - Luokkien perusteet
  10. Osa 10 - Luokkien erikoiset jäsenet
  11. Osa 11 - Luokkien perintä
  12. Liite 1 - C++-kehitysympäristöt
  13. Liite 2 - Valmis C++-kääntäjäpaketti

Kirjoittaja: Metabolix. Vuosi: 2011.

Olio-ohjelmointi, englanniksi object-oriented programming eli OOP, on nykypäivän muotisana. Aiemmin tietorakenteiden yhteydessä koottiin tietueisiin (engl. struct) samaan asiaan liittyviä tietoja. Olio-ohjelmoinnissa kootaan luokkien (engl. class) sisään datan lisäksi myös toimintoja, luokkien jäsenfunktioita (engl. member function), joita olio-ohjelmoinnissa kutsutaan toisinaan metodeiksi. Luokasta luodaan ilmentymiä (engl. instance, "instanssi"), joita kutsutaan olioiksi tai objekteiksi (engl. object). Kunkin luokan on yleensä tarkoitus muodostaa toiminnallinen kokonaisuus, joka sisältää mutkikkaiden asioiden tekemiseen valmiita funktioita, joita ohjelmoijan on sitten helppo käyttää.

Yksinkertaiset luokat

Askel olioiden käyttöön on lyhyt: Aiemmin opassarjassa tietueet ovat sisältäneet vain muuttujia ja funktiot ovat olleet niiden ulkopuolella. Kun funktio esitellään tietueen sisällä, tietuetta aletaankin kutsua luokaksi ja funktiosta tulee jäsenfunktio. Siinä missä tietueen jäsenmuuttujia täytyy käyttää tietuemuuttujien eli objektien kautta, jäsenfunktioita käytetään olioiden kautta. Jäsenfunktio voi käyttää kaikkia kyseisen olion jäseniä, ja lisäksi sille voi määritellä parametreja ja paluuarvon kuten tavallisillekin funktioille. Kun luokan jäsenfunktiota kutsutaan, tilanne vastaa sitä, että ulkoiselle funktiolle annettaisiin parametrina osoitin olioon. Luokan jäsenfunktioissa osoitin this osoittaa olioon, jonka funktiota kutsuttiin.

#include <iostream>

// Tällä yksinkertaisella luokalla on kaksi jäsenmuuttujaa
// ja kaksi jäsenfunktiota.
struct luokka {
	int a, b;

	// Tämä funktio asettaa jäsenten arvot.
	void aseta(int a2, int b2) {
		// Olion jäsenmuuttujat ovat jäsenfunktion käytettävissä.
		a = a2;
		b = b2;
	}

	// Tämä funktio laskee jäsenten summan. Funktio ei muokkaa olion
	// sisältöä, joten se on hyvä merkitä const-sanalla.
	int summa() const {
		// Osoitin this osoittaa olioon, josta funktiota kutsutaan.
		// Jäseniä voi halutessaan käyttää myös this-osoittimen kautta.
		return this->a + this->b;
	}
};

// Olio-ohjelmointiin ei välttämättä tarvita olioita ja luokkia, vaan
// samanlaiset funktiot voisi toteuttaa luokan ulkopuolellakin.
// Jäsenfunktio saa automaattisesti osoittimen käsiteltävään olioon,
// ulkoiselle funktiolle osoitin täytyy erikseen määritellä parametriksi.
// Huomaa const-sanan paikka: nyt osoitinta ei voi muuttaa mutta jäseniä voi!
void ulkoasetus(luokka* const this2, int a2, int b2) {
	this2->a = a2;
	this2->b = b2;
}

// Jäsenfunktion perässä on const, kun se ei muokkaa luokan jäseniä.
// Tämä ulkoinen funktio ei myöskään muokkaa tietueen jäseniä, joten
// parametrina on const-osoitin const-luokkaan.
int ulkosumma(const luokka* const this2) {
	return this2->a + this2->b;
}

int main() {
	// Luodaan kaksi samanlaista oliota. Tehdään sitten samat asiat
	// s:lle jäsenfunktioilla ja u:lle ulkoisilla.
	luokka u, s;

	// Olion jäsenfunktiota kutsutaan samalla pistemerkinnällä, jolla
	// myös tietueen jäseniä käytetään. Asetetaan s.x = 1 ja s.y = 2.
	s.aseta(1, 2);

	// Jäsenfunktion kutsu antaa funktiolle käsiteltävän olion osoittimen.
	// Saman voi tehdä ulkoisella funktiolla tavalliselle tietueelle, kun
	// vain antaa osoittimen (tai viittauksen) funktiolle parametrina.
	// Asetetaan u.x = 1 ja u.y = 2.
	ulkoasetus(&u, 1, 2);

	if (s.summa() == ulkosumma(&u)) {
		std::cout
			<< "Kaksi tapaa, yksi lopputulos."
			<< std::endl
			<< "Olio-ohjelmointi onnistuu ilmankin luokkia!"
			<< std::endl;
	}
}

Esittelyt ja määrittelyt

Luokan jäsenmuuttujat esitellään aina luokan sisällä, kuten tietueiden kohdalla jo nähtiin. Esittelyrivi näyttää samalta kuin tavallisen muuttujan määrittely, mutta jäsenmuuttujan tapauksessa se on kuitenkin vain esittely. Kaikille jäsenille varataan automaattisesti tilaa, kun luodaan luokan tyyppiä oleva muuttuja eli olio.

Myös luokan jäsenfunktiot täytyy aina esitellä luokan sisällä. Määrittely voi olla esittelyn yhteydessä; tällöin jäsenfunktiosta tulee automaattisesti inline-funktio. Usein määrittely on kuitenkin selkeämpää kirjoittaa itse luokan määrittelyn ulkopuolelle, jolloin käytetään samanlaista ::-merkintää kuin nimiavaruuksien kanssa. Joskus – kuten seuraavassa esimerkissä piste::yhdista-funktion kohdalla – määrittelyä ei edes voi kirjoittaa luokan sisään, koska funktio käyttää toista luokkaa, joka määritellään vasta myöhemmin.

Seuraavassa esimerkissä ovat hieman käytännöllisemmät luokat piste ja jana, joilla on joitakin yksinkertaisia jäsenfunktioita.

#include <iostream>
#include <cmath>

// Esitellään jana eli kerrotaan, että sellainen on olemassa.
struct jana;

// Määritellään piste.
struct piste {
	// Pisteen koordinaatit:
	float x, y;

	// Määritellään jäsenfunktio, joka laskee etäisyyden toiseen pisteeseen.
	// Funktiolla on automaattisesti käytössään yhden pisteen jäsenet, ja
	// parametrina on viittaus toiseen pisteeseen. Viimeinen const
	// tarkoittaa, että myös olio itse säilyy funktiossa muuttumattomana.
	float etaisyys(const piste& p2) const {
		// Lasketaan koordinaattien muutokset.
		// x ja y ovat tämän pisteen jäsenet, p2.x ja p2.y toisen.
		float dx = x - p2.x;
		float dy = y - p2.y;

		// Lasketaan etäisyys Pythagoraan lauseen avulla.
		// a*a + b*b = c*c   =>   c = sqrt(a*a + b*b)
		return std::sqrt(dx * dx + dy * dy);
	}

	// Esitellään jäsenfunktio, joka luo janan. Paluuarvona on siis jana,
	// parametrina toinen päätepiste. Tätä varten jana-luokka piti esitellä
	// jo ennen piste-luokkaa. Funktiota ei voi kuitenkaan toteuttaa, koska
	// jana-luokka on yhä määrittelemättä.
	jana yhdista(const piste& p2) const;
};

// Määritellään jana.
struct jana {
	// Janan päätepisteet:
	piste a, b;

	// Määritellään jäsenfunktio, joka laskee janan pituuden.
	float pituus() const {
		// Hyödynnetään pisteen etäisyyttä toisesta.
		// Jäsenfunktion kutsu muistuttaa tavallista funktiokutsua,
		// mutta alussa kerrotaan, minkä olion funktiota kutsutaan.
		// Nyt kutsutaan siis janan pisteen a funktiota.
		return a.etaisyys(b);
	}

	// Määritellään jäsenfunktio, joka venyttää janan kaksinkertaiseksi.
	// Tämän funktion lopussa ei voi olla const-määrettä, koska tämä
	// muuttaa janaa itseään!
	void kaksinkertaista() {
		float dx = b.x - a.x;
		float dy = b.y - a.y;
		a.x -= dx / 2;
		b.x += dx / 2;
		a.y -= dy / 2;
		b.y += dy / 2;
	}
};

// Nyt jana on määritelty, joten toteutetaan pisteen puuttuva jäsenfunktio.
// Tässä tilanteessa täytyy erikseen mainita, minkä luokan jäsenestä on kyse.
jana piste::yhdista(const piste& p2) const {
	// Luodaan ja palautetaan jana tästä pisteestä ja toisesta.
	// Luokkien jäsenfunktioissa this on osoitin olioon, josta
	// funktiota on kutsuttu. Sitä voidaan siis käyttää, jos nykyistä
	// oliota pitää pystyä käyttämään kokonaisena piste-oliona kuten nyt.
	jana j = {*this, p2};
	return j;
}

int main() {
	// Luodaan pisteet (2.1, 5.2) ja (4.1, 6.7). Pisteitä ei tarvitse
	// muuttaa myöhemmin, joten määritellään ne vakioiksi.
	const piste a = {2.1, 5.2}, b = {4.1, 6.7};

	// Luodaan jana pisteestä a pisteeseen b jäsenfunktion avulla.
	// Janaa muokataan vielä, joten se ei voi olla vakio.
	jana j = a.yhdista(b);

	// Tulostetaan janan tiedot ja kasvatetaan janaa kolmesti.
	// Janan pituus lasketaan jäsenfunktion avulla,
	// ja samoin tapahtuu kasvattaminenkin.
	for (int i = 0; i < 3; ++i) {
		std::cout
			<< "Jana alkaa pisteestä (" << j.a.x << ", " << j.a.y << ")"
			<< " ja päättyy pisteeseen (" << j.b.x << ", " << j.b.y << ")"
			<< "." << std::endl;
		std::cout << "Janan pituus on " << j.pituus() << "." << std::endl;
		j.kaksinkertaista();
	}
}

Etenkin isommissa ohjelmissa jokaisen luokan määrittely kirjoitetaan erilliseen otsikkotiedostoon ja jäsenfunktioiden määrittelyt luokalle varattuun kooditiedostoon. Edelleen pätevät samat esittely- ja määrittelysäännöt kuin aiemminkin: luokan määrittelyn pitää sisältyä kaikkiin käännösyksiköihin, joissa luokkaa käytetään, ja jäsenfunktiot täytyy määritellä erillisessä tiedostossa, jotta niiden määrittelyt olisivat ohjelmassa vain kerran. Inline-funktiot kuitenkin saavat kuitenkin sisältyä jokaiseen käännösyksikköön. Usean tiedoston käyttöä esitellään tässä koodivinkissä.

Jäsenten näkyvyysalue

Luokan jäsenfunktioissa voi käyttää kaikkia luokan jäseniä, myös niitä, jotka esitellään vasta myöhemmin. Seuraava koodi on siis täysin laillinen:

struct S {
	// Jäsenfunktio:
	void f() {
		// Käytetään jäsenmuuttujaa x.
		x = 10;
	}
	// Jäsenmuuttuja esitellään kuitenkin vasta täällä!
	int x;
};

Luokan ulkopuolella sen jäsenet tunnistetaan vasta, kun koko luokka on määritelty. Tämän takia aiemmin piste::yhdista-funktio piti kirjoittaa vasta jana-luokan määrittelyn jälkeen. Seuraavassa koodissa funktio hylatty ei toimi mutta sen sijaan funktio onnistunut toimii:

// Esitellään luokka. Muuten sen nimi olisi tuntematon ja aiheuttaisi virheen.
struct yritys;

// Tämän funktion parametrina on viittaus yritys-tyyppiseen olioon.
void hylatty(yritys& y) {
	// Tässä yritetään käyttää olion jäsentä. Tämä on virhe,
	// koska olion jäseniä ei ole vielä esitelty!
	y.arvosana = 0;
}

// Jäsenet esitellään vasta luokan määrittelyssä.
struct yritys {
	int arvosana;
};

// Tämä funktio toimii, koska luokka määriteltiin juuri yllä.
void onnistunut(yritys& y) {
	y.arvosana = 5;
}

Suojamääreet

Tähän asti käsiteltyjen luokkien jäsenet ovat julkisia (engl. public), eli kaikilla on oikeus käyttää niitä. On myös kaksi muuta vaihtoehtoa: yksityinen (engl. private) ja suojattu (engl. protected). Yksityisiä jäseniä voivat käyttää vain luokan omat jäsenfunktiot sekä erikseen määritellyt ystävät (engl. friend). Suojattuja jäseniä voivat näiden lisäksi käyttää luokasta johdetut toiset luokat. (Luokkien johtamista käsitellään opassarjan myöhemmissä osissa.) Rajoitukset asetetaan suojamääreillä (engl. access specifier).

#include <iostream>
#include <string>

struct salaisuus {
	// julkisia:
	int julkinen;
private:
	// yksityisiä:
	int yksityinen;
	int salafunktio() const {
		return 123;
	}
protected:
	// suojattuja:
	int suojattu;
public:
	// julkisia (taas):
	int julkkis;

	// Lisätään luokalle ystäväksi funktio ja toinen luokka.
	// Rivit vastaavat esittelyjä, mutta edessä on sana friend.
	friend struct media;
	friend void muuta(salaisuus& s);
};

// Tämä luokka saa käyttää salaisuuden kaikkia osia.
struct media {
	std::string nimi;
	void kerro(const salaisuus& s) {
		std::cout
			<< nimi << " paljastaa: "
			<< s.yksityinen << ", "
			<< s.salafunktio() << std::endl;
	}
};

// Tämä funktio saa käyttää salaisuuden kaikkia osia.
void muuta(salaisuus& s) {
	s.yksityinen = 112358;
	s.suojattu = 314159;
}

// Muut eivät saa käyttää kuin julkisia osia.
int main() {
	salaisuus s;
	muuta(s);
	media m = {"Salainen media"};
	m.kerro(s);
	// s.suojattu = 0; // VIRHE! Kokeile, mitä kääntäjä sanoo.
}

Oikeanlainen suojamääreiden käyttö on hyvä tapa. Yksi olio-ohjelmoinnin periaatteista on, että jokaisen olion täytyy säilyttää tilansa järkevänä, ja tämä ei onnistu, jos ohjelmoija voi milloin tahansa olion ulkopuolelta muuttaa sen tietoja hallitsemattomasti. Toki pienessä ohjelmassa voi muutenkin muistaa, mitä saa tehdä ja mitä ei, mutta kun on paljon mutkikasta koodia ja useampi ohjelmoija, tällaiset selkeät rajoitukset ovat tervetulleita.

Luokat class-sanalla

Sana struct on peräisin C++:aa edeltäneestä C-kielestä, jossa ei ole luokkia lainkaan. C++ sisältää luokkien määrittelyyn myös uuden sanan class, jota käytetään monissa muissakin ohjelmointikielissä. Se eroaa sanasta struct vain yhdessä suhteessa: siinä missä struct-sanalla määritellyn luokan jäsenet ovat oletusarvoisesti julkisia, class-sanalla määritellyn ovat yksityisiä. Seuraavat kaksi luokkaa ovat siis täsmälleen samat:

struct S {
private:
	int a;
public:
	int b;
	void f();
};

class C {
	int a; // private
public:
	int b;
	void f();
};

Moni ajattelee, että struct on pelkästään tavallisia tietueita varten ja vain class kelpaa oikeaan olio-ohjelmointiin. C++:n standardi ei kuitenkaan mainitse asiasta suuntaan tai toiseen, joten jää jokaisen omalle vastuulle päättää, milloin käyttää mitäkin sanaa.


Kommentit

ErroR++ [16.04.2011 18:24:09]

Lainaa #

Aika hyvä...

Weggo [06.09.2011 18:28:53]

Lainaa #

preferred olio passing on by-reference eikä by-pointer, joten olisi hyvä jos oppaissa käytettäisiin C++:san ominaisuuksia

Metabolix [07.09.2011 16:21:41]

Lainaa #

Weggo, mistähän kohdasta nyt olet tyytymätön? Ensimmäisessä esimerkissä käytetään osoitinta, jotta funktio vastaisi mahdollisimman tarkasti luokan jäsentä (jossa this on nimenomaan osoitin eikä viittaus). Muualla on nähdäkseni käytetty viittauksia.

Uskottavuuttasi lisäisi myös, jos osaisit kirjoittaa suomea.

Weggo [07.09.2011 17:08:28]

Lainaa #

Yleensä osoitinta käytetään suoraan muistin manipulointiin keosta, eikä parametreina jos tarkoituksena on vain luokan sisäisen jäsenen muutto. Osoittimen käyttö lisää myös riskiä varaamattoman muistialueen manipuloimisen, joka kaataa sovelluksen. Lisäksi 'const' osoittimen perässä on turha, koska se ei muuta itse olion osoitinta joka annettiin parametrina funktiolle. Näin ollen viittauksen käyttö on sekä turvallisempaa että selvempää kuin 'dumb pointer':in käyttö.

Kirjoita kommentti

Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.

Muista lukea keskustelun ohjeet.
Tietoa sivustosta