Esikääntäjä¶
Osaamistavoitteet: Osataan käyttää esikääntäjäkäskyjä C-kielisessä ohjelmassa ja tiedostetaan niiden hyödyt laiteläheisessä ohjelmoinnissa.
Tähän asti kurssilla esitellyssä C-koodissa olemme jo käyttäneet mystisiä
#
-alkuisia käskyjä. Nämä ovat esikääntäjän käskyjä (direktiivejä), joilla ohjataan C-kielen kääntäjän käännösprosessia. Osana prosessia voidaan C-kielessä tuoda ohjelmaan ulkopuolisia kirjastoja sekä esitellä ohjelmaan omia vakioita ja makroja. Yleisesti esikääntäjillä on oma ohjelmointikielensä, jonka ei tarvitse olla sidoksissa käännettävään kieleen.
Kirjastot mukaan¶
Otsikkotiedostot ovat kirjastojen funktioiden esittelytiedostoja, sisältäen mm. niiden prototyypit. Niiden sielunelämää käsittelemme tarkemmin kirjastojen yhteydessä, mutta katsotaan
#include
-esikääntäjäkäskyn kaksi eri muotoa. #include <otsikkotiedosto.h>
#include "otsikkotiedosto.h"
Tässä on syntaksissa pari asiaa. Jos käytetään
< >
-merkkejä, hakee kääntäjä otsikkotiedostoa kääntäjäympäristön omista kirjastoista, kuten C-kielen standardikirjasto tai sulautetun laitteen ohjelmointiympäristön kirjastot. Jos taas käytetään " "
-lainausmerkkejä, etsitään otsikkotiedostoa samasta (projekti)hakemisto-rakenteesta missä oma ohjelmakoodimme sijaitsee. Siis ajatuksena on, että ohjelmoijan omiin kirjastoihin tai ohjelmaan ulkoa tuotuihin kirjastoihin käytettäisiin lainausmerkkejä. Sinällään sillä järjestyksellä millä otsikkotiedostoja esitellään koodimodulissa, ei ole merkitystä kääntämisen kannalta. Jos otsikkotiedostojen sisältöön tutustuu paremmin, huomaa että niissäkin tuodaan toisia otsikkokirjastosta reippaasti mukaan ohjelmaan. Tästä lisää hetken päästä..
Esimerkiksi standardikirjaston jo tutuille tulostusfunktioille
printf
, sprintf
tiedämme prototyypit löytyvän otsikkotiedoston stdio.h
:#include <stdio.h>
Vakiot¶
Esikääntäjällä voidaan myös esitellä vakioita, jotka eivät ole sama asia kuin C-kielen (vakiomuotoiset) muuttujat. Vakion arvo voi olla mitä tahansa kunhan se noudattaa C-kielen syntaksia.
Vakioita on kätevä käyttää jonkin usein (ohjelman ulkopuolelta asetetun) esiintyvän, esimerkiksi numeerisen vakioarvon, nimeämiseen koodissa. Esimerkiksi M_PI (matematiikkakirjastosta
math.h
) tuo ohjelmaan standardoidun piin likiarvon, jota on syytä käyttää.. koska.. no, standardi, joka takaa että kaikki piin arvoa tarvitsevat toiminnallisuus toimii sitten samalla tavoin. Nyt huomataan, että näitä esikääntäjävakioita ei ole olemassa kuin esikäännettävässä koodissa. Käännösprosessin aikana esikääntäjä korvaa koodista vakion sen arvolla, kirjaimellisesti yksi yhteen. Tästähän seuraa että esikääntäjävakiolla ei ole muistipaikkaa ja näinollen esikääntäjävakiota ei voi käyttää muuttujan tavoin.
Esikääntäjän
#define
-käskyllä esitellään ohjelmassa vakiota. Syntaksi on seuraava:#define PI 4.0 // Vakiot yleensä kirjoitetaan isoilla kirjaimilla.
float keha = 2 * PI * sade;
Tämä koodi kääntyisi esikääntäjässä seuraavasti:
float keha = 2 * 4.0 * sade;
Kuten todettu, vakion arvo pistetään koodin sellaisenaan.
Toki 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
Uusi enum-muuttujatyyppi¶
Vakioarvojen esittelyyn on myös toinen tapa, nimittäin
enum
-muuttujatyyppi. Nyt enum
esitellyt vakioarvot asetetaan kasvavasti alkaen ensimmäisen vakion alustusarvosta. Jos alustusarvoa ei ole, lähdetään nollasta. enum retro_computer { PET=1, VIC20, C64, C128, AMIGA };
// Esitellään tyypin muuttuja
retro_computer kotikone = C64; // Nyt kotikone = 3
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. Ihan vain siksi, ettei omat vakiomäärittelymme sotke muiden kirjastojen toimintaa. Makrot¶
Vakiolla voidaan myös korvata usein käytettävää koodia, jolloin puhutaan makroista. Esikääntäjän 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 esikääntäjämuuttujan A
, jonka arvoa se kasvattaa yhdellä. Nyt ohjelmoijan täytyy itse huolehtia että makroa käytetään oikein C-koodissa, huomioiden 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. Vastaavasti tällainen vakio on voimassa vain käännösprosessin aikana.
Tätä ominaisuutta tyypillisesti käytetään ohjaamaan kääntäjän toimintaa, niin että voidaan kertoa mitä (laite)kirjastoja tai muuta koodia otetaan valikoidusti ohjelmaan mukaan. Usein portattavan (ts. useissa eri ympäristöissä toimivan) koodin kääntämisen apuna on käyttöjärjestelmä-spesifisia vakioita. Kääntäjäympäristön vakio
_WIN32
esimerkiksi kertoo, että haluamme kääntää koodimme sopivaksi Windows-koneelle. Kääntäjäympäristöt yleensä automaattisesti asettavat nämä vakiot kohdalleen, kun niille kerrotana mille laitteelle olla hommia tekemässä. Joten niistä ei kurssilla tarvitse huolehtia.#define _WIN32
Eri käyttöjärjestelmästä meillä voi olla eri kirjastot oheislaitteiden käyttöön, vaikka ohjelman logiikka muuten toimisikin samoin. Usein tällainen vakio kerrotaan kääntäjälle komentoriviparametrinä ennenkuin se lähtee kääntämään koodia. Esimerkki alustavakioista, joka voitaisiin tuoda käännösprosessiin kääntäjän (gcc) komentoriviparametrillä
gcc -D_WIN32 -DVERSION=190
. #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-else
-ohjausrakenne toteutettuna. Tässä ehtolauseke pitää lopettaa #endif
-käskyllä. Huomataan myös esikääntäjäkäsky #error
. Esikääntäjäehdoille on hyvin oleellinen käyttötapaus kirjastojen toiminnallisuuden tuomisessa ohjemaan. Ehdollisella esikääntämisellä varmistetaan, että ohjelmana tuodun otsikkotiedoston koodi käännetään vain kerran. Nyt aiemman perusteella, kun kirjastoa halutaan käyttää useissa eri koodimoduleissa lisäämme siis sitä vastaavan
#include
-käskyn jokaiseen moduliin. Tämähän kuitenkin tarkoittaisi sitä, että tällöin otsikkotiedoston koodit (mm. prototyypit) esiteltäisiin ohjelmassa useaan kertaan, josta C-kielen kääntäjät eivät tykkää. Tällaisen ongelman kierrämme esikääntäjän ehtolauseella, testaamalla vakion olemassaolo ensimmäisenä otsikkotiedostossa.
#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ää saman koodin uudelleenkäännökset, jos otsikkotiedosto tuodaan jossain muussa modulissa uudelleen käyttöön.Esimerkki¶
Sulautetuissa järjestelmissä ehdollista kääntämistä käytetään tyyppillisesti laitekohtaisen I/O:n määrittelyssä, jotta sama koodi toimisi eri laitteissa / mikrokontrollereissa. Kääntäjäympäristöissä on käytössä saman nimiset vakiot, joiden arvot riippuvat laitteesta.
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
..
// myöhemmin koodissa
if (BUTTON1_DOWN == VAKIO) { ...
...
}
Näistä sulautetun laitteen I/O:hon liittyvistä vakiosta lisää kurssin laiteläheisessä osiossa.
Lopuksi¶
Esikääntäjäkäskyihin ja -kieliin liittyy vielä 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 SensorTagin ohjelmointiin ovat makroja runsaasti esitelleet. Noh, ne makrot ovat tuhannet koodarit ajan saatossa testanneet toimiviksi.
Anna palautetta
Kommentteja materiaalista?