Používání kódu z jiných souborů
Nyní už víme, jak přeložit program skládající se z více jednotek překladu (zdrojových souborů) a následně tyto jednotky spojit dohromady pomocí linkeru. V této sekci si ukážeme, jak můžeme použít kód, který existuje v jiném zdrojovém souboru.
Pokud chceme zavolat funkci, kterou jsme napsali v jiném souboru, můžeme ji prostě zavolat a linker se postará o zbytek:
// soubor1.c
int main() {
moje_funkce();
return 0;
}
// soubor2.c
void moje_funkce() {}
Pokud tyto dva soubory přeložíme a poté slinkujeme, tak se zavolá správná funkce:
$ gcc -c soubor1.c
$ gcc -c soubor2.c
$ gcc soubor1.o soubor2.o -o program
Nicméně, pokud bychom používali kód z jiných souborů takto "naslepo", narazili bychom na několik
problémů. Tím, že překladač v souboru soubor1.c
nemá přístup k signatuře
funkce moje_funkce
, tak nemůže ověřit, jestli jsme jí předali správný počet argumentů se správnými
datovými typy, a ani neví, jaký je datový typ návratové hodnoty této funkce.
Kód "naslepo" navíc nebude vůbec fungovat pro použití (globálních) proměnných. Při pokusu o použití neexistující proměnné by překladač totiž rovnou ohlásil chybu.
Deklarace vs definice
Ideálně bychom potřebovali překladači říct, jak bude kód, který chceme použít, vypadat – jaký bude datový typ a název globální proměnné, popř. jaké budou parametry, návratový typ a název funkce. Toho můžeme dosáhnout pomocí tzv. deklarace (declaration).
Deklarace "slibuje", že bude v programu existovat nějaká proměnná či funkce s konkrétním názvem a typem, ale neříká, kde bude tato proměnná či funkce vytvořena (může to být například v jiném zdrojovém souboru). Samotné vytvoření funkce či proměnné se nazývá definice (definition). Zatím jsme tedy prováděli vždy definice funkcí i proměnných, nyní si ukážeme, jak vytvořit pouze deklaraci.
Deklaraci funkce provedeme tak, že zadáme její signaturu, ale ne její tělo:
int funkce(int a, int b); // deklarace funkce
int funkce(int a, int b) { ... } // definice funkce
Deklaraci globální proměnné lze provést tak, že před ní dáme klíčové slovo extern
1:
1Toto klíčové slovo můžeme použít i před deklarací funkce, nicméně není to potřeba, extern
je
na tomto místě předpokládáno implicitně.
extern int promenna; // deklarace proměnné
int promenna; // definice proměnné
Při sdílení kódu napříč soubory má smysl se bavit pouze o globálních proměnných. Lokální proměnné lze totiž používat vždy pouze v rámci jedné funkce.
Díky deklaracím tak můžeme v jednom zdrojovém souboru určit, jak mají vypadat funkce a proměnné, které chceme používat, aby překladač mohl provádět kontrolu datových typů. Linker pak během linkování použije správné proměnné/funkce z odpovídajících zdrojových souborů. Více o tom, kde a jak deklarace vytvářet, se dozvíme v příští sekci o hlavičkových souborech.
Jednoprůchodový překlad
Z historických důvodů překladače C fungují v tzv. jednoprůchodovém režimu (one-pass compilation). Znamená to, že překladač "čte" náš zdrojový kód shora dolů, a v momentě, kdy chceme například použít nějakou funkci nebo proměnnou, tak již překladač dříve musel vidět (alespoň) její deklaraci, popř. rovnou i definici.
Například v následujícím programu:
void funkce1() {
funkce2();
}
void funkce2() {}
si překladač bude stěžovat na to, že na řádku 2 nezná funkci funkce2
, protože tato funkce je v
souboru nadefinovaná až po funkci funkce1
, která ji používá:
test.c: In function ‘funkce’:
test.c:2:5: warning: implicit declaration of function ‘funkce2’;
2 | funkce2();
Pokud tedy potřebujeme nadefinovat funkci na pozdějším místě, než je její první použití, můžeme nejprve vytvořit její deklaraci a až později (popř. v úplně jiném souboru) vytvořit její definici:
void funkce2(); // deklarace
void funkce1() {
funkce2(); // použití
}
void funkce2() {} // definice
Takovýto program už se přeloží bez varování. Koncept deklarování funkcí či proměnných v jednoprůchodových překladačích se nazývá dopředná deklarace (forward declaration).
Pravidlo jedné definice
V C platí tzv. pravidlo jedné definice (one definition rule). Každá proměnná i funkce musí být v programu nadefinována právě jednou (deklarována může být vícekrát). To platí jak v rámci jednoho souboru, tak v rámci celého programu (tj. napříč všemi zdrojovými soubory).
- Pokud bychom proměnnou či funkci pouze nadeklarovali a/nebo použili bez definice:
Tak by kompilace selhala v době linkování, protože by nenašel žádnou funkci/proměnnou, kterou by mohl použít:// soubor.c void funkce(); int main() { funkce(); return 0; }
$ gcc -c soubor.c $ gcc soubor.o /usr/bin/ld: test.o: in function `main': test.c:(.text+0xe): undefined reference to `funkce' collect2: error: ld returned 1 exit status
- Pokud bychom naopak nadefinovali proměnnou či funkci více než jednou:
Tak by linkování opět selhalo, protože by linker nevěděl, kterou definici použít:// soubor1.c void funkce() {} int main() { funkce(); return 0; } // soubor2.c void funkce() {}
$ gcc -c soubor1.c $ gcc -c soubor2.c $ gcc soubor1.o soubor2.o /usr/bin/ld: soubor2.o: in function `funkce': soubor2.c:(.text+0x0): multiple definition of `funkce'; test.o:test.c:(.text+0x0): first defined here collect2: error: ld returned 1 exit status
Viditelnost funkcí a proměnných
Z jiných souborů lze používat pouze funkce a proměnné, které jsou veřejné. Implicitně jsou
všechny funkce i všechny globální proměnné veřejné. Pokud byste chtěli zamezit tomu, aby mohly
ostatní soubory používat nějakou funkci nebo globální proměnnou, můžete ji označit klíčovým slovem
static
, abyste z nich udělali soukromé funkce či proměnné:
static void soukroma_funkce() {}
static int soukroma_promenna;
Takovéto funkce a proměnné půjde používat pouze v souboru, ve kterém byly nadefinovány. Doporučujeme
static
používat pro označení proměnných a funkcí, které nechcete sdílet se zbytkem programu. Půjde
tak na první pohled poznat, které funkce jsou určeny k použití z jiných souborů a které ne2.
2Použití static
také může v určitých případech vést k vygenerování efektivnějšího kódu a
menší velikosti výsledného spustitelného souboru.
Klíčové slovo
static
lze také použít u lokálních proměnných, zde má ovšem úplně jiný význam než u globálních proměnných! Použitístatic
u lokální proměnné z ní udělá proměnnou uloženou v globální paměti. Takováto proměnná se nainicializuje, když se program poprvé dostane k řádku s její definicí. Proměnná bude existovat po celou dobu běhu programu a udrží si svou hodnotu i po skončení volání funkce:#include <stdio.h> void test() { static int x = 0; x += 1; printf("%d\n", x); } int main() { test(); test(); return 0; }