Když začínáte programovat, máte všechno v jednom souboru. To je v pohodě, dokud je program krátký. Jakmile ale začne růst, stane se z něj nepřehledný moloch. V Céčku se tohle řeší tak, že si program rozdělíte do více souborů — modulů. Každý modul obsahuje funkce, které spolu logicky souvisí. Pojďme se podívat, jak se k tomu dopracovat.
Všechno v jednom souboru¶
Představte si jednoduchý program, který bliká LEDkou a používá k tomu funkci
delay. Celý program máte v jednom souboru main.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
Tohle funguje. Ale co když budu chtít funkci delay použít i v jiném projektu?
Nebo co když bude program mít 20 takových funkcí a celý soubor naroste na 500
řádků? Začne to být nepřehledné a špatně se v tom orientuje.
Přesunu funkci do jiného souboru¶
Přirozeně nás napadne: dám funkci delay do samostatného souboru. Vytvořím
tedy soubor delay.c:
1 2 3 4 5 6 7 8 9 | |
A v main.c ji budu chtít zavolat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Jenže kompilátor při zpracování souboru main.c narazí na volání funkce
delay(500) a netuší, co to je. Nezná její návratový typ, neví, jaké má
parametry. Každý soubor se totiž kompiluje samostatně a kompilátor nevidí do
jiných souborů.
Musíme mu tedy říct: “Hele, tahle funkce existuje, vypadá takhle, ale její tělo je jinde.” Tomu se říká deklarace (nebo také prototyp) funkce.
Deklarace funkce — prototyp¶
Deklarace funkce vypadá jako hlavička funkce, ale bez těla — jen se zakončí středníkem:
1 | |
Tím říkáme: existuje funkce delay, která nic nevrací a přebírá jeden
parametr typu uint16_t. Její definice (tělo) je někde jinde.
Náš main.c by tedy vypadal takto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Teď kompilátor ví, jak funkce delay vypadá, a přeloží main.c bez problémů.
Linker pak při linkování spojí oba objektové soubory (main.rel a
delay.rel) dohromady a dosadí skutečnou adresu funkce delay.
Tohle ale pořád není úplně ideální. Co když budu chtít funkci delay použít
ve třech různých souborech? Musel bych do každého z nich ručně napsat tu
deklaraci. A co když se změní parametry funkce? Musel bych to opravit na
všech místech.
Hlavičkový soubor — #include¶
Řešení je dát deklaraci do hlavičkového souboru (header file) s příponou
.h. Vytvoříme soubor delay.h:
1 2 3 4 5 6 7 8 | |
Direktivy #ifndef, #define a #endif jsou tzv. include guard. Zajistí,
že se obsah hlavičky vloží jen jednou, i kdyby byl #include "delay.h" uveden
vícekrát. To se může stát, když jeden hlavičkový soubor vkládá druhý.
Soubor delay.c si vloží vlastní hlavičku:
1 2 3 4 5 6 7 8 9 | |
A main.c teď vypadá čistě:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Máme tedy tři soubory:
| soubor | co obsahuje |
|---|---|
delay.h |
deklarace (prototyp) funkce |
delay.c |
definice (tělo) funkce |
main.c |
hlavní program, volá delay() |
Co do hlavičkového souboru patří a co ne¶
Do .h souboru patří:
- deklarace funkcí (prototypy)
- definice maker (
#define) - deklarace externích proměnných (
extern) - definice datových typů (
typedef,struct,enum)
Do .h souboru nepatří:
- těla (definice) funkcí
- definice proměnných (bez
extern)
Pravidlo je prosté: hlavička říká, co existuje; zdrojový soubor říká, jak to funguje.
Externí proměnné¶
S funkcemi je to jasné — deklaraci dám do hlavičky a tělo do .c souboru.
Ale co když potřebuji, aby byla jedna proměnná přístupná z více souborů?
Například knihovna milis počítá milisekundy od startu programu. Proměnná
miliseconds se inkrementuje v přerušení uvnitř milis.c, ale chci ji
číst i z main.c.
Proměnná se definuje (tedy vytvoří v paměti) v jednom .c souboru.
Tady žije, tady se pro ni alokuje místo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
V hlavičkovém souboru se proměnná deklaruje pomocí klíčového slova
extern. To říká: “Tato proměnná existuje, ale je definována jinde —
nerezervuj pro ni paměť, jen věz, že někde je.”
1 2 3 4 5 6 7 8 9 10 11 12 | |
Teď můžu v main.c proměnnou miliseconds přímo číst:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Bez klíčového slova extern by kompilátor vytvořil novou proměnnou
miliseconds v každém souboru, který ji definuje. Měli bychom dvě různé
proměnné se stejným jménem a linker by zahlásil chybu, nebo (ještě hůř)
by se program choval nepředvídatelně.
Takže pravidlo: definice proměnné (bez extern) je právě jednou v .c souboru. Deklarace s extern je v .h souboru a říká
ostatním modulům, kde ji hledat.
Možná vás napadne: ale vždyť milis.c si vkládá vlastní hlavičku milis.h
— takže po předzpracování preprocesorem tam bude extern deklarace i
definice pohromadě. Nevadí to? Ne, nevadí. Deklarací (extern) může být
kolik chcete — říkají jen “taková proměnná existuje”. Definice (bez
extern) musí být právě jedna a ta skutečně alokuje paměť. Kompilátor si
obojí spojí a je spokojený. Je to stejné jako u funkcí, kde taky můžete mít
prototyp a tělo ve stejném souboru. A proč si v .c souboru vůbec vkládáme
vlastní hlavičku? Je to pojistka — kompilátor zkontroluje, že deklarace
v hlavičce odpovídá skutečné definici. Kdybyste třeba v hlavičce omylem napsali uint16_t místo uint32_t, kompilátor by zahlásil chybu. Bez toho #include by si nesrovnalosti nikdo nevšiml.
Jak se to přeloží¶
Každý .c soubor se nejprve zkompiluje samostatně do objektového souboru
.rel. Přepínač -c říká kompilátoru: “Jen zkompiluj, nelinkuj.”
1 2 | |
Teď máme dva objektové soubory. Ty se v druhém kroku slinkují dohromady do výsledného programu:
1 | |
Výsledkem je soubor main.ihx ve formátu
Intel HEX, který se nahraje do mikrokontroléru.
Když máte víc modulů, prostě přidáte další .rel soubory:
1 2 | |
A tady je další výhoda oddělené kompilace: když změním jen jeden soubor,
nemusím překompilovat všechno. Pokud upravím jen delay.c, stačí překompilovat delay.rel a znovu slinkovat. Soubor main.rel zůstane
beze změny. U malého projektu to není poznat, ale u většího projektu
s desítkami souborů to ušetří spoustu času.
Automatizace překladu pomocí make¶
Kdo si má pamatovat, co se změnilo a co ne? Od toho je tu program
make. Nástroj make čte soubor Makefile, ve kterém jsou popsané závislosti
— tedy co závisí na čem. Když pak spustíte příkaz make, přeloží se jen
to, co je potřeba.
Zjednodušeně řečeno může kus Makefile vypadat třeba takto:
1 2 3 4 5 6 7 8 | |
Čte se to takto: main.ihx závisí na main.rel a delay.rel. Pokud se
některý z nich změní, spustí se příkaz pod ním. Podobně main.rel závisí na main.c a delay.h — pokud se změní hlavička, překompilují se všechny
soubory, které ji vkládají.
V praxi za vás Makefile řeší tohle všechno automaticky, takže nemusíte příkazy psát ručně. Ale je důležité chápat, co se pod kapotou děje — tedy že se každý soubor překládá zvlášť a teprve linker je spojí dohromady.
Shrnutí¶
- Když je program malý, klidně mějte vše v jednom souboru.
- Jakmile začne růst, rozdělte ho na moduly — každý modul je dvojice souborů
.ca.h. - Do
.hsouboru dejte deklarace (prototypy) funkcí aexternproměnné. - Do
.csouboru dejte definice (těla) funkcí a definice proměnných. - V ostatních souborech použijte
#include "modul.h"a máte vše k dispozici.
Celý proces překladu je podrobněji popsán v článku Céčko.