previous next Title Contents Index

7. C ja C++ - kielten alkeita


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

* C- kielisen ohjelman peruskäsitteet

* C++- kielisen ohjelman peruskäsitteet

* kääntämisen ja linkittämisen merkitys

* vakiotyyliset makrot (#define)

* C++:n vakiot (const)

Syntaksi:

	Seuraavassa lauseke on mikä tahansa jonkin tyypin tuottava ohjelman osa, esim:
	            1+2   sin(x)+9  sormia  henkilon_nimi
	kommentti, C      /* vapaata tekstiä, vaikka monta riviäkin */
	kommentti, C++    // loppurivi vapaata tesktiä
	sisällyttäminen:  #include <tiedoston_nimi>  tai  #include "tiedoston_nimi"
	makro:            #define tunnisteXkorvattava_teksti // X mikä tahansa
	                                                     // ei A-Z,a-z,_,0-9
	                                                     // X tulee mukaan korv.tekst
	vakio, C++:       const tyyppi nimi = arvo;
	tulostus, C:      printf(format,lauseke, lauseke);   // 0-n x ,lauseke
	tulostus, C++:    cout << lauseke << lauseke;        // 1-n x  <<lauseke
Ohjelman toteuttamista varten täytyy valita jokin todellinen ohjelmointikieli. Lopullisesta ohjelmasta ei valintaa toivottavasti huomaa. Valitsemme käyttökielen tällä kurssilla puhtaasti "markkinaperustein": käytetyin ja työelämässä vielä tällä hetkellä kysytyin - C++.

Tässä luvussa käsittelemme rinnakkain C ja C++ - kielistä ohjelmaa. Seuraavissa luvuissa käsitellään pelkästään C++:aa, kuitenkin siten että jos ohjelman tarkeninen on .C, niin ohjelma toimii samalla myös C- kielisenä ohjelmana.

7.1 C/C++

Mikä ero on C ja C++ - kielillä? Hyvin pieniä poikkeuksia lukuunottamatta jokainen C- kielinen ohjelma on myös C++ - kielinen ohjelma. Voitaisiin sanoa että C++ on C:n oliopohjainen laajennus. Tosin C++:ssa on myös lisäyksiä, joilla ei sinänsä ole mitään tekemistä olio- ohjelmoinnin kanssa. Koska C++ ei ole "puhdas" oliokieli, sanotaan sitä hypridikieleksi. Sitä, että C++:lla voi kirjoittaa myös ei- olio- ohjelmia, pitävät monet olio- ohjelmoinnin asiantuntijat pahana. Toisaalta maailmassa on valtava määrä C- kielen osaajia, joille on näin luotu "pehmeä" lasku olio- ohjelmontiin ilman että heidän aikaisemmin kirjoittamansa koodi tulisi kerralla arvottomaksi. Ilman muuta C- kielen "painolasti" haittaa tietyllä tavalla C++- kielen kehittymistä, mutta reaalimaailmassa on tyydyttävä kompromisseihin.

Kumpi sitten kannattaa opetella ensiksi? Puristit sanovat että jokin "leikkikieli", jotkut että C ja "oliogurut" sanovat että ilman muuta jokin oliokieli. Otamme siis tällä kurssilla kultaisen(?) keskitien ja opettelemme C++:sta eräänlaisen "lasten" version, jossa jatkossa käytämme hyväksi myös kielen olio- ominaisuuksia.

7.1.1 Hello World! C- kielellä

Aloitamme C- kielen opiskelun klassisella esimerkillä: Tulostetaan teksti "Hello world!" näyttöön:

c-alk\hello.c - ensimmäinen C-ohjelma

	/* Ohjelma tulostaa tekstin Hello world! */
	#include <stdio.h>
	int main(void)
	{
	  printf("Hello world!\n");
	  return 0;
	}

7.1.2 Hello World! C++ - kielellä

Huomattakoon että edellinen hello.c on sellaisenaan aivan hyvä C++ - ohjelma, mikäli nimi muutettaisiin hello.cpp:ksi. Kirjoitamme kuitenkin uuden version, jossa on käytetty hyväksi C++:an uusia ominaisuuksia:

c-alk\hello.cpp - ensimmäinen C++ ohjelma

	// Ohjelma tulostaa tekstin Hello world!
	#include <iostream.h>
	int main(void)
	{
	  cout << "Hello world!" << endl;
	  return 0;
	}

Tehtävä 7.48 Nimi ja osoite

Kirjoita puhdas C- ohjelma ja iostreamia käyttävä C++- ohjelma, joka tulostaa:
	Terve!  
	Olen Matti Meikäläinen 25 vuotta.
	Asun Kortepohjassa.
	Puhelinnumeroni on 603333. 

7.2 Tekstitiedostosta toimivaksi konekieliseksi versioksi

7.2.1 Kirjoittaminen

Ohjelmakoodi kirjoitetaan millä tahansa tekstieditorilla tekstitiedostoon vaikkapa nimelle HELLO.C. Yleensä tiedoston tarkennin määrää ohjelman tyypin.

7.2.2 Kääntäminen

Valmis tekstitiedosto käännetään ko. kielen kääntäjällä. Käännöksestä muodostuu objektitiedosto, joka on jo lähellä lopullisen ohjelman konekielistä versiota. Objektitiedostosta puuttuu kuitenkin mm. kirjastorutiinit. Kirjastorutiinien kutsujen kohdalla on "tyhjät" kutsut.

7.2.3 Linkittäminen

Linkittäjällä (kielestä riippumaton ohjelma) liitetään kirjastorutiinit käännettyyn objektitiedostoon. Linkittäjä korvaa tyhjät kutsut varsinaisilla kirjastorutiinien osoitteilla kunhan saa selville mihin kohti muistia kirjastorutiinit sijoittuvat. Näin saadaan valmis ajokelpoinen konekielinen versio alkuperäisestä ohjelmasta.

7.2.4 Ohjelman ajaminen

Käännetty ohjelma ajetaan käyttöjärjestelmästä riippuen yleensä kirjoittamalla ohjelman alkuperäinen nimi. Tällöin käyttöjärjestelmän lataaja- ohjelma lataa ohjelman konekielisen version muistiin ja siirtää prosessorin ohjelmalaskurin ohjelman ensimmäisenä suoritettavaksi tarkoitettuun käskyyn. Vielä tässäkin vaiheessa osa aliohjelmakutsujen osoitteista voidaan muuttaa vastaamaan sitä todellista osoitetta, johon aliohjelma muistiin ladattaessa sijoittui. Tämän jälkeen vastuu koneen käyttäytymisestä on ohjelmalla. Onnistunut ohjelma päättyy aina ennemmin tai myöhemmin käyttöjärjestelmän kutsuun, jossa ohjelma pyydetään poistamaan muistista.


Kuva 7.1 Ohjelman kääntäminen ja linkittäminen

7.2.5 Varoitus

Alkuperäisellä editorilla kirjoitetulla ohjelmakoodilla ei ole tavallista kirjettä kummempaa virkaa ennenkuin teksti annetaan kääntäjä- ohjelman tutkittavaksi. Käännöksen jälkeen alkuperäinen teksti voidaan vaikka hävittää! Siis me kirjoitamme tekstiä, joka ehkä (toivottavasti) muistuttaa C- kielen syntaksin mukaista ohjelmaa. Vasta käännös ja linkkaus tekevät todella toimivan ohjelman.

7.2.6 Integroitu ympäristö

On olemassa ohjelmankehitysympäristöjä, joissa editori, kääntäjä ja linkkeri (sekä mahdollisesti debuggeri, virheenjäljitin) on yhdistetty käyttäjän kannalta yhdeksi toimivaksi kokonaisuudeksi. Esimerkkinä Microsoftin Visual- C++ ja Borlandin Turbo- C++, Borland- C++ tai Borland- C++ Builder .

Esimerkiksi Borlandin ympäristöissä ohjelma kirjoitetaan tekstinä ja kun ohjelmakoodi on valmis, saadaan koodi käännettyä, linkitettyä ja ladattua ajoa varten vain painamalla [Ctrl- F9].

7.3 Ohjelman yksityiskohtainen tarkastelu

Seuraavaksi tutkimme ohjelmaa lause kerrallaan:

7.3.1 Kommentti

	/* Ohjelma tulostaa tekstin Hello world! */
Ohjelman alussa on kommentoitu mitä ohjelma tekee. Yleensä ohjelmakoodit on hyvä varustaa kuvauksella siitä, mitä ohjelma tekee, kuka ohjelman on tehnyt, milloin ja miksi. Milloin ohjelmaa on viimeksi muutettu, kuka ja miten.

Lisäksi jokainen vähänkin ei- triviaali lause tai lauseryhmä kommentoidaan. Kommenttien tarkoituksena on kuvata ohjelmakoodia lukevalle lukijalle se mistä on kyse.

Kommentti alkaa /* - merkkiyhdistelmällä ja päättyy */ - merkkiyhdistelmään. Kommentteja voidaan sijoittaa C- koodissa mihin tahansa mihin voitaisiin pistää myös välilyönti. Rivin loppuminen ei sinänsä lopeta kommenttia. Kommentin sisällä SAA esiintyä / ja * - merkkejä yhdessä tai erikseen, muttei lopettavaa yhdistelmää */.

Yleinen virhe on unohtaa kommentin loppusulku pois. Mikäli esimerkissämme puuttuisi kommentin loppusulku, olisi koko loppuohjelma kommenttia ja mitään ohjelmaa ei siis olisikaan. Mikäli kääntäjä antaa vyöryn ihmeellisiä virheilmoituksia, kannattaa aina ensin tarkistaa kommenttisulkujen täsmäävyys. Tosin tähän auttaa nykyisten ohjelmointiympäristöjen värikoodien käyttö eri ohjelman osille, eli esimerkiksi kommentit näkyvät eri värisinä ja puuttuva komenttisulku paljastuu välittömästi.

	// Ohjelma tulostaa tekstin Hello world!
C++:ssa kommentti voidaan ilmaista myös // - merkkiyhdistelmällä, jolloin rivinloppu lopettaa kommentin. Siirrettävyyden takia niissä ohjelman osissa, jotka ovat puhtaasti C- kieltä, voitaisiin käyttää /* */ - kommentointia ja muualla // - kommentointia.

7.3.2 Kirjastofunktioiden esittely

	#include <stdio.h>
	#include <iostream.h>
Tarvitsemme ohjelmassamme tulostusfunktiota printf. Tämä funktio löytyy C- kielen kirjastosta ja se on esitelty otsikkotiedostossa stdio.h. Kääntäjää varten meidän täytyy esitellä millaisia parametreja funktiolle voidaan välittää. Kutakin kirjastoa varten on esittelytiedostot ("header"- tiedostot, yleensä nimetään .h), joissa kirjastofunktioiden parametrilistat on esitelty.. Tässä ohjelmassa stdio.h - tiedostosta käytetään vain printf:n esittelevää riviä ja voitaisiin myös kirjoittaa #include - rivin tilalle printf:n esittely:
	int printf(const char __format, ...);
mutta oikean muodon muistaminen voisi olla vaikeampaa.

C++:an tulostusvirta cout löytyy kirjastosta iostream.h. cout - olion määrittely iostream.h - tiedostossa on niin monimutkainen ettei sitä käytännössä voisi itse edes kirjoittaa! #include on C- kielen esikääntäjän (pre- prosessor) käsky, joka ilmoittaa että perässä olevan niminen tiedosto on luettava ja käsiteltävä koodin sekaan tässä kohti käännöstä.

< > - merkit tiedoston nimen ympärillä ilmoittavat, että ko. tiedostoa etsitään C:n systeemin mukaisesta INCLUDE- hakemistosta. Mikäli nimi suljettaisiin "- merkeillä, etsittäisiin tiedostoa myös käyttäjän kotihakemistosta. Näin voidaan tehdä omia tehtäväkohtaisia kirjastoja.

7.3.3 Päämodulin esittely

	int main(void) 
Seuraavaksi esitellään ohjelman pääohjelma ("oikea" ohjelma koostuu isosta kasasta aliohjelmia ja yhdestä pääohjelmasta, jonka nimi on main). int tarkoittaa, että pääohjelmamme palauttaa kokonaisluvun kutsuvalle ohjelmalle - käyttöjärjestelmälle. Palautettavan luvun arvo on tyypillisesti 0 mikäli kaikki menee ohjelman aikana niinkuin pitääkin ja muilla numeroilla ilmaistaan erilaisia virhetilanteita. MS- DOSissa tätä arvoa voidaan tutkia ERRORLEVEL- muuttujalla.

main tarkoittaa pääohjelman nimeä. Tämä TÄYTYY aina olla main.

(void) ilmoittaa, että funktio jota kirjoitamme ei tarvitse yhtään parametria (eng. void = mitätön). Myöhemmin huomaamme että myös pääohjelmalla voi olla parametrejä.

7.3.4 Lausesulut

{ } C:ssä isompi joukko lauseita kootaan yhdeksi lauseeksi sulkemalla lauseet aaltosulkuihin. Funktion täytyy aina sisältää aaltosulkupari, vaikka siinä olisi vain 0 tai 1 suoritettavaa lausetta.

7.3.5 Tulostuslause

	  printf("Hello world!\n")
printf("?") tulostaa ajonaikana sen tekstin, joka on lainausmerkkien välissä. Myöhemmin opimme, että funktion kutsussa voi olla myös useampia parametreja ja voidaan käyttää myös muuttujia.

\n on C:n erikoismerkki, joka kääntyy merkkijonon sisällä käyttöjärjestelmän rivinvaihtomerkiksi. Tällaisen esiintyminen merkkijonossa aiheuttaa tulostuksen siirtymisen uuden rivin alkuun. Muista käyttää!

	cout << "Hello world!" << endl;
cout on C++:n yksi tulostustietovirtaluokan (output stream class) ostream esiintymä, eli olio jolle (coutin tapauksessa) siirretyt merkit tulostuvat näyttöön (Console OUTput) .

<< on operaattori, jolla C++:ssa on useita merkityksiä. Tässä tapauksessa kun operaattorin vasempana operandina on tietovirtaolio, on kyseessä tietovirtaan siirtämisoperattoori (inserter). Käytämme tästä jatkossa nimitystä tulostusoperaattori. Koska operaattorikin on vain aliohjelma, voisi <<- operaattorin varsinainen kutsumuoto olla esimerkiksi

	operator<<(cout, "Hello world!");
	operator<<(cout, endl);
Koska operaattori palauttaa cout- olion, voidaan kutsu kirjoittaa myös (samoin kuin 1+2 palauttaa kokonaisluvun) ketjumuotoon
	operator<<( operator<<(cout, "Hello world!"), endl);
	// lyhennetty muoto
	(cout << "Hello world!") << endl;
joka ilman sulkuja on ohjelmassa hello.cpp esitetty muoto. Operaattorin muoto voi olla myös:
	cout.operator<<("Hello world!");
	cout.operator<<(endl);
	// josta ketjutettuna:
	(cout.operator<<("Hello world!")).operator<<(endl);
	// lyhennetty muoto:
	(cout << "Hello world!") << endl;
Vertaa vastaavaan ketjuttamiseen + - operaattorin kanssa:
	int i,j;
	i = 1 + 2;  j = i + 4;
	// voidaan kirjoittaa myös:
	j =  ( 1 + 2 ) + 4;
endl on tietovirran muotoilija (manipulator), joka vaihtaa tulostuksen uudelle riville ja tyhjentää tulostuspuskurin. Rivinvaihto voitaisiin tehdä myös jonolla "\n", mutta tulostuspuskuri on muistettava myös tyhjentää. endl olisi hyvä olla vähintään viimeisessä tulostettavassa lauseessa ennen pysähtymistä. Joissakin koneissa ohjelmat voivat toimia myös ilman endl:ää, mutta jos halutaan standardin mukaista koodia, joka toimii KAIKISSA koneissa, on sitä syytä käyttää. Kumpiko tulostuslause sitten on parempi? Jos kirjoitetaan C- koodia, on tietysti käytettävä printf:ää, mutta C++:n tapauksessa molemmat käyvät. cout on parempi sen vuoksi, että siihen liittyvään tulostusoperaattorin voidaan lisätä uusia tietotyyppejä tulostettavaksi (kuormittaa, operator overloading). Toisaalta tulostuksen muotoilu on helpompaa printf:n kanssa. Määrityksissä sanotaan että molempia voi käyttää, muttei samalla tulostusrivillä. Laajennettavuuden takia valitaan coutaina kuin se vain on mahdollista.

7.3.6 Lauseen loppumerkki ;

; puolipiste lopettaa lauseen. Puolipiste voidaan sijoittaa mihin tahansa lopetettavaan lauseeseen nähden. Sen eteen voidaan jättää välilyöntejä tai jopa tyhjiä rivejä. Sen pitää kuitenkin esiintyä ennen uuden lauseen alkua. Näin C- kieli ei ole rivisidonnainen, vaan C- kielinen lause voi jakaantua usealle eri riville tai samalla rivillä voi olla useita C- kielisiä lauseita. Puolipisteen unohtaminen on tyypillinen syntaksivirhe. Ylimääräiset puolipisteet aiheuttavat tyhjiä lauseita, joista tosin ei ole mitään haittaa: "Tyhjän tekemiseen ei kauan mene" - sanoo tyhjän toimittaja.

7.3.7 Funktion arvon palautus

	  return 0
return jokainen C- funktio tulisi lopettaa return- lauseeseen. Mikäli funktio on esitelty muun kuin void- tyyppiseksi, pitää kertoa myös arvo, joka palautetaan. Funktio voi tarvittaessa sisältää myös useita eri return- lauseita. Heti kun kohdataan ensimmäinen return- lause, poistutaan funktiosta. void- tyyppisessä funktiossa return- lause ei ole pakollinen.

7.3.8 Isot ja pienet kirjaimet

Isoilla ja pienillä kirjaimilla on C- kielessä eri merkitys. Siis EI VOIDA KIRJOITTAA:
	Int MAIN(Void)              /* VÄÄRIN! */	

7.3.9 White spaces, tyhjä

Välilyöntejä, tabulointimerkkejä, rivinvaihtoja ja sivunvaihtoja nimitetään yleisesti yhteisellä nimellä "white space". Käännettäessä kommentit muutetaan yhdeksi välilyönniksi, joten myös kommenteista voitaisiin käyttää nimitystä "white space". Jatkossa käytämme nimitystä tyhjä tai tyhjä merkki, kun tarkoitamme "white space".

C- koodi voi sisältää tyhjiä merkkejä missä tahansa, kunhan niitä ei kirjoiteta keskelle sanaa tai tekstiä määrittelevän ""- parin ollessa auki. ""- parin sisällä tyhjätkin merkit ovat merkityksellisiä. Tyhjillä merkeillä ei saa myöskään sotkea esikääntäjälle tarkoitettuja #- direktiivi - rivejä, näiden pitää muodostaa täsmälleen yksi rivi.

Lainausmerkkeihin suljettu jono voidaan tarvittaessa katkaista tyhjillä merkillä sulkemalla ja avaamalla lainausmerkit. Esimerkiksi

	"Kissa"
	"istuu"
	- >  
	"Kissaistuu"
Tarvittaessa C- ohjelman riviä voidaan jatkaa uudelle riville kirjoittamalla \- merkki edellisen rivin loppuun ja sen jälkeen välittömästi rivinvaihto.

Siis kääntäjän kannalta malliohjelmamme voitaisiin kirjoittaa myös seuraavillakin tavoilla:

	#include\	
	<stdio.h>
	int 
	main 
	(
	void
	)
	{
	printf
	(
	"Hel\
	lo "
	"w" /* kommentti keskellä jonoa */ "or"
	"ld!"
	"\n"
	)
	;
	return
	0
	;
	}
	#include <stdio.h>	
	int main(void){
	    printf("Hello "
	           "world!\n"
	                                               );return 0;}
	#include <stdio.h>	
	int main(void){printf("Hello world!\n");return 0;}
Yleinen tyyli on kuitenkin jakaa koodia riveihin ja sisentää lohkoja muutamalla pykälällä. Kunnes lukija on varma omasta tyylistään, kannattaa matkia tässä monisteessa (ei kuitenkaan edellisiä esimerkkejä) esitettyä kirjoitustapaa ohjelmille.

7.4 Makro- direktiivi ja vakiot

#include oli tiedote esikääntäjälle siitä, että koodin sekaan täytyy lukea välillä jokin toinen teksti.

Makro- direktiivi #define on tiedote siitä, että tällä määreellä myöhemmin tekstissä esiintyvät sanat täytyy korvata toisella sanalla/sanoilla/merkeillä.

Yksinkertaisten makrojen käytön hyöty on siinä, että usein ohjelmassa esiintyvät sanat/vakionumerot voidaan koota helposti hallittavaksi ohjelman alkuun tai jopa omaan määrittelytiedostoonsa.

Siis #define- direktiivi on puhdas tekstinkäsittelyotus, joka toimii suurinpiirtein seuraavasti:

c-alk\makrot.c - esimerkki mitä #define korvaa

	#define TERVE "Hello "
tarkoittaisi esikääntäjälle:
	vaihda kaikki TERVE- sanat merkkijonoiksi "Hello "
	
	eli seuraavat vaihdettaisiin:
	p=TERVE+1                /* =>  p="Hello "+1             */
	"Kissa"TERVE"istuu"      /* =>  "Kissa""Terve ""istuu"   */
	TERVE MIEHEEN            /* =>  "Hello" MIEHEEN          */
	
	muttei seuraavia
	TERVEYDEKSI                   /* Ei pelkkä TERVE-sana    */
	TERVE_MIEHEEN                 /* Ei pelkkä sana          */
	"OLEN OLLUT TERVE 2 PÄIVÄÄ"   /* Lainausmerkeissä        */
	Terve MIEHEEN                 /* Eri tavalla kirjoitettu */
Huomattakoon, ettei lainausmerkkien sisällä oleviin sanoihin kajota lainkaan!

Määriteltävä sana voidaan kirjoittaa isoja ja/tai pieniä kirjaimia käyttäen, mutta yleiseen C- tyyliin kuuluu kirjoittaa #define - määritellyt sanat isoilla kirjaimilla.

Vaihdettava sana loppuu ja korvaava jono alkaa ensimmäisestä ei- kirjaimesta tai numerosta, eli

	#define X=2;                /* VAARALLINEN!   X  => =2;  */	
	int i=X;                    /* => int i==2;;             */

7.4.1 Vakiomerkkijonot

Esikääntäjän makro- ominaisuutta hyväksikäyttäen voimme määritellä ohjelmaamme vakioita; eli arvoja jotka esiintyvät ohjelmassa täsmälleen yhden kerran. Näin ohjelmastamme saadaan helpommin muuteltava. Esimerkiksi seuraava ohjelma tulostaisi myös tekstin "Hello world!":

c-alk\hello2.c - tervehdys vakioksi

	/* Ohjelma tulostaa tekstin Hello world! */
	#include <stdio.h>
	#define TERVE   "Hello "
	#define MAAILMA "world"
	int main(void)
	{
	  printf(TERVE MAAILMA"!\n");
	  return 0;
	}
Miksikö? Koska esikääntäjä muuttaisi lauseen
	  printf(TERVE MAAILMA"!\n");
muotoon
	  printf("Hello " "world""!\n");
Kun tästä lisäksi poistetaan ylimääräiset "white space"- merkit saadaan:
	  printf("Hello world!\n");

Tehtävä 7.49 Terve maailma!

Kirjoita edellisestä ohjelmasta suomenkielellä tulostava versio (= suomenna ohjelma).

Tehtävä 7.50 Nimi ja osoite vakioksi

Kirjoita aikaisemmasta "Matti Meikäläinen asuu Kortepohjassa" - ohjelmasta versio, jossa nimi, osoite ja puhelin on esitelty #define- direktiivillä.

7.4.2 Vakiolukuarvot

Vakiomäärittelyä voitaisiin käyttää esimerkiksi kokonaislukuvakioiden määrittelemiseen:

c-alk\kuutio.c - monikulmion tiedot vakioksi

	/* Ohjelma tulostaa tietoja monitahokkaasta */
	#include <stdio.h>
	#define TAHOKAS     "Kuutiossa"
	#define KARKIA      8
	#define SIVUTASOJA  6
	#define SARMIA     12
	int main(void)
	{
	  printf("%20s on %2d kärkeä,\n"     ,TAHOKAS,KARKIA);
	  printf("%20s    %2d sivutasoa ja\n"," "    ,SIVUTASOJA);
	  printf("%20s    %2d särmää.\n"     ," "    ,SARMIA);
	  return 0;
	}

Tehtävä 7.51 Tetraedri

Muuta edellistä ohjelmaa siten, että tulostetaan samat asiat tetraedristä.

Tehtävä 7.52 printf ja %

Mitä arvelet %2d:n ja %20s:n merkitsevän edellisessä esimerkissä, kun ohjelma tulostaa seuraavan tekstin:
	0        1         2         3         4
	1234567890123456789012345678901234567890
	- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
	           Kuutiossa on  8 kärkeä,
	                         6 sivutasoa ja
	                        12 särmää. 

7.4.3 Muita makro- "temppuja"

Koska C- kielen makro on todellakin vain tekstinkorvaus, muuttaa esikääntäjä seuraavan tekstin

c-alk\makroja.c - "turhia" makrotemppuja

	#include <stdio.h>
	#define ALKU  int main(void) {
	#define LOPPU return 0; }
	#define mk *100
	#define km *1000
	ALKU
	  double hinta_penneina,matka_m;
	  hinta_penneina = 5 mk;
	  matka_m        = 3.5 km;
	LOPPU
muotoon
	... kaikki stdio.h:ssa oleva koodi makrot käsiteltynä ...
	int main(void) {
	double hinta_penneina,matka_m;
	hinta_penneina = 5 *100;
	matka_m        = 3.5 *1000;
	return 0; }
Tosin tällaisia makrotemppuiluja kannattaa välttää ellei siitä saa suunnatonta ohjelman ylläpidollista hyötyä. Tarvittaessa pelkkä esikäännös voidaan tehdä vaikka Borland C++:lla:
	C:\OMAT\OHJELMOI\VESA>CPP makroja.c[RET]
	tulee tiedosto  makroja.i

7.4.4 Esikääntäjän toiminnasta

Esiprosessori kerää jokaisen löytämänsä #define - alkuisen rivin omaan sisäiseen listaansa. Jos tulee vastaan #undef jollekin listassa mainitulle sanalle, poistetaan sana listasta. Esikääntäjä lukee varsinaisia rivejä (ei #- alkuisia) token ("sananen") kerrallaan. Tämä token on pienin esikääntäjän ymmärtämä yksikkö (joka on erotettu ympäristöstä ei- muuttujaan sallituilla kirjaimilla). Esimerkiksi rivillä
	a;bd(kissa) 
on 3 tokenia: a, bd ja kissa.

Kun esikääntäjä käsittelee yhtä tokenia, etsii se sitä sisäisestä listastaan. Jos token löytyy listasta, korvataan se vastaavalla merkkijoukolla ja listan ko. token merkitään käytetyksi. Mahdollinen korvaus muodostaa 0 - n uutta tokenia. Nämä kaikki käsitellään rekursiivisesti esikääntäjän listan kanssa kunnes yhtään korvausta ei voida tehdä (token ei löydy listasta tai kaikki listan alkiot on merkitty käytetyiksi). Näin tämä yksi token on saatu muutetuksi. Tämän jälkeen listan kaikki alkiot vapautetaan ja siirrytään rivin seuraavaan mahdolliseen tokeniin.

Miksi listaan merkitään sanoja käytetyksi? Miten kävisi muuten (kävi vanhoilla C- esikääntäjillä) seuraavan ohjelmanpätkän kanssa:

	#define K T T
	#define T K K
	K
	T
Lyhyesti: esikääntäjä ei korvaa sisäkkäisiä #definejä heti, vaan sitten kun ne esiintyvät. Näin useissa normaalitapauksissa (kuten ei edelläkään) ei ole väliä sillä, missä järjestyksessä #define- rivit kirjoitetaan.

Tehtävä 7.53 Sisäkkäiset makrot

Miten edellisestä K T T, esimerkissä korvautuu K? Entä miten T?

7.5 C++:n vakiot

Koska makrojen käyttöön liittyy tiettyjä tyyppitarkistuksiin/oikeellisuuskirjoitukseen liittyviä riskejä, kannattaa C++:ssa mieluummin käyttää tyypitettyjä vakioita (seuraavan esimerkin const- rivit toimivat tosin C- kielessäkin):

c-alk\hello2.cpp - tervehdys "vakioksi"

	// Ohjelma tulostaa tekstin Hello world! 
	#include <iostream.h>
	const char *TERVE = "Hello ";
	const char *MAAILMA = "world";
	
	int main(void)
	{
	  cout << TERVE << MAAILMA << "!" << endl;
	  return 0;
	}
Valitettavasti on tilanteita, joissa makroja on edelleen lähes pakko käyttää. Esimerkiksi merkkijonojen yhdistäminen TERVE MAAILMA tyyliin ei onnistu. Myöhemmin opimme joitakin muitakin tilanteita, joissa makrot ovat "välttämättömiä". Erityisesti lukuvakioiden määrittelyssä const- vakiot ovat omimmillaan:

c-alk\kuutio.cpp - monikulmion tiedot vakioksi

	// Ohjelma tulostaa tietoja monitahokkaasta
	#include <iostream.h>
	#include <iomanip.h>
	const char *TAHOKAS   = "Kuutiossa";
	const int KARKIA      =  8;
	const int SIVUTASOJA  =  6;
	const int SARMIA      = 12;
	int main(void)
	{
	  cout<<setw(20)<<TAHOKAS<<" on "<<setw(2)<<KARKIA    <<" kärkeä,\n";
	  cout<<setw(20)<<" "    <<"    "<<setw(2)<<SIVUTASOJA<<" sivutasoa,\n";
	  cout<<setw(20)<<" "    <<"    "<<setw(2)<<SARMIA    <<" särmää" << endl;
	  return 0;
	}

Tehtävä 7.54 Vakiot #define

Päättele seuraavasta esimerkistä miksi const- vakiot ovat parempia kuin #define - makrot.

c-alk\const.cpp - vakion ja #definen erot

	// Ohjelmalla tutkitaan mikä ero on const ja #define -vakioiden välillä
	#include <iostream.h>
	const int sormia = 1 + 4;
	#define VARPAITA 1 + 4	
	int main(void)
	{
	  int syht = 2 * sormia;
	  int vyht = 2 * VARPAITA;
	  cout << "Sormia = " << syht << " ja varpaita = " << vyht << endl;
	  return 0;
	}
Selitä mistä virhe johtuu? Miten virhe saadaan tässä tapauksessa poistettua?


previous next Title Contents Index