Kirjautuminen

Haku

Tehtävät

Oppaat: C++-ohjelmointi: Osa 6 - Esittelyt, määrittelyt ja elinajat

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

C++:n alkeet on nyt suurelta osin käsitelty. Tässä vaiheessa on hyvä pysähtyä katsomaan hieman tarkemmin asioita, joita ei ole vielä käsitelty minkään yksittäisen aiheen kohdalla ja jotka vaativat kaikkien tähän mennessä esitettyjen rakenteiden tuntemista.

Funktioiden esittelyt ja määrittelyt

Kaikki funktiot täytyy esitellä (engl. declare) ennen käyttöä. Funktion esittely (engl. declaration) on koodirivi, joka kertoo kääntäjälle funktion nimen ja tyypin, jotta kääntäjä osaa tulkita oikein koodirivejä, joilla tätä funktiota käytetään. Funktion määrittely (engl. definition; define, määritellä) eli toteutus (engl. implementation) taas on sen varsinainen ohjelmakoodi, ja tämänkin täytyy toki jollain tavalla tulla ohjelmaan mukaan, jotta funktiota voi käyttää.

Määrittely sisältää aina myös esittelyn, ja tähän asti opassarjassa onkin esiintynyt vain määrittelyjä. Seuraava koodi on siis aivan toimiva, vaikkei siinä olekaan erillistä esittelyä:

#include <iostream>

// Määrittely toimii samalla myös esittelynä...
float keskiarvo(const float a, const float b) {
	return (a + b) / 2;
}

int main() {
	// ... joten kääntäjä toimii tässä oikein.
	std::cout << "keskiarvo(3, 6) = " << keskiarvo(3, 6) << std::endl;
}

Funktion esittely on samanlainen kuin funktion määrittelyn alku. Siinä on siis funktion nimi, paluuarvon tyyppi ja parametrien tyypit. Lopussa on puolipiste, kas näin:

float keskiarvo(const float a, const float b);

Tämän esittelyn jälkeen kääntäjä tietää, että koodissa esiintyvä sana keskiarvo tarkoittaa funktiota, joka ottaa kaksi float-parametria ja myös palauttaa samanlaisen liukuluvun. Nyt funktion määrittely voidaan kirjoittaa vaikka ohjelman loppuun tai erilliseen kooditiedostoon.

#include <iostream>

// Esittely on tarpeen...
float keskiarvo(const float a, const float b);

int main() {
	// ... koska muuten kääntäjä ei tiedä, mistä tässä on kyse:
	std::cout << "keskiarvo(3, 6) = " << keskiarvo(3, 6) << std::endl;
}

// Määrittelyn voi kirjoittaa myöhemminkin.
float keskiarvo(const float a, const float b) {
	return (a + b) / 2;
}

Jos koodin alussa olevan esittelyrivin eteen laittaa kommenttimerkinnän //, aiheutuu virheilmoitus. GCC:n ilmoitus näyttää tältä:

esittely.cpp:8: error: 'keskiarvo' was not declared in this scope

Virheilmoitus siis kertoo, että rivillä 8 eli main-funktiossa oleva sana keskiarvo oli kääntäjälle tuntematon.

Esittelyjen merkitys käytännössä

Kaikissa tämänkin oppaan ohjelmissa on käytetty standardikirjaston funktioita. Otsikkotiedostot kuten iostream sisältävät paljon sellaisten funktioiden ja olioiden esittelyjä, joita ei edes määritellä lähdekoodissa. Niiden määrittelyt on valmiiksi käännetty ja pakattu kirjastotiedostoiksi, jotka sitten linkitetään mukaan ohjelmaan käännöksen jälkeen. Paljon vaivaa säästyy, kun standardikirjaston satoja funktioita ei tarvitse kääntää aina uudestaan. Parhaimmillaan säästetään myös tilaa: funktioiden määrittelyt voivat sijaita dynaamisissa kirjastoissa (Windowsin DLL-tiedostoissa tai Linuxin shared object -tiedostoissa), joista ne etsitään vasta, kun ohjelma ajetaan. Näin standardikirjaston tarvitsee olla tietokoneella vain yhdessä paikassa ja kaikki ohjelmat voivat käyttää samaa kappaletta. Ohjelmiin tarvitaan vain sopivat esittelyt kirjastossa olevista asioista.

Omissakin ohjelmissa koodi kannattaa jakaa moneen tiedostoon ainakin, jos projekti on iso. Myös silloin funktion tai muuttujan määrittelyn pitää olla vain yhdessä tiedostossa, vaikka sitä käytetään monessa muussakin kohdassa koodia. Jokaiseen kooditiedostoon, jossa funktiota tai muuttujaa käytetään, täytyy kuitenkin lisätä sen esittely. Kun kaikki kooditiedostot on käännetty ja niitä linkitetään yhteen, funktion tai muuttujan määrittely löytyy ja eri tiedostoissa olevat viittaukset yhdistetään siihen. Koodin jakamista tiedostoihin käsitellään koodivinkeissä 1770 ja 1901.

On myös tilanteita, joissa esimerkiksi jotkin funktiot käyttävät toisiaan ristiin niin, ettei kumpaakaan voi määritellä, ennen kuin toinen on esitelty. Tällaisessa tilanteessa jompikumpi funktio on pakko esitellä alussa. Hyvä ratkaisu on esitellä saman tien molemmat.

Koodilohkot

Koodilohko on ohjelman sisällä tietynlainen rajattu alue. Tyypillisiä lohkoja ovat funktioiden ja komentorakenteiden (if, while yms.) sisällöt ja muuten ilman syytä aaltosuluilla rajatut alueet. Lohkolla on erittäin olennainen merkitys muuttujien kannalta: kaikki paikalliset muuttujat ovat voimassa vain määrittelystään lohkon loppuun.

#include <iostream>

int main(int argc, char** argv) {
	int b = 1;
	// Tässä on main-funktion koodilohko,
	// tähän lohkoon kuuluvat argc, argv ja b.
	if (argc == b) {
		int c = 2;
		// Tässä on if-lauseen sisältämä lohko,
		// muuttuja c on olemassa vain täällä.
		c += 2;
		// if-lauseen lohko päättyy aaltosulkuun...
	}
	// ... ja tässä kohti voimassa on taas vain main-funktion koodilohko.
	{
		int c;
		// Tämä on erillinen koodilohko, vaikka lohkon alussa ei ole
		// if-lausetta tai muuta erityistä. Täällä muuttuja c on eri
		// kuin if-lauseen lohkossa, alkuarvo on määrittelemätön!
		c = b;
	}
}

Muuttujien elinajat

Objektin eli muuttujan datan elinaika voi olla staattinen, dynaaminen tai automaattinen. Staattiset objektit luodaan ohjelman alussa ja tuhotaan vasta ohjelman lopussa, ja niiden arvo on aina aluksi nolla. Käytännössä staattisia ovat globaalit muuttujat, jotka määritellään funktioiden ulkopuolelle, sekä erikseen static-määreellä terästetyt muuttujat. Muuten funktioiden sisällä olevat muuttujat ovat elinajaltaan automaattisia, jolloin ne luodaan aina määrittelyn kohdalla ja tuhotaan saman koodilohkon lopussa. Dynaamisia objekteja taas luodaan new-operaattorilla, tuhotaan delete-operaattorilla ja käsitellään osoitinten välityksellä. Dynaamisesta muistinkäsittelystä kerrotaan kuitenkin vasta myöhemmin. Sekä automaattisten että dynaamisten objektien sisältö on aluksi määrittelemätön!

#include <iostream>

// Globaali vakio, staattinen elinaika.
const int lopetusarvo = 0;

void laskuri() {
	// Paikallinen muuttuja, staattinen elinaika.
	// Arvo säilyy funktiokutsujen välillä, ohjelman alussa nolla!
	static int lkm;
	++lkm;
	std::cout << "staattinen laskuri = " << lkm << "." << std::endl;
}

int main() {
	std::cout << "Kutsutaan funktiota kolmesti." << std::endl;

	// Paikallinen muuttuja, automaattinen elinaika (silmukan ajan)
	for (int i = 0; i < 3; ++i) {
		std::cout << "Kutsu " << (i+1) << ": ";
		laskuri();
	}
	return lopetusarvo;
}

Muuttujien esittelyt ja määrittelyt

Muuttujien esittelyn ja määrittelyn ajatus on aivan sama kuin funktioidenkin: esittely kertoo muuttujasta, ja määrittely käskee kääntäjän varata sille tilaa. Tähän asti oppaissa on esiintynyt vain määrittelyjä. Erillinen esittely on mahdollinen vain, kun muuttuja on globaali. Esittelyyn käytetään sanaa extern:

// Esittelyssä ei varata tilaa, ja sen saa halutessaan kirjoittaa monta kertaa.
extern int x;
extern int x; // Tämä ei ole virhe; kyseessä on yhä sama x.

// Esittelyt eivät saa olla ristiriidassa keskenään.
// Seuraava rivi olisi siis virhe, koska siinä on väärä tyyppi!
// extern char x;

// Määrittelyssä varataan tila. Kaikki muuttujat pitää määritellä tasan kerran.
int x;

Paikallisia eli funktioiden sisällä olevia muuttujia ei voi erikseen esitellä, vaan ne ainoastaan määritellään.

Näkyvyysalueet

Muuttujan tai funktion näkyvyysalue alkaa aina sen esittelystä. Globaalit muuttujat ovat näkyvillä koko koodin loppuun asti, paikallisten muuttujien näkyvyys rajoittuu koodilohkoon.

Tässä koodissa on monta muuttujaa c. Jokainen määrittely luo uuden muuttujan, ja jokaisessa koodilohkossa saa olla vain yksi muuttuja samalla nimellä. Muuttujista viimeisin on aina etusijalla: Kun funktion sisällä määritellään muuttuja c, kaikki operaatiot kohdistuvat siihen, vaikka tarjolla olisikin myös globaali c. Kun vielä for-silmukassa määritellään kolmas c, for-silmukan sisällä käytetään tätä eikä kumpaakaan muuta. Koodin tulostuksesta näkee, mikä muuttuja milloinkin on kyseessä.

#include <iostream>

char c = 'g';

int main() {
	// Tulostetaan globaali c:
	std::cout << "(main) c: " << c << std::endl;

	char c = 'm';
	// Tulostetaan main-funktion c:
	std::cout << "(main) c: " << c << std::endl;

	for (char c = '1'; c < '4'; ++c) {
		// Tulostetaan for-silmukan c:
		std::cout << "\t(for) c: " << c << std::endl;
		if (c == '2') {
			// Tulostetaan for-silmukan c:
			std::cout << "\t\t(if) c: " << c << std::endl;

			char c = 'i';
			// Tulostetaan if-lohkon c:
			std::cout << "\t\t(if) c: " << c << std::endl;
		}
	}
	// Tulostetaan main-funktion c:
	std::cout << "(main) c: " << c << std::endl;
	{
		// Esitellään globaali c uudestaan:
		extern char c;
		// Tulostetaan globaali c:
		std::cout << "\t(lohko) c: " << c << std::endl;
	}
}

Ohjelmassa voi siis olla monta samannimistä muuttujaa, kunhan ne ovat eri koodilohkoissa.

Nimiavaruudet

Tekstin tulostamisen ja syötteen lukemisen yhteydessä on jo esiintynyt erikoisia merkintöjä: std::cin, std::cout ja std::endl. Nämä eivät ole sellaisenaan kelvollisia nimiä, koska kaksoispiste ei saa esiintyä nimessä. Kaksoispisteillä onkin toisenlainen merkitys: ne yhdistävät nimet nimiavaruuteen, josta kääntäjän pitäisi kyseisiä muuttujia etsiä. Tässä tapauksessa esimerkiksi syötettä tulostetaan cout-oliolla, joka sijaitsee nimiavaruudessa std.

Nimiavaruuksien tarkoitus on rajata selkeitä kokonaisuuksia niin, etteivät käytetyt nimet satu samoiksi kuin ohjelmoijan valitsemat nimet. Koko C++:n standardikirjasto on std-nimiavaruuden sisällä, ja monet lisäkirjastot käyttävät omia nimiavaruuksiaan.

Yksinkertainen nimiavaruus voisi näyttää tältä:

// Määritellään nimiavaruus:
namespace avaruus {
	// Määritellään nimiavaruuden sisällä vakio ja funktio:
	const double valonnopeus = 299792458; // m / s
	double metrit_valovuosiksi(double metrit) {
		// Valonnopeus on samassa nimiavaruudessa, joten se toimii
		// ilman avaruus::-etuliikettä (vrt. seuraava koodi)
		return metrit / valonnopeus / 365.25 / 86400;
	}
}

Tämän nimiavaruuden sisältöä voisi käyttää sitten samaan tapaan, kuin std-nimiavaruuden sisältöäkin on tähän asti käytetty:

#include <iostream>

int main() {
	double matka_m = 1.0e+18; // metriä
	// Tarvitaan "avaruus::", koska käytetään nimiavaruuden sisältöä.
	double matka_vv = avaruus::metrit_valovuosiksi(matka_m);
	std::cout
		<< matka_m << " metriä on "
		<< matka_vv << " valovuotta." << std::endl;
	std::cout
		<< "Valo kulkee nopeudella " << avaruus::valonnopeus << " m/s."
		<< std::endl;
}

Nimiavaruuden sisältö täytyy esitellä esimerkin mukaisesti nimiavaruuden määrittelyn sisällä. Sisällön määrittelyt voi kuitenkin sijoittaa myös nimiavaruuden ulkopuolelle. Tällä tavalla kirjoitettuna esimerkkinimiavaruudesta tulisi tällainen:

// Määritellään nimiavaruus:
namespace avaruus {
	// Esitellään sisältö:
	extern const double valonnopeus;
	double metrit_valovuosiksi(double metrit);
}
// Määritellään sisältö jälkikäteen:
const double avaruus::valonnopeus = 299792458;
double avaruus::metrit_valovuosiksi(double metrit) {
	return metrit / valonnopeus / 365.25 / 86400;
}

Nimiavaruuden voi myös määritellä monessa osassa:

namespace avaruus {
	extern const double valonnopeus;
}
namespace avaruus {
	double metrit_valovuosiksi(double metrit);
}

Nimiavaruuksille voi antaa myös aliaksia eli synonyymeja.

namespace kosmos = avaruus;
// kosmos::valonnopeus ja avaruus::valonnopeus tarkoittavat nyt samaa

Nimiavaruuksien ulkopuolella nimetyt asiat sijaitsevat globaalissa nimiavaruudessa. Siihen voi viitata pelkällä ::-operaattorilla ilman nimiavaruuden nimeä:

int i = 4;
int main() {
	int i = 2;
	return ::i - i * i; // 4 - 2 * 2 == 0
}

Nimiavaruuden sisällön elinaika on sama kuin ilmankin nimiavaruutta.

using-lause

Nimiavaruuden tarkoitus on pitää tietyt nimet erillään muista. Tietyissä tilanteissa kuitenkin on helpompaa, jos nimiavaruuden nimeä ei tarvitse toistaa jatkuvasti. Tällöin nimiavaruuden sisältämät nimet voi tuoda näkyviin using-lauseilla. Näin ei kuitenkaan kannata tehdä turhaan!

#include <iostream>

int main() {
	// Ilmoitetaan, että käytetään avaruus-nimiavaruuden valonnopeutta
	// ja kaikkia std-nimiavaruuden nimiä.
	using avaruus::valonnopeus;
	using namespace std;

	cout << "Valonnopeus on " << valonnopeus << " m/s." << endl;
	/*
	std::cout
		<< "Valonnopeus on " << avaruus::valonnopeus << " m/s."
		<< std::endl;
	*/
}

Lause using avaruus::valonnopeus vastaa näkyvyysalueen kannalta samaa kuin uusi esittely. Se ei siis saa sijaita lohkossa, jossa on jo ennestään jokin valonnopeus.

Lause using namespace std sen sijaan toimii hieman eri tavalla: se lisää nimet käyttöön, kuin ne olisivat globaaleja nimiä. Paikallisiin muuttujiin tämä ei siis vaikuta, mutta jos globaalissa nimiavaruudessa on jo kyseinen nimi, sitä ei voi enää käyttää tarkentamatta, kummasta on kyse:

int i = 0;
namespace X {
	int i = 1;
}

int main() {
	using namespace X;
	// return i; // VIRHE! Kaksi i-vaihtoehtoa!
	return ::i * X::i; // 0 * 1
}

Käännösyksiköt ja liitosalueet

Käännösyksikkö tarkoittaa sitä C++-koodia, jota kääntäjä työstää kerralla. Siihen kuuluvat tavallisesti varsinainen käännettävä kooditiedosto (ohjelma.cpp) sekä kaikki ne tiedostot, jotka on liitetty mukaan esikääntäjän include-komennolla. Ohjelmassa voi olla useita käännösyksiköitä (esimerkiksi kooditiedostot valikko.cpp, sivupalkki.cpp jne.), joista kukin käännetään erikseen ja lopuksi linkitetään yhteen.

Ohjelmassa esiintyvän funktion, muuttujan tai tyypin liitosalue (engl. linkage) kertoo, missä osissa ohjelmaa nimen voisi esitellä uudestaan niin, että se yhä tarkoittaa samaa asiaa. Funktioiden paikallisilla muuttujilla ei ole lainkaan liitosaluetta: kuten aiemmin mainittiin, niitä ei voi erikseen esitellä ja ne on rajattu yhteen funktioon. Sen sijaan funktioiden ja globaalien muuttujien liitosalue on oletusarvoisesti ulkoinen (engl. external linkage), eli sama nimi voi viitata samaan funktioon kaikissa ohjelmaan kuuluvissa käännösyksiköissä. Kolmas vaihtoehto on sisäinen liitosalue (engl. internal linkage), jolloin nimi rajoittuu samaan käännösyksikköön; globaaleilla nimillä, joita edeltää static-määre, on sisäinen liitosalue. Liitosalueet ovat olennaisia siinä tapauksessa, että ohjelma sisältää useita käännösyksiköitä. Yksiköiden välillä jaettavilla funktioilla ja muuttujilla on oltava ulkoinen liitosalue, kun taas vain yhden tiedoston käyttöön tarkoitetuilla funktioilla on usein hyvä olla sisäinen liitosalue, jottei satu törmäyksiä toisessa tiedostossa olevan samannimisen funktion kanssa.

Joskus on tarpeen käyttää C++:lla tehtyjä funktioita C-koodissa tai muissakin kielissä. Tällöin täytyy yleensä käyttää erityistä extern "C" -määrettä funktioissa, jotta niistä tehtäisiin C-kielen kanssa yhteensopivia. Vastaavasti kun C++-koodissa käytetään C:llä tehtyjä funktioita, täytyy käyttää samaista extern "C" -määrettä. Monien kirjastojen otsikkotiedostoissa tehdään näin. C:n kanssa yhteensopivia funktioita ei voi kuormittaa, vaan tällöin samannimisestä funktiosta voi olla vain yksi versio.

Seuraava esimerkkiohjelma koostuu kahdesta tiedostosta, jotka pitää kääntää ja linkittää yhdeksi ohjelmaksi. Ensimmäisessä määritellään funktioita ja muuttujia, ja toisessa esitellään uudestaan kaikki ne, joita on mahdollista käyttää toisessakin käännösyksikössä.

// Tiedosto A

#include <iostream>

// Määritellään muuttuja ja funktio vain tähän käännösyksikköön.
// Niiden liitosalue on siis sisäinen.
static int sisaluku = 0;
static void sisafunktio() {
	std::cout << "sisafunktio(): sisaluku = " << sisaluku << std::endl;
	++sisaluku;
}

// Määritellään muuttuja ja funktio, joilla on ulkoinen liitosalue.
int ulkoluku = 1;
void ulkofunktio_1() {
	std::cout << "ulkofunktio_1(): ulkoluku = " << ulkoluku << std::endl;

	// Sisäfunktiota voi käyttää tässä tiedostossa.
	sisafunktio();
}

// Funktion määrittelyssä voi olla myös sana extern
// merkitsemässä ulkoista liitosaluetta.
extern void ulkofunktio_2() {
	std::cout << "ulkofunktio_2(): ulkoluku = " << ulkoluku << std::endl;
}

// Jos funktion tai muuttujan pitää olla käytettävissä C-kielisestäkin koodista,
// pitää käyttää erityistä extern "C" -määrettä. Muuttuja pitää esitellä ensin
// ja määritellä vasta sitten.
extern "C" int ulkoluku_C1;
int ulkoluku_C1 = 0xC1;

extern "C" void ulkofunktio_C1() {
	std::cout << "ulkofunktio_C1(): ulkoluku_C1 = " << ulkoluku_C1 << std::endl;
}

// Monta C-kielelle tarkoitettua asiaa voi koota samaan extern "C" -lohkoon.
// Lohkossa muuttujan voi määritellä suoraan ilman erillistä esittelyä.
extern "C" {
	int ulkoluku_C2 = 0xC2;
	void ulkofunktio_C2() {
		std::cout << "ulkofunktio_C2(): ulkoluku_C2 = " << ulkoluku_C2 << std::endl;
	}
}
// Tiedosto B

// Staattinen muuttuja ja funktio ovat vain A-tiedoston käytössä.

// Esitellään funktio, jolla on ulkoinen liitosalue. Esittelyssä voi käyttää
// sanaa extern, jotta koodista näkee heti, että funktio on eri tiedostossa.
extern void ulkofunktio_1();
void ulkofunktio_2();

// Esitellään muuttuja, jolla on ulkoinen liitosalue.
// Nyt extern on välttämätön!
extern int ulkoluku;

// Esitellään myös C:lle tarkoitetut funktiot. Koska tämä on C++-koodia,
// täytyy käyttää extern "C" -määrettä tai lohkoa.
extern "C" {
	// Koska extern "C" -lohkossa voi myös määritellä muuttujia, täytyy
	// käyttää uudestaan extern-määrettä, jotta saataisiin esittelyjä.
	extern int ulkoluku_C1;
	extern int ulkoluku_C2;

	// Funktion kanssa voi käyttää tai olla käyttämättä extern-määrettä.
	int ulkofunktio_C1();
	extern int ulkofunktio_C2();
}

// Testataan vielä funktioita ja muuttujia.
int main() {
	ulkofunktio_1(), ulkofunktio_2(), ulkofunktio_C1(), ulkofunktio_C2();

	ulkoluku_C1 = 123;
	ulkoluku_C2 = 456;
	ulkoluku = 789;

	ulkofunktio_1(), ulkofunktio_2(), ulkofunktio_C1(), ulkofunktio_C2();
}

Monen tiedoston käytöstä yhdessä ohjelmassa kerrotaan myös koodivinkeissä 1770 ja 1901, ensimmäisessä C:n ja toisessa enemmän C++:n näkökulmasta.

Loppuhuomautuksia


Kommentit

Mayson [30.01.2014 21:11:36]

#

Haluaisin vaan kysyä olenko se vaan minä ja puutteellinen "ohjelma" vai enkö vain osaa mutta, nuo sinun esimerkit ja opit eivät käänny edes suoraan copy-pastella :(

Metabolix [30.01.2014 21:56:25]

#

Mayson kirjoitti:

Nuo sinun esimerkit ja opit eivät käänny edes suoraan copy-pastella.

Olet varmaankin kopioinut jotain väärin. Kaikki oppaan koodit ovat toimivia, ellei erikseen mainita, että koodi on tahallaan väärin. Aivan kaikki koodit eivät silti ole kokonaisia ohjelmia.

Jos jokin tietty kohta on hankala, voit mielellään lähettää kysymyksen keskusteluun kaikkine koodeineen.

Mayson [31.01.2014 18:04:38]

#

okei elikkä se olen minä miksi ei kääntynyt. tämän halusin tietää :D

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