Moduly a knihovny

[  Moduly  |  Struktura modulu  |  Použití rozhraní  |  Překlad projektu s moduly  |  Moduly  |  Knihovny  |  Tipy pro práci s moduly a knihovnami  ]

Moduly

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ů. Tento 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.
  4. Moduly lze samostatně ladit.
  5. Moduly usnadňují dělbu práce při týmové práci.

Struktura modulu

Při používání jazyka C (nebo C++) je modul tvořen minimálně dvěma soubory. Prvním je tzv. 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. 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:

// vložení lokálního rozhraní
#include "mojerozhrani.h"

// vložení systémového rozhraní
#include <stdio.h>

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 (/usr/include), pokud použijeme uvozovky, bude překladač hledat hlavičkový soubor ve stejném adresáři, z jakého byl spuštěn překlad.

Tento příkaz 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 tímto způsobem:

// 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 

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

$ gcc -Wall -std=c99 -pedantic modul1.c modul2.c modul3.c program.c -o 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ě. 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.

$ 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

U posledního příkazu překladač pozná, co po něm chceme a místo překladu zavolá program zvaný linker, který objektové soubory spojí do jednoho spustitelného binárního souboru.

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.

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

$ gcc -Wall -std=c99 -pedantic -lm matematika.c -o matematika

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 jmenovala atlas.dll. V obou systémech bychom pak překladači o ní řekli 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 na tvorbu archivů. 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í. Takový archiv vytvoříme pomocí přepínačů r, c a s. Tento postup lze použít i ve Windows v MinGW.

$ ar rcs knihovna.a modul1.o modul2.o modul3.o

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

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. Následující pseudokód 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);
}

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

extern int citac;     // globální proměnná
extern const int MAX; // globální konstantní proměnná

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

#include "rozhrani.h"

int citac = 0;
const int MAX = 100;

Autor: David Martinek. Poslední modifikace: 11. February 2009. Pokud v tomto dokumentu narazíte na chybu, dejte mi prosím vědět.