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:
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í.
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.
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.
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.
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.
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.
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í.
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.
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.