A preprocesszor
A C előfeldolgozó¶
A C előfeldolgozó (preprocesszor) egy makroprocesszor, melyet a C fordítóprogram automatikusan meghív, mielőtt a tényleges fordításhoz hozzákezdene. Azért nevezzük makroprocesszornak, mert lehetőségünk van úgynevezett makrók definiálására, hogy a hosszú konstrukciókat lerövidíthessük. A C előfeldolgozó nincs tisztában a C nyelv szintaxisával, csupán egy sororientált szövegszerkesztőről van szó, amely bizonyos behelyettesítéseket, átalakításokat végez a forráskódon, és amelynek éppen ezért az eredménye egy forráskódhoz meglehetősen hasonlító szöveg.
Főbb feladatai:
- Kódtisztítás.
- Fájlok (általában header fájlok) bemásolása.
- Makró behelyettesítés.
-
Feltételes fordítás.
-
Sor vezérlés.
A program forráskódjában elhelyezhetünk az előfeldolgozónak szóló utasításokat.
Ezek az utasítások a #
jellel kezdődnek. Nem fontos, hogy a legelején legyen a sornak ez a karakter, de nem előzheti meg egyéb "értelmes" (azaz nem whitespace) karakter.
A preprocesszor ezeket az utasításokat még a C nyelvű fordítás előtt végrehajtja. Működése független a C szintaktikájától.
Azaz akár használható nem C programok átalakításához is bizonyos célokra.
Átalakítás után azonban (ha eredetileg C programot fordítottunk), akkor az "átdolgozott" forráskód már szigorúan megfelel a C szabványoknak (feltéve, hogy nincs benne hiba).
Kódtisztítás¶
Ez a tevékenység az összes, a gép számára felesleges tartalmat eltünteti.
Ilyenek a kommentek. A /*
és */
közötti részeket kicseréli egy darab szóközre, a //
utáni részt pedig törli a sor végéig.
Sok esetben az olvashatóság érdekében a sorokat tördelni szokás (legfőképp hosszabb makrók esetében).
Ezeket a továbbiak számára felesleges töréseket is eltávolítja a preprocesszor, vagyis a sorvégi \
karaktereket és az utána levő sortörést törli, ezzel összevonva a két sort.
Header fájlok¶
Az előfordítónak az #include
parancs segítségével tudjuk megmondani, hogy a forráskód adott helyére egy másik fájl tartalmát másolja be.
Ilyenkor az előfordító megkeresi a megadott nevű fájlt, a tartalmát bemásolja a parancs helyére, majd a bemásolt fájl elejétől folytatja a feldolgozást.
Ilyen módon a bemásolt fájlok tartalma is feldolgozásra kerül, tehát például a bennük található újabb#include
parancsok hatására újabb fájlok lesznek bemásolva.
Az #include
paranccsal leginkább header fájlokat szoktunk bemásolni.
Ezek olyan fájlok, melyekben gyakran használt konstans- és típus definíciók, függvény- és esetleg változó deklarációk szerepelnek, melyek a program fordításához szükségesek.
Az #include
segítségével nem kell ezeket a definíciókat minden egyes forráshoz külön-külön kézzel bemásolni.
Technikailag kétféle header fájlt lehet megkülönböztetni:
- A fordítókörnyezethez, operációs rendszerhez, szabványos függvénykönyvtárakhoz tartozó header fájlok.
1
#include <header.h>
Az ilyen fájlok előre kijelölt helyeken találhatók (pl. /usr/inlcude
), az előfeldolgozó először itt keresi őket. Ha nem találja, akkor a -I
fordítási kapcsolóval megadott útvonalakat nézi végig.
-
A saját programrendszerünkhöz tartozó header fájlok, melyekben az általunk deklarált vagy definiált, de több helyen felhasznált programelemek találhatóak.
1
#include "header.h"
Az ilyen fájlokat elsősorban az aktuális könyvtárban, a forráskód mellett keresi a preprocesszor, és ha nem találja, akkor nézi végig a
-I
fordítási kapcsolóval megadott útvonalakat.
Makrók¶
#define
¶
A preprocesszor számára a #define
parancs segítségével tudunk szöveg-behelyettesítési szabályokat (makrókat) definiálni.
Az előfeldolgozó ilyenkor egy adott szöveg összes további (a #define
utáni) előfordulását kicseréli egy másik szövegre.
A fordítóprogram -D
kapcsolójának segítségével parancssorból is adhatunk meg makrókat, ezek úgy viselkednek, mintha közvetlenül a program elején definiáltuk volna őket.
Az #undef
parancs segítségével lehetőség van a makrók (adott ponttól érvényes) törlésére is.
Az "üres'' makró is definiáltnak számít.
Emlékezzünk vissza, ezt használjuk ki akkor, amikor a header fájlok többszörös bemásolását akarjuk elkerülni.
Egyszerű és paraméteres makrók¶
Az egyszerű makró egy szimpla szöveg behelyettesítés. Ennek segítségével lehet például valódi konstansokat készíteni.
1 |
|
A fenti #define
esetén például az előfordító a TOMB_MERET
minden előfordulását az 1020-as számleírásra cseréli (egész addig, amíg egy #unset TOMB_MERET
sorral nem találkozik), így a C fordító már csak a sok 1020 értéket fogja látni.
Paraméteres makróval bonyolultabb konstrukciókat is lehet készíteni. A makró ilyenkor a függvényhíváshoz hasonlóan viselkedik, azaz más-más paraméterekkel meghívva más és más kódot kapunk. (De ez nem valódi függvényhívás, hanem még a fordítás előtt megtörténik a behyelyettesítés!)
1 |
|
A fenti makró esetén például a min(a,5)
forráskód-részlet ((a)<(5)?(a):(5))
-tel helyettesdítődik.
Mivel a makróhívások paraméterei tetszőleges kifejezések lehetnek, amiket így tetszőleges kifejezésekbe illeszthetünk be, így ahhoz, hogy a különböző precedenciájú műveletek kiértékelési sorrendje ne okozzon nem várt viselkedést, érdemes a makrón belül a kifejezéseket zárójelezni, ahogy az a példán is látszódik.
Előre definiált makrók¶
A standard előre definiált makrókat az ANSI szabvány rögzíti. Léteznek előre definiált makrók, melyek az operációs rendszer, architektúra, fordítóprogram jellemzőire utalnak:
unix
: Minden UNIX rendszerbenm88k
: Motorola 88000 processzornálsparc
: Sparc processzornálsun
: Minden SUN számítógépensvr4
: System V Release 4 szabványú UNIX operációs rendszerben
Ezek a makrók platformfüggő kódok esetén feltételes fordításnál lesznek hasznosak.
Feltételes fordítás¶
A feltételes fordítás szerepe, hogy bizonyos kódok csak bizonyos esetekben kerülnek át a fordítóhoz:
Itt is az if
alapszó szolgál az egyszerű szelekció megvalósítására, mint magában a C nyelvben, habár ez az if
nem teljesen ugyanaz szintaktikára sem.
Míg a C programban szereplőif
utasítás a program végrehajtása közben érvényesül, addig az előfeldolgozónál használt #if
még a fordítás előtt értékelődik ki és ennek megfelelően más és más forráskód kerül lefordításra.
Miért használunk feltételes fordítást?
-
A géptől és az operációs rendszertől függően más-más forráskódra van szükségünk.
-
Ugyanazt a programot több célból is használni szeretnénk, pl. teszünk bele nyomkövető utasításoka, de a végső változatba már nem.
-
Nem akarjuk, hogy ugyanaz a kódrészlet többször fordításra kerüljön.
A feltételes fordítás direktívái:
#if
#ifdef
#ifndef
#elif
#else
#endif
Egyszerű szelekció, ha a kifejezés igaz, a kód része lesz a programnak, ha hamis, akkor nem:
1 2 3 |
|
Többszörös szelekciónak speciális kulcsszava van, és feltétel nélküli egyébként ág is alkalmazható.
1 2 3 4 5 6 7 |
|
Példa: `szinusz(x) kiszámítása feltételes nyomkövetéssel¶
Sok esetben adódik úgy, hogy egy probléma megoldása nem megy elsőre. Valahol a program mégsem azt csinálja, amit elvárunk tőle. Ilyenkor a megoldás valamilyen debug eszköz használata, amellyel lépésről lépésre haladva tudjuk a programot végrehajtani, minden helyen követve a változók értékeit.
Persze a legtöbb esetben ez a folyamat egyszerűen abból áll majd, hogy egy printf
utasításokkal teletűzdeljük a programot, és a megfelelő pontokon kiíratjuk a számunkra érdekes változó értékét, vagy egyszerűen a printf
-fel jelezzük, hogy adott sorra rákerült-e a vezérlés.
Sokszor ez sokkal célravezetőbb megoldás is, mert sokkal gyorsabban megy, mint maga a debuggolás.
Szóval ha egyszer teletűzdeljük a programunkat mindenféle segédinformációt kiírató sorral, egy idő után talán sikerül kijavítani a programot, és ezekre a sorokra nem lesz szükség... Egészen addig, amíg talán egy újabb fejlesztési lépésben rájövünk, hogy valami azért mégsem annyira jó.
Ha ilyenkor kitöröljük, majd újraírjuk azokat az utasításokat, amik a nyomkövetést segítik, az nyilván feleslegesen kidobott plusz munka.
Ekkor lehet segítségünkre az, ha ezeket az utasításokat feltételes fordításhoz kötnénk.
Nézzük meg a korábban már megismert szinusz(x)
kiszámítását ilyen segéd információkat kiírató sorokkal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Figyeljük meg ebben a kódban azokat a sorokat, amelyek a DEBUG
makró értékét vizsgálják.
Van olyan feltételes sorunk, ami azt nézi, hogy ennek makrónak az értéke nagyobb-e 0-nál, de olyan is van, amely csak a létezését nézi.
Fordítsuk le a programunkat a következő módokon! (Tegyük fel, hogy a programot szinusz-dbg.c állományba mentettük )
Kimenet
$ gcc -Wall -DDEBUG=1 -o szinusz szinusz-dbg.c -lm
$ ./szinusz
Kérem sin(x)-hez az argumentumot
20
[LOG] Tag= 20.0000000000 Osszeg= 20.0000000000
...
...
[LOG] Tag= -0.0000000003 Osszeg= 0.9129452492
sin(20.00000)= 0.9129452492
[LOG] Tag= 1.1504440785 Osszeg= 1.1504440785
...
...
[LOG] Tag= 0.0000000010 Osszeg= 0.9129452507
sin(20.00000)= 0.9129452507
Kimenet
$ gcc -Wall -o szinusz szinusz-dbg.c -lm
$ ./szinusz
Kérem sin(x)-hez az argumentumot
20
sin(20.00000)= 0.9129452507
Ha egy makrót a fordítással szeretnénk definiálni, akkor azt a -D
fordítási kapcsolóval tudjuk megtenni.
Akár értéket is adhattunk itt az adott makrónak.
Sorvezérlés¶
A C forráskód több helyről másolódhat össze.
A preprocesszor által elvégzett "összemásolás" esetén nyilván van tartva, hogy melyik sor honnan származik.
A preprocesszor "előtt" elvégzett másolás, vagy forráskód-generálás során a#line
direktívával adhatjuk meg, hogy eredetileg honnan származik a kód.
1 |
|
Kimenet
$ cat -n preproc .c
1 # define N 30
2
3 # ifdef DEBUG
4 # define STRING "Debug"
5 # else
6 # define STRING "Release"
7 # endif
8
9 # line 200
10 int main () {
11 int unix;
12 char tomb [N] = STRING;
13 for ( unix = N - 1; unix && tomb [unix]; --unix) {
14 tomb [unix] = 0;
15 }
16 return 0;
17 }
Kimenet
$ gcc preproc.c
preproc.c: In function 'main':
preproc.c:203:15: error: lvalue required as left operand of assignment
preproc.c:203:44: error: lvalue required as decrement operand
Kimenet
$ gcc -E preproc .c
# 1 " preproc .c"
# 1 "
# 1 "< command -line >"
# 1 "/ usr / include /stdc - predef .h" 1 3 4
# 1 "< command -line >" 2
# 1 " preproc .c"
# 200 " preproc .c"
int main () {
int 1;
char tomb [30] = "Release";
for(1 = 30 - 1; 1 && tomb [1]; --1) {
tomb [1] = 0;
}
return 0;
}
A példában az állomány 9. sorában levő sor az érdekes, illetve az, hogy prepocesszálás után azt jelzi a kód, hogy az a preproc.c állomány 200. sora. Illetve a hibaüzenetek ehhez viszonyítva jelennek meg 201, illetve 203. sorokban.