Jak na pro­jek­ty v ja­zy­ce C

© Damig, 2004 – 2016
Koncept

Moduly a knihovny

Modulární programování je dalším užitečným prostředkem pro strukturování programů. Moduly umožňují organizovat datové struktury a podprogramy do relativně samostatných a nezávislých celků. Bez modulů se sice dá při programování obejít, ale rozdíl v kvalitě práce bez nich a s nimi je asi takový jako rozdíl mezi manufakturou a moderní továrnou. Modulární přístup ke tvorbě programových projektů má několik výhod:

  1. Moduly zvyšují přehlednost rozsáhlejších programů, protože kód každého modulu je umístěn v samostatném souboru (nepočítáme-li hlavičkové soubory). Velký projekt je tak možno rozdělit do několika menších souborů.
  2. Do modulu umísťujeme podprogramy a datové struktury, které spolu souvisí. Navíc lze řídit, co bude viditelné pro uživatele modulu a co zůstane mimo modul skryto (viz dále).
  3. Dobře navržené moduly zvyšují znovupoužitelnost kódu. Nemusíte tudíž tytéž problémy řešit stále znovu.
  4. Moduly lze samostatně ladit.
  5. Moduly usnadňují dělbu práce při týmové práci.

Struktura modulu

V jazycích C a C++ nejsou moduly součástí jazyka (např. v Pascalu ano), ale jsou záležitostí překladače a linkeru. Modul je zde tvořen minimálně dvěma soubory. Prvním je takzvaný hlavičkový soubor. Tento soubor má tradičně koncovku .h a obsahuje deklarace všech veřejných datových typů, konstant, případně globálních proměnných (opatrně s nimi!) a prototypy veřejných funkcí. V hlavičkovém souboru by se nikdy neměl vyskytovat kód, ani definice proměnných, či konstantních proměnných. Každá konstrukce v hlavičkovém souboru by měla být dobře okomentována, protože hlavičkový soubor slouží jako rozhraní modulu a musí být přístupný každému uživateli tohoto modulu (tedy programátorovi). Toto rozhraní slouží překladači k provedení typové kontroly při používání funkcí.

Druhým souborem je zdrojový soubor s koncovkou .c (nebo .cpp, .c++, .cxx pro jazyk C++). Tento soubor obsahuje deklarace všech skrytých datových typů a konstant, definice a inicializace veřejných globálních proměnných a konstantních proměnných a implementace všech, skrytých i veřejných funkcí modulu. Aby byla zajištěna konzistence mezi rozhraním a zdrojovým souborem, mělo by být ve zdrojovém souboru daného modulu vždy použito jeho vlastní rozhraní. Tím umožníme překladači provést kontrolu, zda prototypy odpovídají implementacím funkcí. Často se totiž stane, že si při implementaci uvědomíme, že je potřeba změnit například parametry funkce, ale už tu změnu zapomeneme udělat v hlavičkovém souboru. Pokud je rozhraní správně vloženo do zdrojového souboru modulu, překladač nás na tuto nekonzistenci při překladu upozorní.

Použití rozhraní

Chceme-li používat modul, musíme do zdrojového kódu vložit jeho rozhraní. Někdy se nevyhneme tomu, že hlavičkový soubor vkládáme do jiného hlavičkového souboru (zejména, když potřebujeme použít datový typ deklarovaný jinde). Vkládání hlavičkových souborů se dělá dvěma variantami příkazu preprocesoru. Buďto použijeme příkaz preprocesoru #include "mojerozhrani.h", když chceme vkládat lokální rozhraní nebo použijeme #include <stdio.h>, když vkládáme rozhraní systémové knihovny.

Rozdíl mezi lokálním a systémovým hlavičkovým souborem je v místě jeho umístění. Pokud použijeme úhlové závorky, bude překladač hledat hlavičkový soubor v systémovém adresáři (např. /usr/include v unixovém systému), pokud použijeme uvozovky, bude překladač hledat hlavičkový soubor ve stejném lokálním adresáři, z jakého byl spuštěn překlad.

Tento příkaz preprocesoru v podstatě před vlastním překladem vloží textový obsah hlavičkového souboru do místa jeho použití. Tím nám umožní splnit pravidlo, že každému použití nějakého identifikátoru musí předcházet jeho deklarace a zároveň umožní překladači provést typovou kontrolu.

Před vlastním popisem překladu projektu s moduly se musím zmínit o jednom praktickém problému s rozhraním modulu. U projektů s moduly se často stává, že stejné rozhraní vkládáme do několika zdrojových, ale i jiných hlavičkových souborů. Když to ale spojíme s tvrzením v předchozím odstavci, zjistíme, že může snadno dojít k vícenásobné deklaraci stejných identifikátorů. Naštěstí preprocesor jazyka C nabízí příkazy pro podmíněný překlad, které nám umožní tomuto problému předejít. Každý hlavičkový soubor by měl být ošetřen způsobem, který je prezentován v ukázce 1.

// pokud ještě nebyl definován tento symbol, přelož zbytek souboru
// pokud už ale definován byl, obsah tohoto souboru už neprocházej
#ifndef __MOJEROZHRANI_H__

// definuj symbol
// toto se definuje jen při prvním průchodu
#define __MOJEROZHRANI_H__

// zde je obsah hlavičkového souboru

// konec bloku podmíněného překladu
#endif
Ošetření hlavičkového souboru proti vícenásobnému vkládání v rámci jenoho modulu.

Překlad projektu s moduly

Při překladu projektu tvořeného více moduly máme dvě možnosti. U jednoduchých projektů lze překládat jedním příkazem, jako v ukázce 2. Všimněte si, že hlavičkové soubory se překladači nepředávají. Je to proto, že příkazy k použití hlavičkových souborů jsou už obsaženy ve zdrojových souborech jednotlivých modulů. Překladač v tomto případě přeloží všechny moduly v zadaném pořadí a rovnou je slinkuje (spojí) do jednoho binárního souboru program.

Tento způsob překladu se však potýká s problémy v případech, že moduly používají svá rozhraní křížově, tj. když například modul č. 1 používá rozhraní modulu č. 2 a modul č. 2 používá zároveň rozhraní modulu č. 1. Proto je obecně výhodnější překládat moduly odděleně, jako v ukázce 3. Tím vzniknou tzv. objektové soubory (s koncovkou .o nebo .obj), které potom musíme pomocí linkeru spojit do koncového binárního souboru naší aplikace.

Ruční překlad souborů s více moduly se může stát velmi pracným. Stačí si uvědomit, že velké projekty mohou být tvořeny stovkami různých modulů. I kvůli dalším výhodám je vhodné pro překlad programů s moduly používat program make, který jsme si popsali v jedné z předchozích kapitol.

$ gcc -Wall -std=c99 -pedantic modul1.c modul2.c modul3.c program.c -o program
Překlad i linkování programu s moduly v jednom kroku.
$ gcc -Wall -std=c99 -pedantic -c modul1.c -o modul1.o
$ gcc -Wall -std=c99 -pedantic -c modul2.c -o modul2.o
$ gcc -Wall -std=c99 -pedantic -c modul3.c -o modul3.o
$ gcc -Wall -std=c99 -pedantic -c program.c -o program.o
$ gcc modul1.o modul2.o modul3.o program.o -o program
Překlad každého modulu probíhá samostatně. Přepínač -c říká, že se má použít pouze překladač a ne linker. Až jsou všechny moduly přeloženy, dojde k jejich slinkování (poslední řádek). Program gcc sám pozná, že dostal jako parametry objektové soubory a tudíž má vyvolat linker.

Knihovny

Z modulů nemusíme skládat pouze spustitelné aplikace, ale i knihovny. Knihovna se liší od aplikace tím, že neobsahuje funkci main a slouží vlastně jako zásobárna datových typů a podprogramů, které lze používat v jiných aplikacích.

Rozlišujeme knihovny statické a dynamické. Statické knihovny se k aplikaci linkují staticky, tedy v době překladu a jejich kód se kopíruje do binárního souboru výsledné aplikace. Výsledný binární soubor pak vychází o něco větší než u dynamických knihoven. Dynamické nebo také sdílené či dynamicky linkované knihovny jsou odděleny od binárního souboru aplikace a při jejich provozu se využívá tzv. dynamické linkování až za běhu aplikace. Jejich výhodou je, že mohou být sdíleny více aplikacemi, které běží zároveň a to tak, že operační systém je natáhne do paměti jenom jednou a ostatní aplikace je využívají až ve chvíli, kdy je to potřeba (tedy když volají knihovní funkce).

Pokud chceme nějakou knihovnu používat, musíme o tom říct překladači. Jedinou výjimkou je systémová knihovna jazyka C, která se linkuje automaticky. Pro přilinkování knihovny slouží přepínač -ljmeno, tedy malé písmeno l následované základem jména knihovny. Jméno knihovny podléhá zvyklostem v používaném operačním systému. Například matematická knihovna se v Linuxových systémem nazývá libm.so, ve Windows by to mohlo být jméno m.dll. Základem jména knihovny je m a proto se k aplikaci linkuje přepínačem -lm, viz ukázka 4.

$ gcc -Wall -std=c99 -pedantic matematika.c -o matematika -lm
Překlad programu s matematickou knihovnou. Pozor! U novějších verzí GCC (od verze 4.8) se už není nutné matematickou knihovnu linkovat ručně, protože se linkuje automaticky. U starších verzí GCC a zřejmě i u jiných překladačů se to dělá tímto způsobem.

Tento přepínač vlastně nepatří překladači, ale linkovacímu programu. Není potřeba jej volal ručně. Program gcc si linker zavolá sám a přílušné přepínače mu předá. Souvisí s tím malá nepříjemnost – přepínače patřící linkeru je nutné zapisovat až jako poslední za název výsledného souboru. Pokud navíc linkujeme více modulů a knihoven, záleží na pořadí v jakém se zapisují.

Pokud bychom měli knihovnu se jménem atlas, potom bychom v Linuxu pravděpodobně našli soubor se jménem libatlas.so, zatímco ve Windows by se jmenoval atlas.dll. V obou systémech bychom pak při kompilaci našeho programu, který jí bude používat, překladači sdělili náš úmysl přepínačem -latlas.

Pokud bychom tuto knihovnu instalovali z distribučních balíčků zvolené Linuxové distribuce, byla by pravděpodobně knihovna rozdělena do dvou balíčků. Balíček se jménem libatlas-1.3.5.deb (v distribucích jako Debian nebo Ubuntu) by obsaloval binární soubor knihovny určený pro použití ostatními nainstalovanými aplikacemi. Druhý balíček by byl určen pro vývojáře a obsahoval by hlavičkové soubory potřebné pro vytváření nových aplikací. Tento vývojářský balíček by se mohl jmenovat například libatlas-1.3.5-dev.deb. Čísla v názvech balíčků znamenají označení verze knihovny.

Statické knihovny mají v Linuxu koncovku .a a vytváří se programem ar, což je program pro tvorbu archivů, viz ukázka 5. V Linuxu je statická knihovna vlastně archivem, ve kterém jsou zabaleny objektové soubory, a který je doplněn tabulkou symbolů, které se v nich nacházejí. Tento postup lze použít i ve Windows v MinGW.

$ ar rcs knihovna.a modul1.o modul2.o modul3.o
Vytváření statické knihovny v unixovém systému pomocí programu ar a přepínači r, c, s (význam viz manuálová stránka).

Dynamické knihovny mají v Linuxu předponu lib a koncovku .so a vytváří se pomocí překladače gcc přepínačem -shared. Podle manuálové stránky programu gcc tento přepínač nemusí fungovat na všech platformách (zřejmě tam, kde operační systém neumožňuje pracovat s dynamicky linkovanými knihovnami). Na Linuxu je navíc potřeba použít přepínač -fpic nebo -fPIC. Bližší informace najdete v manuálové stránce překladače GCC.

$ gcc -shared -fPIC modul1.o modul2.o modul3.o -o libknihovna.so
Vytváření dynamické knihovny.

Tipy pro práci s moduly a knihovnami

Ukrývání informací

Vše, co uvedeme v hlavičkovém souboru, se stává veřejným rozhraním modulu. To znamená, že každý, kdo do svého zdrojového kódu vloží váš hlavičkový soubor makrem #include bude moci používat vše, co bylo v tomto hlavičkovém souboru deklarováno. Na druhou stranu vše, co zůstane pouze ve zdrojovém souboru modulu, aniž by to bylo deklarováno v rozhraní, zůstává viditelné pouze v tomto zdrojovém souboru. Tyto konstrukce označujeme jako skryté nebo jako privátní části modulu.

Tato finta je užitečná, protože umožňuje vytvářet jednodušší a efektivnější servisní podprogramy. Pokud zůstává některý podprogram skryt před uživateli modulu, nemusí mít tak robustní zabezpečení vstupních hodnot, protože nehrozí, že jej někdo použije nepředvídaným způsobem. Toto zabezpečení pak stačí umístit pouze do veřejných podprogramů. V modulech se lze řídit pravidlem, že veřejné podprogramy musí být odolné proti nepředvídanému použití, kdežto u skrytých tato ochrana může být vynechána ve prospěch vyšší efektivity, protože jsme schopni zajistit bezpečné podmínky použití těchto funkcí.

Obalovací funkce

Tento tip přímo souvisí s předchozím tématem. Často potřebujeme, aby modul obsahoval veřejnou funkci reprezentující nějaký algoritmus. Protože jde o veřejnou funkci, musí být odolná vůči jakýmkoli hodnotám parametrů. Často se ale stává, že algoritmus, který by ošetřoval všechny možné vstupní hodnoty bude příliš složitý. Naopak, když vstupní hodnoty vhodně předzpracujeme, může být vlastní algoritmus jednodušší, může fungovat efektivněji, přesněji, prostě lépe. Zde můžeme s výhodou využít princip obalovací funkce.

Princip obalovací funkce spočívá v tom, že vlastně sama žádný výpočet nedělá. Úlohou této funkce je ošetřit vstupní hodnoty parametrů a volat pomocné (skryté) funkce pro předzpracování vstupních hodnot (heuristiku) a funkce realizující samotný výpočet. Pseudokód v ukázce 7 naznačuje, jak tento princip využít. V tomto ukázkovém příkladu jde o implementaci funkce sinus pomocí Taylorovy řady.

void _sinHeuristika(double *x, int *kvadrant)
{
  // upraví argument tak, aby se nacházel v intervalu <0, PI/2>
  // vrací kvadrant, ve kterém se bude počítat
  // využívá periodičnost funkce
}

double _sinTaylor(double x)
{
  // výpočet hodnoty Taylorovou řadou pro x v intervalu <0, PI/2>
}

double _sinKvadranty(double x, int kvadrant)
{
  // volá _sinTaylor a podle kvadrantu přepočítá výsledek
  if (kvadrant == 1) return _sinTaylor(x);
  else ...
}

// Prototyp této funkce je v hlavičkovém souboru.
double sinus(double x)
{
  // ošetří argument
  if (!isfinite(x)) return x;

  int kvadrant;
  _sinHeuristika(&x, &kvadrant);

  return _sinKvadranty(x, kvadrant);
}
Princip obalovací funkce. Funkce sinus zde slouží jako vnější rozhraní. Funkce _sinHeuristika, _sinTaylor a _sinKvadranty implementují daný algoritmus. Zatímco funkce sinus si musí poradit s jakýmkoli vstupem (i s nekonečnem), implementační funkce se díky tomu, že zůstanou uživateli modulu skryté, mohou spoléhat na to, že argumenty budou v mezích, které nezpůsobí problémy.

Globální proměnné

Začátečníci by se měli globálním proměnným vyhýbat, protože způsobují tzv. vedlejší efekty a z toho vyplývající zákeřné chyby. Existují však případy, kdy je použití globálních proměnných výhodné (zvýšení efektivity, sdílená paměť v paralelních aplikacích, atd.), proto je jazyk C umožňuje používat.

Nejjednodušší případ nastává, když je globální proměnná umístěna v hlavním modulu aplikace, tj. v tom, kde se nachází funkce main. V tomto případě se nemusíme o tuto proměnnou nijak zvlášť starat. Problém nastává, když má být globální proměnná součástí jiného modulu.

Před dalším vysvětlováním si musíme zopakovat, jaký je rozdíl mezi deklarací a definicí. Ačkoli v jazyce C ve většině případů oba tyto pojmy splývají, zde je budeme potřebovat samostatně. Deklarace je syntaktická záležitost. Deklarací říkáme překladači, že bude existovat proměnná nebo funkce daného jména, spojená s daným datovým typem. Nic víc. Definice je příkaz překladači, aby pro proměnnou nebo funkci vyhradil v paměti konkrétní místo. Ve většině případů je deklarace proměnné zároveň její definicí. U modulů ale potřebujeme dát deklaraci zvlášť do hlavičkového souboru a definici s inicializací zvlášť do zdrojového souboru.

Před vlastní ukázkou použití si musíme uvědomit jeden technický detail. Globální proměnná je alokována už v době zavádění programu do paměti. Nachází se v tzv. datové oblasti programu, což je část paměti, jejíž obsah je součástí binárního souboru tvořícího program. Z tohoto důvodu si nemůžeme dovolit, aby byla definice proměnné, tedy alokace paměti, umístěna v hlavičkovém souboru. V okamžiku, kdy bychom hlavičkový soubor použili ve více modulech, linker by začal protestovat, že se pokoušíme stejnou proměnnou definovat na několika místech.

Abychom se tomuto problému vyhnuli, musíme oddělit deklaraci globální proměnné od její definice a inicializace. Deklaraci proměnné provedeme v hlavičkovém souboru tak, že ji označíme klíčovým slovem extern. V tomto místě nesmíme proměnnou inicializovat. Následující definice se bude nacházet v hlavičkovém souboru rozhrani.h, viz ukázka 8. Definici a inicializaci proměnné pak uděláme v našem modulu (jen v jednom) poté, co jsme do něj natáhli rozhraní, viz ukázka 9.

extern int citac;     // globální proměnná
extern const int MAX; // globální konstantní proměnná
Deklarace globální proměnné a globální konstantní proměnné v hlavičkovém souboru rozhrani.h.
#include "rozhrani.h"

int citac = 0;
const int MAX = 100;
Definice a inicializace globální proměnné a globální konstantní proměnné v těle modulu.