Kihagyás

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
#define TOMB_MERET 1020

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!)

1
#define min(X,Y)  ((X)<(Y)?(X):(Y))

A fenti makró esetén például a min(a,b) forráskód-részlet ((a)<(b)?(a):(b))-vel 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 rendszerben
  • m88k: Motorola 88000 processzornál
  • sparc: Sparc processzornál
  • sun: Minden SUN számítógépen
  • svr4: 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
#if kifejezés
...
#endif /* kifejezés */

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
#if kifejezés1
... /* első ág */
#elif kifejezés2
... /* második ág */
#else
... /* harmadik ág */
#endif

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 plust 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
/* sin(x) közelítő értékének kiszámítása a beépített sin(x)
 *   függvény alkalmazása nélkül.
 * 1997. Október 25.  Dévényi Károly, devenyi@inf.u-szeged.hu
 * 2014. Május 19. Gergely Tamás, gertom@inf.u-szeged.hu
 */

#include <stdio.h>
#include <math.h>

#define EPSZ    1e-10                           /* a közelítés pontossága */

double szinusz(double x) {
    double osszeg = 0.0;                   /* a végtelen sor kezdőösszege */
    double tag    = x;                   /* a végtelen sor aktuális tagja */
    double xx     = x * x;                                      /* sqr(x) */
    int    j      = 2;                         /* a nevező kiszámításához */

    do {                                                /* ciklus kezdete */
        osszeg += tag;
#if DEBUG > 0
        fprintf(stderr, "[LOG] Tag=% 30.10lf Osszeg=% 30.10lf\n", tag, osszeg);
#endif
        tag = -(tag * xx / j / (j + 1));                 /* következő tag */
        j += 2;
    } while (fabs(tag) >= EPSZ);              /* végfeltétel, ciklus vége */
    return osszeg;
}

int main() {
    double x;                                               /* argumentum */

    printf("Kérem sin(x)-hez az argumentumot\n");
    scanf("%lg%*[^\n]", &x); getchar();            /* egész sort olvasunk */
#ifdef DEBUG
    printf("sin(%8.5f)= %13.10f\n", x, szinusz(x));
#endif

    double x_orig = x;                              /* eredeti argumentum */

    while (x < -M_PI) {                                 /* transzformálás */
        x += 2 * M_PI;
    }
    while (M_PI < x) {
        x -= 2 * M_PI;
    }
    printf("sin(%8.5f)= %13.10f\n", x_orig, szinusz(x));

    return 0;
}

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
#line sorszám

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:201:9: error: expected identifier or '(' before numeric constant
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.


Utolsó frissítés: 2020-10-21 09:51:52