Dělení programu do modulů, pravidla pro jejich vytváření a použití, hlavičkové soubory, výhody odděleného překladu.

Neprve technická vsuvka. Tenhle díl seriálu je kolektivním dílem. Z větší části je založen na Kosťově textu, s menšími změnami z mé (dan) strany. Jsem to já, kdo měl text v prádle naposled a tak je nutné všechny chyby připsat na můj vrub.


Při programování, zvláště pak při práci na rozsáhlejších projektech, je vhodné rozdělit kód programu do více částí. Každá takováto část by pak měla sdružovat funkce určitého typu, například funkce pro práci s grafikou nebo se soubory. Výhodou je, že každá takováto část je víceméně nezávislá a dá se tak použít i při práci na jiných projektech.

Se znalostmi, které zatím máme, jsme tento styl práce mohli uplatňovat pomocí direktivy preprocesoru #include, která jednoduše do zdrojového souboru vložila jiný soubor. Tento způsob je ale, řekněme, jen na půli cesty a vlastně nepřináší příliš velké zjednodušení. Naštěstí ale jazyk C podporuje i oddělený překlad, tedy práci s tzv. moduly, které oproti prostému includování přinášejí mnohé výhody.

Oddělený překlad souborů

Již sám název napovídá, o co vlastně jde. Jednotlivé soubory, které jsme dříve prostě jen spojili do jednoho pomocí direktivy #include, jsou přeloženy každý samostatně. Po překladu jsou pak vzniklé soubory (moduly) spojeny do jednoho programu tzv. linkerem. Výhody jsou zřejmé. Při dalších překladech jsou již kompilovány pouze ty soubory, ve kterých skutečně došlo ke změně, což celý proces značně urychluje. Funkce i proměnné z různých modulů mohou i nemusí být sdíleny, a je tedy možné mít v různých modulech definovány různé funkce se stejným jménem. Další výhodou je pak možnost připojovat a používat i moduly vytvořené v jiném programovacím jazyku, či jiným programátorem.

Sdílení proměnných

Mezi moduly lze sdílet pouze globální datové objekty, neboť ty existují po celou dobu vykonávání programu. Nepřímo pak, voláním sdílené funkce, lze sdílet i statické lokální proměnné. Lokální automatické proměnné sdílet nelze, již kvůli omezené délce jejich trvání (po dobu vykonávání funkce v níž jsou definovány).

Sdílená proměnná musí být definována právě v jednom modulu. V ostatních modulech, ve kterých chceme k této proměnné přistupovat musí být tato proměnná již pouze deklarována (tedy již nedochází k dalšímu přidělení paměti, pouze je překladači oznámena její existence v jiném modulu).

Deklarace sdílené proměnné vypadá stejně jako její definice, jen s tím rozdílem, že před ní ještě uvedeme klíčové slovo extern. V deklaraci proměnné nikdy nesmíme použít její inicializaci. Překladač by pak deklaraci považoval za definici nehledě na klíčové slovo extern.

Následující příklad snad celou záležitost trochu objasní. Mějme dva soubory a.c a b.c, které budeme překládat odděleně, a ve kterých budeme chtít sdílet proměnné x, y a z.

//v souboru a.c:
int x;
int y;
extern int z;

//v souboru b.c:
extern int x;
int y;
extern int z;

Pouze proměnná x je sdílena správně. Proměnná y je definována v obou překládaných souborech a při spojení obou modulů linkerem by tedy došlo ke kolizi. Naopak proměnná z je v obou souborech pouze deklarována, ve skutečnosti tedy fyzicky neexistuje a nedá se tedy použít.

To ale platí jen pro některé (převážně starší) kompilátory a linkery. U novějších použití extern nutné není a dvojí výskyt y kolizi znamenat nebude. Pokud se totiž tentýž symbol vyskytuje ve více modulech a inicializován je nejvýše v jednom, pak moderní linker dokáže při tyto symboly spojit a ve výsledném souboru se bude daný symbol vyskytovat jen jednou. A to bez použití extern v kterémkoliv modulu..

Funkční tak bude kompilace z následujících souborů:

//v souboru a.c:
int x;
int y = 2;

//v souboru b.c:
int x;
int y;

Po sestavení bude v celém programu přítomen jediný symbol x s počáteční hodnotou 0 a jediný symbol y s počáteční hodnotou 2.

Máme-li prostředí, kde funguje tohle, pak použití extern věci jen komplikuje. Představme si, že máme zdrojových modulů pět a v konkrétních případech sestavujeme výsledný program vždy ze tří vybraných podle konkrétní potřeby. Do kterých z modulů dát před symbol extern ? Sestavení se totiž nepovede, pokud budeme symbol ve všech modulech deklarován jako externí.

Takhle to funguje nejen pro prosté proměnné, ale pro všechny identifikátory, včetně například funkcí.

Potíže působí akorát neúplné deklarace. Popsaný postup nelze spolehlivě použít pro

//v souboru a.c:
char *str[] = { "a", "b", "c" };

//v souboru b.c:
char *str[];

V souboru b.c totiž není v okamžiku překladu známa velikost velikost proměnné s. To způsobí nejen méně vážný problém s tím, že nemůžeme použít sizeof(str), ale může způsobit komplikace i při sestavení - například úplně selže.

Nesdílené proměnné

Pokud chceme, aby globální proměnná byla viditelná pouze v rámci svého modulu, uvedeme před její definici klíčové slovo static (Pozor, static se používá i pro automatické lokální proměnné, ale v obou případech je význam jeho použití jiný. viz. Modifikátory paměťové třídy). Protože je taková proměnná viditelná pouze v modulu, kde je definována, v ostatních modulech mohou existovat jiné proměnné se stejným identifikátorem.

Úpravou předchozího příkladu získáme dvě proměnné s identifikátorem y, které jsou ovšem ,narozdíl od proměnné x, nezávislé (změna hodnoty y v jednom modulu neovlivní hodnotu y ve druhém) a navzájem si nepřekážejí.

//v souboru a.c:
int x;
static int y;

//v souboru b.c:
extern int x;
static int y;

Sdílení funkcí

Stejně jako u sdílení proměnných, platí, že i funkce musí být definována právě v jednom modulu a v ostatních modulech musí být uvedena už jen pouze její deklarace (hlavička). Použití klíčového slova extern u deklarací funkcí již není nutná, protože překladač dokáže bezpečně rozlišit deklaraci od definice i bez něho. Pro zvýšení čitelnosti programu lze ale jeho použití doporučit.

Nesdílené funkce vytvoříme, obdobně jako u proměnných, použitím klíčového slova static a i zde platí, že v různých modulech mohou existovat různé nesdílené funkce mající stejný identifikátor.

Vytváření modulů

Při vytváření vlastních modulů bychom se měli řídit několika konvencemi, které celou práci s moduly výrazně ulehčují? Nejprve je nutné ujasnit si samotné rozdělení kódu do jednotlivých modulů. Je jasné, že funkce by měly být do jednotlivých modulů sdružovány podle svého účelu, např. modul obsahující funkce pro práci s grafikou, pro práci s myší apod.

Další věcí, kterou bychom měli stanovit, je závislost jednotlivých modulů. Ideální je vytvářet závislosti ve tvaru stromu, kdy „vysokoúrovňové“ moduly využívají pouze funkcí modulů nižší úrovně. To sice často nejde splnit, přesto je ale dobré se k tomuto požadavku, v zájmu přehlednosti programu, alespoň přiblížit.

Jak už víme, chceme-li využít služeb určitého modulu, musíme nejprve do programu vložit deklarace jeho funkcí a proměnných. Je-li ale např. modul již zkompilovaný, nemusíme vůbec znát hlavičky jeho funkcí. Proto tvůrce modulu vytváří i tzv. hlavičkový soubor .h, kde jsou uvedeny všechny deklarace potřebné k použití modulu. Tento soubor se pak jednoduše includuje do programu.

Vytváříme-li modul sami, měli bychom k němu tedy vytvořit i příslušný hlavičkový soubor (stejného jména). Ten by se měl skládat z následujících částí:

definice všech poskytovaných symbolických konstant a parametrických maker
definice poskytovaných uživatelských typů
deklarace všech exportovaných proměnných a funkcí

Abychom zabránili případným vícenásobným deklaracím, které by mohly nastat při několikanásobném vložení tohoto hlavičkového souboru, celý jeho obsah ohraničíme na začátku zápisem

#ifndef KONSTANTA
#define KONSTANTA

a na konci

#endif

KONSTANTA zde představuje v podstatě libovolný řetězec znaků, který by ale měl být unikátní. V praxi se tato symbolická konstanta tvoří kombinací jména modulu, jména autora a případně dalších znaků, které řetězec definitivně odliší od všech ostatních symbolických konstant.

Samotný zdrojový soubor .c modulu by pak měl mít následující strukturu

inkluze příslušného hlavičkového souboru a všech ostatních potřebných .h souborů
definice globálních exportovaných proměnných, tedy těch, které mohou být viditelné i z jiných modulů
definice symbolických konstant, maker a uživatelských typů, které budou využívány pouze uvnitř tohoto modulu
definice globálních neexportovaných proměnných (jsou definovány jako static)
deklarace neexportovaných (static) funkcí
definice exportovaných funkcí
definice neexportovaných funkcí

Ukažme si nyní, jak by mohl takový modul vypadat:

//soubor modul.c:
#include
#include //vložení hlavičkového souboru modulu

int prom1; //exportovaná proměnná
static int prom2; // soukromá proměnná

static int fce2(); // deklarace neexportované funkce

int fce1() //exportovaná funkce
{
...
}

static int fce2() // neexportovaná funkce
{
...
}

//soubor modul.h:
#ifndef __MODUL_H_JOSEF_NOVAK__
#define __MODUL_H_JOSEF_NOVAK__

extern int prom1; //deklarace exportované proměnné
int fce1(); //deklarace exportované fce

#endif

Používání modulů

Moduly vytvořené výše popsaným způsobem se požívají velice snadno. V programu, kde chceme používat proměnné nebo funkce jiného modulu, jednoduše includujeme hlavičkový soubor tohoto modulu. Tím do programu vložíme všechny informace, které jsou potřebné pro úspěšný překlad. Nakonec se přeložené soubory všech použitých modulů spojí linkerem do jednoho spustitelného souboru. Většina dnešních překladačů již ale přímo podporuje práci s odděleně překládanými moduly (formou tzv. „projektů“), a tak se uživatel o linkování souborů většinou vůbec nemusí starat a stačí mu pouze přidat soubory modulů do projektu.

Vícejazyčné programy
Vše co bylo dosud řečeno platí i tehdy pokud zdrojové kódy nepoužívají stejný zdrojový jazyk. Lze tak spojovat moduly z C s moduly v Assembleru, nebo třeba Pascalu. Nebo moduly přeložené jedním C překladačem s moduly přeloženými jiným C překladačem.

V takovém případě se ale musíme zabývat ještě jedním aspektem - zda zúčastněné překladače používají stejnou volací konvenci. Volací konvence se zabývá tím, kam a jak se před voláním funkce připraví její parametry a zda je bude uklízet volající nebo volaný kód a také kam bude funkce při svém návratu ukládat návratovou hodnotu. Pokud se volající a volaný kód na tomhle neshodnou nebude vzájemné volání fungovat.
Neřešitelným problémem pak bývá správa paměti - pokud ji každý modul provádí "po svém" - a různé jazyky mají prakticky jistě různý přístup ke správě paměti - pak si paměť budou nejspíš vzájemně přepisovat. Problém nenastává, pokud se ve volaném modulu paměť dynamicky nealokuje (ale ani nepřímo, v rámci nějaké volané knihovní funkce).
Obecně lze doufat v úspěch pokud z komplexního jazyka chceme volat funkce psané v assembleru. Spojení dvou kódů vyšších programovacích jazyků už je "vyšší dívčí" - tedy - pokud není alespoň jeden z jazyků na takové konkrétní propojení záměrně připraven.