Kirjautuminen

Haku

Tehtävät

Oppaat: C-ohjelmointi: C:n esikääntäjän perusteet

Kirjoittaja: Metabolix (2008).

C:n esikääntäjä on kääntäjä, joka käsittelee C- ja C++-kooditiedostot ennen niiden lähettämistä varsinaiselle kääntäjälle. Sen vaikutuspiiriin kuuluvat ensisijaisesti kaikki rivit, joiden ensimmäinen näkyvä merkki mahdollisten sisennysten jälkeen on #. Näiden merkityksiin tässä oppaassa keskitytään.

Huomautus oppaan koodilistauksista

Koodilistauksissa kommenteilla on merkitty, millaiseksi edeltävä rivi muuttuu esikääntäjän käsittelyssä. Esimerkiksi listaus

#define A 10
A + A == 20
/*  10 + 10 == 20  */

tarkoittaa, että esikääntäjälle syötetään koodi

#define A 10
A + A == 20

ja se korvaa rivin A + A == 20 rivillä 10 + 10 == 20

Mikä on esikääntäjä?

C-esikääntäjä (C preprocessor) on ohjelma, joka muokkaa C- tai C++-koodia tiettyjen komentojen mukaan ennen sen lähettämistä "oikealle" kääntäjälle. Esikääntäjän toiminta tapahtuu normaalisti huomaamattomasti käännöksen yhteydessä, mutta halutessaan sen voi usein myös pakottaa tapahtumaan erikseen. GCC-paketissa oleva esikääntäjä on nimeltään cpp, tällä voi siis kokeilla esimerkiksi tämän oppaan esimerkkejä ja tehdä lisää omia kokeiluja.

Esikääntäjä muun muassa yhdistelee koodirivejä, poistaa koodista kommentit ja korvaa sanoja toisilla. Sen avulla voi kirjoittaa myös pieniä funktioita muistuttavia makroja. Esikääntäjän varsinaiset komennot ovat rivejä, joiden alussa on #-merkki. Rivi ei saa sisältää tätä ennen kuin sisennysmerkkejä eli välilyöntejä ja tabulaattoreita.

Rivien yhdistäminen (\)

Joskus koodiriveistä tahtoo tulla niin pitkiä rivejä, että ne olisi mukava katkaista. Toisinaan ne halutaan syystä tai toisesta kuitenkin pitää näennäisesti yhtenä rivinä, ja tällöin esikääntäjä voi pelastaa tilanteen: kun rivin viimeinen merkki on \, esikääntäjä liittää rivin edelliseen.

"tässä on \
monen rivin \
teksti."
/*  "tässä on monen rivin teksti."  */

Samaa merkintää voi käyttää myös pitkien esikääntäjäkomentojen rivittämiseen, mikä onkin toisinaan tarpeen pitkien makrojen kanssa. Muuten esikääntäjän komennot pitää kirjoittaa yhdelle riville.

Vakiot (#define)

Komennolla #define voidaan määritellä, että jokin teksti tarkoittaakin jotain muuta. Näin voidaan määritellä esimerkiksi vakioita, jottei tarvitse muistaa lukuja ulkoa ja jotta niiden muuttaminen käy tarvittaessa helpommin:

#define PII 3.14159
float sade = 2.0, ala = PII * sade * sade;
/*  float sade = 2.0, ala = 3.14159 * sade * sade;  */

#define LUKUJA 10
int luvut[LUKUJA];
/*  int luvut[10];  */

Määrittelyissä voi käyttää apuna myös muita määrittelyitä. Määrittelyjen järjestyksellä ei ole merkitystä, vaan korvauksia suoritetaan, kunnes kaikki korvattava on saatu korvattua. Samaa symbolia ei kuitenkaan korvata useaan kertaan, eli korvaussilmukoita ei voi muodostua.

#define A   2 * B
#define B   4
#define C   C + 1
A == B + B != C
/*  ensin:  2 * B == 4 + 4 != C + 1  */
/*  sitten: 2 * 4 == 4 + 4 != C + 1  */

Esimerkistä nähdään siis, että C korvataan ensin merkinnällä C + 1 mutta tässä esiintyvää uutta C:tä ei enää korvata. Sen sijaan A korvautuu 2 * B:llä ja tästä B edelleen 4:llä, jolloin saadaan 2 * 4.

Määrittely voi olla myös tyhjä:

#define X
1, X, 2
/*  1, , 2  */

Oppaan lopussa esitellään joitakin valmiita vakioita.

Ehdot (#if, #elif, #else, #endif, #ifdef, #ifndef)

Esikääntäjä kykenee käsittelemään yksinkertaisia ehtolauseita, joilla voidaan jättää osa koodista käsittelemättä. Ehdot voivat sisältää laskutoimituksia (+, -, *, /), loogisia operaattoreita (!, ||, &&, ^), bittioperaatioita (~, &, |, ^), suuruusvertailuja (<, >, <=, >=, ==, !=) sekä tietenkin sulkuja. Lisäksi voidaan tarkistaa, onko tietty esikääntäjän vakio määritelty. Yksittäisten vakioiden kohdalla arvo 0 on epätosi ja muut arvot tosia. Tyhjä vakio ei kelpaa ehdoksi yksinään.

Ehtolause alkaa #if-komennolla ja päättyy riviin #endif. Yksinkertaisimmillaan ehtolause voisi näyttää tältä:

#if 0
  KOODI
#endif

Ehtona on arvo 0, epätosi. Siispä KOODI ei koskaan sisälly lopulliseen lähdekoodiin. Tämä onkin käytännöllinen tapa poistaa C-ohjelmasta osia silloin, kun kommenttimerkit (/**/) eivät jostain syystä sovi tarkoitukseen.

Operaattorilla defined voidaan tarkistaa, onko tietty nimi määritelty #define-komennolla, ja tätä yleistä tarkistusta on nopeutettu lyhenteillä #ifdef ja #ifndef. Näistä ehdoista ensimmäinen toteutuu, jos nimi on määritelty, ja toinen, jos ei ole. Seuraavat koodit ovat siis täysin vaihtokelpoiset:

#if defined VAKIO
  /*  VAKIO on määritelty  */
#endif
#if ! defined VAKIO
  /*  VAKIO ei ole määritelty  */
#endif
#ifdef VAKIO
  /*  VAKIO on määritelty  */
#endif
#ifndef VAKIO
  /*  VAKIO ei ole määritelty  */
#endif

#else-rivi lopettaa #if-rivistä alkaneen lohkon ja aloittaa uuden, joka suoritetaan vain, jos ehto oli epätosi. #elif tarvitsee tämän lisäksi vielä uuden ehdon, jonka se puolestaan tarkistaa. Luonnollisesti #elseä seuraa koodilohkon jälkeen #endif.

Mutkikkaampi ehtorakenne voisi siis olla tällainen:

#define A 7
#define B 12
#ifndef A
  /*  A:ta ei ole määritelty.  */
#elif !defined(B) || ((A + 0) | (B + 0)) == 0
  /*  B:tä ei ole määritelty, tai A tai B on tyhjä tai nolla.  */
#elif A < 10 || B < 10
  /*  A tai B on pienempi kuin 10.  */
#elif B >= A && A < 0
  /*  B on suurempi tai yhtä suuri kuin A, ja A on pienempi kuin nolla.  */
#else
  /*  Tilanne ei ole mikään aiemmista jokin muu.  */
#endif

Virheet (#error)

Joskus voi olla tarpeen aiheuttaa käännösvirhe koodiin. Esimerkiksi jokin kirjasto voi määritellä vakion KIRJASTON_VERSIO, ja oma ohjelma voi käännösvaiheessa tarkistaa, onko käytössä riittävän uusi versio. Virheen saa tuotettua komennolla #error.

#if KIRJASTON_VERSIO < 4
  #error Kirjastosi on liian vanha!
#endif

Makrot (#define)

Makrot ovat kuin esikääntäjän funktioita, ja C:ssä niitä toisinaan käytetäänkin pienten funktioiden tilalla, jottei ohjelmaan tule oikeaa funktiokutsua. Makro määritellään vakion tavoin #define-rivillä, ja se voi ottaa yhden tai useampia parametreja, joita se käyttää määrittelyssään. Vakiota voikin pitää makrona, joka ei ota yhtään parametria.

Yksinkertainen makro voisi olla vaikkapa tällainen:

#define LAUSEKE(a, b)  a * (a + b)
LAUSEKE(2, 3)
/*  2 * (2 + 3)  */

Makron määrittelyssä esiintyvät symbolit a ja b korvataan siis annetuilla luvuilla 2 ja 3.

Esikääntäjän ehtolauseissakin voi käyttää makroja.

#define ndef(a)  (! defined (a))
#if ndef(V1) || ndef(V2)
  /*  #if (! defined (V1)) || (! defined (V2))  */
#endif

Makro voi olla myös tyhjä:

#ifdef HALUTAAN_TULOSTAA
  #define TULOSTA(x)  puts(x); /*  "täysi"  */
#else
  #define TULOSTA(x) /*  tyhjä  */
#endif
{ TULOSTA("moi") }
/*  { puts("moi"); } TAI { }  */

C99 toi virallisestikin mahdollisuuden käyttää makrolla myös vaihtelevan pituisia parametrilistoja, mutta monet kääntäjät ovat tukeneet näitä jo aiemmin. Kun makron viimeiseksi parametriksi merkitään kolme pistettä (...), näiden paikalle voi syöttää useita parametreja, jotka muodostavat yhdessä parametrin __VA_ARGS__.

#define TULOSTA(fmt, ...) (puts(# __VA_ARGS__), printf(fmt, __VA_ARGS__))
TULOSTA("%s, %d kg\n", nimi, massa);
/*  (puts("nimi, massa"), printf("%s, %d kg\n", nimi, massa));  */

Määrittelyjen kumoaminen (#undef)

Minkä tahansa #define-komennolla tuotetun asian voi poistaa komennolla #undef. Näin täytyy tehdä esimerkiksi silloin, kun halutaan vaihtaa määrittely toiseksi tai lopettaa sen vaikutus.

#define TARKISTA(x, y) ((x) < (y))
if (TARKISTA(a, b)) puts("Kaikki kunnossa.");
/*  if ((a) < (b)) puts("Kaikki kunnossa.");  */

#undef TARKISTA
#define TARKISTA(x, y) (funktio((x), (y)) == 3)

if (TARKISTA(luku, 123)) puts("Kaikki kunnossa.");
/*  if ((funktio((luku), (123)) == 3)) puts("Kaikki kunnossa.");  */

Tiedostojen liittäminen (#include)

Koodiin voi liittää ulkoisia tiedostoja komennolla #include. Myös nämä muut tiedostot käsitellään tällöin esikääntäjällä, ja eri tiedostoissa määritellyt vakiot ja muut ovat voimassa myös toisissa tiedostoissa. Kun esikääntäjä kohtaa #include-komennon, se siis hyppää heti tämän komennon määräämään tiedostoon, aivan kuin se kuuluisi samaan koodiin.

Yleisimmin #include-komennolla liitetään mukaan valmiita otsikkotiedostoja tai itse tehtyjä otsikoita. Jokainen tätä lukeva on varmaankin jossain vaiheessa nähnyt jo tällaisia rivejä:

/*  Liitetään C:n standardikirjaston tulostusfunktiot:  */
#include <stdio.h>
/*  Liitetään C++:n standardikirjaston tulostusluokat ja -oliot:  */
#include <iostream>

Komennolle on muitakin käyttötapoja. Toisinaan joistakin ohjelmassa esiintyvistä tiedoista tulee pitkä lista, jonka ei ole kuitenkaan ohjelman toiminnan kannalta olennainen. Silloin onkin kätevää siirtää nämä tiedot omaan tiedostoonsa, jotta koodista tulisi selkeämpi. Lisäksi tiedot voi esittää tiedostossa makron parametreiksi sijoitettuna, jolloin niiden muotoiluun voi vaikuttaa määrittelemällä kyseisen makron ennen liittämistä. Seuraavassa esimerkissä henkilöiden nimet ja iät on sijoitettu erilliseen tiedostoon HENKILO-makron parametreiksi, ja täältä ne sitten liitetään koodin sekaan sopivassa muodossa.

henkilot.txt:

HENKILO("Pontus", 5)
HENKILO("Pelle", 17)

koodi.c:

struct henkilo {
  int ika;
  char nimi[10];
};
/* Määritellään sopiva muotoilumakro */
#define HENKILO(NIMI, IKA)   {IKA, NIMI},
struct henkilo henkilot[] = {
  #include "henkilot.txt"
  {0, ""} /* merkitään taulukon loppu nimettömällä henkilöllä */
};
#undef HENKILO

for (i = 0; strlen(henkilot[i].nimi) > 0; ++i) {
  printf("%s on %d vuotta vanha.\n", henkilot[i].nimi, henkilot[i].ika);
}

Tulos:

struct henkilo {
  int ika;
  char nimi[10];
};

struct henkilo henkilot[] = {
{5, "Pontus"},
{17, "Pelle"},
  {0, ""}
};

for (i = 0; strlen(henkilot[i].nimi) > 0; ++i) {
  printf("%s on %d vuotta vanha.\n", henkilot[i].nimi, henkilot[i].ika);
}

Tiedoston nimi voi olla joko "lainausmerkeissä" tai <kulmasulkeissa>. Jälkimmäisessä tapauksessa tiedostoa etsitään ennalta määrätyistä paikoista kuten kääntäjän include-hakemistosta. Lainausmerkkien kanssa tiedostoa etsitään myös nykyisestä käännöshakemistosta, ja siksi tämä onkin omien tiedostojen liittämisessä yleisin tapa.

Nimen voi määritellä myös esikääntäjän vakiolla:

#define T "tiedosto2"
#include T

Sanojen yhdisteleminen makroissa (##)

Makrojen parametreja voi yhdistää koodin sanoihin niin, että muodostuu uusia sanoja. Tällä tavalla voidaan esimerkiksi määritellä kätevästi samankaltaisia funktioita, joilla on eri nimi ja erityyppiset parametrit:

#define MIN(tyyppi)  static tyyppi min_ ## tyyppi(tyyppi a, tyyppi b) { \
  return a < b ? a : b; \
}

MIN(int)
/*  static int min_int(int a, int b) { return a < b ? a : b; }  */
MIN(float)
/*  static float min_float(float a, float b) { return a < b ? a : b; }  */

Parametreja voi toki yhdistää keskenäänkin:

#define YHDYSSANA(alku, vali, loppu) alku ## vali ## loppu
YHDYSSANA(potku, pallo, peli)
/*  potkupallopeli  */

Tekstin sijoittaminen lainausmerkkeihin (#)

Seuraava makro ei tee aivan sitä, mitä voisi ehkä kuvitella:

#define MOI(nimi)  "Moi, nimi, mitä kuuluu?"
MOI(Antti)
/*  "Moi, nimi, mitä kuuluu?"  */

Operaattoria # tarvitaan, kun halutaan liittää tekstiä lainausmerkkien sisään. Tämäkin on hieman harhaanjohtavasti sanottu, koska tämä operaattori itse asiassa vain lisää tekstin ympärille lainausmerkit, kas näin:

#define TEKSTIKSI(x)  # x
TEKSTIKSI(Alkavat "vitsit" loppua...)
/*  "Alkavat \"vitsit\" loppua..."  */

Samalla tapahtuu myös eräs automaattinen muunnos: "-merkkien eteen lisätään kenoviivat (\"), jottei syntynyt tekstivakio olisi niiden takia viallinen.

Operaattorin juju piilee siinä, että C:ssä peräkkäiset tekstivakiot yhdistetään. Monirivisenkin tekstin voisi alun esimerkistä poiketen kirjoittaa myös näin:

"Tässä on "
"pitkä teksti."

Juuri tällä tavalla saadaan makrossakin aikaan pitempiä tekstivakioita:

#define MOI(nimi)  "Moi, " # nimi ", mitä kuuluu?"
MOI(Antti)
/*  "Moi, " "Antti" ", mitä kuuluu?"  */
/*  C-kääntäjä yhdistelee: "Moi, Antti, mitä kuuluu?"  */

Tekstiksi muuttamisesta voi olla hyötyä myös debug-tarkoituksissa. Esimerkiksi C:n standardikirjaston assert-makro toimii suunnilleen näin:

#define assert(ehto) \
  if (!(ehto)) { \
    printf("Epätosi: " # ehto "\n"); \
    abort(); \
  }
assert(1 == 0)
/*  if (!(1 == 0)) { printf("Epätosi: " "1 == 0"); abort(); }  */

Valmiita vakioita

Nämä hyödylliset vakiot on määritelty valmiiksi. Itse asiassa ne eivät ole oikeastaan vakioita vaan muuttuvat esikäännöksen edetessä.

nimiarvon kuvausesimerkkiarvo
__FILE__käsiteltävän tiedoston nimi"koe.c"
__LINE__käsiteltävän rivin numero13
__DATE__päivämäärä"Jan 13 1970"
__TIME__kellonaika"01:23:45"

Esimerkiksi tiedostoa ja rivinumeroa voi käyttää kätevästi debug-tulostukseen. Seuraavassa esimerkissä esikääntäjä vaihtaa MISSA-sanan paikalle tulostuskomennon, joka kertoo kooditiedoston ja rivinumeron. Näiden avulla virheet on helpompi löytää.

#define MISSA printf("Ollaan tiedoston " __FILE__ " rivillä %d\n", __LINE__)
if (a == 10) {
  MISSA;
}
MISSA;

Esikäännöksen tulos olisi seuraava:

if (a == 10) {
  printf("Ollaan tiedoston " "koe.c" " rivillä %d\n", 3);
}
printf("Ollaan tiedoston " "koe.c" " rivillä %d\n", 5);

Monet kääntäjistä määrittelevät aivan itsekseen joitakin muitakin vakioita. Esimerkiksi vakion WIN32 olemassaolosta voi päätellä, että käännös tapahtuu Windowsissa. Tämän voi tietenkin tarkistaa #ifdef-ehtorakenteella.

Vakiot komentorivillä (gcc/g++)

Useimpien kääntäjien kanssa vakioita voi määritellä myös komentoriviparametreina. Tällöin ohjelman toimintaan voidaan vaikuttaa käännösvaiheessa ilman, että tarvitsee muokata itse koodia. Yleisin käyttötarkoitus on sisällyttää ohjelmaan debug-tulosteita, jotka voi asettaa päälle tai pois komentorivin kautta.

GCC:n vakioita voi asettaa parametrilla -D. Esimerkiksi -DKOE=10 vastaa samaa kuin seuraava rivi:
#define KOE 10

Seuraavassa koodissa käytetään komentoriviparametria debug-tulosteen säätämiseen:

#ifdef TULOSTA_VAROITUS
  #define VAROITUS(fmt, ...) (printf(fmt, __VA_ARGS__))
#else
  #define VAROITUS(fmt, ...) ((void) 0)
#endif

if (maara < 0) {
  VAROITUS("Määrä on alle nollan (%d)\n", maara);
}

Kun koodi käännetään parametrin -DTULOSTA_VAROITUS kanssa, saadaan koodi, joka tulostaa if-lauseessa varoituksen.
gcc koe.c -DTULOSTA_VAROITUS

if (maara < 0) {
  printf("Määrä on alle nollan (%d)\n", maara);
}

Ilman tätä parametria taas saadaan koodi, jonka ehtolauseen sisällä ei tapahdu mitään. Tämäkin koodi on silti laillinen ja kääntyy.
gcc koe.c

if (maara < 0) {
  ((void) 0);
}

Sudenkuoppa: laskujärjestys

Kysymys: Mitä seuraava koodi tekee?

#define KAKSI   1 + 1
#if KAKSI * KAKSI != KAKSI + KAKSI
  #error Nyt ei jokin täsmää!
#endif

Vastaus: Toimituksesta "KAKSI * KAKSI" tuleekin "1 + 1 * 1 + 1", josta normaalin laskujärjestyksen mukaisesti lasketaan ensin kertolasku ja vasta sitten yhteenlaskut, jolloin tulokseksi saadaan kolme eikä neljä. Tämän takia ehtolauseen ehto on siis tosi (eli luvut ovat erisuuret), joten aiheutuu käännösvirhe (#error).

Vastaavia virheitä sattuu vielä helpommin makrojen kanssa:

#define TULO(a, b) a * b
TULO(1+1, 1+1) == 3 != 4
/*  1+1 * 1+1 == 3 != 4  */

Tämän takia on usein hyvä sisällyttää vakioihin sulut ja käyttää makroissa sulkuja parametrien ja ehkä tuloksenkin ympärillä, jotta näitä varmasti käsitellään yhtenäisinä kokonaisuuksina:

#define KAKSI (1 + 1)
#define TULO(a, b) ((a) * (b))
TULO(1+1, 1+1)
/*  ((1+1) * (1+1))  */
KAKSI * KAKSI
/*  (1 + 1) * (1 + 1)  */

Loppusanat

Esikääntäjää pidetään usein vain C-kielestä jääneenä reliikkinä, ja C++:n kanssa sen merkitys kieltämättä onkin paljon vähäisempi. Asiaa voi kuitenkin katsoa myös käänteiseltä kannalta: esikääntäjä mahdollistaa joidenkin C++:n ominaisuuksien toteuttamisen varsin vaivattomasti myös C:llä, kunhan tietää, mitä tekee. Esimerkiksi C++:n kaavaimia (template) muistuttava, vain vähän mutkikkaampi ratkaisu esitettiin ##-operaattorin yhteydessä.

Hullu paljon työtä tekee, viisas pääsee vähemmällä — eli kannattaa pitää esikääntäjä mielessä. Yleensä esikääntäjää ei käytetä juuri muuhun kuin valmiiden otsikkotiedostojen liittämiseen ja toisinaan vakioiden ja lyhyiden makrojen määrittelyyn, ja esimerkiksi operaattorit # ja ## ovat monelle kokeneellekin C-ohjelmoijalle tuntemattomat, vaikka niistäkin kerrotaan C:n standardissa kohdassa "Preprocessing directives". Liika yrittäminen ei toki juuri koskaan johda hyviin tuloksiin, tunari saa esikääntäjällä vain sotkettua koodinsa ja lisättyä virheiden määrää. Kuitenkin taitavalla esikääntäjän käytöllä voi säästää satoja rivejä koodia, selkeyttää mutkikkaita kohtia ja tehdä ohjelmasta siitä huolimatta tehokkaan.

Kommentit

php-Niko [24.12.2008 11:41:05]

#

Hyvä opas. Aika hyvin tiesin noi, vaikka vaan silloin tällöin C++:lla ohjelmoin (siis yritän ohjelmoida ;)).

Propsit sulle! ++

eq [24.12.2008 19:27:44]

#

Esikääntäjän elseif-syntaksi taisi kuitenkin olla:

#elif /* ehto */

Hyvä opas ja hyvää joulua kaikille :)!

nomic [08.01.2009 00:32:31]

#

Mainio opas. :)

Juhko [09.02.2009 19:56:19]

#

Tästä on minulle hyötyä, kiitos.

Torgo [25.02.2009 16:15:04]

#

Hyvä perusopas. Lisää tietoa löytyy esim.
Gnu Preprocessor
ja
Visual C++ Preprocessor

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