Kirjautuminen

Haku

Tehtävät

Oppaat: C++-ohjelmointi: Osa 7 - Viittaukset, osoittimet ja dynaaminen muisti

  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 (2009).

Tähän asti dataa on käsitelty nimettyjen muuttujien välityksellä, jolloin kääntäjä on pitänyt huolen siitä, missä kohti tietokoneen muistia tiedot ovat. Tähän liittyy kuitenkin ongelmia: Muuttujan näkyvyysalue rajoittaa usein sitä, miten muuttujaa voi käyttää, eikä funktion parametrin arvon muuttaminen vaikutakaan muuttujaan, jonka arvo funktiolle annettiin funktiokutsussa. Lisäksi muuttujien, tietueiden ja taulukoiden koko on ennalta määrätty, joten ohjelmoijan pitäisi valmiiksi päättää, miten isoja tiedostoja tekstinkäsittelyohjelmalla voi käsitellä tai miten isoja kuvia piirustusohjelmalla voi piirtää. Näihin pulmiin on onneksi ratkaisuja.

Viittaukset

Funktion parametrit ovat itsenäisiä muuttujia. Parametrin arvon muuttaminen funktion sisällä ei vaikuta arvoihin muualla ohjelmassa:

#include <iostream>

void f(int x) {
	x = 123;
	std::cout << "x = " << x << std::endl;
	// "x = 123"
}

int main() {
	int luku = 0;
	f(luku);
	std::cout << "luku = " << luku << std::endl;
	// "luku = 0"
}

Joskus arvoa on tarpeen muuttaa. Paras keino tähän on antaa funktiolle arvon sijaan viittaus (engl. reference) muuttujaan. Viittaus on kuin uusi nimi samalle muuttujalle: itse viittausta ei voi funktiossa muuttaa, vaan kaikki operaatiot kohdistuvat muuttujaan, johon viitataan. Viittausta merkitään tyypin ja nimen väliin kirjoitettavalla &-merkillä.

#include <iostream>

// Parametrin tyyppi on int& eli viittaus int-muuttujaan.
void f(int& x) {
	x = 123;
	std::cout << "x = " << x << std::endl;
	// "x = 123"
}

int main() {
	int luku = 0;
	f(luku);
	std::cout << "luku = " << luku << std::endl;
	// "luku = 123"
}

Käyttämällä yhtä tai useampaa viittausta äskeisen esimerkin tapaan funktio voi myös palauttaa useita arvoja. Seuraavassa esimerkissä funktio palauttaa käyttäjältä pyydetyt tiedot sijoittamalla ne viittauksena annettuun muuttujaan. Lisäksi funktio palauttaa perinteiseen tapaan bool-arvon, joka kertoo, antoiko käyttäjä kelvolliset arvot.

#include <iostream>

struct piste {
	float x, y;
};

// Parametrin tyyppi on piste& eli viittaus piste-muuttujaan.
bool pyyda(piste& p) {
	std::cout << "Anna x-koordinaatti: ";
	// Luetaan ja tarkistetaan onnistuminen.
	std::cin >> p.x;
	if (!std::cin.good()) {
		// Meni pieleen, lopetetaan kesken.
		return false;
	}
	std::cout << "Anna y-koordinaatti: ";
	std::cin >> p.y;
	// Tarkistetaan onnistuminen.
	return std::cin.good();
}

int main() {
	piste p;
	// Pyydetään tiedot muuttujaan p.
	// Todellisesta paluuarvosta nähdään, onnistuiko lukeminen.
	if (pyyda(p)) {
		std::cout << "Annoit pisteen (" << p.x << ", " << p.y << ")." << std::endl;
	} else {
		std::cout << "Annoit virheellisen syötteen!" << std::endl;
	}
}

Viittaus on myös hyödyllinen silloin, kun välitettävä parametri on suurikokoinen tietue: suuren tietomäärän kopioiminen uuteen paikkaan funktiota varten voi olla hidasta, kun taas viittaus ei vaadi paljonkaan tilaa.

Jos funktiolle annettavaa muuttujaa ei kuitenkaan ole tarkoitus muuttaa, viittauksen voi määritellä vakioksi. On myös koko joukko tilanteita, joissa todella viitataan vakioon ja viittauksenkin on pakko olla vakioviittaus. Näitä tilanteita tulee vastaan erityisesti myöhemmin käsiteltävien C++:n standardikirjaston luokkien kanssa.

Viittauksia voi käyttää muuallakin kuin funktioiden parametreissa. Tällöin niistä on hyötyä lähinnä siksi, että ne voivat lyhentää mutkikkaita lausekkeita. Seuraavassa koodissa silmukan sisällä toistuva ilmaus taulu[i] korvataan nimeltään lyhyemmällä vakioviittauksella x. Esimerkki on teennäinen; todellista hyötyä lyhentämisesti olisi vasta, jos lyhennettävä lauseke olisi kymmeniä merkkejä pitkä ja toistuisi paljon useammin.

#include <iostream>

int main() {
	int taulu[9] = {14, 26, 78, 12, 34, 61, 65, 82, 42};
	int paras_i = 0, paras_x = taulu[0];
	std::cout << "Taulukko:" << std::endl;
	for (int i = 0; i < 9; ++i) {
		// Luodaan vakioviittaus.
		const int& x = taulu[i];
		// Käytetään viittausta taulu[i]:n sijaan.
		if (x > paras_x) {
			paras_x = x;
			paras_i = i;
		}
		std::cout << "   " << x;
	}
	std::cout << std::endl;
	std::cout << "Luku " << paras_x << " kohdassa taulu[" << paras_i << "] on suurin." << std::endl;
}

Opassarjan myöhemmissä osissa esitellään vielä joitain tilanteita, joissa viittaukset ovat tarpeen.

Osoittimet

Osoitin (engl. pointer) on erityinen muuttuja, joka ilmoittaa jonkin kohdan tietokoneen muistissa. Tavallisen muuttujan tapaan myös osoittimella on tietty tyyppi, joka kertoo, minkä tyyppistä tietoa kyseisessä muistiosoitteessa on. Osoittimen arvo on sen muistipaikan eli tavun järjestysnumero, josta jokin tieto alkaa; jos tieto vie useamman tavun, se sijaitsee osoittimen kertomassa osoitteessa ja sitä seuraavissa muistipaikoissa. Eri tietotyyppien kokoja käsiteltiin opassarjan 2. osassa.

Toiminnaltaan osoittimet muistuttavat suurelta osin viittauksia. Niillä voi kuitenkin tehdä enemmän – myös enemmän virheitä. Keskeisiä eroja on kaksi: osoittimen arvoa voi muuttaa jälkikäteen, ja osoittimen arvo voi olla myös 0, joka merkitsee tyhjää osoitinta. Standardikirjastossa määritellään tyhjiä osoittimia varten vakio NULL, jonka arvo on nolla.

Osoitinta merkitään asteriskilla eli tähtimerkillä tyypin perässä, ennen muuttujan nimeä. Esimerkiksi osoitin int-arvoon on int*. Toisen muuttujan osoite haetaan &-operaattorilla, ja tämän arvon voi sijoittaa oikeanlaiseen osoittimeen. Tämän jälkeen itse dataan voi viitata merkinnällä *osoitin.

// int x eli int-muuttuja nimeltä x.
int x;

// int* p eli int*-muuttuja nimeltä p, siis osoitin int-arvoon.
// Osoittimet on hyvä alustaa varmuuden vuoksi tyhjiksi!
int* p = 0;

// *-merkin ympärillä saa käyttää välejä mielensä mukaan.
int *q = 0;  int * r = 0;  int*s = 0;

// p on tyypiltään int*, sijoitetaan siihen x:n osoite &x.
p = &x;
// *p on tyypiltään int, sijoitetaan siihen luku.
*p = 2; // x = 2;
// Sijoitetaan p:hen lopuksi nolla eli merkitään osoitin tyhjäksi.
p = 0;
// Nyt x on 2. Sen sijaan *p:n käyttäminen olisi virhe.

Osoitinta voi käyttää viittauksen tapaan arvon välittämiseen ulos funktiosta. Funktiolle voi kuitenkin antaa myös tyhjän osoittimen, ja siksi funktion täytyy tarkistaa, että annettu osoitin on kelvollinen. Viittausten kanssa tätä ongelmaa ei käytännössä ole. (Vastaava tilanne syntyy, jos viittauksen arvoksi annetaan tyhjän osoittimen osoittama muuttuja, jota ei siis todellisuudessa olekaan. Tämän voi kuitenkin välttää, kun tarkistaa osoittimen arvon ennen virheellisen viittauksen antamista.)

#include <iostream>

// Kolmas parametri: osoitin int-muuttujaan; oletusarvona nolla eli tyhjä.
int jako(int a, int b, int* jaannos = 0) {
	// Täytyy tarkistaa, että osoitin osoittaa jonnekin.
	// Jos osoite on 0 eli "tyhjä", jakojäännöstä ei lasketa.
	// Tarkistuksen voi kirjoittaa mm. seuraavilla tavoilla:
	// if (jaannos), if (jaannos != 0), if (jaannos != NULL)
	if (jaannos) {
		// Jos osoite on muu kuin nolla, tallennetaan jakojäännös.
		// *jaannos tarkoittaa osoitettavaa objektia (muuttujaa).
		*jaannos = a % b;
	}
	// Osoittimesta riippumatta palautetaan itse jakolaskun tulos.
	return a / b;
}

int main() {
	int a = 7, b = 3, tulos, jaannos;
	// Lasketaan; kolmas parametri on jaannos-muuttujan osoite.
	tulos = jako(a, b, &jaannos);

	// Tulostetaan murtoluku (esim. 7/3) sekalukuna (esim. 2 1/3).
	std::cout << a << "/" << b << " = ";
	if (tulos || !jaannos) {
		std::cout << tulos;
	}
	if (jaannos) {
		if (tulos) {
			std::cout << " ";
		}
		std::cout << jaannos << "/" << b;
	}
	std::cout << std::endl;
}

Osoitin on tavallaan vain lukuarvo, joka kertoo muistipaikan numeron, ja siksi lukuarvon voikin muuttaa osoittimeksi. Tälle on melko vähän käyttöä, mutta esimerkiksi DOS-ohjelmoinnissa tai omaa käyttöjärjestelmää tehdessä pitää joskus saada osoittimeen juuri ennalta määrätty osoite.

// Tämä koodi ei toimi tavallisena ohjelmana esim. Windowsissa tai Linuxissa!
// Koodi toimii BIOS-pohjaisella x86-koneella 32-bittisessä tilassa silloin,
// kun käyttöjärjestelmä ei estä muistin mielivaltaista käsittelyä.
int main() {
	// VGA-näyttömuistin osoite on 16-järjestelmässä ilmaistuna A0000.
	// Luvusta tehdään osoitin tavallisella tyypinmuunnoksella.
	char* nayttomuisti = (char*) 0xA0000;

	// Täytetään näyttömuisti sillä ja tällä.
	for (int i = 0; i < 0x20000; ++i) {
		nayttomuisti[i] = (char) i;
	}
}

Osoittimet taulukoihin

Osoitinta voi käyttää myös taulukkojen yhteydessä: kun tunnetaan taulukon ensimmäisen alkion osoite, voidaan laskea seuraavienkin alkioiden osoitteet. Funktion parametrina tällaista taulukko-osoitinta voi merkitä myös tyhjillä hakasuluilla parametrin nimen perässä.

#include <iostream>
#include <cstddef>
// Jälkimmäinen otsikkotiedosto varmistaa, että vakio NULL on määritelty.
// Sen arvo on aina 0, mutta sitä tavataan perinteiden vuoksi käyttää
// merkitsemään nimenomaan osoittimen arvoa 0.

// Seuraava funktio etsii taulukosta pyydetyn luvun.
// taulu: lukutaulukko eli osoitin taulukon alkuun.
// maara: taulukon pituus.
// etsittava: etsittävä luku.
// paluuarvo: osoitin lukuun, jos se löytyi, tai muuten 0-osoitin.
// Käytetään taulukko-osoittimen merkintään tyhjiä hakasulkuja.
const int* etsi(const int taulu[], const int maara, const int etsittava) {
	// Täytyy tarkistaa, että annettu osoitin on kelvollinen.
	if (taulu == NULL) {
		// Muuten voidaan heit palauttaa 0-osoitin eli NULL-osoitin.
		return NULL;
	}
	for (int i = 0; i < maara; ++i) {
		// Osoitin toimii kuten taulukko:
		if (taulu[i] == etsittava) {
			// &taulu[i] palauttaa alkion taulu[i] osoitteen.
			return &taulu[i];
		}
	}
	// Ei löytynyt, palautetaan 0-osoitin eli NULL-osoitin.
	return NULL;
}

// Sama funktio eri merkinnöillä.
// Käytetään osoittimen tavallista merkintätapaa eli asteriskia.
const int* etsi2(const int* taulu, const int maara, const int etsittava) {
	// NULL-tarkistuksen voi tehdä yksinkertaisemmin huutomerkillä
	// tai vertaamalla osoitinta nollaan.
	if (!taulu) {
		// NULL-vakion sijaan voi hyvin käyttää tavallista nollaa.
		return 0;
	}
	for (int i = 0; i < maara; ++i) {
		// Osoittimilla voi myös laskea: taulu + i tarkoittaa
		// osoitetta, joka on i lukua edempänä kuin se, mitä itse
		// taulu osoittaa. Kääntäjä huomioi tässä alkion koon,
		// eli tulos riippuu siitä, onko taulu int-osoitin vai
		// esimerkiksi char-osoitin (char vie yhden tavun, int useita).
		// Tätä uutta osoitinta voi taas käyttää *:n avulla:
		if (*(taulu + i) == etsittava) {
			return taulu + i;
		}
	}
	return 0;
}

// Seuraava funktio tulostaa etsinnän tuloksen.
// taulu: lukutaulukko, josta haku on tehty.
// osuma: osoitin löydettyyn lukuun.
void tulosta(const int* taulu, const int* osuma) throw (const char*) {
	// Jos taulua ei annettu, heitetään poikkeus.
	if (!taulu) {
		throw "Taulu puuttuu!";
	}
	// Jos jotain löytyi
	if (osuma) {
		// Osoitinten välisen etäisyydenkin voi laskea:
		// a - b kertoo, montako alkiota osoitinten välissä on,
		// eli osuma - taulu kertoo osuman indeksin taulukossa.
		int i = (osuma - taulu);
		std::cout << "Luku " << *osuma << " on kohdassa " << i << std::endl;
	} else {
		std::cout << "Haettua lukua ei löytynyt." << std::endl;
	}
}

int main() {
	int t[10] = {1, 2, 3, 5, 8, 13, 21, 34, 55, 89};
	const int haettava = 34;

	// Etsitään taulukosta luku.
	// Taulukon nimi toimii itsessään osoittimena, eli t == &t[0].
	const int* osuma = etsi(t, 10, haettava);

	// Tulostetaan hakutulos.
	tulosta(t, osuma);

	// sizeof(t) kertoo taulukon koon (tavuina),
	// sizeof(t[0]) kertoo yhden alkion koon,
	// jakolaskun tulos on siis alkioiden määrä.
	// Etsintäfunktion sisällä tätä ei voi käyttää, koska siellä
	// tunnetaan vain yksittäinen osoitin mutta taulukosta ei ole tietoa!
	const int* osuma2 = etsi2(t, sizeof(t) / sizeof(t[0]), haettava);

	// Osoittimia voi myös vertailla, jolloin selviää,
	// osoittavatko ne samaan paikkaan.
	if (osuma2 == osuma) {
		std::cout << "Toinen haku antoi saman tuloksen." << std::endl;
	} else {
		std::cout << "Ohjelmassa taitaa olla bugi..." << std::endl;
	}
}

Jos osoitin osoittaa johonkin rakenteeseen (struct, union, class), rakenteen jäseniin pääsee käsiksi pisteen sijaan nuolella (->) tai muutamalla mutkikkaammalla tavalla:

struct piste {
	float x, y;
};

piste a;
piste* p = &a;
// Jäseniä käsitellään nuolella:
p->x = 1;
// *p tarkoittaa a:ta, joten (*p).x tarkoittaa a.x:ää.
(*p).x = 2;
// Osoitinta voi käyttää myös taulukon tapaan. Nyt osoitin osoittaa
// tosin vain yhteen muuttujaan, joten [0] on ainoa kelvollinen indeksi.
p[0].x = 3;

Jos ohjelmassa käyttää virheellistä tai tyhjää osoitinta, ohjelma voi kaatua tai toimia hyvin kummallisesti. Osoitinten kanssa pitää siis olla erittäin tarkkana! C++-ohjelmissa on turvallisempaa käyttää viittauksia; teknisesti nämä kaksi toimivat yleensä aivan samalla tavalla, mutta kääntäjä rajoittaa viittausten käyttöä niin, ettei virheitä tule yhtä helposti.

Osoitin void-tyyppiin ja tyypinmuunnokset

C-kielen (ja siten myös C++:n) standardikirjastossa on funktio, joka kopioi dataa muistissa paikasta toiseen. Kopioitavan muistin määrä ilmoitetaan funktiolle tavuina, ja datan tyypillä ei ole merkitystä. Siksi on tarkoituksenmukaista, että funktiolle voi antaa minkä tahansa osoittimen. Tällaisessa tilanteessa osoittimen osoittamaksi tietotyypiksi voidaan laittaa void; minkä tahansa osoittimen voi muuttaa void-osoittimeksi.

// Funktion parametrit ovat
// - void-osoitin, joka kertoo, minne data kopioidaan,
// - void-osoitin, joka kertoo, mistä dataa kopioidaan, ja
// - kokonaisluku, joka kertoo, montako tavua kopioidaan.
// Toisen parametrin const tarkoittaa, että osoittimen
// osoittamaa muistialuetta ei muokata funktiossa.
void kopioi(void* kohde, const void* lahde, unsigned int tavuja);

Tällaisella osoittimella ei voi tehdä juuri mitään. Osoittimenkin tyyppiä voi kuitenkin muuttaa; esimerkiksi kopiointifunktiossa osoittimet voitaisiin muuttaa tavuosoittimiksi, jotta datan voi kopioida tavu kerrallaan. Tyypinmuunnos tapahtuu kuten tavallisillakin tietotyypeillä. Tuloksena on osoitin toisenlaiseen dataan. On hyvin tärkeä muistaa, että tyypinmuunnos ei muuta itse dataa. Esimerkiksi int-osoittimen muuttaminen float-osoittimeksi on harvoin järkevää!

#include <iostream>

// Funktio kopioi määrätyn määrän tavuja kohdasta toiseen muistissa.
// Tällainen olisi standardikirjastossakin (<cstring>, std::memcpy).
void kopioi(void* kohde, const void* lahde, unsigned int tavuja) {
	// Muutetaan parametreina annetut osoittimet käsiteltävään muotoon.
	char* kohde_c = (char*) kohde;
	const char* lahde_c = (const char*) lahde;

	// Lasketaan osoitin kohtaan, jota ei enää kopioida.
	const char* lahde_loppu_c = (const char*) lahde + tavuja;

	// Kopioidaan data tähän asti.
	while (lahde_c != lahde_loppu_c) {
		// Kopioidaan merkki, siirretään osoittimia eteenpäin.
		*kohde_c = *lahde_c;
		++lahde_c;
		++kohde_c;
	}
}

// Funktio tulostaa osoitetusta taulukosta short-lukuja.
void tulosta(const short* luvut, int maara) {
	int i;
	// a, b, c, ...,
	for (i = 0; i < maara - 1; ++i) {
		std::cout << luvut[i] << ",\t";
	}
	// ..., d.
	std::cout << luvut[i] << "." << std::endl;
}

// Apufunktio pienemmän arvon laskemiseen.
inline int pienempi(int a, int b) {
	return a < b ? a : b;
}

int main() {
	// Täytetään ja tulostetaan float-taulukko.
	const int maara = 8;
	float luvut_float[maara];
	for (int i = 0; i < maara; ++i) {
		luvut_float[i] = i * (i + 15) * 123;
		if (i < maara - 1) {
			std::cout << luvut_float[i] << ",\t";
		} else {
			std::cout << luvut_float[i] << "." << std::endl;
		}
	}

	// Kopioidaan muistia float-taulukosta short-taulukkoon.
	// Pitää muistaa kopioida tavuja pienemmän taulukon mukaan!
	// Kopiointifunktiolle voi antaa mitä tahansa osoitintyyppejä;
	// tässä short* ja float* muutetaan automaattisesti void*-tyyppisiksi.
	short luvut_short[maara];
	const int koko = pienempi(sizeof(luvut_float), sizeof(luvut_short));
	kopioi(luvut_short, luvut_float, koko);

	// Tulostetaan tulostusfunktiolla kopioitu taulukko.
	// Tulostetaan myös alkuperäinen, mutta muutetaan osoittimen tyyppi.
	// (Funktio ei kelpuuta float-taulukkoa vaan vaatii short-taulukon.)
	// Tulos on aivan pielessä, koska muistissa on molemmissa tapauksissa
	// oikeasti float-lukuja. (Kuitenkin nämä kaksi ovat keskenään samat.)
	tulosta(luvut_short, maara);
	tulosta((short*) luvut_float, maara);
}

Dynaaminen muistinvaraus

Harvassa todellisessa ohjelmassa tiedetään ennalta tarkalleen, minkä verran dataa pitää käsitellä. Tekstitiedosto voi olla muutaman rivin tai koko kirjan mittainen; kuva voi olla ohjelman kuvake tai tarkka satelliittikuva koko Euroopasta. Olisi tuhlausta varata muistia suurimman mahdollisuuden mukaan, ja samalla kävisi niin, ettei ohjelma ehkä toimisi lainkaan tietokoneilla, joilla ei ole tarpeeksi muistia suurinta tapausta varten. Onneksi muistia voi varata myös kesken ohjelman – ja tämä onkin osoitinten tärkein käyttötarkoitus.

Muistia voi varata new-operaattorilla ja vapauttaa delete-operaattorilla.

#include <iostream>

int main() {
	// Määritellään osoitin.
	int* lkm;
	// Varataan int-muuttujan verran muistia ja otetaan osoite talteen.
	lkm = new int;

	// Osoitin toimii aiemmin opittuun tapaan.
	std::cout << "Montako lukua laitetaan?" << std::endl;
	std::cin >> *lkm;

	// Vaaditaan luku väliltä 1 - 10.
	if (*lkm <= 0 || *lkm > 10) {
		std::cout << "Määrä ei kelpaa." << std::endl;
		return 0;
	}

	// Määritellään osoitin ja varataan *lkm luvun taulukko.
	float* taulu = new float[*lkm];

	// Pyydetään käyttäjältä luvut.
	// Osoitin toimii taas aiemmin opittuun tapaan.
	for (int i = 0; i < *lkm; ++i) {
		std::cout << "Anna luku: ";
		std::cin >> taulu[i];
	}

	// Tulostetaan luvut.
	for (int i = 0; i < *lkm; ++i) {
		std::cout << (i+1) << ". luku: " << taulu[i] << std::endl;
	}

	// Vapautetaan muisti. Jos näin ei tehdä, tämäkin muisti jää varatuksi,
	// ja lopulta muisti voi loppua kesken. Nykyaikaiset käyttöjärjestelmät
	// onneksi vapauttavat lopun muistin, kun ohjelma sammuu.
	delete lkm;
	// Taulukoiden vapauttamiseen tarvitaan vielä hakasulut.
	delete [] taulu;
}

Kun ohjelmista tulee mutkikkaampia, muistin varaamiseen ja vapautukseen tulee helposti virheitä. Ehtolauseiden takia muisti voi jäädä vapauttamatta, tai jossain saatetaan vahingossa käyttää osoitinta, jonka muisti on jo vapautettu. Näiden ongelmien välttämiseksi kannattaakin käyttää muistin varaamiseen standardikirjaston valmiita luokkia kuten std::string ja std::vector. Niissä kaikki tarvittavat new- ja delete-rivit on piilotettu ohjelmoijalta, ja olioiden ansiosta dynaamisestikin varattu muisti saadaan noudattamaan tuttuja sääntöjä muuttujien elinajoista. Luokkia yleisesti käsitellään opassarjan 9. osasta eteenpäin.

Funktio-osoittimet

Datan lisäksi myös funktioihin voi osoittaa osoittimilla. Funktioiden ohjelmakoodi on jossain kohti tietokoneen muistia, ja osoittimen arvoksi tulee tämä kohta. Funktio-osoitinta voi suoraan käyttää funktiokutsuissa funktion oikean nimen paikalla. Funktio-osoittimen tyyppiin sisältyvät samat tiedot kuin funktioiden tyyppeihin: paluuarvo ja parametrit.

Funktio-osoittimien määrittelyyn on kaksi tapaa. Yleensä on selkeämpää nimetä funktion tyyppi typedef-rivillä, mutta on mahdollista myös sisällyttää funktion tyyppi suoraan osoittimen määrittelyyn.

#include <iostream>

// Funktioita, jotka ottavat kaksi float-lukua ja myös palauttavat float-luvun.
float summa(float a, float b) {
	return a + b;
}
float erotus(float a, float b) {
	return a - b;
}

// Määritellään osoitin tyypiltään tällaiseen funktioon.
// Sijoitetaan osoittimeen summa-funktion osoite.
// (Tätä osoitinta ei käytetä ohjelmassa, tämä on vain malliksi.)
float (*osoitus)(float, float) = summa;

// Määritellään funktion tyyppi erikseen typedef-rivillä, ja määritellään
// sitten osoitin typedef-nimen avulla. Globaalit muuttujat nollataan
// automaattisesti, joten osoittimen alkuarvo on 0.
typedef float laskufunktio(float, float);
laskufunktio* lasku;

int main() {
	char toimitus = 0;
	std::cout << "Anna + tai - sen mukaan, lasketaanko summa vai erotus: ";
	std::cin >> toimitus;
	if (toimitus == '+') {
		// Sijoitetaan osoittimeen summafunktion osoite.
		lasku = summa;
	} else if (toimitus == '-') {
		// Sijoitetaan osoittimeen erotusfunktion osoite.
		lasku = erotus;
	}

	float a = 0, b = 0;
	std::cout << "Anna laskettavat luvut, esim. 12.3 45.6: ";
	std::cin >> a >> b;

	// Tulostetaan lauseke
	std::cout << a << ' ' << toimitus << ' ' << b << " = ";

	// Funktio-osoitintakin voi verrata arvoon 0 tai NULL.
	if (lasku == 0) {
		std::cout << "tuntematon; valitsit huonon laskun!" << std::endl;
	} else {
		// Kutsussa osoitin toimii aivan kuin tavallinen funktiokin.
		std::cout << lasku(a, b) << "." << std::endl;
	}
}

Funktio-osoittimia voisi järkevästi käyttää esimerkiksi funktiossa, joka tekee kaikille lukutaulukon jäsenille saman laskutoimituksen:

#include <iostream>

// Muunnosfunktion tyyppi: parametrina ja paluuarvona int.
typedef int muunnos(int);

// Tämä funktio muuntaa annetut luvut annetulla funktiolla.
void muunna(int* luvut, const int maara, muunnos* funktio) {
	for (int i = 0; i < maara; ++i) {
		// Funktio-osoitinta käytetään aivan tavallisen funktion tavoin.
		luvut[i] = funktio(luvut[i]);
	}
}

// Tämä funktio laskee luvun toisen potenssin eli neliön.
// Tyyppi on oikea muunnosfunktion tyyppi.
int nelio(int x) {
	return x * x;
}

// Tämä funktio tulostaa taulukon luvut.
void tulosta(int* luvut, const int maara) {
	for (int i = 0; i < maara; ++i) {
		if (i < maara - 1) {
			std::cout << luvut[i] << ",\t";
		} else {
			std::cout << luvut[i] << "." << std::endl;
		}
	}
}

int main() {
	const int maara = 10;
	int luvut[maara];

	// Täytetään ja tulostetaan taulukko.
	for (int i = 0; i < maara; ++i) {
		luvut[i] = i;
	}
	tulosta(luvut, maara);

	// Muunnetaan kaikki taulukon luvut funktiolla nelio ja tulostetaan.
	// Funktion nimi kelpaa suoraan funktio-osoittimen arvoksi.
	muunna(luvut, maara, nelio);
	tulosta(luvut, maara);
}

Myös monien kirjastojen kanssa tarvitaan funktio-osoittimia. Esimerkiksi C:n standardikirjaston lajittelufunktio qsort tarvitsee funktion, joka osaa verrata kahta taulukon alkiota. Myös C++:n vastaava sort-funktio voi käyttää lajitteluun funktiota.

#include <iostream>
#include <algorithm> // C++:n sort-funktio
#include <cstdlib>   // C:n qsort-funktio

// Funktio luvun numeroiden summan laskemiseen.
int numeroiden_summa(unsigned int i) {
	int s = 0;
	while (i != 0) {
		// Otetaan jakojäännöksellä viimeinen numero.
		s += i % 10;
		// Jaetaan 10:llä, jolloin viimeinen numero putoaa pois.
		i /= 10;
	}
	return s;
}

// C:n qsort-funktiota varten vertailufunktio. Vertaillaan lukujen
// numeroiden summia. Funktio saa osoittimet vertailtaviin asioihin,
// ja sen pitää palauttaa vertailun tuloksen mukaan jokin kokonaisluku:
//  *            0, jos vertailtavat ovat yhtäsuuret,
//  * negatiivinen, jos a kuuluu ennen b:tä,
//  * positiivinen, jos a kuuluu b:n jälkeen.
int vertailu_c(const void* a, const void* b) {
	// Muutetaan void-osoittimet int-osoittimiksi, otetaan luku ja
	// lasketaan numeroiden summa funktiolla.
	int as = numeroiden_summa(* (const unsigned int*) a);
	int bs = numeroiden_summa(* (const unsigned int*) b);

	// Palautetaan vähennyslaskun tulos; sen etumerkki on tässä oikea.
	return as - bs;
}

// C++:n sort-funktiota varten vastaavanlainen vertailufunktio.
// Funktio saa parametreinaan vertailtavat asiat, ja sen pitää palauttaa
// true, jos a kuuluu ennen b:tä, ja muuten false.
bool vertailu_cpp(unsigned int a, unsigned int b) {
	// Pienimmät ensin: jos a < b, palautetaan true; a kuuluu ennen b:tä.
	return numeroiden_summa(a) < numeroiden_summa(b);
}

int main() {
	const int maara = 10;
	unsigned int c[maara], cpp[maara];
	const int kerroin = 17;

	// Täytetään taulukot.
	for (int i = 0; i < maara; ++i) {
		c[i] = cpp[i] = i * kerroin;
	}
	// Lajitellaan.
	// C:n qsort-funktio tarvitsee osoittimen taulukkoon,
	// alkioiden määrän ja koon sekä vertailufunktion osoittimen.
	std::qsort(c, maara, sizeof(int), vertailu_c);
	// C++:n sort-funktio tarvitsee osoittimen taulukon alkuun ja
	// loppua seuraavaan kohtaan sekä vertailufunktion.
	std::sort(cpp, cpp + maara, vertailu_cpp);

	// Tulostetaan.
	std::cout << "qsort:\tluku\tsumma\t\t" "sort:\tluku\tnumeroiden summa" << std::endl;
	for (int i = 0; i < maara; ++i) {
		std::cout
			<< (c[i] / kerroin) << " * " << kerroin << "\t= "
			<< c[i] << "\t" << numeroiden_summa(c[i]) << "\t\t"
			<< (cpp[i] / kerroin) << " * " << kerroin << "\t= "
			<< cpp[i] << "\t" << numeroiden_summa(cpp[i])
			<< std::endl;
	}
}

Osoittimet ja kirjastot

Osoittimet olivat olemassa jo C-kielessä, viittaukset sen sijaan eivät. Suuri osa kirjastoista on ohjelmoitu C-kielellä, ja siksi niissä käytetään paljon osoittimia. Tilanteita on kahta lajia: niitä, joissa kirjasto hallitsee muistia, ja niitä, joissa vastuu on käyttäjällä.

Monet osoittimet liittyvät kirjaston sisäisiin tietoihin, ja niitä yleensä hallitaankin kirjastojen funktioilla. Esimerkiksi grafiikkakirjasto voisi sisältää tyypin kuva ja funktiot lataa_kuva, piirra_kuva ja tuhoa_kuva. Näistä funktioista ensimmäinen lataisi kuvan annetusta tiedostosta ja palauttaisi osoittimen varaamaansa muistiin. Toiselle pitäisi antaa argumentteina ladattu kuva ja koordinaatit, joihin se piirretään. Kolmas funktio vapauttaisi kuvalle varatun muistin. Ohjelmoijan ei siis tarvitsisi lainkaan kirjoittaa omia new- ja delete-rivejä, vaan muistin varaaminen tapahtuisi lataa_kuva-funktiossa ja vapauttaminen tuhoa_kuva-funktiossa.

On harvinaista, että käyttäjän pitäisi varata muistia kirjaston omille asioille kuten äskeisen esimerkin kuvalle. Sen sijaan useinkin kirjastot palauttavat arvoja osoitinten välityksellä – siis melkein kuin viittausten yhteydessä näytettiin. Näissä tilanteissa täytyy käyttää omia muuttujia, joihin tiedot tallennetaan. Esimerkiksi kuvan mitat voisi saada funktiolta kuvan_koko, joka ottaisi kuvan osoittimen sekä osoittimet lukuihin, joihin tiedot tallennetaan. (Todellisuudessa tällaiset tiedot saa usein haettua suoraan kuva-rakenteesta ilman funktiokutsua.)

Näillä kuvitteellisilla kuvafunktioilla tehty ohjelma voisi näyttää tältä:

#include <iostream>

// Kirjastoilla on omat otsikkotiedostonsa, usein tiedostopääte on .h.
#include <kuvakirjasto.h>

// Otsikon olennainen sisältö:
// struct kuva {...};
// kuva* lataa_kuva(const char* tiedostonimi);
// void kuvan_koko(kuva* k, int* leveys, int* korkeus);
// void piirra_kuva(kuva* k, int x, int y);
// void tuhoa_kuva(kuva* k);

int main() {
	// Kirjaston tyypit esitellään kirjaston otsikossa, ja
	// sen jälkeen niitä voi käyttää normaaliin tapaan.
	kuva* pallo;

	// Ladataan kuva ja tarkistetaan onnistuminen. Usein funktiot
	// palauttavat arvon NULL eli nolla, jos jokin menee vikaan.
	pallo = lataa_kuva("pallo.bmp");
	if (!pallo) {
		std::cout << "Kuvan lataaminen epäonnistui!" << std::endl;
		return 1;
	}

	// Selvitetään kuvan koko funktiolla.
	// (Tiedot saattaisi saada myös suoraan kuva-rakenteesta,
	// esimerkiksi muuttujista pallo->leveys ja pallo->korkeus.)
	int leveys, korkeus;
	kuvan_koko(pallo, &leveys, &korkeus);

	// Piirretään kolme palloa päällekkäin.
	piirra_kuva(pallo, leveys, 0);
	piirra_kuva(pallo, leveys, korkeus);
	piirra_kuva(pallo, leveys, 2 * korkeus);

	// Vapautetaan muisti, kuten asiaan kuuluu.
	tuhoa_kuva(pallo);
}

Kommentit

nerootto [30.08.2009 12:48:08]

#

Kivasti käytetään näissä koodeissa kommenttia.

ankzilla [25.09.2009 12:37:27]

#

Joo kyllä pitäis kaikkien ymmärtää ku kommenttia on yhtä paljo ku koodia. :D

leonarven [20.10.2009 17:19:08]

#

Funktiopointterit olisi myös kiva lisä tähän :)

Metabolix [24.10.2009 18:52:19]

#

leonarven kirjoitti:

Funktiopointterit olisi myös kiva lisä tähän :)

Nyt opas käsittelee myös funktio-osoittimet. Ei kaikkea muista ensimmäisellä kerralla kirjoittaa. :)

punppis [08.05.2010 00:27:41]

#

Miksei taulukon kanssa käytetä tuota *-merkkiä, niinkuin normaalin kokonaisluvun kanssa käytetään?

Tarkoitan siis näitä rivejä:

int* lkm = new int;
std::cin >> *lkm;

float* taulu = new float[*lkm];
std::cin >> taulu[i];

Metabolix [16.09.2010 18:56:49]

#

punppis: Kuten oppaassa mainitaan, p[0] on sama asia kuin *p, ja esimerkiksi p[123] on sama asia kuin *(p + 123). Nämä ovat kaksi täysin toisistaan riippumatonta merkintätapaa.

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 kirjoitusohjeet.
Tietoa sivustosta