A C procedurális mivoltából azt gondolhatnánk, hogy mindenhonnan meghívhatunk minden függvényt. Ez lényegében így is van, de vannak limitációi. Például ha egy másik forrásfájlban lévő függvényt szeretnénk meghívni, akkor a hívó oldalon is definiálnunk kellene a függvény szignatúráját, hogy egyáltalán meg tudjuk hívni.
Itt jönnek képbe a header fájlok, amelyek .h kiterjesztéssel rendelkeznek. Ezek a mi életünket teszik egyszerűbbé azáltal, hogy nem kell kézzel beírogatnunk a másik forrásfájlban definiált függvények deklarációit a saját forrásfájljaink elejére, hanem elég csak megkérnünk az előfordítót, hogy szúrja be azokat a header fájlokból.
A header fájlok csak a függvények definícióit szokták tartalmazni, a tényleges implementáció egy, a header fájl nevével megegyező .c fájlban szokott lenni, mert így például egy printf implementáció nem fog többször szerepelni a programban, hanem csak egyszer. A headerben levő deklaráció egy jelzés a fordító felé, hogy valahol majd lesz egy olyan függvény, ezért nyugodtan figyelmen kívül hagyhatja a fordítás során. Úgy is tekinthető, mint egy referencia, amit majd a linker fog feloldani.
A header fájlok másik előnye, hogy akár tekinthetünk rájuk úgy is, mint egyfajta interfészre, ami adatrejtést tesz lehetővé. Például van egy nagy algoritmusunk, amely implementációjában több, kisebb specifikus függvényt használ, amelyeket nem szeretnénk, hogy a könyvtár használója csak úgy egyszerűen meghívjon. Ebben az esetben a header-ben csak az algoritmus fő belépési függvényét publikáljuk ki, a tényleges implementáció során használt segédfüggvényeket pedig el tudjuk „rejteni”. A hangsúly itt az idézőjeleken van, mert a memóriában és a lefordított programban is ott lesznek ezek a függvények és végső soron ismerve a szignatúrát meghívhatóak bárhonnan.
Azonban ez egy olyan lehetőség, mint C# esetén a reflection. Bizonyos esetekben nagyon hasznos, de nem minden programban és probléma megoldásnál van helye.
Nézzünk egy példát, hogy hogyan épül fel egy header fájl:
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
int add(int a, int b);
int subtract(int a, int b);
#endif
A fenti functions.h fájl két egyszerű függvényt deklarál: egy add és egy subtract függvényt, de az implementációjukat nem. Ezeket akár nevezhetjük függvény prototípusnak is. A header fájlt #ifndef és #endif utasításokkal szokás körbevenni és definiálni egy, a header fájl nevére utaló szimbólumot. Erre azért van szükség, hogy a header-ben található definíciók csak egyszer legyenek behelyettesítve a fordítás során. Például ha a header fájlunk-ban #include utasítással behúzunk egy header-t és a header nincs #ifndef utasítással körbevéve, akkor fordítási hibát kapnánk abban az esetben, ha az alkalmazott header az implementációban is megjelenik. Ez azért történik, mert az #include egyszerű behelyettesítést végez. Ez pedig azt eredményezi, hogy egy fordítási egységben többször is definiálva lesz ugyanaz a függvény vagy struktúra, ami fordítási hibát okoz.
Az újabb C fordítók esetén a #pragma once utasítás a header fájl elején ki tudja váltani a #ifndef és #endif blokkokkal körbevevést. Azonban ha maximális kompatibilitásra törekedünk más fordítókkal is, akkor érdemes továbbra is a #ifndef és és #endif blokkok használata mellett maradni.
A header, mint láthattuk az interfész, amihez tartoznia kell egy implementációnak is. Ez lesz a functions.c, ami így fog felépülni:
#include "functions.h"
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
A functions.c tartalmában nincs semmi különleges, maximum annyi, hogy a nevének célszerű megegyeznie a header nevével, de a kiterjesztésnek .c fájlnak kell lennie .h helyett. A tesztelő fő program pedig:
#include <stdio.h>
#include "functions.h"
int main()
{
int num1 = 10;
int num2 = 5;
printf("add(10, 5): %d\n", add(num1, num2));
printf("sub(10, 5): %d\n", subtract(num1, num2));
return 0;
}
A fő programban a saját header fájlunk elérési útját idézőjelek között kell megadnunk. Ez az elérési útvonal relatív az éppen fordított C fájlhoz képest. A header fájlok útvonalában a / jelet kell használni, mint mappa elválasztó. Tehát ha a functions.h az includes mappán belül található, akkor a helyes útvonal: includes/functions.h.
A kérdés már csak az, hogy hogyan kell lefordítani a több fájlból álló programokat? Egyetlen nyelv fordítója sem rendelkezik fordító szinten projekt fájl fogalmával, mivel ez tipikusan a build eszköz feladata. Vannak nyelvek és platformok, ahol ez része a nyelvi eszközöknek (pl. C# és .NET esetén), de a C ezeket jóval megelőzi és 50 éves történelme alatt számos eszköz fejlődött ki. Az alap szolgáltatásokat azonban a fordító szolgáltatja. Ez azt jelenti, hogy a gcc segítségével is le tudjuk fordítani a programunkat, csak több lépésben.
Első lépésben le kell fordítanunk a functions.c és main.c fájlokat:
gcc -c functions.c
gcc -c main.c
A -c kapcsoló arra utasítja a fordítót, hogy a .o kiterjesztésű objektum kód fájlokat gyártson. Ezek a fájlok már a C kódunkból fordított gépi utasításokat tartalmazzák binárisként, de még nem futtathatóak. Futtathatóvá a linkelési folyamat után válnak. A linkelési folyamat csinál nekünk futtatható binárist. Ez szintén egy gcc parancs segítségével indítható:
gcc -o tobbfajlpelda main.o functions.o
A fordítás elvégezhető egy parancs segítségével is akár: gcc -o tobbfajlpelda functions.c main.c, de ettől a háttérben az előzőleg részletezett több lépéses fordítási folyamat zajlik le.