Obsah
     _                 _       ____
    | | __ _ _____   _| | __  / ___|
 _  | |/ _` |_  / | | | |/ / | |
| |_| | (_| |/ /| |_| |   <  | |___
 \___/ \__,_/___|\__, |_|\_\  \____|
                 |___/


	Jazyk C
	Základy praktického programování

	V Praze 2oo7 pro SSPŠ
	Tomáš Harvie Mudruňka a kolektiv

TODO mergnout adresare c a sbirka do wiki…

Předmluva

Tento text vznikl puvodne jako skripta pro Smíchovskou Střední Průmyslovou Školu, nyní ho v jeho vlastním zájmu chci publikovat na wiki, kde je volně k použití i k editaci…

Skripta si nekladou za cil byt dokonalou referencni priruckou, ani wiki o jazyce C, ucelem je vytvorit ucebni texty pro jednotlive lekce kurzu programovani v jazyce C v rozsahu stravitelnem studenty stredni skoly.

Se skripty v celém rozsahu můžete zatím nakládat v souladu s licencí Creative Commons: Uveďte autora-Neužívejte dílo komerčně-Zachovejte licenci 3.0 Česko

Kromě plaintextové verze mám v plánu text vyexportovat také do následujících formátů

~~Tomáš Mudruňka~~

Rozpis lekcí

  1. Zadání samostatné práce →možno využít při suplování←

Co bychom měli znát než začneme programovat

Ohledně jakékoli nejasnosti ohledně čehokoli se prosím kdykoli informujte u vyučujícího („když už tu teda trčíme…“).

Také nezapomeňte připomenout svému lektorovi, aby nezapoměl, že nemá zapomenout vám ukázat doplňkové studijní materiály, připravil jsem si pro vás totiž zábavná videa a animace :-)

Předmluva

Účelem této příručky je přiblížit vám základy programování hned z několika pohledů:

Počátky

Vývoj jazyka C začal v Bellových laboratořích AT&T mezi léty 1969 a 1973. Ritchie tvrdí, že nejpřínosnější období bylo v roce 1972. Pojmenování „C“ zvolili, protože mnoho vlastností přebírali ze staršího jazyka zvaného „B“, jehož název byl zase odvozen od jazyka BCPL (ale to není jisté, neboť Thompson také vytvořil jazyk Bon na poctu své ženy Bonnie).

V roce 1973 se stal jazyk C dostatečně stabilním. Většina zdrojového kódu jádra Unixu, původně napsaného v assemleru PDP-11, byla přepsána do C. Unix tedy patří mezi první operační systémy, které byly napsané v jiném než strojovém jazyce či assembleru. Předchozí byl například systém Multics (napsaný v PL/I) a TRIPOS (napsaný v BCPL).ovat, opravit - to vše pokud možno nezávisle na platformě).

Protože C je velmi komplikovaný jazyk, budeme se držet základů a věcí nezbytných k tomu, abychom měli kompaktní soubor znalostí vhodných k tomu, abychom napsali nějaký pěkný program.

Proč C?

C je programovací jazyk, který vyvinuli Ken Thompson a Dennis Ritchie pro potřeby operačního systému Unix. V současné době je to jeden z nejpopulárnějších jazyků, zřejmě nejčastější pro psaní systémového softwaru, ale velmi rozšířený i pro aplikace.

C je nízkoúrovňový, kompilovaný, relativně minimalistický a rychlý programovací jazyk. Je dostatečně mocný na většinu systémového programování, mimoto podporuje také možnost využít ve zdrojovém kódu jazyk assembler, který pracuje víceméně s instrukcemi procesoru pro případ, že by něco (velmi nízkoúrovňového) nebylo možné napsat, to ale pro naše účely nebudeme potřebovat.

Z toho vyplývá, že operační systémy, překladače, knihovny a interpretry vysokoúrovňových jazyků jsou často implementovány právě v C.

Ukládání dat je v C řešeno třemi základními způsoby: statickou alokací paměti (při překladu), automatickou alokací paměti na zásobníku, dynamickou alokací na haldě (heap) pomocí knihovních funkcí. Jazyk nedisponuje žádnou abstrakcí nad alokací: s pamětí se pracuje přes datový typ zvaný ukazatel, který drží odkaz na paměť, ale je na něm možné provádět aritmetické operace. Ukazatelé tedy existují nezávisle na proměnných, na které odkazují, a je na odpovědnosti programátora, aby neukazovaly na paměť nealokovanou.

Jazyky Java a C#, oba odvozené od C, používají méně univerzální způsob odkazování alokovaných proměnných, který snižuje pravděpodobnost chyby v programu. Jazyk C++, původně rozšíření jazyka C, si ovšem ukazatele zachoval.

Mnoho dalších moderních programovacích jazyků (Nejen kompilovaných) přebírá způsob zápisu (neboli syntaxi) z jazyka C. Patří mezi ně například zmíněná Java či Perl a PHP.

Dalším plusem je, že se při výuce a psaní programů v C se můžete naučit mnoho podrobností o funkci počítače a rozšířit je na víc než „Tam ten procesor tam počítá s nějakejma číslama a něco si dává někam do paměti“, C vám ukáže, jak funguje práce s pamětí, pokud se blíže podíváme na strojový kód (vzniklý kompilací zdrojového kódu) dozvíme se podrobněji jak funguje procesor a také specifika konkrétní architektury.

V C můžeme nejen psát textové nebo grafické aplikce, ale dokonce programovat webové aplikace podobně, jako například v PHP, nebo dokonce psát programy pro jednočipové mikropočítače (šváby).

Historie jazyka C

Počátky

Vývoj jazyka C začal v Bellových laboratořích AT&T mezi léty 1969 a 1973. Ritchie tvrdí, že nejpřínosnější období bylo v roce 1972. Pojmenování „C“ zvolili, protože mnoho vlastností přebírali ze staršího jazyka zvaného „B“, jehož název byl zase odvozen od jazyka BCPL (ale to není jisté, neboť Thompson také vytvořil jazyk Bon na poctu své ženy Bonnie).

V roce 1973 se stal jazyk C dostatečně stabilním. Většina zdrojového kódu jádra Unixu, původně napsaného v assemleru PDP-11, byla přepsána do C. Unix tedy patří mezi první operační systémy, které byly napsané v jiném než strojovém jazyce či assembleru. Předchozí byl například systém Multics (napsaný v PL/I) a TRIPOS (napsaný v BCPL).

K&R C

V roce 1978, Ritchie a Brian Kernighan vydali první vydání knihy The C Programming Language. Tato kniha, mezi programátory C známá jako „K&R“, sloužila po mnoho let jako neformální specifikace jazyka. Verze C, kterou takto popsali, bývá označována jako „K&R C“. (Druhé vydání knihy popisovalo novější standard ANSI C.)

K&R C je považován za základní normu, kterou musejí obsahovat všechny překladače jazyka C. Ještě mnoho let po uvedení ANSI C to byl „nejmenší společný jmenovatel“, který využívali programátoří v jazyce C kvůli maximální přenositelnosti, protože zdaleka ne všechny překladače plně podporovaly ANSI C.

V několika letech následujících po uvedení K&R C bylo uvedeno a přidáno několik „neoficiálních“ vlastností jazyka, které byly podporovány překladači od AT&T a některých dalších dodavatelů.

ANSI C a ISO C

V pozdních sedmdesátých letech začalo C nahrazovat BASIC jako přední programovací jazyk pro mikropočítače. Během osmdesátých let bylo přejato pro použití na platformě IBM PC a jeho popularita se značně zvýšila. Tou dobou Bjarne Stroustrup a další v Bellových laboratořích začali pracovat na rozšiřování C o objektově orientované prvky. Jazyk, který vytvořili, zvaný C++, je dnes nejrozšířenější programovací jazyk pro aplikace na Microsoft Windows. C zůstává stále populárnější ve světě Unixu.

Jedním z cílů standardizačního procesu ANSI C byl vytvořit nadmnožinu K&R C zahrnující mnoho „neoficiálních vlastností“. Navíc standardizační komise přidala několik vlastností jako funkční prototypy (vypůjčené z C++) a schopnější preprocesor.

ANSI C je dnes podporováno téměř všemi rozšířenými překladači. Většina kódu psaného v současné době v C je založena na ANSI C. Jakýkoli program napsaný pouze ve standardním C je přeložitelný a spustitelný na jakékoli platformě, která odpovídá tomuto standardu. Nicméně mnoho programů se dá přeložit pouze na jedné platformě nebo jedním překladačem, kvůli (i) použití nestandadních knihoven, např. pro grafiku, a také (ii) některé překladače v implicitním módu neodpovídají standardu ANSI C.

C99

Po standardizaci jazyka v roce 1989 se většina vývoje soustředila na jazyk C++. Přesto však na konci 90. let došlo k vydání dokumentu ISO 9899:1999 (obvykle nazývaný C99), který byl následně v březnu 2000 přijat i jako ANSI standard.

C99 představil několik nových vlastností, které byly mnohdy v překladačích už implementovány jako rozšíření.

Standard C99 je v některých ohledech přísnější než původní standard C89; například je zakázáno odkazovat na stejnou paměť ukazateli jiných typů. Toto umožňuje vylepšenou optimalizaci, ale může způsobit problémy s kompilací starších programů.

Žádný kompilátor zatím neobsahuje kompletní implementaci C99, přestože některé jsou poměrně blízko (GCC). Firmy jako Microsoft nebo Borland neprojevily velký zájem o implementaci C99, především kvůli tomu, že většinu nových vlastností poskytuje C++ a to často nekompatibilně s C99 (datový typ complex v C99 versus třída complex v C++).

Ahoj, světe!

Tzv. „Ahoj, světe!“ (v originále „hello, world“) je jednoduchý program, který prostě jen vypíše „hello, world“, ten je sám o sobě prakticky k ničemu, ale pro programátory je důležitý protože se naučíme jak vypadá program, který nic vlastně nedělá, jak se zkompiluje a spustí, tedy všechny formality, které nás dělí od vlastního programování.

A takto tedy vypadá hello world v C:

hello.c
  #include <stdio.h>
 
  int main(int argc, char *argv[]) //Pro primitivni programy muzeme pouzit jen: int main(void)
  {
    //Toto je komentar
    /* 
     * A toto je
     * viceradkovy komentar
     */
    printf("hello, world\n");
    return(0);
  }

Tento text uložíme pomocí nejobyčejnějšího textového editoru do souboru s příponou .c (tedy např.: „hello.c“). Na konci souboru musí být minimálně jeden prázdný řádek a soubor nesmí končit v otevřeném komentáři tedy např.: “/*Za touto vetou bude konec souboru“. Jinak kompilátor (nebo spíše preprocesor) nahlásí chybu.

Rozbor prvního programu

Po kompilaci a spuštění tohoto programu začne počítač vykonávat naše příkazy tak, jak jsou napsány ze shora dolů, z leva do prava. Teď si ukážeme co program udělá krok za krokem, řádek po řádku:

#include <stdio.h>

To není příkaz, který by se prováděl při spuštění programu, ale ještě před kompilací. Jde o tzv. „direktivu preprocesoru“ (začínají # - křížkem), preprocesor je program, který připraví náš kód pro kompilátor (odstraní nepotřebné mezery, prázdné řádky, rozvine makra,…). Direktiva preprocesoru #include <soubor> způsobí to, že bude nahrazena obsahem souboru „soubor“, čili na začítek našeho programu se vloží soubor stdio.h, který obsahuje především funkce na práci s příkazovým řádkem a soubory. Pokud je jméno souboru ve špičatých závorkách (#include <soubor>), hledá se soubor v adresáři pro hlavičkové soubory, který je součástí kompilátoru, pokud bychom napsali jméno souboru mezi uvozovky (#include „soubor“), preprocesor by hledal soubor ve stejném adresáři, jako se nachází náš program.

int main(int argc,char *argv[]) { /*Nase prikazy...*/ }

Toto je deklarace funkce main, funkce main je první funkce, která je zavolána po spuštění programu, jinými slovy příkazy uvedené ve složených závorkách ({}) budou provedeny neprodleně po spuštění našeho programu. Klíčové slovo „int“ před jménem funkce říká, že funkce vrací integer (tedy celé číslo). Dvě proměnné nadeklarované v závorkách za jménem funkce (int argc, char *argv[]) se používají k získání parametrů od našeho OS, jde o argumenty z příkazové řádky (nebo třeba jména souborů přetažená na ikonku našeho programu). přičemž celé číslo argc obsahuje počet parametrů a pole řetězců (polí znaků) argv[] obsahuje text těchto argumentů.

/* Komentáře */

Vše, co je umístěno mezi /* a */ (v našem příkladu můžete vidět zdobnější zápis s hvězdičkou na každé řádce, to zůstává na fantazii programátora…) bude ignorováno, můžeme to tedy použít pro popisování našich programů (to je velmi důležité, jinak se v našem programu ztratíme). Také můžeme použít komentář pomocí , kdy vše od až do konce řádku je ignorováno.

printf("hello, world\n");

Tento řádek vypíše na obrazovku text „hello, world“ a přejde na nový řádek („\n“ značí newline dle zvyku na vašem OS, tj. ekvivalent enteru). Funkce printf() je nadefinovaná v souboru stdio.h (viz. výše) a jako první parametr přijímá text, který chceme vypsat, nebo formátovací řetězec. Tímto prvním argumentem je vždy řetězec (např. text v uvozovkách). Pokud bychom chtěli vypsat uvozovku, musíme použít \“ pokud lomítko, tak
a pokud znam procenta, tak %%. Všiměte si, že deklarace, definice a volání funkce (náš případ) musí končit středníkem (;), nikoli však novým řádkem, stačí mezera, nebo tabelátor.

return(0);

Jde opět o volání funkce. Funkce return() (jména funkcí píšeme standartně s () na konci, aby bylo na první pohled jasné, že jde o funkci) způsobí to, že je funkcí, ze které je zavolána (v našem případě main()), vrácena hodnota zadaná jako první parametr této funkce return(). Pokud toto zavoláme z funkce main, znamená to pro nás ukončení programu s návratovou hodnotou 0. Návratové hodnoty jsou čísla (int main(), tedy funkce main() je typu integer == celé číslo), které programy předávají operačnímu systému po skončení, aby měl přehled o úspěšnosti vykonání programu (0 znamená bez chyby a obecně pak platí, že čím vyšší číslo, tím horší chyba).

Pokud program dojde na konec kódu funkce, je automaticky navrácena hodnota 0, pokud to není změněno pomocí funkce return();

Co budeme potřebovat pro psaní a spuštění programů v C

V první řadě budeme potřebovat plaintextový editor (jsme začátečníci a budeme tedy raději konzervativní, takže kódovani utf-8, nebo cp-1250, raději žédné wide-chary a už vůbec ne textové editory typu Word!!!). Naopak se nám hodí editor se zvýrazněnou syntaxí a jinými „vychytávkami“, které nám mnohonásobně usnadní psaní kódu a například nám i napovídá, jeden z nejlepších nejuniverzálnějších editorů je SciTe (na enginu Scintilla).

Můžeme použít například editory dostupné pro náš OS Microsoft:

Unixy:

Potom budeme také potřebovat program, který převede námi psaný a čitelný strojový kód na strojový kód tj. procesorem čitelné instrukce (ty se ukládají v binárním spustitelné souboru čitelném operačním systémem). A samozřejmě standartní knihovny ANSI C (verzi pro vývojáře).

Na platformách od Microsoftu to znamená instalci jednoho z těchto balíčků

Na Unixových platformách pak pravděpodobně potřebujeme tyto balíčky

Nebo můžeme sáhnout po celých IDE (Integrované vývojové prostředí) Jde o jakýsi balíček Vše-V-Jednom. To v praxi znamená potřebný editor uživatelsky přívětivě spolupracující s kompilátorem, jinými slovy nainstalujeme jeden balíček programů místo všech výše zmíněných.

Ze všech IDE pro Unixy i MS systémy je pro nás nejvýhodnější multiplatformní IDE Dev-C++ (dev-cpp) - (doporučuji). Pro jednotlivé systémy existují samozřejmě i další IDE:

Na Windows máme např.

Na Unixech pak stojí za zmínku

Samostatné cvičení

Následující body platí i pro všechna budoucí cvičení:

Datové typy, deklarace, identifikátory, přiřazení, inicializace, definice

Jazyk C disponuje několika základními datovými typy, jiné si můžeme vytvořit sami. Datový typ v praxi popisuje formát nějaké hodnoty, která vyžaduje alokaci předem daného místa v paměti (většinou měříme v bytech), na to se ukládá požadovaná hodnota (samozřejmě binárně), které přísluší (což není úplně tak závazné) konkrétní interpretace (logická hodnota, číslo, znak, text).

Základní jednoduché datové typy

Celočíselné

S Plovoucí desetinnou čárkou

pozn.: Hodnoty se mohou lišit podle konkrétní implementace (kompilátoru). Velikost konkrétního typu v bytech získáte také pomocí makra preprocesoru sizeof(), tedy např sizeof(char) bude nahrazeno číslem 1.

Například datový typ char má velikost 1B, to odpovídá 256ti možným kombinacím, standartně jednoduché datové typy počítají s možností uložení záporného čísla, takže např. char může dosahovat hodnot -128 až 127, podobné je to se všemi jednoduchými typy.

Pokud ale nechceme zbytečně alokovat (zabírat) paměť pro záporná čísla, když víme, že toho nevyužijeme, můžeme použít operátoru „unsigned“ (česky „bez znaménka“), tím docílíme toho, že se rozsah všech možných hodnot našeho typu „posune“ na 0 a výše. Např. tedy datový typ „unsigned char“ může nést hodnotu 0 až 255 tj. nějaký libovolný byte.

Deklarace, identifikátory

Takže ve zkratce známe několik důležitých datových typů a určitě nás zajímá jejich využití. První možností je, jak již bylo naznačeno, ukládání informací do paměti. Abychom mohli ukládat data do paměti, musíme si nejprve vytvořit proměnnou (staticky-na pevno alokovat část paměti), toho docílíme jednoduchým kusem kódu, vše ostatní už vyřeší kompilátor.

Této proceduře se říká „deklarace“, to znamená, že v paměti zabereme místo a přiřadíme mu „identifikátor“ (jméno), který budeme pokaždé používat při práci s tímto prostorem v paměti (to není podmínkou, identifikátoru je totiž přiřazena nějaká konkrétní adresa v paměti, ale o tom až v pokračování pro pokročilé).

Identifikátory mohou sice obsahovat různé znaky, my se ale zatím budeme držet malých písmen (v případě potřeby lze několik slov oddělit např. podtržítkem („_“)), to nám musí stačit, také se můžeme držet některých tradic (znaky začínájí na „c“ („cmuj_znak“), proměnné používané k řízení cyklů se většinou označují pouze „i“ - může nám stačit pro celý jednodušší program bez vnořených cyklů, atd…)

A takto proběhne deklarace z pohledu programátora:

datovy_typ identifikator[, dalsi_identifikator, ...]; //Deklarace konci strednikem

Takže pokud například potřebujeme tři proměné, pro uložení hodnot 0 - 4 294 967 295, použijeme tuto konstrukci:

unsigned int moje_cislo, moje_dalsi_cislo, moje_posledni_cislo;

Pozor ovšem na redeklaraci - tj. pokus o opětovnou deklaraci/definici libovolné proměnné, funkce, nebo čehokoli jiného, jejíž identifikátor je již používán, to je chyba, která většinou zkončí chybou překladače (kompilátoru).

Přiřazení, inicializace

Přiřazení je operace, která na danou adresu ukládá námi vybraná data, probíhá pomocí operátoru rovnítko (“=“) levé a pravé hodnoty (l-hodnota, r-hodnota - od left, right).

Obecný zápis je tento:

l-hodnota = r-hodnota; //I prirazeni opet konci strednikem

L-hodnota je v levo, tj. cíl přiřazení, r-hodnota je v pravo, tj hodnota přiřazení, po úspěšném přiřazení platí, že se l-hodnota rovná r-hodnotě. Pokud bychom tedy do námi deklarované proměnné moje_cislo chtěli přiřadit hodnotu 5, postupujeme takto:

moje_cislo = 5;

Pokud je hodnota do proměnné jednou přiřazena, lze ji samozřejmě přepsat jinou (až na vyjímky, o tom později…).

Pokud je to poprvé, co do nově nadeklarované proměnné přiřazujeme hodnotu, takovému přiřazení říkáme také inicializace.

Definice

Definice je výraz pro inicializaci provedenou zároveň při deklaraci (nebudeme polemizovat o rozdílu slov deklarace a definice z pohledu českého jazyka…).

Zápis definice je tento:

datovy_typ identifikator = nova_hodnota[, dalsi_identifikator = nova_hodnota, ...]; //Samozrejme strednik

Konkretne napriklad takto:

unsigned char pocet_psu = 10;

Ovšem zde se naskytuje další možnost udělat chybu a to je redeklarace:

unsigned char pocet_psu = 10;
unsigned char pocet_psu = 11; //Chybne pridani jednoho psa - pokus o redeklaraci!

Spravny zapis je samozřejmě tento:

unsigned char pocet_psu = 10; //Definice
pocet_psu = 11; //Spravne pridani jednoho psa - pouze prirazeni

Funkce printf() podrobněji

funkce printf nabízí kromě výpisu textu tak, jak je zadán do jejího parametru také možnost výpisu obsahu proměnných. Pokud bychom například chtěli vypsat proměnné pocet_psu a pocet_kocek, docilime toho timpo zpusobem:

printf("Pocet psu: %d\nPocet kocek: %d\n", pocet_psu, pocet_kocek); //Ansi-C funkce ze stdio.h - Nezapomeneme na #include <stdio.h>!!!

Symbol %d bude nahrazen číslem načteným z naší proměnné, s každým podobným operátorem přibyde funkci printf() jeden povinný parametr, tzn., že pokud je v našem řetězi dvakrát “%d“, musíme kromě prvního parametru (tzv. formátovacího řetězce) specifikovat ještě další dva parametry, tj. např. dvě proměnné v požadovaném pořadí.

Tady je seznam všech nejdůležitějších operátorů pro formátovací řetězec funkce printf:

Pro čísla typu int

Pro ostatní typy

Pro pole

Samostatné cvičení

Výhody a nevýhody programování textových aplikací

V našich seminářích se budeme zabívat především psaním aplikací pro CLI (tedy příkazový řádek). Účelem této hodiny je poskytnout vám objektivní pohled na tyto aplikace ve srovnání s aplikacemi grafickými.

Pokud v C píšeme konzolové aplikace, získáme tyto výhody:

Nevýhody textových programů

Výhody grafických aplikací

Nevýhody grafických aplikací

GUIBuildery - prostředí pro jednoduchý návrh grafického rozhraní programů v C/C++

Konstanty, konstantní proměnné

Konstata je výraz pro jakákoliv data, se kterými program pracuje a píšeme je přímo do zdrojového kódu. Konstantou může být např. číslo, znak, řetězec, pointer, a další… Konstanta se chová jako výraz, jako R-Hodnota (lze ji přiřadit, ale nelze přiřadit do ní). S konstantami jsme se již setkaly v kapitole probírající přiřazení.

Typy konstant s okomentovaným příkladem

int	32		//cele cislo (Integer)
long	L940		//cislo typu long
float	2.345		//cislo s plovouci radovou carkou (teckou)
float	F940		//Jiny zapis floatu (bez tecky by nebylo jasne, ze jde o float)
double  3.45		//cislo double
char	'A'		//znak (pro binarni data pouzivejte unsigned char nebo lepe int)
char[] "Muj text"	//retezec - pole znaku - viz. znak

Možnosti zápisu celých čísel (příklady)

decimálně	123
hexadecimálně	0x1A6F
oktálně	029

Možnosti zápisu znaků (literály - platí i pro řetězce)

decimálně	\0232 ('\0232')
hexadecimálně	\x0A ('\x0A')

Speciální znaky

\n ('\n')	Přechod na další řádek
\r ('\r')	Přechod na začátek řádku
\t ('\t')	Tabelátor

Příklady literálů

char str[] = "\tPole ch\x41ru, neboli retezec,\nje to tak.";
char str[] = "Preruseny"	" retezec\n";
char str[] = "Preruseny " "retezec "
	     "muze pokracovat i na dalsi radce!\n";

Konstantní proměnné

Konstantní proměnná je taková proměnná, která se po inicializaci stává konstantou (nelze ji měnit, je to tedy neproměnná proměnná ;D). Deklarace konstantní proměnné probíhá pomocí tzv. „typového modifikátoru“, v našem případě „const“.

Příklad:

const int a, b = 32; //Promenna b jiz nepujde zmenit, promenna a pujde zmenit pouze jednou (inicializovat).

Základní operátory

R-hodnota (viz. dříve) se může skládat z více prvků s vlastnostmi r-hodnoty. To uděláme za pomoci nějakých operátorů, se dvěmi hodnotami můžeme provádět různé operace (aritmetické, logické a další). k tomu nám slouží různé operátory.

Příklad

  int x, a, b = 2;
  x = (a = (b * 2) - 3);

Na konci tohoto kódu budou proměnné „x“ a „a“ držet hodnotu 1.

Z toho také vyplívá, že celé přiřazení (l-hodnota = r-hodnota) je zároveň r-hodnotou a lze ho přiřadit do jiné l-hodnoty (např. proměnné). Také si všimněme, že lze použít závorky () k seskupování výrazů a úpravě jejich priorit (víme, že násobení má přednost před sčítáním), tato základní asociativita funguje stejně jako v matematice, pokud si nejsme jisti, nemusíme experimentovat a prostě se pojistíme závorkou.

Přehled operátorů

Zde naleznete seznam různých operátorů. Některé z nich budeme používat a vysvětlíme si je, ale většinu z nich zatím potřebovat nebudeme, ale k tomuto seznamu se určitě budete potřebovat vracet.

+	sčítání
-	odčítání
*	násobení
/	dělení
%	dělení modulo (zbytek po dělení)
++, --  inkrementace resp. dekrementace hodnoty, prefixový i postfixový zápis
= *= /= %= += -= <<= >>= &= |= ^= 	jednoduché přiřazení a přiřazení s výpočtem
< 	menší než
> 	větší než
<= 	menší nebo rovno
>= 	větší nebo rovno
== 	rovnost
!= 	nerovnost
&& 	logický součin (and)
|| 	logický součet (or)
!  	logická negace
~ 	bitová negace
<<, >>  bitový posun vlevo resp. vpravo
& 	bitový součin (and)
| 	bitový součet (or)
^ 	bitový vylučovací (exkluzivní) součet (xor)
?:  	podmíněné vyhodnocení  	zprava doleva
. 	tečka, přímý přístup ke členu struktury
-> 	nepřímý přístup ke členu struktury
, 	čárka, oddělení výrazů
sizeof	získání velikosti objektu v bytech
(typ)  	přetypování na typ uvedený v závorkách
&  	reference (získání adresy objektu)
* 	dereference (získání objektu dle adresy)

Porovnávání

V kapitole, kde budeme probírat podmínky a cykly určitě využijeme porovnávací operátory (<,>,⇐,>=,==,!=), logickou negaci (!) a logický součin a součet (&&,||). Pokud jde o porovnávací operátory, tak vždy vrací hodnotu true (1 nebo jiná nenulová hodnota), nebo false (0), tj. pravda/nepravda. Logický součin (&&) vrací true pouze pokud jsou obě hodnoty true. Pokud je první hodnota false, tak se druhá nevyhodnocuje (zjednodusene vyhodnocovani). Logický součet (||) vrací true pokud je alespoň jedna z hodnot true. Pokud je první hodnota true, tak se druhá nevyhodnocuje (zjednodusene vyhodnocovani). Logická negace vrací opak (!1 je 0, !0 je 1).

Příklady

int a = 2, b = 3, x = 4, y = 2;
b = !(b == a); //b bude true
b = (b != a); //b bude true (jde o prakticky jiny zapis stejneho vypoctu)
b = (a = 2); //casta chyba - nedojde k porovnani, ale prirazeni 2 do a a nasledne do b
x = (x <= y); //x bude false
y = !((1 <= 2)||(b = 7)); //y bude false, prirazeni 7 do b neprobehne (pouzito zjednodusene vyhodnocovani)
y = 128; //y bude true
y = 0; //y bude false
y = !y; //y bude true

Inkrementace, dekrementace,...

Hodnotu proměnné můžeme změnit i jiným způsobem, než je přiřazení:

int a = 31, b;
a++; //a bude zvětšena o 1 (jiný zápis: a = a+1; )
a *= 2; //a bude vynásobeno 2 (jiný zápis: a = a*2; )
b = a++; //a bude přiřazeno do b a následně zvětšeno o 1
b = --a; //a bude zmenšeno o 1 a nasledne prirazeno do b

Operátor sizeof()

sizeof je makro preprocesoru, které nám vrátí velikost daného datového typu (viz. kapitola jednoduché datové typy) v bytech. Víme, že například celé číslo (int) zabírá v paměti většinou 2 byty, výraz sizeof(int) nám tedy vrátí u většiny kompilátorů číslo 2.

Samostatné cvičení

Standartní vstup a výstup

Řádkový výstup

Již jsme se seznámili s použitím funkce printf(), další užitečnou funkcí je putchar(), která vypíše jeden znak/byte.

Vstup

Samozřejmě budeme potřebovat také odezvu ze strany uživatele, k tomu nám budou sloužit funkce scanf(), která je opakem printf() a getchar(), která je opakem putchar().

Funkce getchar() nám vrací ASCII hodnotu jediného načteného znaku.

Funkce scanf() nám oproti tomu zapíše získaná data na danou adresu v paměti, to nebudeme hlouběji rozebírat, důležité ale je, že před identifikátor proměnné musíme napsat operátor ampersand (“&“). Pokud tedy budeme například chtít načíst z klávesnice desítkové číslo, použijeme tento zápis:

  int a;
  scanf("%d", &a);

Převod řetězce na číslo

Často potřebujeme, aby nám uživatel zadal číslo, z terminálu můžeme číst ale pouze znaky (případně řetězce). Naštěstí pro nás existují v Ansi C funkce, které nám dovolí uskutečnit tento převod, jsou to tyto:

Souhrnné příklady

  char text[] = "Toto je nějaký text";
  int cislo = 5;
  printf("Toto je text: %s a tohle cislo: %d\n", text); //Vypíše: Toto je text: Toto je nějaký text a tohle cislo: 5(a odřádkuje)
 
  char ascii_cislo[] = "64";
  cislo = atoi(ascii_cislo); //Priradi do promene cislo cislo urcene retezcem ascii_cislo[], tj.: 64.
 
  char vstup[1024]; //Toto není příliš optimální řešení, protože se v něm skrývá bezpečnostní hrozba, nebo možnost kolapsu našeho programu, pokud uživatel zadá více jak 1024 znaků... My se tímto problémem ale zatím zabývat nebudeme, ovšem nutno poznamenat, že správné řešení by bylo úplně jiné.
  int vstup_cislo;
  scanf("%s", &vstup);
  scanf("%d", &vstup_cislo);

Další převody

Další velice používanou funkcí pro převody (např. čísla na řetězec) je sprintf(). Tato funkce pracuje obdobně, jako printf() (kterou doufám již všichni známe), s tím rozdílem, že sprintf() přijímá ještě první argument, kterým je identifikátor řetězce, do kterého bude po zformátování výsledný řetězec uložen (printf() by ho jen vypsalo).

Řešený příklad

calc.c
/* calc.c
 * Napište program, který načte z řádky dvě čísla, sečte je a výsledek vypíše formou rovnice.
 * K získání prvního celého čísla použijte přímo scanf(), druhé převeďte z řetězce získaného ze scanf() pomocí atoi().
 * Program zkompilujte a otestujte.
 */
 
#include <stdio.h>
#include <stdlib.h>
 
int main() {
	char a[10];
	int x, y;
	printf("Prvni scitanec: ");
	scanf("%d", &x); //Prvni cislo nacteme rovnou jako dekadickou hodnotu
 
	printf("Druhy scitanec: ");
	scanf("%s", &a); //Do retezce a ulozime ascii zapis cisla (pismenka) z klavesnice
	y = atoi(a); //Do integeru y ulozime cislo ziskane z retezce a
 
	printf("Vysledek: %d+%d = %d\n", x, y, (x+y)); //Vypiseme vysledek
}

Například takto vypadá program za běhu:

harvie@harvie-ntb:~/Desktop/skripta/c$ gcc calc.c -o calc -std=c99 && chmod +x calc #kompilace
harvie@harvie-ntb:~/Desktop/skripta/c$ ./calc #spusteni
Prvni scitanec: 2
Druhy scitanec: 3
Vysledek: 2+3 = 5
harvie@harvie-ntb:~/Desktop/skripta/c$

Přesměrování vstupu a výstupu

Zjistěte si, jakým způsobem umí váš operační systém přesměrovávat vstup a výstup do souborů a ze souborů, tak aby se výstup programu nevypsal na obrazovaku, ale do souboru, nebo naopak se vstup načetl ze souboru, nebo byl výstup přesměrován na vstup jiného programu. (většinou k tomu slouží operátory příkazového řádku >,»,<,|).

Kromě použití operátorů příkazové řádky tohoto také můžeme docílit pomocí funkce freopen() aplikované na handly stdin, stdout, nebo stderr, to ale pro nás není zatím nutné umět.

Samostatná cvičení

Podmínky a cykly

Tyto dvě důležité věci jsou častou součástí většiny programů.

Podmínka

Podmínka je operátor, který nám umožní vykonat nějaký kód pouze pokud je splněna nějaká podmínka. Podmínka se vyhodnocuje tak, že se vypočítá hodnota výrazu předaného podmínce a pokud tato hodnota odpovídá logické pravdě (to většinou znamená nenulové číslo), vykoná se daný příkaz.

Jednoduchá podmínka může vypadat například takto:

int a = 1, b = 2;
if(a<b) printf("a je mensi nez b");

Jaké operátory máme a jak se vyhodnocují jsme se již dozvěděli z kapitoly operátory, na druhou stranu to nemusí být na první pohled jasné, proto ještě v rychlosti shrnu alespoň srovnávací (komparační) operátory:

== - je rovno
!= - není rovno
>  - větší než
<  - menší než
>= - větší nebo rovno
<= - menší nebo rovno
!  - logická negace
&& - logické AND (A)
|| - logické OR (NEBO)

Pokud tyto operátory použijeme na dvě čísla (operandy), výslednou hodnotou bude vždy logická hodnota (pravda/nepravda - true/false - 1/0), u většiny kompilátorů je to 0 nebo 1. Pokud tedy vyhodnocujeme podmínku (např. pomocí if()), pokaždé, když ji předáme nenulovou hodnotu, je proveden následující příkaz. Např.:

if(1) if(255) if(!0) printf("tento prikaz bude proveden vzdy\n");
if(!1) if(0) printf("tento prikaz nebude proveden nikdy\n");

S if souvisí ještě jeden operátor a tím je else, tím můžeme určit co se stane, když podmínka splněna nebude.

if(a>b) printf("a je mensi nez b\n"); else printf("a je vetsi nez b\n");

Složený příkaz (Blok kódu)

Pokud několik příkazů uzavřeme do složených závorek, začnou se vůči kódu mimo ně chovat jako jeden příkaz. V podmínkách a cyklech můžeme tedy vykonat více příkazů tam, kde bychom mohli normálně vykonat pouze jeden. např. následující kód vypíše 3x „Nejaky text\n“.

int podminka = 1;
 
if(podminka) printf("Nejaky text\n");
if(podminka) { printf("Nejaky "); printf("text\n"); }
if(podminka) {
	printf("Nejaky ");
	printf("text\n");
	}

Následující kód pak zmenší hodnotu většího ze dvou čísel.

if(a>b) {
	a--;
} else {
	b--;
}

Cyklus while

Jde o základní cyklus, jediné, čím se liší od podmínky je to, že se příkaz neustále opakuje, dokud je podmínka splněna. Příklady:

while(1) printf("#"); //Budeme do nekonečna vypisovat znak '#'
 
int i = 30;
while(i>10) { //Budeme zmensovat i, dokud bude vetsi nez 10
	i--;
}

Cyklus for

Cyklus for je podobný cyklu while, akorát s tím rozdílem, že kromě podmínky obsahuje také příkazy, které se spustí před začátkem cyklu a pak při každé iteraci (opakování).

Následující while cyklus

int i = 30;
while(i>10) {
	i--;
	printf("%d", i);
}

tedy můžeme zapsat pomocí for-u i takto:

int i;
for(int i=30;i>10;i--) printf("%d", i);

Což je mnohem přehlednější.

Vnořené (zahnízděné) cykly

Dobré je si také uvědomit, že můžeme mít například dva cykly v sobě.

Řešený příklad

Napište program, který vypíše za sebe čísla od 0 do 10 a zpět oddělené čárkami.

loops.c
#include <stdio.h>
#include <stdlib.h>
 
int main() {
	int x=1, a=0;
	for(int i=0;i<=20;i++){
		printf("%d", a);
		if(i<20) printf(",");
		if(a>=10) x=(-1);
		a+=x;
	}
	printf("\n");
}
 
//EOF

Výstup:

harvie@harvie-ntb:~/Desktop/skripta/c$ ./loops
0,1,2,3,4,5,6,7,8,9,10,9,8,7,6,5,4,3,2,1,0

Samostatná cvičení

Pole

Pole je datový typ skládající se z určitého počtu jiných datových typů. Zatím jsme se s poli setkali jen jako s řetězci (řetězec je pole jednotlivých znaků, pole končí znakem s ASCII hodnotou \x00).

Nejlepší bude, pokud si to vysvětlíme na jednoduchém příkladu. Například budeme potřebovat uložit 10 čísel int, které budou všechny sloužit k podobnému účelu, ale je možné, že se jejich počet bude v budoucnosti měnit, nebo jsme líní pro je jednotlivě deklarovat (a byli bysme hloupí, kdybychom to tak dělali).

Použijeme tedy např. tuto deklaraci:

int cisla[10];

Tím jsme poprosili systém o 10*sizeof(int) bytů (neboli paměť potřebnou pro uložení 10 integerů, typicky to bude 20B).

Nyní můžeme s čísli jednoduše pracovat, musíme si však pamatovat, že ačkoli jsme nadeklarovali 10 integerů, k prvnímu přistupujeme pomocí [0] a k desátému pomocí [9], proč tomu tak je si vysvětlíme za chvilku.

Příklad

int cisla[10], a = 2;
cisla[0] = 1;
cisla[1] = 2;
cisla[a] = cisla[0]*cisla[1]+cisla[2];
cisla[2]++;
//promenna cisla[2] drzi nyni hodnotu 5
 
cisla[10] = 18; //Zde by zase pravděpodobně došlo k pádu programu, protože se pokoušíme přistupovat k neexistující 11. položce pole.

Uložení pole v paměti

držme se naší deklarace z předchozího příkladu, tedy:

int cisla[10];

V tom případě jsme si říkali, že bude v paměti alokováno 10*sizeof(int) bytů, to bude vypadat následovně:

 __ __ __ __ __ __ __ __ __ __ 
|00|00|00|00|00|00|00|00|00|00|
 -- -- -- -- -- -- -- -- -- --
^                          ^
|_ Toto je adresa &cisla   |_  Toto je adresa &cisla+9*sizeof(int) (neboli cisla[9])
   (neboli cisla[0])

Pokud si tedy zkusíme vytisknout printf(“%d\n“, &cisla);, získáme adresu v paměti (to dělá operátor &), na které je naše pole uložené, touto adresou je obyčejné číslo, je ale zvykem zapisovat ho šestnáctkově, toto je pro nás ale zatím dostačující.

Tím, že použijeme zápis cisla[0] dojde k tomu, že získáme přístup k sizeof(int) bytům na adrese &cisla, tedy k prvnímu integeru, když chceme přistupovat k druhému integeru v poli použijeme cisla[1], protože tím se dostáváme z adresy &cisla o jeden int dále (tedy: &cisla+1*sizeof(int)), je tedy zřejmé, že pokud se pokusíme pracovat s cisla[10], octneme se již v části paměti, která nepatří našemu programu a náš program bude ukončen, protože v jiném případě by náš program mohl ovlivňovat práci jiného programu.

Zjištění velikosti pole

Je možné, že neznáme velikost pole, a potřebujeme ho zpracovat (např. v cyklu), k tomu opět použijeme operátor sizeof(). V následujícím příkladu si také můžete všimnout, že při deklaraci pole můžeme jeho velikost určit jinou celočíselnou proměnnou (to je možné až v novějších verzích jazyka C). Následující kus kódu ukazuje, jak zjistit a vypsat velikost pole.

int i = 30;
int cisla[i];
//Nejaky jiny kod
printf("Pocet cisel: %d\n", sizeof(cisla)/sizeof(int));

Příkaz sizeof(cisla) nám vrátí počet bytů, které pole skutečně zabírá v paměti, to může být různé. nás ale zajímá kolik prvků (int) pole má a s kolika můžeme ve skutečnosti pracovat. K tomu musíme ještě toto číslo vydělit hodnotou sizeof(int) (nebo sizeof(jiny_typ_naseho_pole)), ta totiž určuje, kolik každý prvek pole zabírá v paměti.

Když si to shrneme: Víme, kolik místa zabírá pole v RAMce a víme, kolik má zabírat jeden jediný prvek, jednoduše tedy vydělíme (vrátíme se ve vzpomínkách do 2. třídy ZŠ ;) a máme požadované číslo, pokud nás ovšem zajímá nejvyšší „offset“ (to číslo v hranatých závorkách), jaké můžeme použít, nesmíme ještě zapomenout odečíst 1.

N-rozměrná pole

Můžeme také vytvářet mnohorozměrná pole (např.: dvou-rozměrná, tří-rozměrná, sto-rozměrná, n-rozměrná), takové mnohorozměrné pole je laicky řečeno pole polí, nebo pole polí polí polí polí, typickým příkladem je pole řetězců.

Deklarace takového pole může vypadat takto:

int a[3][40]; //a je pole 40ti polí o 3 integerech
//Podobná pole si většinou představujeme jako 3x40 čtverečků (40 je výška)
char c[4][6][8];
//Takové pole si pro změnu můžeme vizualizovat jako 8mi patrový kvádr, přičemž každé patro se zkládá z 4x6 kostiček (každá kostička drží nějaký znak).

U složitějších polí si můžeme představit například několik takových kvádříků uspořádaných do několika polic v několika skříních v několika řadách,… Prostě jakkoli.

Pokud máme například pole int pole[2][3], pak pole[0] je pole 3 integerů jako např. int druhe[3].

Ostatní práce s poli je naprosto analogická k polím jednorozměrným.

Řešený příklad - zpracování pole v cyklu

Toto je velmi často používaná věc! Více se dozvíte v kapitole „podmínky a cykly“, tady jen uvádím jednoduchý příklad. Můžeme si zde všimnout dvou totožných cyklů, přičemž jeden je while() a druhý for().

arrays.c
/* arrays.c
 * Napiste program, ktery naplni pole integeru nahodnymy cisly a potom je vypise.
 * Pouzijte cykly for i while
 */
 
#include <stdio.h>
#include <stdlib.h>
 
int main() {
	int i, cisla[10];
 
	i=0;
	while(i<sizeof(cisla)/sizeof(int)) {
		cisla[i] = rand(); //K generovani nahodnych cisel slouzi funkce rand()
		i++;
	}
 
	for(i=0;i<sizeof(cisla)/sizeof(int);i++) printf("%d. cislo je: %d\n", i, cisla[i]);
}
 
//EOF

Samostatná cvičení

Argumenty programu

Další možností, jak můžeme našemu programu předat nějaká data, je to, že mu je zdělíme už při spouštění. To se provádí pomocí tzv. argumentů (můžeme se také setkat s pojmem parametry), ty předáme programu tak, že je napíšeme za jeho název oddělené mezerami. To je přesně to, k čemu dochází pokud např. ve vašem operačním systému otevíráte nějaký textový dokument, video nebo cokoli v jiném programu. V takovém případě grafické prostředí, které pravděpodobně používáte k ovládání počítače vyvolá příkaz podobný těmto:

Můžeme si všimnout, že všechny parametry jsou odděleny mezerami, pokud je nutné, aby parametr obsahoval mezeru, dáme ho buď do uvozovek, nebo před mezeru dáme zpětné lomítko (to se na windows nepoužívá), pokud ovšem potřebujeme, aby argument obsahoval uvozovku, dáme před ni také zpětné lomítko.

Také by bylo dobré poznamenat, že prvním argumentem (i když se takto neoznačuje) je vždy název programu.

Argumenty z hlediska programátora

Operační systém předává našemu programu argumenty pomocí dvou proměnných. První z nich je int argc, která obsahuje počet argumentů, pokud tedy například předáme programu dva argumenty, tak bude platit, že argc == 3, protože kromě těchto dvou souborů bylo programu předáno ještě jméno spustitelného souboru (tedy název programu). Druhou proměnnou, která už obsahuje data konkrétních argumentů je char *argv[], jde o pole řetězců, to tedy znamená, že např. argv[0] bude první argument (název programu), argv[1] druhý, atd…

Aby náš program mohl od systému parametry převzít, musíme je nadeklarovat v hlavičce funkce main(), tedy např.:

int main(int argc, char *argv[]) {
	//Telo programu
}

Padám, padáš, padáme!

V případě, že by se náš program pokusil přistupovat k nezadanému argumentu, dojde k jeho pádu, protože operační systém toto místo (kde náš argument není) již používá k jiným účelům a je nežádoucí, aby k němu náš program získal přístup. Je tedy vhodné pomocí hodnoty argc vždy ověřovat, jestli konkrétní argument existuje. V opačném případě bychom se mohli od operačního systému dočkat ukončení programu a vypsání podobné chybové hlášky: „Neoprávněný přístup do paměti (SIGSEGV)“, nebo známější anglická hláška „Segmentation fault. Core dumped.“.

Takové ošetření by mohlo vypadat například takto:

int main(int argc, char *argv[]) {
	if(argc > 1) {
		printf("Byl zadán argument %s\n", argv[1]);
	} else {
		printf("Nebyl zadán žádný argument!\n");
	}
}

Řešený příklad

args.c
/* args.c
 * Napiste program, ktery vypise pocet parametru, prvni parametr a nasledujici dva secte jako cela cisla a vypise ve tvaru rovnice
 */
 
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[]) {
	int x, y;
	printf("Pocet parametru: %d\n", argc);
	printf("Jmeno programu: %s\n", argv[0]);
	x = atoi(argv[1]);
	y = atoi(argv[2]);
	printf("Prvni scitanec: %d\n", x);
	printf("Druhy scitanec: %d\n", y);
	printf("Vysledek: %d+%d = %d\n", x, y, (x+y));
}

Výstup programu:

harvie@harvie-ntb:~/Desktop/skripta/c$ ./args 2 3
Pocet parametru: 3
Jmeno programu: ./args
Prvni scitanec: 2
Druhy scitanec: 3
Vysledek: 2+3 = 5

Nyní se program pokusí přistupovat k neexistujícímu argumentu:

harvie@harvie-ntb:~/Desktop/skripta/c$ ./args 2
Pocet parametru: 2
Jmeno programu: ./args
Neoprávněný přístup do paměti (SIGSEGV)

Samostatná cvičení

Preprocesor

Už jsme si nastínili, že než se námi napsaný zdrojový kód dostane do kompilátoru, projde vyčištěním, doplněním a částečnou optimalizací v tzv. preprocesoru. Nyní si ukážeme, jak používat některé jeho direktivy (funkce), předtím je snad jen dobré poznamenat, že direktivy preprocesoru se označují znakem # (hash,mřížka,křížek,sharp).

Inkludování - vkládání jiného souboru

Preprocesor nám umožňuje vložit do zdrojového kódu veškerý obsah jiného souboru, této direktivy využijeme např. při odděleném překladu, nebo při vkládání hlavičkových souborů různých knihoven. Například inkludováním <stdio.h> si zpřístupníme Ansi C funkce pro práci s textovým vstupem a výstupem (klávesnice, obrazovka, soubory).

Vložení probíhá pomocí direktivy pomocí direktivy #include, např.:

#include <stdio.h> //Se špičatými závorkami se soubor hledá v adresáři, kde má překladač standartní hlavičkové soubory (jako je stdio.h)
#include "mujprogram.h" //S uvozovkami se soubor hledá ve stejném adresáři, jako je náš zdrojový kód, pokud není nalezen, hledá se ve standartním adresáři
#include NAZEV_KONSTANTY //Viz. symbolické konstanty

Musíme si ale dávat pozor, aby jsme omylem neinkludovali sami sebe, nebo jiný soubor, který inkluduje nás, to by pravděpodobně vedlo k zaseknutí preprocesoru v nekonečné smyčce. Jak tomu předcházet: viz. podmíněný překlad.

Symbolické konstanty

Rozdílem použití této konstanty (ve srvonání např. s definicí běžné konstanty) je to, že ji nelze po kompilaci měnit, protože preprocesor představuje z tohoto hlediska pouze pomůcku, která vyhledá v kódu identifikátory makra a nahradí je hodnotou tohoto makra (asi jako to dělá váš oblíbený textový editor pomocí funkce „najít a nahradit“). To je výhodné např. pro uchování číselných hodnot (dříve se makra používala k definici velikosti statického pole, dnes již musí Ansi C podle normy umět alokovat pole za běhu, aby bylo možné jeho velikost určit pomocí proměnné), naopak jsou makra nevhodná například pro uchování větších řetězců, které používáme na více místech (rozkopírování několika stejných desítek bytů na několik míst v kódu nikomu ještě nepřidalo).

Použití:

Nahradit všechny výskyty NAZEV_MAKRA textem retezec:

#define NAZEV_KONSTANTY [retezec]

Zrušit definici makra NAZEV_KONSTANTY:

#undef NAZEV_KONSTANTY

Jak jste si mohli všimnout, je zvykem psát identifikátory symbolických konstant celé velkými písmeny (opět až na vyjímky), abychom je oddělili od normálních proměnných.

Makra

Pokud jste ještě nečetli kapitolu Funkce, tak následující odstavec klidně přeskočte.

Také existují ještě zajímavější druhy symbolických konstant a těmi jsou makra, ty se liší tím, že obsahují (podobně jako funkce) argumenty, takové nám mohou nahradit menší funkci, to sice také prodlužuje kód (a velikost výsledného programu), ale nadruhou stranu tím ušetříme čas na volání funkce (což může být někdy poměrně dost), o případných zbytečných automatických přetypování a přiřazování ani nemluvě.

Použití:

#define nasobek(a,b) ((a)*(b))

zápis:

nasobek(1,2)

se potom rozvine na:

((1)*(2))

Zde musíme dát pozor na závorky, pokud bychom napsali jen: #define nasobek(a,b) a*b, mohlo by se nám potom stát, že makro použijeme (slovo zavoláme by nebylo příliš vhodné) například tímto způsobem:

int i = nasobek(2+3,1+2)*7

pak by se makro rozvinulo v:

int i = 2+3*1+2*7

a to je úplně jiný výraz než námi předpokládaný

int i = ((2+3)*(1+2))*7

Podmíněný překlad (podmínky preprocesoru)

Podmíněný překlad je možnost preprocesoru vypouštět některé části kódu na základě toho, jestli je definována nějaká symbolická konstanta, typicky jde o potlačení ladících výpisů (které by nám v případě, že je zakážeme pomocí běžné podmínky if() a například čísla, kterým je ovládáme zbytečně okupovali prostor v paměti a výkon procesoru).

Použití:

#define LADIT //Budeme ladit
 
int main() {
	//nejake prikazy
	#if defined(LADIT)
		//ladici prikazy	
	#endif
}

Dalším využitím podmíněného překladu je například předcházení zacykleným inkludováním, následující soubor se nenainkluduje dvakrát:

#define UZ_INKLUDOVANO
#ifndef UZ_INKLUDOVANO
	//Zamysleny obsah souboru
#endif

Předdefinované symbolické konstanty preprocesoru

Z preprocesoru můžeme také zjišťovat různé informace a zařídit se podle nich, k tomu nám pomůžou předdefinované symbolické konstanty mezi ně patří např.:

Funkce - úvod

Při programování neustále voláme nějaké funkce, funkci, která nám vypíše to, zjistí tamto, vypočítá tohle, ukončí program, otevře soubor, čte ze souboru a takřka cokoli složitějšího děláme pomocí funkcí.

Volání funkce

Volání funkce (jak už víme) probíhá například zápisem:

int b = 4;
int y = secti(1, b);

Podobný zápis předá jedničku a hodnotu proměnné int b funkci secti() a do proměnné x nám uloží návratovou hodnotu funkce (konkrétně tohoto callu - neboli zavolání). Tuto hodnotu není nutné nikam ukládat. Můžeme ji také např. použít v jiném výrazu, jako argument jiné funkce, nebo dokonce prostě zahodit/nepoužít.

Definice funkce

Ovšem se nám může stát, že budeme potřebovat použít jeden algoritmus, nebo blok kódu na více místech našeho programu a pokud bychom ho jen rozkopírovali, nebo (dokonce) opsali, byli bysme pro smích všem programátorům (včetně začátečníků). Napíšeme si tedy vlastní funkci, je to jednoduché:

//Definice nasi prvni funkce - nachazi se mimo funkci main()
int secti(int prvni, int druhe) { //Sem za hlavicku funkce je dobre psat jednoduchy popis funkce (napr. "secte dve cisla int")
	//Telo funkce secti():
	int x;
	x = prvni+druhe; //Secteme prvni a druhe a ulozime do x
	return x; //Vratime x
} //Pozor!!! tady jiz neni strednik
 
//A nyni muzeme vyzkouset nas stary dobry ukazkovy kod
int main(int argc, char *argv[]) { //Volani ale jiz provedeme z funkce main():
	int b = 4;
	int y = secti(1, b);
	printf("%d\n", x);
}

Pravidla

Při psaní nových funkcí samozřejmě také platí jistá pravidla, například to jsou:

Parametry funkce

Funkci předáváme parametry (tím, jak to ve skutečnosti probíhá v procesoru se zatím zatěžovat nebudeme), jsou to např. nějaká čísla, se kterými můžeme ve funkci dále pracovat. Rozlišujeme dvoje parametry - tzv. skutečné parametry a tzv. formální parametry.

Návratová hodnota

Návratová hodnota je cokoli, co funkce vrátí (zavoláním funkce return(), nebo dosažením konce funkce), vždy jde o stejný datový typ, jako je typ funkce (návratová hodnota je jakousi hodnotou této funkce). Pokud nezavoláme funkci return(), ale funkce se ukončí dosažením konce svého kódu, je automaticky vrácena 0.

Cvičení

Samostatná cvičení

Globální proměnné, přetěžování funkcí, funkční prototypy, rekurzivní funkce

Minule jsme si ukázali základy práce s funkcemi, dnes si to ještě o něco ztížíme.

Globální proměnná

Říkali jsme si, že pokud ve funkci pracujeme s proměnnými, tyto neovlivňují zbytek programu, naopak ani nemůžeme pracovat s jinými proměnnými v programu (funkce běží v jiném tzv. kontextu). Pokud bychom ale stejně chtěli nadefinovat proměnnou, která bude použitelná v celém programu (má tzv. Globální viditelnost), můžeme to udělat například její definicí/deklarací mimo funkci main, takový kód by vypadal zhruba takto:

int vypis_x() { printf("x = %d\n", x); } //Vypise globalni promennou x
int inc_x() { x++; } //Zvetsi globalni promennou x o 1
 
int x = 2; //Globalni prommena x = 2
 
int main(int argc, char *argv[]) { //Hlavni funkce (ta se vetsinou nekomentuje!)
	vypis_x();
	x = 4; //x = 4
	inc_x(); //x = 5
	vypis_x();
	x = inc_x(); //x = 0 (funkce inc_x() sice x zvysi na 6, ale nasledne vrati hodnotu 0, ktera je zde do x prirazena)
	vypis_x();
}

Můžeme si všimnout, že když definujeme novou funkci, děláme to také raději mimo fci main(), tato naše funkce je pak „viditelná“ (dostupná) kdekoli.

Přetěžování funkcí

Psal jsem, že identifikátor funkce nesmí s ničím kolidovat. To není tak úplně pravda. Můžeme mít několik funkcí se stejným jménem, ale jiným počtem neby typem parametrů, nejlépe to je vidět na následujícím příkladu:

int secti(int a, int b) { return a+b; }
int secti(int a, int b, int c) { return a+b+c; }
int secti(char c) { putc c; }

Pak pomocí volání secti() můžeme sečíst dvě, nebo tři čísla (a nemusíme tedy přemýšlet, jak se jmenuje funkce na sečtení tří čísel), pokud zavoláme secti() s jedním parametrem typu char, tak dokonce tato funkce udělá něco úplně jiného a to je vypsání znaku (což je stejně nečekané, jako sčítání s jedním číslem ;).

Funkční prototypy

Zatím jsme vždy funkci definovali jen před místem, kde jsme ji použili (to je logické, protože proměnnou také musíme vždy nejdříve nadefinovat, než s ní začneme pracovat). Mohlo by se ale stát, že otevřeme soubor se zdrojovým kódem našeho programu, a než se dostaneme k funkci main(), která obsahuje hlavní logiku našeho programu, budeme se muset prokousat spoustou pomocných funkcí, které pro nás nemají tak velký význam.

Můžeme tedy funkci nad main() jen „nadeklarovat“ (toto označení není úplně na místě, ale přesto je takřka pravdivé) a napsat (nadefinovat) ji níže. Tomu se říká použití funkčního prototypu. Funkční prototyp je vlastně pouze hlavička dané funkce zakončená středníkem. Funkční prototyp také nemusí obsahovat seznam parametrů (a jejich datových typů), v tom případě ale funkci nelze přetěžovat a kompilátor neví jaké parametry má očekávat (jede odzhora dolů) a proto nemůže opravit chyby jako volání s nesprávným počtem, nebo typem parametrů, které takřka vždy musí skončit s oblíbenou hláškou „Segmentation fault“, neboli neoprávněný přístup do paměti.

V případě, že používáme funkční prototypy, tak stručný komentář píšeme k prototypu, nikoli k tělu funkce.

Bývá také zvykem, že se funkční prototypy dávají do hlavičkového souboru (viz. kapitola Preprocesor), který se jmenuje stejně jako náš zdrojový kód, s tím rozdílem, že místo přípony .c má příponu .h, to je užitečné, pokud chceme tyto funkce používat i v jiných souborech našeho projektu (tomu se říká tzv. oddělená kompilace). Tento hlavičkový soubor se pak includuje nejen do našeho zdrojového kódu, ale do všech dalších souborů, které chtějí mít k těmto funkcím přístup.

int tisk(int x); //Funkce vypise prvni parametr jako int a vrati jeho dvojnasobek (pozor, tady strednik pro zmenu je!)
//int tisk(); //Toto je druha moznost zapisu funkcniho prototypu, tedy bez parametru...
 
int main(int argc, char *argv[]) {
	tisk(4);
}
 
int tisk(int x) {
	printf("%d\n", x);
}

Rekurzivní funkce

Rekurzivní funkce je funkce, které volá sama sebe, musíme ale pamatovat na to, že pokud nebude počet rekurzivních volání konečný, tak bude nekonečný, to v praxi znamená, že takový program se logicky zasekne v nekonečné smyčce, což vyústí v jednu ze tří věcí. Buď dojde paměť, nebo jiné prostředky, program začne velmi špatně hospodařit s časem CPU a „vycucne“ výkon, který potřebují jiné programy, nebo zůstane stát na místě a dokola opakovat nějakou jednoduchou akci.

Možná není na první pohled zřejmé, k čemu jsou takové funkce dobré, uvedu vám tedy příklad z praxe, který jsem sám několikrát použil a je používaný ve spoustě programů. Představte si, že potřebujete vypsat, nebo prohledat obsah adresáře včetně podadresářů a jejich podadresářů, atd… Jak to tedy uděláme? Jednoduše si napíšeme funkci, která otevře zvolený kořenový adresář (jeho jméno se předává jako parametr) a projíždí si ho položku po položce, když narazí na soubor, tak ho zpracuje (např. vypíše jeho název), pokud ale narazí na adresář, tak ho nejen vypíše, ale potom ještě rekurzivně zavolá sama sebe a předá si cestu k tomuto adresáři tak, že k názvu kořenového adresáře připojí jméno adresáře k prohledání, aby vznikla celá cesta. Dovedete si tedy představit, že ve finále si tímto postupem projedeme všechny složky a podsložky a jejich podsložky ve zvoleném adresáři.

Takže rekurzivní funkce jsou velmi výhodné, je-li třeba operovat například nad nějakými stromovými strukturami, na druhou stranu se ale dají použít i jinak než ke zkoumání nebo vytváření (např. kreslení) stromů.

Tady je příklad jednoduché rekurzivní fce:

int rekurze(int x, int y, int z) {
	if(x>z) return(rekurze(x-y, y, z));
	return(x);
}

Pokud tuto funkci zavoláme (např.: rekurze(10, 1, 0);), bude volat sama sebe a postupně odečítat od zadaného parametru x parametr y, dokud nebude platit, že x⇐z, pak vrátí x, v našem případě bude návratem hodnota 0.

Samostatná cvičení

Doteď platilo, že když jsme potřebovali pracovat s nějakým kusem paměti (proměnnou), jednoduše jsme ji nadeklarovali (nebo nadefinovali) a kompilátor se za nás postaral o to, aby se po spuštění programu nalézala v paměti a my s ní mohli pracovat.

Referencování

Základní operací kterou musíme znát je použití referenčního (& - Ampersand) a dereferenčního (* - Hvězdička) operátoru.

Pokud aplikujeme referenční operátor na některou z proměnných (nebo cokoli jiného, co je fyzicky uložené v paměti), dostaneme adresu v paměti, na které je tento objekt umístěn. Např. následující kód, který by normálně měl vytisknout hexadecimální (šestnáctkovou) interpretaci hodnoty proměnné cislo nám díky operátoru “&“ před identifikátorem proměnné vytiskne adresu v paměti (na které je samozřejmě uložen obsah naší proměnné).

int cislo = 2;
printf("0x%X\n", &cislo);

Nutno ještě podotknout, že bývá zvykem, že se šestnáctkové číslo (to, které vypíšeme pomocí “%x“ nebo “%X“) pro rozlišení od čísel v jiných číselných soustavách (desítkové, dvojkové, osmičkové) zapisuje s „0x“ na začátku, formátovací řetězec pro 'slušné' vypsání čísla v šestnáctkovém tvaru pomocí standartní funkce printf() (adresy v paměti a adresy vůbec nebývá zvykem udávat jinak než šestnáctkově) by tedy vypadal například takto: „0x%X“, s odřádkováním pak takto: „0x%X\n“.

A proč se operátoru “&“ říká „referenční“? Je to jednoduše proto, že nám vrací referenci (neboli odkaz) na proměnnou a sice v podobě její adresy.

Dereferencování

Ještě užitečnější možností, než je referencování je možnost tzv. dereference. Dereferencování nám umožňuje naopak pracovat s pamětí, pokud známe její adresu. Dereferencování probíhá pomocí operátoru „*“. To můžete vidět na následujícím příkladu:

int data, adresa;
 
data = 32; //Inicializace promenne
adresa = &data; //Do promenne adresa ulozime adresu promenne data (pomoci referencovani)
printf("0x%X\n", adresa); //Vytiskneme si adresu
printf("%d\n", *adresa); //Na promennou adresa pouzijeme dereferencni operator a vytiskneme tedy hodnotu promenne data
 
*adresa = 23; //Zapiseme 23 do pameti urcene adresou ulozenou v promenne adresa
printf("%d\n", data); //Tim, ze jsme zapisovali na adresu ziskanou referenci romenne data, jsme zmenili i hodnotu promene data
 
//Nyni lze tedy tvrdit, ze hodnoty data a *adresa jsou to same.
//Stejne jsou i hodnoty &data a adresa.

Pokud používáme proměnnou pouze na uložení jiné adresy a pomocí dereference přes ní přistupujeme k datům na této adrese, takové proměnné říkáme pointer. Toto označení není špatné ani v případě, že jím titulujeme přímo tuto adresu.

Dalším pěkným příkladem může být také toto:

int data;
*(&(*(&data))) = 32; //To je to same jako: data = 32;

Neoprávněný přístup do paměti

Neoprávněný přístup do paměti je zatím asi jediná chyba, která nás při programování potkala, pokud tedy mluvíme o chybách, které neodchytí kompilátor (nebo preprocesor), ale nastanou až za běhu programu. V podstatě jde o to, že pokud se program pokusí číst nebo zapisovat do paměti, kterou si nealokoval (tudíž je buď volná, nebo patří někomu jinému), systém ho zarazí a z bezpečnostních důvodů ukončí (takový přístup může signalizovat, že se někdo pokouší program nabourat, nebo sám o sobě způsobit jiné škody).

Jak si takový neoprávněný přístup nasimulovat?

int *a, b;
a = 31337; //Toto je vymyslena adresa, ktera pravdepodobne nepatri nasemu programu (teoreticky by mohla, ale pravdepodobnost je celkem miziva)
*a = 32; //Pokusime se zapsat na neplatnou adresu -> Pad programu
b = *a; //Pokusime se cist z neplatne adresy -> Taktez Segmentation fault (sem se uz program nedostane, protoze havaroval)

Správná deklarace pointerů

Všiměte si, že jsem v nadpisu použil slovo deklarace pointerů, nikoli definice. To proto, že adresa uchovávaná v pointeru by měla být zjištěna až za běhu programu. Pokud jí zapíšeme přímo do kódu, tak program buď spadne, nebo bude-li fungovat, tak jenom někde a někdy, podle toho, co na dané adrese je.

Chceme li tedy vytvořit pointer například na integer, uděláme to takto:

int *cislo, a;
cislo = &a; //inicializace

S tím jsme se již setkali v předchozích příkladech, ale já jsem to záměrně nechal až na konec. Proč je u pointeru nutné určit, na jaký datový typ bude ukazovat si povíme později. Pro nás je ale důleřité, že pointer nezabírá tolik místa, jako může být nutné pro uložení dat, která budou na cílové adrese. To si jednak musíme uvědomit při deklaraci a za druhé nám to může urychlit program tak, že nějaké funkci předáme pouze pointer (ta ho musí očekávat) a funkce tak pracuje skrze tento pointer (adresu) přímo nad našimi daty, aniž by se vytvářela jejich kopie v kontextu funkce. Této možnosti se říká předání odkazem a dalo by se to přirovnat k situaci ze skutečného života, kdy potřebujete vykonat nějakou stavební úpravu a můžete (s nadsázkou) buď odnést celý dům k zedníkovi, nebo můžete zedníkovi donést jen papírek s adresou tohoto domu (tedy pointer).

Dynamická alokace paměti

Než začneme s dynamickou alokací paměti, je ještě je dobré vědět, že je slušné, dobré, praktické, důležité a nevím jaké ještě to, že pokud máme pointer na neexistující objekt (v C znamená pojem objekt něco jiného, než v oběktových, nebo objektově orientovaných jazycích), tak bysme měli tento pointer nastavit na hodnotu NULL (my se spokojíme s přiřazením 0), takovému pointeru se pak říká nulový pointer, nebo pointer na NULL. Například takto:

int a, b;
a = &b;
*a = 32;
a = NULL; //Zrusime pointer

Funkce pro dynamické přidělování paměti

Abychom mohli využívat tyto funkce, musíme nainkludovat hlavičku stdlib.h (tedy: #include <stdlib.h>). Je pravděpodobné, že na některých kompilátorech (především těch z GNU rodiny) by se vám program zkompiloval i bez vložení tohoto hlavičkového souboru, protože jde o jeden z nejzákladnějších, tak je možné, že již je nainkludován, ale to není dobře, protože pokud se takový program pokusíte zkompilovat jinde, tak se vám to nepodaří, to také vyčtete z varovných hlášek, kterými vás gcc poučí, pokud soubor nenainkludujete. Jde především o funkce malloc() a free(). První z nich nám umožní za běhu programu alokovat potřebné množství paměti, ta druhá ji potom zase dokáže uvolnit (tedy navrátit operačnímu systému a umožnit tak její využití k jiným účelům).

Funkce malloc()

Tato funkce přijímá jediný parametr, kterým je počet bytů, které si má od OS vyžádat, potom vrátí pointer na alokované místo (jeho první byte). Pokud se z nějakého důvodu nepodaří tuto paměť získat, malloc() vrací nulový pointer. Když potřebujeme alokovat např. integer, musíme si zjistit, jakou velikost má int na našem kompilátoru. Již víme, že se to provádí pomocí operátoru sizeof(), tedy sizeof(int). Následující příklad ukazuje správnou alokaci a inicializaci dvou integerů, jeden staticky, druhý za běhu programu:

dynamic.c
#include <stdlib.h>
 
int main() {
	int a, *b;
	b = NULL; //Pro jistotu a ze zvyku priradime do b nulu (abychom se omylem nepokusili pracovat s cizi pameti)
 
	a = 2600;
 
	b = malloc(sizeof(int)); //Pokusime se alokovat pamet o velikosti integeru
	if(!b) { //Pokud se nam nepodari alokovat pamet
		printf("Chyba pri malloc()!\n"); //Vypiseme chyb. hlasku
		exit(1); //A skoncime s chybovym kodem 1
	}
 
	*b = 1337;
}

Možná je dobré vědět, že operačnímu systému se nemusí vždy hodit množství paměti, jaké vyžadujete a přidělí vám trochu víc (nebo samozřejmně nic, tedy NULL).

Funkce free()

Funkci předáme jako parametr pointer/adresu již předem alokované paměti, funkce se postará o to, aby byla tato paměť uvolněna a navrácena OS k dalšímu použití. Věřím, že na demonstraci postačí jednoduchý příklad:

int *a;
a = NULL;
a = malloc(sizeof(int));
if(!a) {
	printf("Chyba pri malloc()!\n");
	exit(1);
}
*a = 31337;
 
free(a); //Uvolnime pamet
a = NULL; //Pointer, ktery ukazuje na "cizi pudu" nastavime na NULL

Takže si musíme pamatovat, že takový pointer, který nám zůstal po uvolnění funkcí free() musíme co možná nejdříve nastavit na NULL. Nebývá žádnou zvláštností, když se tyto dvě neoddělitelné operace zapisují na jeden řádek (kompaktnější vzhled a logika kódu):

free(a); a = NULL;

Kam se starým smetím?

Může se nám také díky nějaké chybě v logice programu, nebo nějaké jiné nepozornosti stát, že budeme alokovat a alokovat paměť, ale už ji zapomeneme uvolňovat. A protože C (ani C++) nemá tzv. Garbage collector (Sběrač smetí), musíme uvolnit paměť, než pointer na ní zahodíme. Z následujícího příkladu by to mělo být jasné:

int *data;
data = malloc(sizeof(int));
data = malloc(sizeof(int)); //Alokujeme podruhe, tim prijdeme o adresu prvniho alokovaneho prostoru, a uz nikdy se nam ho nepodari uvolnit

Většinou se samozřejmě paměť uvolní při ukončení programu (OS by měl uvolnit vše, co po programu v paměti zbylo), ale určitě nelze 100% věřit tomu, že například M$ Windows někde sem tam něco nezapomenou. Zkuste si například tento kód:

while(1) malloc(1024);

Otevřete si správce procesů (htop, top, tasklist, taskmgr) a zjistěte, kolik paměti náš proces zabírá (virtuální, fyzické i celkové). Pošlete procesu signál pro ukončení (nebo ukočete program, jak jste na vašem OS zvyklí) a sledujte, jak se paměť pomalu uvolňuje.

Opakovací ukázka pointerů a dynamické alokace paměti

hadanka.c
#include <stdio.h>
#include <stdlib.h>
 
int main() {
 
        int prvni; //Staticky (za prekladu) alokovana promenna typu int
 
        int *druha; //Staticky alokovany pointer na promennou typu int
        druha = malloc(sizeof(int)); //Dynamicky (za behu) alokovana promenna typu int
        if(!druha) {
                printf("Chyba!\nNepodarilo se alokovat promennou *druha\n");
                exit(1);
        }
 
        prvni = 11;
        prvni = 22;
        prvni = *(&prvni)+1;
        //druha = 22; //Ukazkova chyba -> SIGSEGV
        *druha = 22;
 
        //Hodnoty promennych
        printf("Hodnoty promennych:\n");
        printf("prvni == %d\n", prvni);
        printf("druha == %d\n\n", *druha);
 
        //Vypis adres, na kterych jsou promenne ulozeny
        //Desitkove a pak hexadecimalne (s prefixem 0x)
        printf("Adresy v promennych pameti:\n\n");
        printf("prvni je na %d = 0x%X\n", &prvni, &prvni);
        printf("druha je na %d = 0x%X\n\n", druha, druha);
 
        free(druha); //Uvolnime alokovanou pamet
 
        exit(0); //Na konci programu se samozrejme teoreticky uvolni vse
 
}

Dynamická pole

V této kapitole si představíme způsob, kterým můžeme pracovat s polem alokovaným až za běhu programu.

Alokace

Na následujícím příkladu si ukážeme, jak je to doopravdy s definicí (nebo deklarací) polí.

unsigned char static[10];
unsigned char *dynamic;

V obou případech vytvoříme pointer na char (případně pole), jediný rozdíl je v tom, že static je konstanta, kterou nelze měnit a obsahuje pointer na již alokované místo v paměti (to se alokovalo při definici), ale dynamic neobsahuje nic (resp. není inicializovaný) a nealokuje žádné místo (alokuje pouze místo pro sebe).

Takže když chceme dynamic začít používat stejně jako static, stačí nám použít tento zápis:

unsigned char static[10];
unsigned char *dynamic; //Deklarace pointeru na char (případně pole charů)
 
dynamic = malloc(10); //Alokujeme 10 bytů a pointer na ně přiřadíme do pole
if(!dynamic) {
	printf("Nelze alokovat paměť!\n");
	exit(1);
}

Nyní můžeme k oběma polím přistupovat následujícím způsobem:

static[3] = 'a';
dynamic[3] = 'a';
 
*(static + 3 * sizeof(char)) = 'a';
*(dynamic + 3 * sizeof(char)) = 'a';

Jak vidíte, k oběma polím (jak statickému tak dynamickému) můžeme přistupovat naprosto totožným způsobem, tedy pomocí operátoru [], který (jak jsme nyní zjistili) také zahrnuje dereferenci, nebo pomocí čistě dereferenčního operátoru a pointerové aritmetiky.

Pokročilé postupy

Pokud potřebujeme pole o proměnné velikosti, tak jediným možným způsobem zůstává vždy alokovat nové pole (o jiné velikosti) a stará data do něj v cyklu přetáhnout. Je samozřejmě vhodné staré pole uvolnit (pomocí free()).

Přetečení bufferu

Dobré je také vědět, že pokud načítáme jakákoli data od uživatele (např. z argumentů, klávesnice, sítě nebo souboru) a chceme je uložit do pole (např. řetězce), musíme pohlídat, aby data nepřetekla nad velikost pole, v takovém případě by mohlo dojít k pádu programu pokud by programu byl předán příliš dlouhý řetězec, nebo dokonce zápisu nebezpečného kódu na významná místa v paměti, pokud by se někdo pokusil program napadnout záměrně. Je tedy nutné si hlídat, jestli nepřekračujeme maximální offsety pole.

Samostatná cvičení

Spouštění externích programů

Tato kapitola by měla nastínit, jakým způsobem je možné zavolat externí program (příkaz), stejně, jako byste ho třeba napsali do příkazové řádky.

Funkce system()

Funkce system() dokáže spustit příkaz a to dokonce s využitím operátorl příkazového interpretu (např.: <,>,»,|), ten jí předáme jako obyčejný řetězec, tedy například system(„ls“);, s tím, že výstup tohoto příkazu bude vypsán na obrazovku.

Samozřejmě si musíme uvědomit, že ve chvíli, kdy použijeme podobné volání externího programu, se náš (relativně přenositelný) program stává závislý na existenci daného příkazu na cílové platformě. Tedy například pokud budeme chtít uživateli zobrazit seznam souborů voláním příkazu „ls“, program nám bude bezproblémově fungovat na většině UNIXových systémů naopak na Windows bychom museli tento příkaz změnit na „dir“.

V případě, že je nezbytně nutné volat externí programy, je dobré na začátku programu nadefinovat symbolické konstanty s cestou k tomuto programu, nebo příkazy například načítat z nějakého konfiguračního souboru, který se bude pro každou platformu lišit, také je možné aby náš program nějakým způsobem automaticky ověřoval, jestli program existuje, nebo třeba od někud zjistil, na jaké běží platformě a podle toho se zařídil.

Další funkce pro práci s externími programy

Existuje spousta dalších funkcí, které vám umožní pokročilejší práci se spustitelnými soubory, jako je například nahrazení našeho programu v paměti jiným, spuštění na pozadí, otevření procesu jako souboru a čtení a zápis dat na jeho I/O.

Práce s proměnnými prostředí

Proměnné prostředí jsou proměnné, které nám poskytuje program, který náš program spouští (většinou příkazový řádek, nebo grafické prostředí). Tyto proměnné jsou nahrány do paměti našeho programu spolu s jeho kódem.

Typickou proměnnou prostředí je „PATH“, proměnná, ve které jsou uložené cesty k adresářům, ve kterých se hledá program, který se pokoušíme spustit, pokud není nalezen v aktuálním adresáři. Pokud vás zajímá, jaké další proměnné váš OS běžně poskytuje, zkuste si příkazy export na UNIXech, nebo set na Windows, pomocí těchto příkazů lze také nastavit nové proměnné, nebo změnit stávající. Existuje funkce getenv(), která nám vrátí pointer na danou proměnnou. Následující příklad ukazuje, jak vytisknout námi zvolenou proměnnou prostředí:

getenv-path.c
#include <stdio.h>
#include <stdlib.h>
 
int main ()
{
  char *p;
  p = getenv( "PATH" );
  if (p != NULL) printf ("PATH: %s", p);
}

Větvení procesu, paralelní zpracování programu

Funkce fork()

Pokud potřebujeme v našem programu opustit lineární řízení, jinak řečeno - dělat dvě (a více) věcí najednou. Můžeme pomocí systémového volání fork() vytvořit dokonalou kopii procesu našeho programu, která se bude lišit pouze tím, že návratová hodnota fork() v ní bude 0 a samozřejmě bude mít jiné PID (identifikační číslo procesu). Volání fork() (neboli větvení) je výsadou pouze UNIXových (a samozřejmě UNIX-Like) systémů a na MS Windows bychom podobnou věc museli řešit jinak a daleko složitěji.

Abych vás nenapínal, tady je první příklad:

int main() {
	if(fork()) return;
	/* Tento kod se bude jiz provadet na pozadi... */
	sleep(10);
	printf("BAF!\n");
}

V našem případě jsem zavolali fork() hned na začátku programu, tím se vytvořili kopii našeho procesu, v originále se návratová hodnota volání fork() rovnala 1, to jsme ověřili podmínkou, takže v prvním procesu se nám vykonal return() a tím pádem se první proces zavřel. Máme tedy nový proces, který ovšem není tak pevně svázán s naší příkazovou řádkou a ta se tedy uvolní (a budeme moci normálně dále pracovat a spouštět další programy). Protože se nám ale v procesu nakopírovalo číslo file deskriptoru (ukazatele na soubor), pomocí kterého se dá vypisovat do konzole (na STDOUT), může náš tak trochu záškodnický program na pozadí za 10 sekund vypsat nic netušícímu uživateli „BAF!“ a to klidně doprostřed výpisu jiného programu (podobně, jako se vám třeba v BASHi můžou vypisovat oznámení o nových mailech).

Spuštění programu na pozadí je ale pouze začátkem toho, co fork() doopravdy umí. Podívejme se na následující kód.:

	/* nejaky kod */
	if(!fork()) {
		udelej_neco();
		return;
	}
	/* dalsi kod */

V tomto případě se zavolá na pozadí pouze funkce udelej_neco() a program bude ihned pokračovat dále. Typickým příkladem může být stahování nějakého velkého souboru (nebo více souborů) z webového serveru (pro zjednodušení jsem použil externí program wget volaný pomocí system()):

fork.c
#include <stdio.h>
 
int main() {
	if(!fork()) { system("wget http://server/soubor1"); return; }
	if(!fork()) { system("wget http://server/soubor2"); return; }
	if(!fork()) { system("wget http://server/soubor3"); return; }
	if(!fork()) { system("wget http://server/soubor4"); return; }
	return;
}

Tím docílíme toho, že se budou všechny 4 soubory stahovat najednou a ne jeden podruhém, jako kdybychom pouze 4x pod sebou zavolali system(). Díky tomu, že jsem do podmínky přidal vikřičník jsem si zajistil to, že se soubor bude stahovat v kopii a nikoli v originálním procesu. Původnímu procesu se říká parrent (rodič) a kopie se označuje jako child (dítě) i když v případě fork() jsou oba procesy spíše bratry či sestřičkami. Důležité je také to, že po stažení souboru zavoláme return(), protože kdybychom to neudělali, tak by se každý soubor mohl stáhnout vícekrát, protože se náš program v paměti zkopíroval i s tím, co má dělat potom.

Stinnou stránkou celé věci je to, že jednotlivé procesy se nemohou vzájemně příliš dorozumívat, musíme pak přistoupit k použití prostředků jako jsou soubory, pojmenované pajpy (fifo roury), unix domain sockety a nebo dokonce síťové sockety.

Další věcí je, že náš program bude velmi těžkopádný, pokud budeme používat fork() ve větším měřítku, například pokud bychom psali webový (nebo jiný) server, tak musíme obsluhovat více uživatelů najednou. Pokud bychom ale na každého uživatele vytvořili jednu kopii procesu, může se nám stát, že nám brzo začnou docházet systémové prostředky (mluvíme o serverech s ~100 requesty najednou), pokud nám nedojdou hardwarové prostředky, tak se nám zaplní process table (vyčerpáme maximální počet procesů povolený operačním systémem), proto je lepší počet 'forků' omezit a ještě lépe používat thready (viz. dále).

Pro zájemce: Existuje lokální DoS útok pojmenovaný ForkBomb, který způsobí zaplnění tabulky procesů a vede neodvratně k zamrznutí systému, protože nelze vytvořit žádný nový proces, nemůžeme ani spustit program, který by útok ukončil. Uvádím to proto, že k podobné věci může dojít pokud někde voláme fork() v cyklu (např. potřebujeme n podprocesů) a omylem vytvoříme nekonečnou smyčku.

Např. takto:

while(1) fork();

Na UNIXových systémech se dá proti nečekané chybě bránit tím, že nastavíme limit počtu procesů pro každého uživatele, v případě podobného výmrzu můžeme jako jiný uživatel (typicky root) procesy pozabíjet. Na Windows proti této chybě takřka není ochrana. A ačkoli v jádře systému Windows nic jako fork() neexistuje, může nastat podobný problém, pokud se nám omylem podaří to, že jeden program neustále spouští sám sebe.

Thready

Další možností jak může náš program dělat více věcí najednou jsou takzvané thready (vlákna) ty se od forku liší v tom, že nejde o kopii procesu, ale jeho součást. Thread je tím pádem jakýsi podřadný proces. Narozdíl od forku existují thready i na Windows. Další výhodou je, že thready mohou dále komunikovat se zbytkem procesu, například pomocí globálních proměnných (například proměnné deklarované mimo funkci main()), potom je ale třeba hlídat, aby se dva thready nepokusily zapisovat do stejné proměnné (== stejného místa v paměti), potom by mohlo dojít k poškození těchto společných dat. K tomu se používá tzv. synchronizace threadů. Práce s thready už je ale složitější a proto si ji zde nebudeme ukazovat…

Samostatná cvičení

./ramecek * "Ahoj lidi," jak\ se máte?
***************************
* Ahoj lidi, jak se máte? *
***************************

Práce se soubory

Doteď jsme naše data ukládali jen do paměti RAM. O všechna data uložená v paměti RAM přijdeme v okamžiku ukončení programu, nebo vypnutí počítače. To nemusí bý vždy postačující a můžeme chtít svoje data uložit nějakým trvanlivějším způsobem, ideálním řešením bude jejich zapsání na pevný disk, konkrétně do souboru.

Klasickým příkladem práce se soubory je jednoduchý tetový editor (nano, gedit, SciTE, notepad, atd…). Takový editor při svém spuštění načte zvolený soubor do paměti, kde s ním může uživatel pracovat a před svým ukončením tato data z paměti uloží i s úpravami zpět do souboru na příslušném paměťovém médiu.

Ovšem si pamatujte, že problematika základní práce se soubory je mnohem komplikovanější, než jak je zde popsána a pokud máte zájem, doporučuji si projít odkazy na konci této publikace.

Otevření souboru

Abychom mohli pracovat se souborem, musíme ho nejdříve otevřít. Se soubory se dá pracovat například pomocí tzv. file deskriptorů, neo také ukazatelů (je to podobné, jako nám již známé pointery, s tím rozdílem, že neukazují na disk, ale obsahují číslo, které operační systém tomuto souboru přiřadil a podle kterého ho dále rozlišuje). Funkce na práci se soubory opět zpřístupníme vložením hlavičkového souboru <stdio.h>.

Jak tedy takové otevření a čtení ze souboru funguje, to pochopíte z následujícího kusu kódu:

FILE *fp; //Nadeklarujeme deskriptor fp
fp = fopen("soubor.txt", "r"); //Otevíráme "soubor.txt" v režimu "r", tedy "pro čtení"
if(fp == NULL) { //Osetrime chyby (napr. soubor neexistuje, nebo nemame prava pro cteni)
	printf("Nepodarilo se otevrit soubor!\n");
	exit(1);
}
 
char retezec[101];
fgets(retezec , 100, fp); //Nacteme 100 bytů (znaků) z fp do retezce retezec
puts(retezec); //Vytiskneme tento retezec na obrazovku
 
fclose(fp); //Zavreme soubor

A tady je například správný postup, jak vypsat celý soubor znak po znaku na obrazovku, všimněte si, že každý znak po celou dobu porovnávám s konstantou EOF, v případě, že jí odpovídá, znamená to, že je soubor na konci (EOF znamená End Of File):

int c;
while((c = fgetc(fp)) != EOF) putc(c);

Režimy fopen()

Když pomocí fopen() otevíráme soubor, můžeme ho otevřít celkem asi v 9ti režimech. Toto jsou 3 základní:

Ke každému režimu můžeme ještě připojit „b“, tedy: „rb“, „wb“, „ab“, to znamená, že se soubory bude zacházeno čistě po binární stránce, tedy že to, co do souboru zapíšete v něm určitě bude a naopak, že přečtete to, co v něm skutečně je. Bez písmenka b dochází k automatické konverzi tzv. line-end kódování, já nechci zabíhat příliš do podrobností, ale v zásadě jde o to, že každá skupina operačních systémů (UNIX + UNIX-like, Windows a staré MAC OSy) ukládají znak odřádkování (Enteru) jinak. Na linuxu je to jen LF, na Windows CR+LF a na starších MAC OSech je to LF+CR, pokud ale v C pracujeme se soubory v textovém režimu (bez „b“), tak s těmito odlišnostmi můžeme pracovat naprosto transparentně (tj. Enter je pokaždé „\n“ nezávisle na systému). Pokud bychom ale například pracovali s nějakým binárním (např. spustitelný soubor, hudba, video), mohlo by takřka náhodné změnění některých bytů na jiné způsobit poměrně velké potíže, proto vždy pracujeme s binárními soubory jinak. U binárních souborů je také špatně, pokud je byte po bytu načítáme do charu, protože ačkoli char je datový typ o velikosti 1B, stejně nemohou být všechny hodnoty korektně interpretovány, proto k načítání bytů z bin. soubori použijeme integer.

Funkce na čtení ze souboru ("r")

Funkce na zápis do souboru ("w", "a")

Předotevřené soubory

Je dobré vědět, že v C jsou otevřené následující soubory, se kterými pracují funkce jako printf() a pod, když tisknou na obrazovku, nebo načítají z klávesnice, vy toho můžete využít tak, že je zpracujete pomocí funkcí na práci s normálními soubory, nebo je třeba zavřete pro potlačení výstupu. Pak je můžete dokonce opět otevřít a způsobit tak, že všechen výstup vašeho programu, který se normálně provádí na obrazovku bude místo toho uložen do vámi zvoleného souboru. Na druhou stranu nesmíme zapomenout na to, že už nelze zaručit, že znovu půjde otevřít tyto soubory do původního stavu, aby například opět vypisovali na obrazovku. Např. v Linuxu to možné je, v ostatních UNIX-like systémech asi také, ale s jinými takové zkušenost nemám. Na Windows to pravděpodobně možné nebude.

Samostatná cvičení

Napojení na webové aplikace (CGI a PHP)

V někerých případech se může stát, že budete potřebovat, aby váš program napsaný v C spolupracoval s vaší webovou aplikací, dnes si představíme dva nejjednodušší způsoby, jak toho docílit. První se bude zabívat CGI rozhraním (Common Gateway Interface) a druhý nám ukáže, jak napojit program na webovou aplikaci pomocí wrapperu napsaného v PHP.

Common Gateway Interface (CGI)

CGI je rozhraní, které nám umožní, aby webserver předával požadavek přímo našemu programu. Jaké jsou výhody a nevýhody C proti jiným webovým server-side jazykům (jako PHP, JSP, nebo ASP) je celkem průhledné. C je sice rychlejší, ale musí být zkompilováno pro danou platformu. Nemůžete tedy jednoduše vzít vaší aplikaci, kterou jste do teď provozovali na Linuxu (na procesoru řady x86) a všechny soubory jednoduše uploadnout na HPBSD server (s procesorem PA-RISC), ale budete muset všechny binární soubory ze zdrojových kódů překompilovat pro tuto platformu.

Když napíšete a zkompilujete CGI aplikaci, potřebujete ještě webserver, který CGI podporuje a musíte ho nastavit podle instrukcí dle konkrétního serveru, potom nahrajete spustitelné soubory do příslušného adresáře. Nastavení serveru tady rozebírat nebudu.

Jaký je tedy rozdíl mezi normálním programem a CGI programem? Jde o obyčejný textový program pro příkazový řádek, s tím rozdílem, že výstup (například text vypsaný pomocí printf()) se nevypisuje na obrazovku, ale do obsahu vrácené webové stránky, nejdříve, ale musíme poslat alespoň minimální HTTP hlavičku (pro více informací si přečtěte něco o HTTP protokolu), pokud tedy chceme například vygenerovat HTML dokument s nadpisem první úrovně, kód bude vypadat takto:

cgi-hello.c
#include <stdio.h>
 
int main() {
	printf("Content-type: text/html\n"); //HTTP hlavicka udavajici typ obsahu
	printf("\n"); //Po posledni hlavicce se posila prazdny radek.
	//Nyni uz muzeme zahajit vystup:
	printf("<h1>Hello world!</h1>\n");
}

Ještě bychom měli vědět, jak se dostaneme k datům předávaným metodami GET nebo POST. Oba dva typy parametrů jsou předávány jako řetězec ve formátu, v jakém je server dostane od klienta, většinou takový řetězec vypadá například takto (běžný URL query string):

prvnipromena=224&druha=Zapis%20data&treti&ctvrta=posledni

Pokud jsou data předávána v GETu, nahdete je v proměnné prostředí pojmenované „QUERY_STRING“, pokud tedy například chceme získat a vytisknout celý GET požadavek, uděláme to pomocí funkce getenv() zhruba takto:

cgi-get.c
#include <stdio.h>
#include <stdlib.h>
 
int main () {
	char *get;
	get = getenv("QUERY_STRING"); //Získáme pointer na QUERY_STRING
 
	printf("Content-type: text/html\n\n");
	if (p != NULL) printf("<h1>CGI - GET</h1>\n<textarea>%s</textarea>", get); //Vytiskneme QUERY_STRING
	return 0;
}

Pokud ale chceme získat data z POSTu, budou v přesně stejném tvaru, jako v případě GETu, s tím rozdílem, že budou čekat na standartním vstupu, jinými slovy to znamená, že je můžeme načítat například pomocí funkcí scanf(), nebo getc(), to tady dále rozebírat nebudu, protože to je poměrně základní znalost.

Wrapper v PHP

PHP (Portable Hypertext Preprocesor) je interpretovaný programovací jazyk (podobně, jako PERL, nebo Python), který se dá provozovat například i na webovém serveru. Pro nás je důležité, že obsahuje některé funkce, pomocí kterých se lze odvolávat i na binární spustitelné soubory, požadavky na spustitelný soubor jsou víceméně stejné, jako u CGI, ale s tím rozdílem, že tento soubor nemusí být primárně určen jako webová aplikace, nebo se nacházet ve speciálním adresáři, úplně postačí, když bude mít dobře nastavená práva pro spouštění.

Jak toto propojení v PHP vytvoříme? Poslouží nám k tomu funkce jako system(), nebo popen() (které se mimochodem chovají stejně, jako jejich jmenovci v Ansi C), shell_exec() a nebo proc_open(). Z následující tabulky pochopíte, co jednotlivé funkce umožňují:

Jednoduchý PHP skript, který nám spustí náš smyšlený program napsaný v C, který může obsluhovat smyšlený teploměr by vypadal například takto:

c.php
<h1>Teploty</h1>
<?php
	echo("<h2>Vevnitř</h2>\n");
	echo(htmlspecialchars(shell_exec('./thermo -in'))."\n");
	echo("<h2>Venku</h2>\n");
	echo(htmlspecialchars(shell_exec('./thermo -out'))."\n");
?>

Všiměte si, že jsem použil ještě funkci PHP nazvanou htmlspecialchars(), která zajistí, že například špičaté závorky budou převedeny na příslušné HTML entity, místo toho, aby způsobily nekorektní zobrazení dokumentu. Sám podobný skript používám, abych si mohl přes webové rozhraní vypisovat aktuální informace o serveru (teplota procesoru, disků, volné místo na disku, přihlášení uživatelé, spuštěné procesy, atd…). To je tedy vše, co si o spolupráci programů napsaných v C s vebovými aplikacemi můžeme říct, aniž bychom přesáhli rámec naší hodiny.

Jiné systémy

Podobně, jako z PHP voláme program napsaný v C, můžeme z C také volat jiný program, nebo můžeme například pomocí socketů vytvořit spojení například s naslouchajícím serverem. Propojení dvou programů může také probíhat pomocí roury, nebo pojmenované FIFO (first in, first out) roury.

Freestyle Coding Contest

Pozn.: Tato lekce může být použita suplujícím v případě absence vyučujícího, je ale nutné slevit z nároků a říct studentům, aby aplikace upravili tak, aby nemuseli používat znalosti, které ještě nemají, nebo si vymysleli jednodušší zadání, jehož správnost (a především správnost vypracování programu) bude ověřena při první možné příležitosti vyučujícím lektorem.

V poslední lekci jsem si pro vás připravil zadání různých malých (a do jisté míry užitečných) prográmků, které byste se svými znalostmi měli být schopni vytvořit. Protože ale nemusí být úplně průhledné, jak takový program napsat (ačkoli teoretické znalosti byste na to mít měli), v případě nejasností požádejte profesora o pomocnou berličku v místech, která vám nejsou úplně jasná (samozřejmě až po chvilce namáhání hlavy ;). Pokud nemáte žádný vlastní nápad, můžete si vybrat jeden (nebo několik) z následujících projektů:

Kalkulátor

Pokuste se napsat co nejkomplexnější a nejpřívětivější program na provádění různých druhů výpočtů. Program můžete například doplit i o pokročilé funkce, jako vykreslení grafu (pomocí písmenek) průběhu různých funkcí při různých vstupních hodnotách (stačí například sin()), nebo libovolnou jinou funkci dle zájmu (výpočet obvodu kruhu, nebo cokoli, co ve své kalkulačce chcete mít).

Interpreter jednoduchého jazyka

Vymyslete si jednoduchý interpretovaný programovací jazyk a napište pro něj interpreter. Stačí například 3-5 příkazů, pro zjednodušení může mít každý příkaz délku 1 znak, nebo řádek. V jednodušší obdobě nemusí jazyk být schopný dělat nic smysluplného, například pokud první písmenko řádku určuje akci a zbytek jsou data, která se mají zpracovat a „P“ znamená vytisknout a „M“ vynásobit dvěmi a vytisknout, může náledující program:

PTento text bude vypsan
CToto je komentar
M23 //toto cislo bude vynasobeno dvěmi a vypsáno

Po zpracování vaším interpreterem například vytvořit následující výpis:

Tento text bude vypsan
46

Co bude jazyk dělat je čistě na vás, může také například do dvojrozměrného pole kreslit čáru (L,P,N,D,T = vlevo, vpravo, nahoru, dolu, tisk) a když narazí na příkaz pro tisk, tak toto „plátno“ vytiskne (vypíše).

Tedy program DDDPPPNNLLDT vypíše například toto:

#
####
## #
####

Můžete si samozřejmě doplnit další příkazy (změna písmenka/barvy), nebo napsat něco úplně jiného.

Shell

Napište jednoduchý příkazový řádek (podobný například BASHi, nebo CMD) implementujte do něj funkci pro spouštění zadaných příkazů. Na internetu zjistěte, jakým způsobem lze zjistit aktuální adresář a vypisujte ho jako součást výzvy (na začátek řádky). Můžete implementovat i další virtuální příkazy na mazání, přesouvání a kopírování souborů, apod… Záleží opět jen na vaší fantazii.

Mnohojazyčná aplikace

Napište program, který obsahuje několik jednoduchých textových menu a libovolnou funkčnost (můžete se inspirovat jinými příklady). Veškeré texty v programu se budou načítat ze souboru specifikovaného v jiném (konfiguračním souboru). Vytvořte najméně dva datové soubory s lokalizacemi programu do vašeho mateřského jazyka a libovolného jiného. Soubor s jazykem nastavený v konfiguračním souboru by měl jít dočasně změnit pomocí argumentu programu.

Engine textové hry

Napište engine a alespoň jeden datový soubor pro textovou hru (elektronický gamebook), uživatel bude o pohybu mezi místnostmi této adventury (načtené ze souboru, jehož jméno zadá v parametru) rozhodovat zadáním čísla místnosti, číslo místnosti může odpovídat jedné konkrétní řádce v textovém souboru. Do enginu můžete dodělat možnosti soubojů (pomocí náhodných čísel), nebo kontrolu, jestli hráč dostal skutečně na výběr číslo místnosti, které zadal.

Primitivní textový editor

Program pro CLI (příkazový řádek) přijme jako argumenty režim (R - přečíst (vypsat na obrazovku), W - přečíst a zapsat, nebo A - přečíst a přídat) a jméno souboru, bude ho vypisovat po 15 řádkách (pokaždé počká na enter), až se dostane na konec souboru, tak pokud je pomocí prvního argumentu aktivován režim, který umožňuje zápis do souboru, vypíše řetězec „\n—– NEJAKY TEXT —–\n“ (NEJAKY TEXT budou informace o souboru) a vše, co od té chvíle uživatel napíše na klávesnici zaznamená do souboru (přepíše celý soubor). Uživatel svůj vstup ukončí pomocí programátorem zvolené klávesy stlačené současně s Control (CTRL), nesmí ale jít o kombinace CTRL+C nebo CTRL+D. Pokud uživatel zadá volbu R, program místo toho rovnou skončí.

Pokud program argument s názvem souboru neobdrží, automaticky vypíše seznam souborů (pomocí volání system() a příslušného programu - tj. ls, dir,…) a na jméno souboru se dotáže. Pokud soubor neexistuje, nabídne jeho vytvoření. Pokud je zadán první argument, tento soubor zpracuje v daném režimu, v jiném případě se uživatele taktéž zeptá.

Ascii Grafika

Tento program by měl dokázat vykreslovat (alespoň) 3 libovolné různé obrazce, prvním parametrem je určen tvar, druhým znak (barva) a dalšími potřebné parametry (rozměry). Způsob, jakým bude tvar vykreslován a jakým bude prováděn výpočet nechám na vás, důležité je ale, aby se tvar zobrazoval korektně nezávisle na zadaném rozměru. Výstup (u těch opravdu nejjednodušších obrazců) může vypadat například takto:

Čtverec (c):
./grafika c # 4 4
####
#  #
#  #
####

Kruh (k):
./grafika k o 3
 o  o
o    o
 o  o

Trojúhelník (t):
./grafika t A 5
  A
 A A
AAAAA

Další nápady

Můžete samozřejmě vymyslet a zrealizovat svůj ještě zajímavější nápad, nebo libovolná zadání libovolně modafakovat.

Nejlepší programátor (čistota kódu*nápaditost) bude odměněn diplomem „Fr335ty13 C0din9 C0nt35+ - Zlatý byte“ (nebo taky ne ;)…

Vysvětlivky

Akronym - Zkratka vzniklá spojením počátečních písmen slov ve slovním spojení, nebo větě.
BFU - Běžný Franta Uživatel - slangové ozn. pro uživatele bez hlubších znalostí, pravděpodobně to jednou bude váš typický uživatel. Aplikace je nutno přizpůsobovat těmto uživatelům.
CLI - Command Line Interface - Akronym pro příkazový řádek.
IDE - Integrated developing environment - Integrované vývojové prostředí - Zkládá se především z editoru a kompilátoru (případně může obsahovat debugger nebo GUIBuilder).
UI - User Interface - Uživatelské rozhraní - rozhraní, pomocí kterého uživatel komunikuje s aplikací.
GUI - Graphical User Interface - Specifický druh UI využívající grafiku (většinou tzv. okenní aplikace).
C - Programovací jazyk.
Ansi C - Americká norma popisující konvence, které musí dodržet kompilátor. Používá se zároveň i jako označení současné verze jazyka C.
C++ - Programovací jazyk vycházející z jazyka C. Byl rozšířen především o podporu objektů.
Decimální - Desítkový
HexaDecimální - Šestnáctkový
Binární - Dvojkový
Oktální - Osmičkový
Preprocesor kompilátoru - Program, který připravuje zdrojový kód pro kompilátor (maže nepotřebné mezery, komentáře, atd...).
Kompilátor - Program, který převádí zdrojový kód na binární objektový kód.
Assembler - Jazyk symbolických adres. Programovací jazyk využívající přímo instrukce procesoru (jen zapsané čitelnější formou).
Linker - Program vytvářející spustitelný soubor z objektového kódu vytvořeného při kompilaci.

Prameny

Tady naleznete seznam zdrojů, které sem (kromě svých vlastních znalostí) použil k napsání těchto skript a které vám důrazně doporučuji si pročíst, nebo je prohledávat při nejasnostech ohledně Ansi C, nebo C jako takového.

Povinná četba každého Céčkaře

Knihovní funkce Ansi C

Všeobecné informace

Všechno ostatní * google.com