Moduly a knihovny

[  Moduly  |  Struktura modulu  |  Pouzití rozhraní  |  Preklad projektu s moduly  |  Moduly  |  Knihovny  |  Tipy pro práci s moduly a knihovnami  ]

Moduly

Modulární programování je dalsím uzitecným prostredkem pro strukturování programu. Moduly umoznují organizovat datové struktury a podprogramy do relativne samostatných a nezávislých celku. Tento prístup ke tvorbe programových projektu má nekolik výhod:

  1. Moduly zvysují prehlednost rozsáhlejsích programu, protoze kód kazdého modulu je umísten v samostatném souboru (nepocítáme-li hlavickové soubory). Velký projekt je tak mozno rozdelit do nekolika mensích souboru.
  2. Do modulu umístujeme podprogramy a datové struktury, které spolu souvisí. Navíc lze rídit, co bude viditelné pro uzivatele modulu a co zustane mimo modul skryto (viz dále).
  3. Dobre navrzené moduly zvysují znovupouzitelnost kódu.
  4. Moduly lze samostatne ladit.
  5. Moduly usnadnují delbu práce pri týmové práci.

Struktura modulu

Pri pouzívání jazyka C (nebo C++) je modul tvoren minimálne dvema soubory. Prvním je tzv. hlavickový soubor. Tento soubor má tradicne koncovku .h a obsahuje deklarace vsech verejných datových typu, konstant, prípadne globálních promenných (opatrne s nimi!) a prototypy verejných funkcí. V hlavickovém souboru by se nikdy nemel vyskytovat kód, ani definice promenných, ci konstantních promenných. Kazdá konstrukce v hlavickovém souboru by mela být dobre okomentována, protoze hlavickový soubor slouzí jako rozhraní modulu a musí být prístupný kazdému uzivateli tohoto modulu (tedy programátorovi). Toto rozhraní slouzí prekladaci k provedení typové kontroly pri pouzívání funkcí.

Druhým souborem je zdrojový soubor s koncovkou .c. Tento soubor obsahuje deklarace vsech skrytých datových typu a konstant, definice a inicializace verejných globálních promenných a konstantních promenných a implementace vsech, skrytých i verejných funkcí modulu. Aby byla zajistena konzistence mezi rozhraním a zdrojovým souborem, melo by být ve zdrojovém souboru daného modulu vzdy pouzito jeho vlastní rozhraní. Tím umozníme prekladaci provést kontrolu, zda prototypy odpovídají implementacím funkcí. Casto se totiz stane, ze si pri implementaci uvedomíme, ze je potreba zmenit napríklad parametry funkce, ale uz tu zmenu zapomeneme udelat v hlavickovém souboru. Pokud je rozhraní správne vlozeno do zdrojového souboru modulu, prekladac nás na tuto nekonzistenci pri prekladu upozorní.

Pouzití rozhraní

Chceme-li pouzívat modul, musíme do zdrojového kódu vlozit jeho rozhraní. Nekdy se nevyhneme tomu, ze hlavickový soubor vkládáme do jiného hlavickového souboru (zejména, kdyz potrebujeme pouzít datový typ deklarovaný jinde). Vkládání hlavickových souboru se delá dvema variantami príkazu preprocesoru:

// vlození lokálního rozhraní
#include "mojerozhrani.h"

// vlození systémového rozhraní
#include <stdio.h>

Rozdíl mezi lokálním a systémovým hlavickovým souborem je v míste jeho umístení. Pokud pouzijeme úhlové závorky, bude prekladac hledat hlavickový soubor v systémovém adresári (/usr/include), pokud pouzijeme uvozovky, bude prekladac hledat hlavickový soubor ve stejném adresári, z jakého byl spusten preklad.

Tento príkaz v podstate pred vlastním prekladem vlozí textový obsah hlavickového souboru do místa jeho pouzití. Tím nám umozní splnit pravidlo, ze kazdému pouzití nejakého identifikátoru musí predcházet jeho deklarace a zároven umozní prekladaci provést typovou kontrolu.

Pred vlastním popisem prekladu projektu s moduly se musím zmínit o jednom praktickém problému s rozhraním modulu. U projektu s moduly se casto stává, ze stejné rozhraní vkládáme do nekolika zdrojových, ale i jiných hlavickových souboru. Kdyz to ale spojíme s tvrzením v predchozím odstavci, zjistíme, ze muze snadno dojít k vícenásobné deklaraci stejných identifikátoru. Nastestí preprocesor jazyka C nabízí príkazy pro podmínený preklad, které nám umozní tomuto problému predejít. Kazdý hlavickový soubor by mel být osetren tímto zpusobem:

// pokud jeste nebyl definován tento symbol, preloz zbytek souboru
// pokud uz ale definován byl, obsah tohoto souboru uz neprocházej
#ifndef __MOJEROZHRANI_H__

// definuj symbol
// toto se definuje jen pri prvním pruchodu
#define __MOJEROZHRANI_H__

// zde je obsah hlavickového souboru

// konec bloku podmíneného prekladu
#endif 

Preklad projektu s moduly

Pri prekladu projektu tvoreného více moduly máme dve moznosti. U jednoduchých projektu lze prekládat jedním príkazem. Vsimnete si, ze hlavickové soubory se prekladaci nepredávají. Je to proto, ze príkazy k pouzití hlavickových souboru jsou uz obsazeny ve zdrojových souborech jednotlivých modulu. Prekladac v tomto prípade prelozí vsechny moduly v zadaném poradí 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 zpusob prekladu se vsak potýká s problémy v prípadech, ze moduly pouzívají svá rozhraní krízove, tj. kdyz napríklad modul c. 1 pouzívá rozhraní modulu c. 2 a modul c. 2 pouzívá zároven rozhraní modulu c. 1. Proto je obecne výhodnejsí prekládat moduly oddelene. 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 nasí 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 príkazu prekladac pozná, co po nem chceme a místo prekladu zavolá program zvaný linker, který objektové soubory spojí do jednoho spustitelného binárního souboru.

Rucní preklad souboru s více moduly se muze stát velmi pracným. Stací si uvedomit, ze velké projekty mohou být tvoreny stovkami ruzných modulu. I kvuli dalsím výhodám je vhodné pro preklad programu s moduly pouzívat program make.

Knihovny

Z modulu nemusíme skládat pouze spustitelné aplikace, ale i knihovny. Knihovna se lisí od aplikace tím, ze neobsahuje funkci main a slouzí vlastne jako zásobárna datových typu a podprogramu, které lze pouzívat v jiných aplikacích.

Rozlisujeme knihovny statické a dynamické. Statické knihovny se k aplikaci linují staticky, tedy v dobe prekladu a jejich kód se kopíruje do binárního souboru výsledné aplikace. Výsledný binární soubor pak vychází o neco vetsí nez u dynamických knihoven. Dynamické nebo také sdílené ci dynamicky linkované knihovny jsou oddeleny od binárního souboru aplikace a pri jejich provozu se vyuzívá tzv. dynamické linkování az za behu aplikace. Jejich výhodou je, ze mohou být sdíleny více aplikacemi, které bezí zároven a to tak, ze operacní systém je natáhne do pameti jenom jednou a ostatní aplikace je vyuzívají az ve chvíli, kdy je to potreba (tedy kdyz volají knihovní funkce).

Pokud chceme nejakou knihovnu pouzívat, musíme o tom ríct prekladaci. Jedinou výjimkou je systémová knihovna jazyka C, která se linkuje automaticky. Pro prilinkování knihovny slouzí prepínac -ljmeno, tedy malé písmeno l následované základem jména knihovny. Jméno knihovny podléhá zvyklostem v pouzívaném operacním systému. Naprí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 prepínacem -lm.

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

Pokud bychom meli knihovnu se jménem atlas, potom bychom v Linuxu pravdepodobne nasli soubor se jménem libatlas.so, zatímco ve Windows by se jmenovala atlas.dll. V obou systémech bychom pak prekladaci o ní rekli prepínacem -latlas. Pokud bychom tuto knihovnu instalovali z distribucních balícku zvolené Linuxové distribuce, byla by pravdepodobne knihovna rozdelena do dvou balícku. Balícek se jménem libatlas-1.3.5.deb (v distribucích jako Debian nebo Ubuntu) by obsaloval binární soubor knihovny urcený pro pouzití ostatními nainstalovanými aplikacemi. Druhý balícek by byl urcen pro vývojáre a obsahoval by hlavickové soubory potrebné pro vytvárení nových aplikací. Tento vývojárský balícek by se mohl jmenovat napríklad libatlas-1.3.5-dev.deb. Císla v názvech balícku znamenají oznacení verze knihovny.

Statické knihovny mají v Linuxu koncovku .a a vytvárí se programem ar, coz je program na tvorbu archivu. V Linuxu je statická knihovna vlastne archivem, ve kterém jsou zabaleny objektové soubory, a který je doplnen tabulkou symbolu, které se v nich nacházejí. Takový archiv vytvoríme pomocí prepínacu r, c a s. Tento postup lze pouzít i ve Windows v MinGW.

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

Dynamické knihovny mají v Linuxu predponu lib a koncovku .so a vytvárí se pomocí prekladace gcc prepínacem -shared. Podle manuálové stránky programu gcc tento prepínac nemusí fungovat na vsech platformách (zrejme tam, kde operacní systém neumoznuje pracovat s dynamicky linkovanými knihovnami). Na Linuxu je navíc potreba pouzít prepínac -fpic nebo -fPIC. Blizsí informace najdete v manuálové stránce prekladace 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í

Vse, co uvedeme v hlavickovém souboru, se stává verejným rozhraním modulu. To znamená, ze kazdý, kdo do svého zdrojového kódu vlozí vás hlavickový soubor makrem #include bude moci pouzívat vse, co bylo v tomto hlavickovém souboru deklarováno. Na druhou stranu vse, co zustane pouze ve zdrojovém souboru modulu, aniz by to bylo deklarováno v rozhraní, zustává viditelné pouze v tomto zdrojovém souboru. Tyto konstrukce oznacujeme jako skryté nebo jako privátní cásti modulu.

Tato finta je uzitecná, protoze umoznuje vytváret jednodussí a efektivnejsí servisní podprogramy. Pokud zustává nekterý podprogram skryt pred uzivateli modulu, nemusí mít tak robustní zabezpecení vstupních hodnot, protoze nehrozí, ze jej nekdo pouzije nepredvídaným zpusobem. Toto zabezpecení pak stací umístit pouze do verejných podprogramu. V modulech se lze rídit pravidlem, ze verejné podprogramy musí být odolné proti nepredvídanému pouzití, kdezto u skrytých tato ochrana muze být vynechána ve prospech vyssí efektivity, protoze jsme schopni zajistit bezpecné podmínky pouzití techto funkcí.

Obalovací funkce

Tento tip prímo souvisí s predchozím tématem. Casto potrebujeme, aby modul obsahoval verejnou funkci reprezentující nejaký algoritmus. Protoze jde o verejnou funkci, musí být odolná vuci jakýmkoli hodnotám parametru. Casto se ale stává, ze algoritmus, který by osetroval vsechny mozné vstupní hodnoty bude prílis slozitý. Naopak, kdyz vstupní hodnoty vhodne predzpracujeme, muze být vlastní algoritmus jednodussí, muze fungovat efektivneji, presneji, proste lépe. Zde muzeme s výhodou vyuzít princip obalovací funkce.

Princip obalovací funkce spocívá v tom, ze vlastne sama zádný výpocet nedelá. Úlohou této funkce je osetrit vstupní hodnoty parametru a volat pomocné (skryté) funkce pro predzpracování vstupních hodnot (heuristiku) a funkce realizující samotný výpocet. Následující pseudokód naznacuje, jak tento princip vyuzít. V tomto ukázkovém príkladu jde o implementaci funkce sinus pomocí Taylorovy rady.

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 pocítat
  // vyuzívá periodicnost funkce
}

double _sinTaylor(double x)
{
  // výpocet hodnoty Taylorovou radou pro x v intervalu <0, PI/2>
}

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

// Prototyp této funkce je v hlavickovém souboru.
double sinus(double x)
{
  // osetrí argument
  if (!isfinite(x)) return x;

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

  return _sinKvadranty(x, kvadrant);
}

Globální promenné

Zacátecníci by se meli globálním promenným vyhýbat, protoze zpusobují tzv. vedlejsí efekty a z toho vyplývající zákerné chyby. Existují vsak prípady, kdy je pouzití globálních promenných výhodné (zvýsení efektivity, sdílená pamet v paralelních aplikacích, atd.), proto je jazyk C umoznuje pouzívat.

Nejjednodussí prípad nastává, kdyz je globální promenná umístena v hlavním modulu aplikace, tj. v tom, kde se nachází funkce main. V tomto prípade se nemusíme o tuto promennou nijak zvlást starat. Problém nastává, kdyz má být globální promenná soucástí jiného modulu.

Pred vlastní ukázkou pouzití si musíme uvedomit jeden technický detail. Globální promenná je alokována uz v dobe zavádení programu do pameti. Nachází se v tzv. datové oblasti programu, coz je cást pameti, jejíz obsah je soucástí binárního souboru tvorícího program. Z tohoto duvodu si nemuzeme dovolit, aby byla definice promenné, tedy alokace pameti, umístena v hlavickovém souboru. V okamziku, kdy bychom hlavickový soubor pouzili ve více modulech, linker by zacal protestovat, ze se pokousíme stejnou promennou definovat na nekolika místech.

Abychom se tomuto problému vyhnuli, musíme oddelit deklaraci globální promenné od její definice a inicializace. Deklaraci promenné provedeme v hlavickovém souboru tak, ze ji oznacíme klícovým slovem extern. V tomto míste nesmíme promennou inicializovat. Následující definice se bude nacházet v hlavickovém souboru rozhrani.h

extern int citac;     // globální promenná
extern const int MAX; // globální konstantní promenná

Definici a inicializaci promenné pak udeláme v nasem modulu (jen v jednom) poté, co jsme do nej 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 vedet.