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 extern1:

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:
    // soubor.c
    void funkce();
    
    int main() {
        funkce();
        return 0;
    }
    
    Tak by kompilace selhala v době linkování, protože by nenašel žádnou funkci/proměnnou, kterou by mohl použít:
    $ 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:
    // soubor1.c
    void funkce() {}
    
    int main() {
        funkce();
        return 0;
    }
    // soubor2.c
    void funkce() {}
    
    Tak by linkování opět selhalo, protože by linker nevěděl, kterou definici použít:
    $ 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;
}