/ janturon.cz / Výuka / C / Velké programy

Velké programy

Více souborů

Náročnější programy je vhodné rozvrhnout do více souborů tak, aby každý z nich řešil nějakou snadno pochopitelnou věc a tak, aby se daly snadno poskládat do řešení, které by bylo příliš náročné řešit najednou.

Zjednodušený příklad

Mějme program v těchto souborech:

putint.c #include <stdio.h> void putint(int n) { printf("%d",n); } numbers1.c void numbers1() { putint(1); putint(2); } numbers2.c void numbers2() { putint(3); putint(4); } main.c #include "putint.c" #include "numbers1.c" #include "numbers2.c" void main() { numbers1(); numbers2(); }

To můžeme zkompilovat jako gcc main.c -o main.exe, který bude dávat kýžený výsledek 1234.

Problémy zjednodušeného příkladu

Kdybychom však soubor putint.c načetli až jako poslední, dostali bychom varování conflicting types. Kompilátor pustupuje takto:

  1. Načte soubor numbers1.c, ve kterém je volání nedefinované funkce, a tak si ji implicitně nadeklaruje (předpokládá, že bude mít tvar) jako funkce vracející int.
  2. Načte soubor numbers2.c, kde už je funkce putint deklarovaná. Nikde není problém, pokračuje načítání.
  3. Načte soubor putint.c. Zde je konflikt typu (funkce vrací void místo int). To se dá vyřešit, kompilátor proto pouze varuje.
  4. Vytvoří vstupní bod programu z funkce main.

To je antivzor race hazard - fungování programu by nemělo záviset na pořadí načtených knihoven. Určit pořadí bývá v rozsáhlých programech obtížné. Hlavní zlo je ale mnohem temnější: všechno totiž funguje. Ale nenechte se zmást: výraz varování u lidí znamená znepokojení, ale všechno v pořádku, kdežto v řeči kompilátorů je pravý význam tohoto slova zákeřná chyba, která se těžko dohledává, objeví se jen občas a v nejméně vhodnou chvíli. Warning je mnohem horší než Error. Ukažme si, co všechno může nastat:

  1. int byl za starých časů takový univerzální typ: cokoliv na zásobníku bylo možné převést na int (4B). Ukazatelé na 64b strojích ale mají problém, zabírají 8B. Zavádí se proto speciální typ size_t, který pojme velikost ukazatele na daném stroji.
  2. Při volání funkce se všechny její argumenty uloží na vrchol zásobníku. Implicitní deklarace neřeší argumenty, proto když funkci zavoláme s menším počtem argumentů, než očekává definice, začnou se dít nedefinované věci.
  3. Pokud před sestavením zkompilujeme části kódu zvlášť (ukážeme si za chvíli), kompilátor neodhalí ani konflikt definic a programátora tudíž ani nevaruje.

Tyto důvody vedly k tomu, že novější kompilátory implicitní deklaraci funkce rovnou označují jako chybu a překlad odmítnou.

Deklarace funkce

V javascriptu jsme měli definici a volání funkce a deklaraci proměnné, o deklaraci funkce nebyla řeč. V Cčku lze i funkci deklarovat: je to zápis její hlavičky (tj. bez těla, tedy bloku ve složených závorkách) ukončený středníkem. Je to slib kompilátoru, že definici najde někde dále v kódu a její hlavička bude vypadat přesně takhle. Funkce může být deklarována pouze jednou a před definicí. Doplňme proto kód o deklarace, abychom předešli uvedeným problémům:

numbers1.c void putint(int); void numbers1() { putint(1); putint(2); } numbers2.c void putint(int n); void numbers1() { putint(3); putint(4); }

Hlavičkové soubory

Zde už kompilátor implicitní deklarace provádět nemusí a žádné nedefinované chování zde není. Všimněte si, že deklarace funkce nevyžaduje jména argumentů, stačí typ. Přesto je z důvodu srozumitelnosti vhodné jméno uvádět. Kdyby se ale změnila definice putint, bylo by nutné projít všechny soubory a provést změnu na mnoha místech. Pak navíc hrozí, že někde na tu změnu zapomeneme a do programu se tak zavlečou další skryté chyby. Proto bývá zvykem umisťovat deklarace do hlavičkových souborů, a ty pak načítat tam, kde je potřeba:

putint.h // hlavičkový soubor mívá příponu .h void putint(int n); numbers1.c #include "putint.h" void numbers1() { putint(1); putint(2); } numbers2.c #include "putint.h" void numbers1() { putint(3); putint(4); }

Případnou změnu pak stačí provést v putint.h a preprocesor tyto změny provede do všech souborů. Všimněte si, že název píšeme do uvozovek, ne do špičatých závorek jako u stdio.h. Význam toho je následující:

Předkompilace

Zkusme přeložit pouze soubor numbers1.c - dostaneme chybovou hlášku undefined reference (pochopitelně, definici putint slíbenou v deklaraci jsme nedodali), ale chyba se nachází v souborech s příponou , což je (dočasný) soubor se strojovým kódem. To znamená, že kompilace proběhla v pořádku a havaroval až linker. Můžeme tedy zkompilovat kód s přepínačem -c, který zabrání účasti linkeru, výsledkem bude (stálý) soubor .o v aktuálním adresáři: gcc -c numbers1.c.

Kompilace programu sestávajícího se z mnoha souborů trvá i na rychlých strojích řadu minut. Proces urychluje, když máme vše předkompilované a při úpravě programu zkompilujeme pouze ty soubory, které se změnily a jejich závislosti. Vytvořme tedy ve stejném duchu numbers1.h a numbers2.h a upravme main.c.

#include "numbers1.h" #include "numbers2.h" void main() { numbers1(); numbers2(); }

Proveďme kompilace částí:

gcc -c putint.c gcc -c numbers1.c gcc -c numbers2.c gcc -c main.c

A nyní můžeme program sestavit:

gcc putint.o numbers1.o numbers2.o main.o -o main.exe

Make

Z pohledu kompilátoru je to mnohem rychlejší a pohodlnější, než když prováděl gcc main.c -o main.exe bez hlavičkových souborů, ale z pohledu člověka to s tou rychlostí a pohodlím není až tak jednoznačné. Ve skutečnosti by to byla u rozsáhlejších projektů práce na hodiny. Proto si k práci přizvěme ještě jednoho softwarového pomocníka: sestavovač make.

To je program, kterému se předá soubor závislostí Makefile: podívá se, které soubory se změnily a které z nich je nutné znovu zkompilovat.

Pravidla se zapisují takto:

cílový soubor: závislosti oddělené mezerou [tabelátor] příkaz k sestavení cílového souboru

Vytvořme Makefile pro náš program:

test.exe: putint.o numbers1.o numbers2.o test.o gcc putint.o numbers1.o numbers2.o main.o -o main.exe putint.o: putint.c gcc -c putint.c numbers1.o: numbers1.c gcc -c numbers1.c numbers2.o: numbers2.c gcc -c numbers2.c main.o: main.c gcc -c main.c

Teď už stačí spustit make a sledovat, jak se všechna práce dělá sama.

Záhadou zůstává, kde je zdrojový soubor stdio.c, jeho strojový kód stdio.o a jaktože ho nemusíme kompilovat do našich zdrojových souborů. Strojový kód těchto funkcí (označovaných jako C standard Library) bývá pod Linuxem v dynamické knihovně libc.so spouštěn přímo jako součást OS.

Pod Windows, který používá jiný formát dynamických knihoven dll, není C standard library součástí OS a kompilátory to musí řešit po svém. MinGW je staticky linkuje ze souboru lib/msvcrt.a.

Více o knihovnách bude v následující lekci.

Vícenásobné načtení

Kdybychom v nějakém kódu načetli oba soubory

#include numbers1.h #include numbers2.h

Kompilátor by si stěžoval, že jsme funkci putint deklarovali vícekrát. Vskutku, příkaz preprocesoru #include funguje jako copy&paste. Dá se zajistit, aby se hlavičkový soubor načetl pouze při prvním vložení - příkazem #pragma once. Správně by měl nášl hlavičkový soubor vypadat takto:

putint.h #pragma once void putint(int n);

Více o preprocesoru v následující lekci.

 

Otázky