Esikääntäjä¶
C-koodissa olemme jo käyttäneet mystisiä
#
-alkuisia käskyjä. Nämä ovat esikääntäjän direktiivejä, eli käskyjä, joilla ohjataan C-kielen kääntäjän käännösprosessia. Osana tätä prosessia voidaan esitellä ohjelmaan omia vakioita ja makroja ja tuoda koodiin ulkopuolisia ohjelmakirjastoja.Kirjastot mukaan¶
Ulkopuolista ohjelmakoodia voidaan esitellä koodissamme
#include
-käskyllä, joka syntaksi on seuraava:#include <otsikkotiedosto.h>
#include "otsikkotiedosto.h"
Otsikkotiedostot ovat kirjastojen funktioiden esittelytiedostoja, joita käsittelemme tarkemmin kirjastojen yhteydessä.
Jos includen yhteydessä käytetään
< >
-merkkejä, hakee kääntäjä otsikkotiedostoa kääntäjän tiedostoista esim. standardikirjastosta. Jos taas käytetään " "
-lainausmerkkejä, etsitään otsikkotiedostoa samasta hakemistorakenteesta missä ohjelmakoodimme sijaitsee. Eli ajatuksena on, että ohjelmoijan omiin kirjastoihin tai ohjelmaan ulkoa tuotuihin kirjastoihin käytettäisiin lainausmerkkejä. Esimerkiksi standardikirjaston jo tutuille tulostusfunktioille (printf, sprint) tiedämme löytyvän otsikkotiedoston
stdio.h
, joka tuodaan koodiin käskyllä:#include <stdio.h>
}}}.
== Vakiot ==
Vakioita on kätevä käyttää jonkin usein esiintyvän, esimerkiksi numeerisen vakioarvon, käyttämiseen koodissa. Vakion arvo voi olla mitä tahansa kunhan se noudattaa C-kielen syntaksia.
(Esikääntäjä)vakioita ei ole olemassa kuin esikäännettävässä koodissa, koska '''esikääntäjä korvaa koodista''' vakion sen arvolla literaalisesti yksi yhteen. Eli ''vakiolla ei ole muistipaikkaa''. Tästä tietenkin seuraa, ettei ohjelmakoodissa vakioon voi tallettaa arvoa, tai käyttää sitä muuttujan tavoin. (Voisimme tietenkin esitellä vakioita myös muuttujilla, mutta tällöin ne varaavat muistia. Jos vakion arvoa halutaan muuttaa, riittää että muutetaan vain koodin alusta vakion arvoa, ihan kuten muuttujissakin.)
Esikääntäjän {{{#define}}}-käskyllä voimme esitellä koodissamme vakiota. Syntaksi on seuraava:
{{{highlight=c
#define PI 3.14159 // Tyylillisesti vakiot yleensä kirjoitetaan isoilla kirjaimilla.
float keha = 2 * PI * sade;
Koodi kääntyisi esikääntäjässä seuraavasti:
float keha = 2 * 3.14159 * sade;
Eli vakiolle ei tehdä mitään muunnoksia, sen arvo pistetään koodin sellaisenaan. Tässä on kääntäjän toiminnassa poikkeuksia, vakiota ei voi laittaa lainausmerkkien sisään eikä vakiona pidetä sanan osaa.
#define TRUE 1
...
printf("TRUE"); // tätä ei käsitetä vakiona, kokeile!
int x = TRUETRUE; // tätä ei käsitetä yhtenä eikä kahtena vakiona
enum-muuttujatyyppi¶
Vakio-arvojen esittelyyn on myös toinen tapa, nimittäin
enum
-muuttujatyyppi. Nyt enum:n arvot asetetaan kasvavasti alkaen ensimmäisen vakion alustusarvosta. Jos alustusarvoa ei ole, lähdetään nollasta. enum retro { PET=1, VIC20, C64, C128 };
// Esitellään tyypin muuttuja
retro kotikone = C64; // Nyt kotikone = 3
enum
-vakioita voidaan käyttää kuten #define-vakioitakin, mutta erona on että niitä voidaan tarkastella koodissa, toisin kuin esikääntäjässä katoavat #define-vakiot.Luonnollisesti, voimme tehdä totuusarvot ohjelmaamme seuraavasti
enum
:n avulla. Nyt siis vakion FALSE tulee olla 0 ja TRUE:n 1 eli siis erisuuri kuin nolla. enum boolean { FALSE=0, TRUE };
...
boolean onko_totta = FALSE;
Hox! Usein eri kehitysympäristöissä on oma kirjastonsa (otsikkotiedosto), jossa vakioille TRUE ja FALSE on jo annettu valmiiksi. Käytetään näitä mieluummin kuin luodaan omat vastaavat vakiot.
Makrot¶
Vakiolla voidaan myös korvata usein käytettävää koodia, jolloin puhutaan makroista. Makroille voidaan myös antaa argumentteja!
#define FOREVER for(;;)
#define INC(A) A++;
Koodissa esikääntäjä korvaa makrot ihan vastaavasti kuin vakiot.
Jälkimmäisessä
INC
-vakiossa annamme makrolle parametriksi muuttujan, jonka arvoa se kasvattaa yhdellä. Nyt ohjelmoijan täytyy itse huolehtia että makroa käytetään oikein, huomioidaan muuttujatyypit jne. Tämä voi helposti johtaa hyönteisiin koodissa, jos esimerkiksi emme ympäröi makroa sulkeilla, jolloin suoritusjärjestys voi olla jotai muuta kun mitä ajateltiin. Tai vakion määrittelyssä sen perään laitetaan ;
käskyn merkiksi, jolloin emme voi käyttää INC
-makroa osana esimerkiksi for-lausetta.#define INC(A) A++;
for (x=0; x < 100; INC(x)) {}
// vakio korvautuisi täsmälleen sen arvolla, jolloin
// sijoituslauseen x++ perässä on nyt ylimääräinen puolipiste.
for (x=0; x < 100; x++;) {}
Jos jätämme vakion määrittelystä puolipisteen pois, sitä voidaan käyttää koodissa muualla lisäämällä puolipiste perään
INC(x);
.Ehdollinen kääntäminen¶
Voimme esitellä vakion myös ilman arvoa. Tällöin vakio on voimassa, mutta sitä ei voi käyttää koodissa esikääntäjän ulkopuolella.
Tätä voidaan tyypillisaesti käyttää ohjaamaan kääntäjän toimintaa, eli mitä kirjastoja tai kirjastojen osia otetaan ohjelmaamme mukaan. Usein portattavan (ts. useissa eri ympäristöissä toimivan) koodin kääntämisen apuna on näitä käyttöjärjestelmä-spesifisia vakioita. Allaoleva vakio
_WIN32
esimerkiksi kertoo, että haluamme kääntää koodimme Windows-koneessa. (Kääntäjäympäristöt yleensä automaattisesti asettavat nämä vakiot kohdalleen, mutta saatamme itse joutua asettamaan joitain niistä, kuten allaolevan math.h-kirjaston vakion.)#define _WIN32
#define _USE_MATH_DEFINES
Eri käyttöjärjestelmästä meillä voi olla eri kirjastot oheislaitteiden käyttöön, vaikka ohjelma muuten toimisikin samoin. Tällöin puhutaan ehdollisesta mukaan ottamisesta. Usein tällainen vakio kerrotaan kääntäjälle, esimerkiksi komentoriviparametrinä, ennenkuin se lähtee kääntämään koodiamme. Esimerkki alustavakiosta.
#ifdef __unix__ // Testaa onko vakio __unix__ asetettu
#include <unix.h>
#elif defined _WIN32 // Testaa onko vakio _WIN32 asetettu
#include <windows.h>
#endif
#if VERSION < 190
#error versions below 1.9.0 not supported
#endif
Esikääntäjässä on siis myös tuttu if-else if-rakenne toteutettuna. Ehtolauseke pitää lopettaa #endif-käskyllä.
Ehtoja joudumme käyttämään kirjastojen yhteydessä varmistamaan että otsikkotiedoston koodi käännetään vain kerran. Jos kirjastoa halutaan käyttää useissa eri koodimoduleissa, lisäämme siis sitä vastaavan #include-käskyn jokaiseen moduliin. Nyt kuitenkin kääntäjä kääntäisi koodin uudelleen jokaisen moduulin käännösvaiheessa. Tästähän kääntäjä ei tykkää, koska jo toisella käännöskerralla koodi on jo olemassa ja käännös päättyy virheilmoitukseen.
Tämän ongelman kierrämme esikääntäjän ehtolauseella seuraavasti, testaamalla vakion olemassa otsikkotiedoston ihan alussa.
#ifndef _HDR // Tarkista onko vakio _HDR asetettu
#define _HDR // Aseta vakio _HDR
...
// Otsikkotiedoston esittelyt
...
#endif
Tässä ohjaamme kääntäjää siis asettamalla ensimmäisellä käännöskerralla vakion
_HDR
, jonka olemassaolo estää uudelleenkäännökset.Esimerkki¶
Sulautetuissa järjestelmissä ehdollista kääntämistä käytetään saman koodin kääntämiseksi eri mikrokontrollereille. Luodaan alustakohtaiset vakiot ja esikääntäjällä ohjataan, että haluttu vakio tulee halutussa alustassa käyttöön.
Alla luodaan vakio, jolla tarkistaan onko nappia 1 painettu riippumatta siitä mikä MCU meillä on (joko ATmega128 tai ATtiny13). Huomataan, että eri MCU:ssa painonappi on kytketty eri I/O-porttiin, joille on laitteen kirjastoissa määritelty omat vakiot PINA, PAO, PINB, ja PB2.
#ifdef __AVR_ATmega128__
#define BUTTON1_DOWN (!(PINA & (1 << PA0)))
#else ifdef __AVR_ATtiny13__
#define BUTTON1_DOWN (!(PINB & (1 << PB2)))
#endif
Nyt siis ehdollisella kääntämisellä mahdollistetaan saman koodin toiminta eri hardiksella.
Lopuksi¶
Esikääntäjämakroihin liittyy paljonkin asioita, joita emme tässä käyneet läpi. Kurssilla ei ole tarpeen määritellä omia makroja, mutta käyttämämme laiteläheiset kirjastot niitä ovat meille turvallisesti saattaneet määritellä.
Anna palautetta
Kommentteja materiaalista?