Obsah
Poslední změna: pondělí 9. března 2009
Jazyk „C“ se svojí koncepcí nachází přibližně na polovině cesty od assemblerem k moderním objektově orientovaným jazykům s automatickou správou paměti. Jedná se o jazyk relativně starý (vyvinut mezi roky 1970 a 1978) velmi těsně spjatý s operačním systémem Unix. Vyvstává tedy otázka, proč se tímto jazykem zabývat. Důvodů je více a množství z nich je velmi podstatných:
jazyk a jeho knihovny byly vyvíjeny s koncepcí a jádrem Unixu a je tedy nejpřirozenějším prostředím pro přímou komunikaci se systémovými voláními takovéhoto systému, zároveň koncept základních volání Unixového/Posixového jádra (
open
,read
,write
,close
,ioctl
, atd.) je natolik univerzální, že byl v podstatě přejat do všech systémů (s malými odchylkami i Windows NT a výše).protože je knihovna standardních funkcí jazyka již dlouhodobě zavedená a i s jazykem standardizovaná, je přenositelnost programů mezi systémy ve formě zdrojových kódů relativně bezproblémová
jedná se o jazyk kompilovaný a kompilátor jazyka „C“ je v podstatě vždy první, který je potřeba při portaci na nový procesor vytvořit. Většinou se tedy na tvorbě nebo alespoň optimalizaci kompilátoru přímo podílí výrobce procesoru a výsledkem je velmi rychlý kód. Většina architektur a instrukčních sad procesorů je již při vývoji ovlivněna požadavky a filozofií jazyka „C“
další vyšší jazyky (C++, Java, C#, Python) v podstatě vždy staví na knihovnách vytvořených v jazyce „C“ a existující prostředí s tímto jazykem tedy na cílové platformě vyžadují. Většina interpretrů vyšších jazyků je také v jazyce „C“ napsaných (Java, Python, Perl, Lisp, atd.).
velké množství dalších jazyků do určité míry vychází ze zvyklostí a klíčových slov jazyka „C“
základní datové typy jazyka jsou definovány pružně tak, aby co nejlépe odpovídaly vlastnostem cílového procesoru. Někdy to může vyžadovat více přemýšlení při psaní přenositelného kódu, ale odměnou je kvalitní a dobře optimalizovatelný kód. Jazyk umožňuje psát kód přenositelný mezi 8, 16, 32 i 64-bitovými procesory i procesory DSP, které někdy používají i různé specifické číselné reprezentace
i přes výrazný boom vyšších programovacích jazyků v současné době je stále množství kódu napsaného v jazyce „C“ největší a knihovny v tomto jazyce se dobře začleňují i do prostředí vyšších jazyků.
Jedná se o jazyk, ve kterém je napsaná naprostá většina jader operačních systémů a ve kterém se píše většina ovladačů pro tyto systémy. Existují i výjimky, např. L4 Fiasco je psáno v C++, jeho předchůdce byl psán v assembleru, existuje například i miniaturní systém napsaný v Javě a s využitím kompilátoru GNU GCJ z části zkompilovaný do binární podoby, interpretr pro zbytek je však opět v jazyce „C“.
jedná se o jazyk vhodný pro psaní kódu řídicích a vnořených (embedded) aplikací. I komfortní grafické vývojové nástroje jako je Real Time Workshop (RTW) pro Matlab:Simulink v několika krocích převedou blokovou reprezentaci na zdrojový kód v jazyce „C“ a ten je pak na cílový DSP procesor zkompilován kompilátorem jazyka „C“. Pokud je potřeba vytvořit vlastní bloky pro obsluhu periferií cílového procesoru, je opět nutné využít jazyka „C“
jazyk umožňuje i velmi nízkoúrovňový zápis kódu natolik přizpůsobený zpracování instrukcí procesorem, že v moderních systémech je pouze 1% nebo i méně kódu napsáno v jazyce symbolických adres (assembleru). Jedná se většinou pouze o funkce pro zprovoznění prostředí pro jazyk „C“, nejnižší část obsloužení přerušení, přepínání procesů a zprávu virtuální paměti
psaní větších celků v assembleru je nevýhodné, protože přenos na jinou architekturu se stává velmi náročným a je potřeba v podstatě veškerý kód přepisovat. Opravdu jen ty nejčastěji volané vnitřní části cyklů může někdy být výhodné pro zrychlení běhu programu přepsat ručně do assembleru. Je to však velmi náročné a kromě kodeků pro video a audio aplikace to přichází v úvahu pouze u specializovaných zařízení a aplikací
Jazyk Java je braný za základ proto, že právě on je základem výuky
programování v prvním ročníku. Konstrukce pro řízení běhu programu
(if
, then
, else
,
while
, switch
,...) jsou v jazyce „C“
v podstatě stejné.
Jazyk „C“ nedefinuje koncept objektů. Jedná se o čistě procedurální
jazyk založený na psaní sekvencí příkazů tvořících těla funkcí. To je
rozdíl od objektově orientovaných jazyků, které zavádí metody pracující
nad objekty. Metody však při určitém zjednodušení nejsou nic jiného než
funkce s implicitním prvním parametrem
this
/self
. V jazyce „C“ dále chybí
zabudovaná možnost odvozování/rozšiřování typů a ukazatel na tabulku
virtuálních metod objektu (VMT položka uložená v objektu), systém
zachytávání výjimek a je tedy nutné vystačit s ukazateli do paměti a na
struktury. Veškerou správu paměti si musí také zajistit program sám. Není
ovšem problém programovat objektovým stylem i v jazyce „C“ a je-li to
nutné, tak lze použít i některou z knihoven pro automatickou správu paměti
(např. Hans Boehm garbage collector).
Následuje seznam konstrukcí a vlastností, na které je potřeba si dát obzvláště pozor a ve kterých se velmi často chybuje:
hlavičkové soubory (
*.h
) nejsou zakompilované do objektových souborů a knihoven, knihovny jsou pouze archivy objektových souborů (*.o) a ke svému použití vyžadují dostupnost a distribuci původních hlavičkových souborůpokud je hlavičkovým souborem mezi více zdrojovými kódy zpřístupněna globální proměnná, měla by v hlavičkovém souboru být deklarována s modifikátorem
extern
a pouze v jednom ze zdrojových souborů definována bez tohoto modifikátoruhlavičkový soubor, který jiným modulům zpřístupňuje určitou funkci nebo i proměnnou by měl být použit/zatažen (#include) i do zdrojových textů s implementací. Pouze za této podmínky kompilátor při rozdílné definici typu mezi hlavičkovým souborem a implementací vyhlásí chybu
kuriózně trochu netradiční, přesto však někdy výhodný, systém masek
struct
/enum
/union
, které nejsou totéž co nový typ, který se definuje s využitím klíčového slovatypedef
. Otázkou zde ovšem zůstává, co je to tradice, když „C“ existovalo mnohem dříve než většina ostatních jazykůsystém základních typů, které nemají pevně dané bitové délky a reprezentace (char - adresovatelná jednotka, int pro daný procesor nejpřirozenější celočíselný/ordinal typ). Pro reprezentace základních typů jazyka je pouze dané, že počet bitů reprezentace typu
char
<=short int
<=int
<=long int
<=long long int
.char
musí reprezentovat alespoň osm bitů (byte). Novější normy definují i minimum proint
a typy pro plovoucí řádovou čárku.je potřeba dát si pozor na asynchronní změny proměnných (z přerušení, z jiného vlákna, atd.). U takto měněných proměnný je potřeba uvést modifikátor typu
volatile
.
Fibonacciho posloupnost je číselná řada daná předpisem
x0=0, x1=1,
xn=xn-2+xn-1.
Jednoduchý program, který tuto funkci implementuje přímo podle této
definice naleznete v souboru fibo1.c
. Soubor obsahuje
vlastní funkci fibofnc
a funkci
main
. Funkce fibofnc
přijímá
jeden vstupní argument typu celé číslo (int
n
), který reprezentuje pořadové číslo požadovaného
prvku. Návratová hodnota funkce je též celé číslo (int) a
představuje vypočítanou hodnoty požadovaného prvku.
int fibofnc (int n) { if (n <= 0) return 0; if (n == 1) return 1; return fibofnc (n - 1) + fibofnc (n - 2); }
Druhá funkce (main
) je standardem jazyka „C“
definovaný vstupní bod do programu. Funkce vrací celé číslo, které je po
skončení programu/procesu systémem předáno jako návratový kód tomu
procesu, který právě končící proces spustil (při cvičeních se jedná o
shell běžící v terminálovém okně). Zvykem je, že nulová návratová hodnota
představuje normální ukončení programu, hodnoty z rozsahu 1 až 255 jsou
vráceny, pokud program sám sebe ukončí předčasně z důvodu nekorektních
vstupních dat či jiné detekované chyby. Při volání programu lze na
příkazové řádce za jméno programu (v uvažovaném případě
./fibo1
) uvést další textové parametry. Zjednodušeně
lze říct, že každé samostatné slovo oddělené od ostatních mezerami se
stává samostatným řetězcem. Funkci main
jsou pak tyto
parametry programu předány jako pole ukazatelů
argv
[] na jednotlivé řetězce (slova). Parametr
argc
funkce main pak značí počet položek v tomto
poli. Položka na první pozici (pozice s indexem 0) obsahuje ukazatel na
jméno, pod kterým byl program spuštěn. Další položka (index 1) obsahuje
první slovo za jménem programu. Pokud je tedy toto slovo uvedeno, slouží v
uvažovaném příkladu jako vstup pořadového čísla počítaného prvku. O převod
řetězce na celočíselnou hodnotu se stará funkce
strtol
. Pokud parametr uveden není, je programu
předáno pouze jméno, pod kterým byl spuštěn, argc
je rovné 1 a program se zeptá na vstupní hodnotu na terminálu. Po volání
vlastní funkce fibofnc
jsou zadaná hodnota i
vypočítaný výsledek vypsány na terminál funkcí formátovaného výstupu
printf
.
int main (int argc, char *argv[]) { int n; int res; char *p; if (argc >= 2) { n = strtol (argv[1], &p, 0); if ((p == argv[1]) || *p) { fprintf (stderr, "The \"%s\" string is not a number\n", argv[1]); return 1; } } else { printf ("Input number: "); scanf ("%d", &n); } res = fibofnc (n); printf ("fibofnc(%d)=%d\n", n, res); return 0; }
Další sada souborů pak slouží ke kompilaci programu fibo2, který již
v prvním kroku napočítá a do pole uloží napočítané prvky Fibonacciho
posloupnosti až do zadaného pořadí posledního prvku. O alokaci pole a jeho
naplnění hodnotami prvků se stará funkce fiboser
.
Tato funkce pak vrací do výstupního parametru předaného funkci referencí
(adresou cílového umístění) hodnotu ukazatele na začátek pole s prvky
posloupnosti. Dále je z hlavního programu (funkce
main
) zavolána funkce fiboprint, která posloupnost
vytiskne. Nakonec je vypsán počet volání funkce
fibofnc
.
int fiboseries (int **series, int n) { int *p; int i; if (n < 0) return 0; p = malloc (sizeof (int) * (n + 1)); *series = p; if (!p) return 0; for (i = 0; i <= n; i++) { p[i] = fibofnc (i); } return 1; }
V tomto úseku kódu je již použita práce s poli a je nutné pochopit, jakým způsobem se v jazyce „C“ pracuje s alokací paměti a poli.
V tomto odstavci je ukázáno na příkladech, jakým způsobem se v jazyce „C“ pracuje s pamětí. Z pohledu procesoru i jazyka „C“ si můžeme paměťový prostor libovolného procesu představit jako jedno velké pole bajtů (nebo lépe adresovatelných jednotek char). Proto lze pro každou proměnnou umístěnou v paměti určit její adresu, to je index na místo, kde je hodnota uložena v tohoto vše obsahujícím poli.
Pole, to je vlastně prostor, pro uložení více prvků shodného typu za sebou můžeme pro konstantní délku automaticky vytvořit na zásobníku či definovat v globální paměti následující konstrukcí
int pole[30];
Takto byl vytvořen prostor pro uložení třiceti prvků typu
int. K prvkům můžeme přistupovat jednoduše indexováním od
indexu 0 do indexu 29. Například pole[0]
,
pole[1]
, pole[29]
. I když je
proveden pokus o přístup za konec pole, tak tento pokus není kompilátorem
detekován. Dojde-li k tomuto pokusu za běhu programu, tak je chování
programu nedefinovatelné. Při zápisu může dojít i k poškození důležitých
dat a následnému pádu programu. I pouhý pokus o čtení z oblasti, pro
kterou není mapování paměti nadefinováno nebo pro která není procesu
přístupná, způsobí na Unixových systémech vyvolání signálu SIGBUS či
SIGSEGV. Tyto signály pak vedou k ukončení programu.
Následující ukázky ilustrují práci s ukazatelem na celočíselnou
hodnotu. Ukazatel není nic jiného, než proměnná, jejíž hodnota představuje
adresu umístění dat v paměti. Při představě paměťového prostoru procesu
jako pole znaků (char) se tedy jedná o index znaku/byte, ve
kterém je umístěn první znak/byte uložené hodnoty. Pro ilustraci práce s
ukazateli je nadefinována proměnná celočíselného typu int
pojmenovaná i
. Dále je nadefinován ukazatel
p
na paměťovou lokaci tohoto typu.
int i; int *p;
Ukazatel lze naplnit adresou, na které se proměnná
i
nachází následovně
p=&i;
Každá z pěti následujících řádek poté provede stejnou operaci,
přičtení čísla 1 k hodnotě proměnné i
i=i+1; *p=*p+1; i++; (*p)++; p[0]++;
K prvku, na který právě ukazatel ukazuje tedy můžeme přistupovat jak
přes referenci *p
, tak přes index zápisem
p[0]
.
Pokud je požadováno naplnění ukazatele adresou prvního prvku pole
pole
, tak toto přiřazení můžeme provést následujícími
dvěma způsoby
p=pole; p=&pole[0];
Zvýšení všech prvků pole při využití přístupu přes indexování lze provést například následovně
for(i=0;i<30;i++) pole[i]++;
Stejného efektu lze však docílit i s využitím ukazatele, který postupně ukazuje na jednotlivé prvky pole
p=pole; for(i=0;i<30;i++){ (*(p++))++; }
Operaci *(p++)++;
lze rozepsat do dvou
jednodušších operací *p=*p+1; p=p+1;
. Přičtení
jedničky k ukazateli představuje vlastně posun ukazatele na následující
položku. Protože v případě procesoru i386 v 32-bitovém režimu zabírá
celočíselná hodnota int čtyři byte, jedná se o přičtení
hodnoty 4 k adrese uložené v proměnné/ukazateli p
.
Operace p++ je tedy ekvivalentní příkazu
p=(int*) ((char*)p + sizeof(int));
Výrazy (int*)
a (char*)
představují tzv. přetypování. Překladač je přetypováním informován o tom,
že má vypočítanou hodnotu části výrazu za operátorem přetypování převést
na požadovaný typ. V tomto případě se pouze mění předpokládaný typ
hodnoty, na kterou ukazatel bude ukazovat. Přetypování na ukazatel na znak
vede k tomu, že přičtení čísla odpovídá přičtení tohoto čísla k hodnotě
adresy obsažené v ukazateli. Adresu uloženou v ukazateli lze převést na
číselnou hodnotu přetypováním této hodnoty na typ long nebo
správněji na typ uintptr_t definovaný normou C99.
Dalším možným příkladem, kde lze využít ukazatelů, je, pokud je
požadováno, aby nějaká funkce měnila více výstupních hodnot naráz. Funkce
může jako návratovou hodnotou předat jen jedno číslo, adresu a dokonce i
strukturu. Pokud má však funkce posunovat ukazatel do znakového řetězce za
funkcí zpracovanou část vstupu (například funkce
strtol
), či vrací více hodnot, tak je potřeba předat
tyto parametry odkazem (referencí). Pro názornost je předveden normální
způsob návratu hodnoty spočítané ve funkci
int vysledek; int f1(int a) { return a*2; } int main() { vysledek=f1(10); ...
Na začátku je nadefinována globální proměnná
vysledek
a dále pak funkce f1
.
Nakonec je tato funkce zavolána a výsledek uložen do deklarované proměnné.
Stejného efektu lze však docílit i s využitím předání výstupního parametru
referencí
void f2(int *y, int a) { *y=a*2; } int main() { f2(&vysledek,10); ...
V tomto případě je funkci předána adresa, na kterou má výsledek výpočtu uložit.
Pochopit předložené příklady programů
Upravit druhou verzi tak, aby výpočet probíhal výrazně rychleji. Je podstatné, aby byl zachován koncept, kdy je nejdříve celé pole naplněno a pak vypsáno. Předložené řešení je však z hlediska exponenciálního růstu algoritmické náročnosti s počtem prvků krajně nevýhodné. Vaším úkolem je program zrychlit. Dobu provádění programu si můžete změřit jeho voláním přes utilitu time (zápis například
time ./fibo2
).
Jednoduchý příklad:
Příklad s knihovnou:
Úvod do programovania v jazyku C, Pavel Horovčák, CSc., Igor Podlubný (http://www.tuke.sk/podlubny/C/index.htm)
Jan Kučera, Kurz PB071 (http://www.fi.muni.cz/usr/jkucera/pb071/)
Všechny připomínky k předmětu, obsahu stránek, objevené chyby v ukázkových programech apod. adresujte na autory: