Předmluva
Obsáhlé monografie věnované programovacímu jazyku C si kladou za cíl pouze popsat syntaxi jazyka, nebo naučit čtenáře programovat v tomto jazyku. Tato útlá skripta si kladou za ambiciózní cíl obě tyto věci.
Tyto skripta jsem našel válet někde na internetu, převedená z elektronické verze do PDF. Napsali je původně Stanislav Kračmar a Jiří Vogel a vyšly v knižní podobě v ČVUT. Protože byla hodně stará, tak jsem je upravil, aby byla použitelná na moderních překladačích jazyka C.
| Poznámka k "elektronické" podobě skript. Skripta jsem přepsal do Asciidocu, aby byla publikovatelná na webu. |
Úvod do jazyka C
V této kapitole se seznámíme se základními pojmy programovacího jazyka C. Většinu těchto pojmů budeme ilustrovat na jednoduchých programech (nebo částech programů) zapsaných v tomto jazyce.
Téměř každá učebnice jazyka C začíná programem, který je analogií následujícího programu.
První programy
Začneme jednoduchým programem, který na monitoru počítače zobrazí text Muj prvni program.
#include <stdio.h>
int main( void )
{
printf("Muj prvni program.");
return 0;
}
V programovacím jazyce C (narozdíl od mnoha jiných programovacích jazyků např. FORTRAN, COBOL, BASIC) jsou rozlišována velká a malá písmena.
|
Jména |
Program v jazyce C je tvořen posloupností definic funkcí a definic tzv. globálních objektů (o těch se zmíníme později, viz Globální a lolální proměnné).
Každá funkce je identifikována svým jménem (identifikátorem). Definici nové funkce lze vytvářet voláním jiných funkcí. Některé funkce nemusíme sami programovat, ale máme je k dispozici v knihovnách.
Součástí ANSI normy jazyka C je i specifikace standardních knihoven, těmi se budeme zabývat v poslední kapitole těchto skript. Tzv. podprogramy (vlastní procedury) v jazyce C jsou považovány za zvláštní funkce, které nevracejí žádnou hodnotu jako výsledek; přesněji vrací hodnotu prázdného typu void.
Právě jedna z funkcí programu se jmenuje main. Po spuštění programu se začíná plnit první instrukce této funkce. Funkce main je tedy obdoba tzv. hlavního programu známého z jiných programovacích jazyků.
Uvedený program se skládá z definice právě jedné funkce (která se tudíž musí jmenovat main), v programu není definován žádný globální objekt.
Definice každé funkce je tvořena hlavičkou funkce a tělem funkce. Hlavička funkce main má tvar main(), nemá tedy žádné parametry a nemá ani explicitně určen typ výsledku.
Jak uvidíme později, implicitně se doplní typ hodnoty funkce na celá čísla. [1]
Tělo funkce tvoří blok { printf("Muj prvni program.");} , který obsahuje vyvolání jedné knihovní funkce printf, což je standardní knihovní funkce jazyka C pro formátovaný výstup.
Jeden nebo více jejích parametrů se zapisují do okrouhlých závorek ().
Funkce printf opíše na standardní výstup (např. na obrazovku) řetězec znaků tvořící v našem případě jediný argument funkce.
Řetězec (řetězcový literál) je posloupnost znaků uzavřená mezi uvozovky: "Muj prvni program.".
Tiskne se ovšem řetězec bez uvozovek, tedy
Muj prvni program.
Řádky programu začínající znakem # obsahují příkazy (direktivy) pro tzv. předpřekladač (preprocesor).
Předpřekladač je součást překladače, která upravuje zdrojový text, vynechává např. komentáře, zajišťuje vložení hlavičkových souborů, rozvoj maker atd. Výsledkem práce
předpřekladače je opět textový soubor. První řádek našeho programu, příkaz #include <stdio.h>
zajistí, aby na toto místo v programu byl vložen (textový) hlavičkový soubor stdio.h (standardní input/output) obsahující popis standardních funkcí pro vstup a výstup, tedy mj. i popis funkce printf.
Dalším příkazem, který budeme od začátku často využívat, je direktiva #define. Např.
#define MAX 5
zajistí, že od místa výskytu tohoto příkazu až do konce programu se každý identifikátor MAX nahradí konstantou 5. Takto zavedená hodnota MAX se nazývá symbolická konstanta. Systematicky se příkazy pro předpřekladač budeme zabývat v kapitole .
Pro úpravu programu platí v jazyce C jen málo omezení. Pravidla zápisu programu např. v jazyce FORTRAN nebo Python určují striktně formu programu. Volnost zápisu v jazyce C může být využita více nebo méně šťastným způsobem. Následující dvě verze programu č.1 jsou z hlediska překladače zcela v pořádku a dosahují stejného výsledku jako původní program, nejsou však příliš přehledné. Při zápisu dalších programů budeme proto uplatňovat jisté stylistické konvence, které většina programátorů v jazyce C dodržuje.
#include <stdio.h>
int main(void){printf("Muj prvni program.");return 0;}
#include <stdio.h>
int main
( void
)
{
printf("Muj prvni program."); return 0;
}
Programy 2 až 3 se liší úpravou, tj. rozmístěním tzv. bílých znaků. Bílé znaky jsou znaky, které textový editor použitý při prohlížení a editaci textového souboru reprezentujícího program nezobrazí, ale interpretuje je např. jako vynechání prázdného místa, přechod na další řádek, tabulátor apod.; to jsou tzv. bílé znaky na úrovni zdrojového programu.
Kdybychom např. program 1 spustili dvakrát po sobě, vytisklo by se
Muj prvni program.Muj prvni program.
Kdybychom chtěli, aby se tento text vypisoval při opětovném spuštění programu vždy z nové řádky, doplnili bychom do řetězce dvojznak \n, které mají význam přechodu na nový řádek na výstupu. Dvojznak \n tedy umožňuje vkládaní určitého bílého znaku do výstupního, programem zpracovávaného textového souboru. Vyvolání funkce printf by tedy v tomto případě vypadalo takto:
printf("Muj prvni program.\n");
V programech 1 až 3 byla funkce printf používaná pro zobrazení řetězce znaků. Funkci printf je možné použít i pro výstup hodnot výrazů a proměnných.
#include <stdio.h>
#define M 10
#define N 20
int main( void )
{
int sum;
sum = M + N;
printf("\nTisk hodnoty celociselne konstanty: %d",100);
printf("\nCislo M=%d", M);
printf("\nCislo N=%d", N);
printf("\n\nSoucet cisel M a N je %d", sum);
printf("\nDruha mocnina tohoto souctu je %d\n", sum*sum);
return 0;
}
Příkazy #define jsou zavedeny symbolické konstanty M a N.
Předpřekladač (preprocesor) nahradí identifikátory M a N v textu programu konstantami 10 a 20, k této náhradě nedojde uvnitř řetězců.
První řádka těla funkce je tzv. definice.
Tak jako v mnoha jiných programovacích jazycích i v jazyce C musí být proměnné před použitím definovány.
Proměnná je místo v paměti, do kterého mohou být zapisována data (hodnoty) určitého typu.
V jazyku C na proměnné zpravidla odkazujeme pomocí identifikátorů, které představují jména těchto míst v paměti.
V našem programu je definicí vyhrazeno určité místo v paměti pro ukládání hodnoty typu int tj. celočíselné hodnoty a toto místo je označeno identifikátorem sum.
Stručněji v této situaci budeme říkat, že proměnná sum je definována jako typ int.
Označení int je příklad tzv. klíčového slova. Klíčová slova jsou vlastně vyhrazené identifikátory, které slouží pro označení standardních typů dat, některých příkazů jazyka C apod.
Druhá řádka těla funkce, která zajistí, že se sečtou hodnoty 10 a 20 a výsledná hodnota se uloží na místo v paměti označené jako sum, je příkladem přiřazovacího příkazu. Výraz na pravé straně symbolu = se vyhodnotí a výsledná hodnota se přiřadí proměnné zapsané na jeho levé straně. Středník je zde součástí zápisu přiřazovacího příkazu, nelze ho tedy vynechat ani za posledním přiřazením. V tomto smyslu není oddělovacím znakem od ostatních příkazů jako v některých jiných programovacích jazycích.
Příkazy vyvolání knihovní funkce printf obsahují dva parametry: znakový řetězec a aritmetický výraz, jehož hodnota se bude tisknout zároveň se znakovým řetězcem. Znak d bezprostředně následující za znakem % určuje, že např. hodnota 30 proměnné sum při vyvolání knihovní funkce printf se vytiskne jako dekadické celé číslo. Také zde je středník součástí vyvolání knihovní funkce.
Výstup programu bude následující:
Tisk hodnoty celociselne konstanty: 100
Cislo M=10
Cislo N=20
Soucet cisel M a N je 30
Druha mocnina tohoto souctu je 900
V tomto programu lze najít všech šest druhů symbolů, které v jazyce C ze syntaktického hlediska rozeznáváme: identifikátory, klíčová slova, konstanty, řetězce, operátory a oddělovače.
-
identifikátory main printf sum
-
klíčová slova int
-
konstanty 100
-
řetězce "\n\nSoucet cisel M a N je %d"
-
operátory + *
-
oddělovače { } ( ) ; " ,
Pro umístění bílých znaků platí následující pravidlo: Klíčová slova, identifikátory a konstanty, pokud se skládají z více znaků, nesmějí být rozděleny vložením bílého znaku. Totéž platí pro operátory, které se skládají z více znaků, jež se zapisují bezprostředně za sebou. Do textu řetězce také nesmí být vkládány bílé znaky kromě mezery, která řetězec nerozděluje, ale stává se jeho součástí.
K dobrým programátorským zvykům patří vkládat do programů komentáře. Usnadňují čtení programů při jejich ladění a případných pozdějších úpravách. Následující program ilustruje používání komentářů na příkladu předchozího programu.
/* Ukazka pouziti komentare: (1)
** Tento program vypocita soucet dvou celociselnych
** konstant a priradi vysledek promenne 'sum' typu
** integer. Vysledna hodnota bude zobrazena na monitoru
** pocitace spolu se svou druhou mocninou a doplnujicim
** textem. */
#include <stdio.h>
#define M 10
#define N 20
int main()
{
int sum; // definice promenne 'sum' (2)
sum=M+N; // prirazeni souctu promenne 'sum'
/**** nasleduje zobrazeni hodnot '100','M','N','sum' ***********/
printf("\nTisk hodnoty celociselne konstanty: %d",100);
printf("\nCislo M=%d",M);
printf("\nCislo N=%d",N);
printf("\n\nSoucet cisel M a N je %d",sum);
/**** a druhe mocniny 'sum' ***********/
printf("\nDruha mocnina tohoto souctu je %d\n",sum*sum);
return 0; /* ukonceni main() */
}
| 1 | blokový komentář |
| 2 | řádkový komentář |
Začátek komentáře je označen dvojicí znaků /*, konec je označen stejnými dvěma znaky, zapsanými
v obráceném pořadí, tedy */. Text komentáře je překladačem ignorován. Podle ANSI normy jazyka C
komentáře nemohou být vnořené (vhnízděné). (Řada existujících implementací tuto možnost ale
programátorovi nabízí.) Do komentáře je povoleno zapisovat bílé znaky bez omezení, tyto bílé znaky
ale nesmí rozdělovat dvojici znaků /* resp. */, která otevírá resp. uzavírá komentář.
Pro umístění komentáře platí stejná omezení jako pro vkládání bílých znaků do programu v jazyce C. Komentář nelze např. vkládat do řetězců, protože takto umístěný "komentář" by překladačem nebyl ignorován. Příkazem return se budeme zabývat v části pojednávající o funkcích.
Řádkový komentář nebyl v původní definici jazyka C, objevil se později. Začíná znaky // a končí na konci řádku. Dnes je možné ho používat směle.
V následujícím výkladu budeme obvykle komentáře používat k podrobnému glosování jednotlivých programů.
/* Tento program vypocita soucet prvnich MAX prirozenych **
** cisel a soucet druhych mocnin techto cisel. */
#include <stdio.h>
#define MAX 10
int main( void )
{
int i,s1,s2; /* definice promennych 'i' 's1' a 's2' */
i=1; s1=0; s2=0; /* prirazeni pocatecnich hodnot promennym */
while(i<=MAX) /* cyklus pokracuje, pokud i<=MAX */
{
s1=s1+i; /* soucet prirozenych cisel 1 az MAX */
s2=s2+i*i; /* soucet jejich druhych mocnin */
i=i+1;
}
/* tisk vysledku: */
printf("\n\n\nSoucet prvnich %d prirozenych cisel je %d.",MAX,s1);
printf("\nSoucet jejich druhych mocnin je %d.",s2);
}
Příkaz while je jeden z příkazů cyklu jazyka C. Plnění příkazu while začíná vyhodnocením podmínky, uzavřené v okrouhlých závorkách za tímto klíčovým slovem. Jestliže je výsledek nenulový - což odpovídá v jazyce C kladné pravdivostní hodnotě - potom je splněn jediný příkaz, který následuje za okrouhlými závorkami obsahujícími podmínku příkazu while. Několik příkazů je možné sloučit do jediného složeného příkazu pomocí složených závorek jako v uvedeném programu. Celý proces se opakuje a začíná opětným vyhodnocením podmínky příkazu while. Cyklus se provádí do té doby, dokud výsledek vyhodnocení podmínky je nenulový. Když je podmínka vyhodnocena jako nulová hodnota - což odpovídá záporné pravdivostní hodnotě - potom se cyklus přeruší a program pokračuje plněním následujícího příkazu. Poznamenejme ještě, že v uvedeném programu definice proměnných a přiřazení počátečních hodnot
int i,s1,s2;
i=1; s1=0; s2=0;
může být nahrazeno tzv. definicí s inicializací
int i=1,s1=0,s2=0;
V následujícím programu se seznámíme s funkcemi určenými pro vstup a výstup znaků.
/*
** Tento program kopíruje posloupnost znaku ze standardniho vstupu
** na standardni vystup. Jako ukoncovaci znak je pouzit znak '@'.
*/
#include <stdio.h>
#define ZNAK '@'
int main( void )
{
int c; /* definice promenne 'c' */
/* cyklus pokracujici dokud neni z klavesnice precten znak ZNAK */
while((c=getchar())!=ZNAK)
putchar(c);
return 0;
}
Funkce getchar() je určená pro čtení znaku ze standardního vstupního zařízení tj. zpravidla z klávesnice. Funkce nemá parametry, návratová je typu int, funkce vrací celočíselný kód přečteného znaku. Funkce putchar() je určena pro výstup jednoho znaku na standardní výstupní zařízení. Funkce má jeden parametr typu int - kód tisknutého znaku. Dvojznak != je relační operátor "nerovná se". Funkcí getchar() jsou tedy postupně čteny znaky z klávesnice a porovnávány se znakem zadaným instrukcí #define.
Lexikální elementy
Znaky
Znaky hrají důležitou roli ve standardním jazyku C. Program v jazyku C je zapsán v jednom nebo v několika zdrojových souborech. Tyto zdrojové soubory jsou textové soubory zapsané pomocí znaků, které můžeme přečíst, jestliže zdrojové soubory zobrazíme na monitoru nebo vytiskneme na tiskárně. Znaky jsou definovány tzv. znakovou sadou, která každému znaku přiřazuje i určité celé nezáporné číslo - kód.
Nulová hodnota kódu je v každé znakové sadě vyhrazena pro nulový znak (null character, null).
Minimální znaková sada, ve které lze zapsat libovolný program v jazyku C (minimal C character set) obsahuje následující znaky, které mají grafickou podobu (tisknutelné znaky):
Velká písmena: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
malá písmena: a b c d e f g h i j k l m n o p q r s t u v w x y z
číslice: 0 1 2 3 4 5 6 7 8 9
podtržítko: _
pomocné znaky: ! " # % & ' ( ) * , - . / : ; < = > ? [ ] \ ^ { } ~ |
Minimální znaková sada dále obsahuje kódy pro některé další (netisknutelné) znaky:
Znak Význam space vynechání mezery (space) BEL zvukové znamení (alert) BS posun kurzoru vlevo (backspace) FF odstránkování (form feed) NL nová řádka (newline) CR návrat kurzoru na začátek řádky (carriage return) HT horizontální tabelátor VT vertikální tabelátor null znak, jehož kódová hodnota je v každé znakové sadě 0
Tyto netištitelné znaky (tedy znaky, které nemají grafickou reprezentaci) a dále čtyři následující tištitelné znaky
' " ? \
se zapisují pomocí znaku \ změnovými posloupnostmi (tzv. escape sekvence). Změnovou posloupnost \n jsme už používali v programech 1.1 až 1.3 pro zápis znaku NL. Některé často používané změnové posloupnosti jsou uvedené v následující tabulce:
Změnová Označení Změnová Označení znaku: posloupnost: znaku: posloupnost: ' \' FF \f " \" NL \n ? \? CR \r \ \\ HT \t BEL \a VT \v BS \b null \0
Další možností, jak vyjádřit libovolný znak, jsou tzv. numerické změnové posloupnosti tj. zápis
\D nebo \xH nebo \XH
kde D je oktalový (osmičkový) a H je hexadecimální (šestnáctkový) kód znaku v dané znakové sadě, která je nejčastěji zadána ASCII tabulkou. D obsahuje 1 až 3 významové cifry, H obsahuje 1 až 2 významové cifry. Viz příklad v odst. .
Celočíselné a racionální konstanty
Celočíselné konstanty jsou celá čísla, která mohou být zapsána v desítkové (decimální), osmičkové (oktalové) nebo šestnáctkové (hexadecimální) soustavě.
Desítková celočíselná konstanta je posloupností desítkových číslic nezačínající číslicí 0 (s výjimkou čísla nula).
4563 58 10 0
(Znaménko není v jazyku C součástí syntaxe konstanty, ale je aplikací unárního operátoru + nebo - na tuto konstantu. Pro zjednodušení výkladu budeme ale celočíselnou konstantou rozumět nadále i posloupnost cifer opatřenou znaménkem, např. tedy i zápisy: -68 +9525 apod.
Do zápisu není povoleno vkládat čárku nebo bílý znak.
Oktalová konstanta je zapsána jako posloupnost oktalových cifer (0 až 7) začínající cifrou 0.
05671 0255 0000 01
Hexadecimální konstanta se zapisuje jako posloupnost hexadecimálních cifer začínající 0x nebo 0X. Hexadecimální cifry zahrnují číslice 0 až 9 a písmena A až F (nebo a až f). Písmena reprezentují hodnoty 10 až 15.
0XFF 0x48 0x1f 0XfF
Racionální konstanty zapisujeme v desetinném nebo v semilogaritmickém tvaru. V zápisu používáme desetinnou tečku a exponent označujeme písmenem e nebo E (tak jako je to obvyklé i u jiných programovacích jazyků.)
Příklady zápisu číselných konstant:
Matematický zápis Zápis v C -500 -500 10-7 1E-7 2,345 2.345 6·108 6e8 -0,5 -.5 (23)8 023 6,25 103 6.25E3 (FA)16 0XFA
Znakové konstanty
Znaková konstanta je právě jeden znak zapsaný mezi apostrofy,
'X' '\'' '\n' '\x0a' '\0'
Hodnota znakové konstanty je kód příslušného znaku v používané znakové sadě. Pokud se vám zdá, že v předchozích příkladech jsou někde mezi apostrofy zapsány dva nebo více znaků, podívejte se, jak jsou znaky definovány a jak se zapisují.
'\012' '\12' 10 '\xa' 0xa
'\0012' '\0xa' xa
Řetězce
Řetězec je v jazyku C posloupnost znaků uzavřená mezi uvozovkami.
Např. řetězcová konstanta "xyz" je posloupnost znakových konstant uložených v po sobě jdoucích bytech paměti, za kterými následuje byte obsahující nulovou hodnotu:
'x', 'y', 'z', '\0'
Vyskytuje-li se v řetězci znak uvozovka, musí jej předcházet znak zpětné lomítko. Tisk textu:
Tisk v palcich (")
se zobrazí např. příkazem:
printf("Tisk v palcich (\")");
ANSI C umožňuje automatické zřetězení řetězců:
"Dlouhy retezec lze "
"takto rozdelit"
Toto lze např. s výhodou použít při rozdělení konverze v příkazu printf():
printf("Tento text se bude"
" tisknout na jedne radce");
Identifikátory
Identifikátor (nebo též jméno) je posloupnost maximálně 31 písmen, číslic a podtržítek začínající písmenem nebo podtržítkem. Identifikátory používáme k označení objektů jazyka (proměnné, pole symbolické konstanty apod.). V jazyku ANSI C se rozlišují velká a malá písmena, tedy
Nejneobhospodarovatelnejsi je jiným identifikátorem než
nejneobhospodarovatelnejsi
Zápis je ovšem nevhodným užitím identifikátorů nejen pro využití rozdílu velkého a malého písmene, ale i pro délku identifikátorů. Identifikátory mají být pokud možno krátké a hledáme-li optimum mezi stručností a mnemotechnikou měli bychom se snažit o délku šesti, maximálně osmi znaků. Je vhodné používat jednoho druhu písma, obvykle malých písmen.
proud_1 proud_2 x633 zz znak_a
Klíčová slova.
Pojem klíčového slova jsme probrali v části 1.1. V jazyce ANSI C je 32 následujících klíčových slov tj. jmen, které mají pro překladač speciální význam: auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while
Tato slova jsou v ANSI C vyhrazenými identifikátory, které se nesmějí používat k označení objektů. ANSI norma zavádí nová klíčová slova const, enum, signed, void, volatile.
Typy, operátory, výrazy
V tomto článku vysvětlíme podrobněji definice a deklarace typu pro jednoduchou proměnnou a vytváření výrazů pomocí aritmetických a logických operací definovaných v jazyku.
Základní datové typy
V jazyku ANSI C jsou definovány následující standardní typy:
-
char - obvykle jedna slabika (byte) pro uchování jednoho znaku.
-
int - celé číslo, (obvykle dvě slabiky)
-
float - racionální číslo (obvykle čtyři slabiky)
-
double - racionální číslo dvojnásobné přesnosti
Slovo obvykle v uvedených příkladech se vztahuje na většinu implementací. Konkrétní rozsahy pro jednotlivé datové typy jsou uvedeny v hlavičkových souborech limits.h a float.h, které jsou součástí každé implementace.
Kromě toho existují v jazyce C modifikátory short tj. "krátký", long tj. "dlouhý", signed tj. se znaménkem a unsigned tj. bez znaménka, kterými lze standardní typy modifikovat. Ne všechny kombinace jsou ovšem povoleny. Pokud je nějaká kombinace povolená, potom lze zapisovat klíčová slova v libovolném pořadí. K modifikátorům patří i klíčové slovo const. Identifikátoru označenému v definici tímto modifikátorem se zároveň přiřadí hodnota; identifikátor se stává tzv. symbolickou konstantou.
const float PI=3.141593; /* definice symbolicke konstanty */
const L=3; /* definice celociselne symbolicke konstanty */
const int K=235; /* definice celociselne symbolicke konstanty */
short i=0; /* definice zkracene celociselne promenne */
short int j=1; /* definice zkracene celociselne promenne */
long int m=12345678; /* definice dlouhe celociselne promenne */
unsigned int n; /* definice nezaporné celociselne promenne */
unsigned nm; /* definice nezaporne celociselne promenne */
V následující tabulce jsou uvedeny povolené kombinace klíčových slov pro základní datové typy s typovými modifikátory a jejich zkrácené ekvivalentní vyjádření:
signed char (většinou totéž co char, závisí na implementaci!)
unsigned char
int = signed = signed int
short = short int = signed short = signed short int
unsigned short = unsigned short int
long = long int = signed long = signed long int
unsigned long = unsigned long int
float
double
long double
Z uvedené tabulky je patrné, že některá klíčová slova se pokládají za implicitní ( int, signed). Konkrétní rozsahy pro jednotlivé datové typy jsou rovněž uvedeny v hlavičkových souborech limits.h a float.h. Uveďme proto pouze, že typ short nemůže být delší a typ long nemůže být kratší než typ int.
Definice a deklarace jednoduchých proměnných
Pod pojmem definice se míní příkaz, kterým se zavádí nová proměnná, tj. příkaz, kterým se v paměti počítače vyhradí určité místo pro určitý datový typ a toto místo je nazváno určitým jménem (identifikátorem).
Deklarace je naopak příkaz, který pouze informuje překladač o typu určité proměnné, která musí být definována na jiném místě programu, viz čl. 2.1.
Jednoduchá proměnná je proměnná, která v každém okamžiku nabývá hodnoty jedné číselné nebo znakové konstanty.
V následujícím příkladu jsou definovány některé jednoduché proměnné:
int i;
unsigned long int number_a,number_b;
char znak1,znak2;
double eps_1=1.0e-5,eps_2=1.0e-10;
unsigned char lomitko='/',procenta='%';
Ve čtvrtém a pátém řádku je zapsána tzv. definice s inicializací.
Objekty jazyka
Kdybychom chtěli zcela přesně definovat další datové struktury, pole (množiny prvků téhož typu hodnot), struktury (množiny prvků různých typů hodnot), uniony atd., výklad by byl sice přesný, ale ztratil by na přehlednosti. Budeme zatím pracovat pouze s jednoduchými proměnnými. Se všemi dalšími objekty se podrobně seznámíme ve druhé kapitole.
Aritmetické operátory a výrazy
Programovací jazyk C podporuje obvyklé aritmetické operace: sčítání +, odčítání -, násobení *, dělení /. Kromě toho obsahuje i operátor dělení modulo %, jehož výsledkem je zbytek po dělení dvou celočíselných hodnot. Právě vyjmenované operátory patří k binárním operátorům, které se aplikují na dva operandy (argumenty). Pokud oba operandy dělení / jsou celočíselné, potom se provede tzv. celočíselné dělení, tzn. že se čísla vydělí a od výsledku se odřízne desetinná část.
Součástí jazyka C jsou i unární operátory - a unární operátor +, přičemž poslední z nich nebyl k dispozici v normě K&R a byl zaveden až v ANSI normě jazyka C.
24*den -1024 60*(60*hodiny+minuty)+sekundy
V prvním příkladu se násobí celočíselná konstanta 24 hodnotou proměnné den. Ve druhém příkladu se používá unární operátor - pro vyjádření záporné celočíselné hodnoty.
| Třída operátorů: | Operátor: | Asociativita: |
|---|---|---|
unární |
+ - |
zprava doleva |
multiplikativní |
* / % |
zleva doprava |
aditivní |
+ - |
zleva doprava |
V tabulce jsou operace uspořádány podle priority od nejvyšší k nejnižší. Multiplikativní operátory se tedy např. provádějí dříve než aditivní. U operací se stejnou prioritou rozhoduje o pořadí operací tzv. asociativita.
Asociativita zleva doprava znamená, že z hlediska syntaxe jazyka C je výraz 1+2+3 ekvivalentní výrazu (1+2)+3. Je třeba upozornit, že ani přesně vyznačené pořadí operací není pro kompilátor závazné.
| a | b | a/b | (a/b)*b | a%b | (a/b)*b+a%b |
|---|---|---|---|---|---|
13 |
5 |
2 |
10 |
3 |
13 |
-13 |
5 |
-2 |
-10 |
-3 |
-13 |
13 |
-5 |
-2 |
10 |
3 |
13 |
-13 |
-5 |
2 |
-10 |
-3 |
-13 |
Konverze
Při vyhodnocování aritmetického výrazu dochází k implicitním konverzím (změnám) typu. Nejprve proběhnou následující celočíselné implicitní konverze:
-
Jestliže se v aritmetickém výrazu objeví operand typu char nebo typu short int, je konvertován na typ int.
-
operandy typu unsigned char nebo unsigned short jsou konvertovány na typ int pouze tehdy, jestliže jsou reprezentovatelné typem int; jinak jsou konvertovány na typ unsigned int.
Po těchto změnách dochází k následujícím konverzím v tomto pořadí:
-
Jestliže jeden z operandů je typu long double, je i druhý operand konvertován na typ long double.
-
Jestliže jeden z operandů je typu double, je i druhý operand konvertován na typ double.
-
Jestliže jeden z operandů je typu float, je i druhý operand konvertován na typ float.
-
Jestliže jeden z operandů je typu unsigned long int, je i druhý operand konvertován na typ unsigned long int.
-
Jestliže jeden z operandů je typu long int, je i druhý operand konvertován na typ long int.
-
Jestliže jeden z operandů je typu unsigned int, je i druhý operand konvertován na typ unsigned int.
V ANSI C tedy narozdíl od K&R C tedy operandy typu float nemusí být nutně implicitně konvertovány na typ double.
Hierarchii konverzí datových typů je možné schematicky znázornit takto:
int → unsigned int →long int → unsigned long int → float → double → long double
Relační a logické operátory
Relační operátory jsou
< <= => >
které mají stejnou prioritu, nižší než dříve uvedené aritmetické operátory. Za nimi, pokud se týká priority, následují operátory rovnosti == a nerovnosti != .
Logické operátory jsou logický součin (tzv. konjunkce) && , logický součet (tzv. disjunkce) || a negace !. Výrazy se vyhodnocují zleva doprava do okamžiku, kdy je z logického hlediska zřejmý výsledek. Výraz tvaru y!=0 && z/y je v jazyku C správný narozdíl od mnohých jiných jazyků, nulová hodnota proměnné y zde nemá za následek případnou havárii programu. Na tomto místě znovu upozorňujeme, že logické hodnoty pravda, resp. nepravda jsou reprezentovány v jazyku C celočíselnými hodnotami různými od nuly, resp. nulou.
Logické operátory po bitech není možné aplikovat na typy float a double. Mají vyšší prioritu než operace disjunkce a konjunkce.
& |
operátor logického součinu po bitech |
| |
operátor logického součtu po bitech |
^ |
operátor neekvivalence po bitech |
<< |
operátore posunu vlevo |
>> |
operátor posunu vpravo |
~ |
unární operátor jednotkového doplňku |
Příklady:
a) n & 0177 nuluje vše s výjimkou posledních sedmi bitů, protože oktalová konstanta 0177 v binárním zápisu má např. tvar 000 000 001 111 111
b) x << 3 posune hodnoty jednotlivých bitů o tři místa vlevo, což znamená, že se hodnota vynásobí osmi. Poslední tři bity zprava se doplní nulami.
c) x >> 3 posun o tři místa vpravo (tj. u typu unsigned dělení osmi, u typu signed je tato operace implementačně závislá).
d) n & ~0177 vynuluje posledních sedm bitů.
Operátory přiřazení, přiřazovací výraz
Přiřazovací operátor umožňuje přiřazení určité nové hodnoty některé proměnné programu. Nejjednodušší forma přiřazovacího výrazu je:
proměnná = výraz
Operátor = způsobí, že se vyhodnotí výraz na pravé straně a výsledná hodnota se přiřadí proměnné na straně levé. Pokud nejsou oba operandy téhož typu je typ výrazu na pravé straně změněn (konvertován) na typ proměnné na levé straně. Priorita operátoru = a jeho asociativita je dána tabulkou v odstavci . Nízká priorita tohoto operátoru zaručuje, že přiřazení bude provedeno, až po vyhodnocení pravé strany.
Přiřazení
proměnná = výraz
je opět výraz (přiřazovací výraz), který má hodnotu pravé strany po případné konverzi této hodnoty na typ proměnné, která stojí na levé straně. Přiřazovací výraz je tedy možné používat v kterémkoliv místě programu, kde syntaktická pravidla jazyka C výraz připouštějí, např. jako součást zápisu některých příkazů. Následující příklad je syntakticky správným zápisem přiřazovacího výrazu v jazyce C.
w=(a=b+c)*(x=y*z)
Jak jsme se zmínili, při vyhodnocení přiřazovacího výrazu se přiřazuje (v podstatě jako vedlejší efekt) nějaká (nová) hodnota určité proměnné.
Přitom dochází k implicitní konverzi hodnoty pravé strany na typ proměnné stojící na levé straně. Například, je-li na pravé straně přiřazovacího výrazu hodnota typu int a na levé straně proměnná typu double, tj. schematicky
double=int
dojde před přiřazením k automatické konverzi hodnoty int na typ double. Při této konverzi hodnoty na vyšší typ ("vyšší" ve smyslu hierarchického uspořádání zavedeného v odstavci ) nedochází k problémům, tyto operace jsou jednoznačně definovány a nejsou implementačně závislé.
Pokud je ale konvertována hodnota na "nižší typ", může dojít k různým (implementačně závislým) jevům. Např. při přiřazení typu
int=double
je-li hodnota typu double mimo rozsah typu int, pak výsledek této operace není definován a závisí na implementaci. Leží-li tato hodnota v rozsahu typu int, pak se proměnné na levé straně přiřadí hodnota, získaná z původní hodnoty odříznutím desetinné části.
V jazyce C je možné použít i tzv. vícenásobná přiřazení.
výraz: |
x=y=z=p+q |
je interpretován jako: |
x=(y=(z=p+q)) |
Kromě uvedeného přiřazovacího operátoru existují v jazyce C ještě tzv. složené přiřazovací operátory. Přiřazení, ve kterých se opakuje levá strana na pravé straně např.: sum=sum+4 je možno zapsat s použitím operátoru += takto:
sum += 4
Obdobně lze použít i operátory op , kde op je jeden z následujících operátorů:
- * / % << >> & ^ |
Výraz
proměnná op výraz
je ekvivalentní s výrazem
proměnná proměnná op (výraz)
Všimněte si v uvedeném zápisu dvojice závorek. Výraz prom_1*=koef+10
tedy znamená totéž jako prom_1=prom_1*(koef+10)
a ne např.: prom_1=prom_1*koef+10
V části jsou uvedeny priorita a asociativita těchto operátorů.
d/=c+=b*=a d/=(c+=(b*=a)) d=d/(c=c+(b=b*a))
Operátory inkrementace a dekrementace
V programovacím jazyce C existují dva speciální unární operátory:
inkrement ++ dekrement --
Oba operátory lze použít jako předpony i jako přípony s následujícím odlišným významem:
++promenna
promenna++
| stručný zápis: | zápis pomocí přiřazovacího výrazu: |
|---|---|
i++ |
i=i+1 |
i-- |
i=i-1 |
++i |
i=i+1 |
--i |
i=i-1 |
Operátor čárka
vyraz_1,vyraz_2
Význam operátoru: vyraz_1 je vyhodnocen a výsledek tohoto vyhodnocení je zapomenut. Pak se vyhodnotí vyraz_2, a to je závěrečný výsledek.
Příklad: Nechť i má hodnotu 7 a j má hodnotu 5, pak výraz (3+i--,i-j) bude mít hodnotu 1. Vedlejším efektem je přiřazení hodnoty 6 do proměnné i.
Podmíněný výraz
Chceme-li vyhodnotit výraz v závislosti na nějaké logické podmínce, použijeme tzv. podmíněný výraz, který v C se zapisuje pomocí tzv. ternárního operátoru ? : .
Chceme-li např. vypočítat hodnotu sin(x) / x pro x ≠ 0 a hodnotu 1 pro x = 0 , píšeme
x ? sin(x)/x : 1
Zápis se obvykle zpřehledňuje nepovinnými závorkami, např.
a > b ? a : b
(a > b) ? a : b
Priorita operátorů
V tomto článku shrneme priority a asociativitu probraných operací, které uvedeme v přehledné tabulce. Uvedeme všechny operátory. Některé z nich probrány nebyly a zmíníme se o nich v dalších kapitolách na vhodných místech.
| Operátor | Asociativita |
|---|---|
() [ ] → . |
zleva doprava |
! ~ ++ — + - ( typ) * & sizeof |
zprava doleva |
* / % |
zleva doprava |
+ - (unární plus a minus) |
zleva doprava |
<< >> |
zleva doprava |
< ⇐ >= > |
zleva doprava |
== != |
zleva doprava |
& |
zleva doprava |
^ |
zleva doprava |
| |
zleva doprava |
&& |
zleva doprava |
|| |
zleva doprava |
?: |
zprava doleva |
= += -= *= /= %= >>= <⇐ &= /= ^= |
zprava doleva |
, |
zleva doprava |
Ve třetím řádku je třeba upozornit, že operátory +, - v třetím řádku jsou unární, narozdíl od operátorů +, - v pátém řádku, které jsou binární. Také operátor adresa & ve 8. řádku je jiným operátorem, než operátor logického součinu & v řádku devátém, viz .
Tabulka je uspořádána od operací s nejvyšší prioritou po prioritu nejnižší. Operátory v témže řádku mají tutéž prioritu.
Příkazy
Příkazy jazyka C jsou prováděny v pořadí, v jakém byly zapsány (sekvenčně). Některé příkazy ale mohou změnit toto přirozené pořadí a mohou přenést řízení do jiné části programu (skoky, příkazy cyklu, volání funkcí, přepínač atd.) Začneme od nejjednodušších příkazů jazyka C.
Prázdný příkaz
Syntaxe tohoto příkazu je tvořena samotným znakem středníku:
;
Prázdný příkaz je možné označit návěštím a přenést na něj řízení skokem goto, viz .
Výrazový příkaz
Výrazový příkaz je výraz ukončený středníkem, výrazový příkaz má tedy následující tvar:
vyraz;
K výrazovým příkazům patří např. přiřazovací příkaz (viz ), volání funkce (i v případě, že je typu void) apod.
a=b+c; i++; printf("Ahoj!"); 5; x+y;
Poslední dva příklady výrazových příkazů jsou sice ze syntaktického hlediska správně, ale nemá smysl je používat.
Složený příkaz — blok
Skupinu příkazů lze pomocí složených závorek sloučit do tzv. složeného příkazu:
{a=b+c; i++; printf("Ahoj!");}
Kromě toho je možné umístit na začátek této skupiny příkazů definice proměnných. Vzniká tak konstrukce nazývaná blok.
{int a,b,c; a=b+c; i++; printf("Ahoj!");}
Protože středník je v jazyce C ukončovacím znakem některých příkazů a nikoliv oddělovacím znakem jako v některých jiných programovacích jazycích, je třeba ho použít v uvedeném příkladu i za posledním příkazem bloku. Za uzavírací závorkou bloku } se středník nedává, pokud ho tam umístíme, bude chápán jako prázdný příkaz. Syntakticky správně je i konstrukce prázdného složeného příkazu { }.
Jedním z příkazů bloku může být opět blok, mluvíme pak o bloku vnořeném a bloku nadřazeném. Proměnné, které jsou v bloku definovány, jsou v něm lokální - je možné je tedy využívat pouze v rámci tohoto bloku a z jiných částí programu nejsou viditelné. Pokud se identifikátor lokální proměnné bloku kryje s identifikátorem proměnné v nadřazeném bloku (nebo s identifikátorem globální proměnné, viz ) bude identifikátor z nadřazeného bloku (nebo identifikátor globální proměnné) zastíněn.
#include<stdio.h>
void main()
{
int i=10; {int i=1; i++;} printf("\n i=%d",i);
}
Uvedený program tedy vytiskne hodnotu i=10, protože tělo funkce main() tvoří nadřazený blok a proměnná i tohoto bloku je vlivem zastínění nedostupná ve vnořeném bloku.
Přiřazovací příkaz
Přiřazovací příkaz má následující formu:
přiřazovací_výraz;
tj. přiřazovací výraz ukončený středníkem. Přiřazovací výraz byl rozebrán v 3.7.
V jazyce C je možné, jak už víme, použít i tzv. vícenásobná přiřazení.
int iii;
float fff;
fff=iii=3.14;
Po provedení tohoto příkazu získá proměnná iii hodnotu typu integer rovnou 3 (dochází k odříznutí desetinné části), proměnná fff hodnotu typu float rovnou 3.0.
/* ... */
double a,b,c,d;
a=3;b=4;c=5;d=34;
/* ... */
d/=c+=b*=a;
/* Tento prikaz ma stejny vyznam jako d/=(c+=(b*=a));
** a tedy jako prikaz d=d/(c=c+(b=b*a));
** (vysledek je d=2.)
... */
Cykly
V části 1. jsme se setkali s cyklem while. Zabývejme se nyní podrobněji jednotlivými typy cyklů:
Příkaz while
Syntaxe příkazu cyklu while je
while(výraz) příkaz
Podmínkou opakování je výraz, je číselného typu, typu char nebo je to ukazatel. V případě, že je výraz nulový (nepravdivý), příkaz tvořící tělo cyklu se neprovede a provádění cyklu se ukončí. Je-li výraz nenulový (pravdivý), provede se příkaz tvořící tělo cyklu, opět se vyhodnotí výraz atd. Vyhodnocení podmínky se tedy provádí před každým krokem cyklu.
/******************** Znaky a jejich kody ************************/
#include <stdio.h>
main()
{
int c;
printf("\nZadavej znaky, konec = znak '@'\n");
while((c=getch()) != '@') /* cteni znaku c, jeho porovnani s '@' */
printf("\n %c: %d %o %x", c, c, c, c);
return;
}
Funkce getch() má podobný význam jako funkce getchar(); znak se ale čte ihned po stisknutí příslušné klávesy, nikoliv jako u funkce getchar() až po stisknutí ENTER. Výraz c=getch() musí být v závorce, protože priorita operátoru != je vyšší než operátoru =, viz 3.11.
Příkaz do
Tento příkaz cyklu má tvar
do příkaz while(výraz);
Pro podmínku opakování výraz platí totéž co pro podmínku cyklu while. Na rozdíl od cyklu while se testování podmínky výraz provádí až po provedení těla cyklu. Tělo cyklu do se tedy provede alespoň jednou.
Příkaz for
Tvar příkazu for je následující:
for(výraz_1;výraz_2;výraz_3) příkaz
Tato konstrukce má stejný význam jako následující konstrukce s příkazem while:
výraz_1; while(výraz_2) příkaz výraz_3;
Výraz_1 se obvykle nazývá inicializace, výraz_2 je podmínka, která se testuje a výraz_3 je inkrementace (nebo dekrementace). Příkaz je tělo cyklu. Ve výrazu_1 je možné definovat proměnnou (nebo více proměnných), která má platnost jenom pro tělo cyklu.
V příkazu for je ale možné vynechat kterýkoliv z výrazů výraz_1 výraz_2, výraz_3, počet středníků však se však nezmění. V případě, že se vynechá výraz_2, je považován za nenulový, tj. za pravdivý.
for(;;){
...
}
Příkaz break
Příkaz break se používá k ukončení cyklů while, do, for. Pokud je více cyklů do sebe vložených, ukončuje jen nejvnitřnější z cyklů, ve kterých se nalézá. Příkaz break se uplatní zejména tam, kde není výhodné testovat podmínku opakování cyklu na začátku nebo na konci každé smyčky.
#include<stdio.h>
#include<conio.h>
void main()
{ int a,b,c,d;
clrscr(); putchar('\n'); /* Smazani obrazovky, odradkovani.*/
putchar(a=getch()); /* Cteni a zapis prvnich 3 znaků. */
putchar(b=getch());
putchar(c=getch());
/* Nekonecnou smycku 'while(1)' je mozne nahradit 'for(;;)' */
while(1){
putchar(d=getch()); /* Precteni dalsiho znaku */
if(a=='*' && b=='*' && c=='*' && d=='*')
break; /* Ukončení cyklu. */
a=b;b=c;c=d;
}
}
Funkce main() je popsána jako funkce vracející typ void; příkaz return pak není nutný.
Příkaz continue
Uveďme si nejprve fragment programu, ve kterém se používá příkaz continue pro zjednodušení zápisu programu:
/* ... */
for(i=0;i<20;i=i+1){
if(pole[i]==0) continue;
pole[i]=(pole[i]+1.)/pole[i]; /* pole[i] je indexovana promenna
v datove strukture pole, ktera je podrobne probirana napr.
ve 2. kapitole */
}
/* ... */
Příkaz continue ukončí právě probíhající krok cyklu; cyklus pokračuje bezprostředně dalším krokem. V uvedeném příkladu je ukončen krok cyklu, ve kterém nastane pole[i]=0.
Podmíněný příkaz
Podmíněný příkaz if-else má v jazyce C následující tvary:
-
tzv. úplný podmíněný příkaz if-else
if(výraz) příkaz_1 else příkaz_2
Příklad 4.5.5.1 je možné zapsat i bez použití příkazu continue s využitím podmíněného příkazu:
for(i=0;i<20;i=i+1){
if(pole[i]==0)
;
else{
pole[i]=(pole[i]+1.)/pole[i];
...
}
}
Část else je nepovinná, je tedy možné použít následující
-
neúplný (zkrácený) podmíněný příkaz if
if(výraz) příkaz_1
Za klíčovým slovem if následuje výraz povinně uzavřený do závorek: (výraz). Je-li jeho hodnota nenulová, potom se provede příkaz_1, je-li vyhodnocen jako nula, pak se provede příkaz_2, pokud není vynechán. Přitom část else vztahuje překladač k nejbližšímu příkazu if, kterému zatím žádná část else nebyla přidělena.
Jestliže do části else umístíme další příkaz if atd. získáme následující konstrukci:
-
podmíněný příkaz if-else if
if(výraz_1) příkaz_1
else if(výraz_2) příkaz_2
else if(výraz_3) příkaz_3
...
else příkaz_n
Na závěr tohoto odstavce si uveďme o něco složitější příklad na použití příkazů cyklu, příkazu break a podmíněného příkazu. Uvedeme zde text celého programu, i když jeho části týkající se práce se soubory budou objasněny až ve třetí kapitole. Čtenář se zde může soustředit na příkazy řízení toku, tj. na příkazy while, if-else, break atd.; v ostatních částech programu se může zatím spolehnout na komentáře a vrátit se k tomuto programu později.
#include<stdio.h>
main()
{ /******* otevreni souboru prog1.c, prog2.c ************************/
FILE *fr,*fw;
int a,PZL=0,PZH=0;
if((fr=fopen("prog1.c","r"))==NULL) printf("Chyba otevreni prog1.c");
if((fw=fopen("prog2.c","w"))==NULL) printf("Chyba otevreni prog2.c");
while((a=getc(fr))!=EOF){/* getc() funkce pro precteni znaku ze
souboru, EOF znak ukoncujici soubor */
if(PZL==0 && a!='/')
putc(a,fw); /* putc() funkce pro zapis znaku do
souboru */
else if(PZL==0 && a=='/')
PZL=1;
else if(PZL==1 && a=='*'){ /* ZACAL KOMENTAR! */
PZH=0;
while((a=getc(fr))!=EOF){
if(PZH==0 && a!='*');
else if(PZH==0 && a=='*')
PZH=1;
else if(PZH==1 && a=='/'){ /* SKONCIL KOMENTAR! */
PZL=0;break;
}
else if(a!='*') PZH=0; /* pripad: PZH==1 && a!='/' */
}
}
else{ /* zde muze nastat pouze: PZL==1 && a!='*' */
putc('/',fw);
if(a!='/') { putc(a,fw); PZL=0;
}
}
}
fclose(fr);fclose(fw);return;
}
Proměnné PZL resp. PZH se zde používají k indikaci faktu, že předchozí načtený znak ze souboru bylo lomítko (PZL=1) resp. že předchozí načtený znak byla hvězdička (PZH=1). V opačných případech jsou PZL resp. PZH nulové.
Přepínač (switch)
Pro větvení výpočtu podle hodnoty celočíselného nebo znakového výrazu slouží tzv. přepínač (switch). Tvar tohoto příkazu je následující:
switch(výraz) {
case hodnota_1: příkaz_1
case hodnota_2: příkaz_2
...
case hodnota_n: příkaz_n
default: příkaz
}
#include <stdio.h>
main()
{
int b=1, hod=0;
printf("\nZadej bodove ohodnoceni (0 - 18),"
" konec = zaporny pocet bodu \n\n");
do {
scanf("%d",&b);
hod=0;
if (b>=0) hod=4;
if (b>=9) hod=3;
if (b>=13) hod=2;
if (b>=16) hod=1;
if (b>18) hod=0;
switch(hod) {
case 1 : printf("Vyborne\n\n"); break;
case 2 : printf("Velmi dobre\n\n"); break;
case 3 : printf("Dobre\n\n"); break;
case 4 : printf("Neuspesny pokus\n\n"); break;
default: printf("Mimo rozsah stupnice\n\n");
}
} while (b>=0);
return(0);
}
Poznámka: Standardní funkce scanf() je určena pro vstup hodnot ze standardního vstupního souboru, viz odst. 3.1.3. Čtení a přiřazení hodnoty jednoduché proměnné b je nutné v této funkci realizovat operátorem &, vysvětlení čtenář najde v čl. 2.3.1.
Příkaz goto
Jazyk C obsahuje i příkaz skoku goto. Vedle strukturovaných příkazů skoku např. zmíněných break, continue je goto příkladem nestrukturovaného příkazu skoku. Použití příkazu goto je omezeno tělem jedné funkce, není ale třeba řídit se blokovou strukturou, je možné nejen z bloku vyskočit ale i skočit dovnitř bloku. Každý program obsahující goto je možné přepsat i bez něho, obecně lze doporučit pouze velice umírněné používání tohoto příkazu. Jednou z mála situací, ve kterých se zdá být použití goto snad smysluplné, je jeho použití pro ukončení provádění několika do sebe vnořených cyklů.
Program v následujícím příkladu přečte jeden znak ze standardního vstupu a určí, zda je tento znak obsažen ve dvourozměrném poli abc typu char inicializovaném při spuštění programu. Proměnnou abc si lze představit jako tabulku typu 4x4 zadaných znaků. Program zde používáme pro ilustraci příkazu goto, poli se budeme systematicky zabývat ve druhé kapitole, kde se také ještě k tomuto příkladu vrátíme.
#include <stdio.h>
#include <conio.h> (1)
#define M 4
#define N 4
main(){
/******** definice dvourozměrného pole s inicializací ******/
char abc[][N]={{'1','2','*','4'},
{'a','b','c','d'},
{'$','%','#','@'},
{'+','-','^','/'} }, znak;
int i,j,nalezen;
printf("\nZadej znak: "); znak=getchar();
for(nalezen=0,i=0;i<M;i++) /* pouziti operatoru carka */
for(j=0;j<N;j++)
if(abc[i][j]==znak){nalezen=1;goto konec;}
konec: /* toto je navesti prikazu goto */
clrscr(); /* smazani obrazovky */
/* pouziti podmineneho vyrazu v ridicim retezci fce printf: */
printf("\nZadany znak %s nalezen",(nalezen?"byl":"nebyl"));
printf("\n\nStisknete libovolnou klavesu.");
getch();
}
| 1 | conio.h se kdysi používalo na DOSu. |
V programu je v cyklu for použit operátor čárka, viz odst. 3.9.
Funkce
Funkce je základní programovou jednotkou v jazyce C. Každý program obsahuje alespoň jednu funkci - main(). Definice libovolné funkce má podobnou strukturu jako definice funkce main().
Definice a volání funkce
Základní rysy definice funkce a její volání si ukážeme na jednoduchém příkladu. V tomto odstavci rozebereme případ, kdy parametry funkce nemění ve funkci hodnotu, představují tedy vstupní hodnoty a výstup je zprostředkován návratovou hodnotou funkce.
#include<stdio.h>
#define PI 3.141592
/* nasleduje definice funkce */
double objem_valce(double v,double r)/* hlavicka fce, neni zde ';' */
{
double objem; /* zacatek tela funkce */
objem=PI*r*r*v; /* vypocet objemu valce */
return(objem); /* urceni navratove hodnoty funkce objem_valce() */
} /* konec tela funkce */
main()
{
double v,r;
while(1){
printf("\n\n\nZadej vysku a polomer valce\n");
scanf("%lf%lf",&v,&r);
if(v<0.||r<0.) return;
printf("\nv=%f r=%f objem=%f",v,r,objem_valce(v,r));
}
}
Definice funkce nesmí být součástí definice jiné funkce narozdíl např. od programovacího jazyku Pascal, nesmí být tedy zapsána v těle žádné funkce, tedy např. ani funkce main(). Definice funkce v jazyce C se skládá z hlavičky funkce a z těla funkce. V hlavičce funkce je uveden typ návratové hodnoty funkce, identifikátor funkce a typy parametrů.
Příkaz return výraz; return(výraz); použitý ve funkci objem_valce() definuje návratovou hodnotu funkce jako hodnotu výraz, ukončí funkci objem_valce() a předá řízení programové jednotce, která objem_valce() vyvolala tj. funkci main(). Přitom dochází k případné konverzi hodnoty výraz na typ návratové hodnoty funkce.
-
K ukončení funkce lze použít příkaz return bez parametru. V našem příkladu byla takto ukončena funkce main().
-
Funkce nemusí obsahovat ani jeden příkaz return.
-
Příkaz exit(výraz); má podobný význam, ukončí ale nejen příslušnou funkci, ve které byl použit, ale všechny funkce včetně hlavní funkce main(). Ukončí celý program.
Hlavička funkce je v našem příkladu zapsána v souladu s doporučením novější normy ANSI, která ale zároveň připouští i zápis podle starší normy K&R.
| ANSI | K&R |
|---|---|
|
|
Obě konstrukce jsou naprosto ekvivalentní.
Programové jednotky odpovídající funkcím main() a objem_valce() mohou být zapsány v libovolném pořadí. V případě, že by funkce main byla zapsána jako první, nebyl by ve funkci main znám typ návratové hodnoty objem_valce(). V tomto případě je vhodné informovat překladač o typu návratové hodnoty, i o typu parametrů tj. deklarovat funkci uvedením tzv. úplného funkčního prototypu podle normy ANSI.
#include <stdio.h>
#define PI 3.141592
/* uplny funkcni prototyp (ANSI), (pozor, je zde ';') */
double objem_valce(double v,double r);
int main( void )
{
double v,r;
while(1){
printf("\n\n\nZadej vysku a polomer valce\n");
scanf("%lf%lf",&v,&r);
if(v<0.||r<0.) return;
printf("\nv=%f r=%f objem=%f",v,r,objem_valce(v,r));
}
return 0;
}
/* nasleduje definice funkce */
double objem_valce(double v,double r) /* hlavicka funkce */
{
double objem; /* zacatek tela funkce */
objem=PI*r*r*v; /* vypocet objemu valce */
return(objem); /* urceni navratove hodnoty funkce objem_valce() */
} /* konec tela funkce */
Funkční prototyp může být zapsán i bez identifikátorů parametrů. Oba zmíněné způsoby jsou z hlediska překladače ekvivalentní.
double objem_valce(double v,double r);
double objem_valce(double, double);
double objem_valce();
Nevýhoda zde spočívá v tom, že překladač nezná typ parametrů. Pokusíme-li se např. vyvolat funkci s parametry typu int
objem_valce(1,1);
dojde k chybě, protože se neprovede konverze parametrů na typ double. V případě že byl uveden funkční prototyp, typ parametrů je znám a konverze na typ double se provedou. Používání funkčního prototypu se tedy jeví jako výhodnější a doporučujeme ho. V jazyku C++ je tento způsob povinný.
Poznámka: Ze syntaktického hlediska není chyba, jestliže funkční prototyp podle ANSI normy nebo deklaraci podle normy K&R neuvedeme. V tomto případě se uplatní implicitní konverze typů hodnot a jestliže nesouhlasí se skutečnými typy, nelze předem určit, co proběhne. Norma ANSI se zde jeví jako nebezpečně tolerantní, v jazyku C++ je proto vynechání funkčního prototypu považováno za syntaktickou chybu. Zdá se nám proto, že uvedení funkčních prototypů všech používaných funkcí na začátku programu je dobrým programátorským zvykem. Ze stejného důvodu je vhodné umístit příkazy #include, které vkládají do programu hlavičkové soubory, na začátek programu.
Rekurzivní volání funkce
Funkce může být definována rekurzivně, tj. funkce může volat sama sebe (většinou s jinou hodnotou parametru). V následujícím příkladu je rekurzivní volání použito pro výpočet faktoriálu.
#include <stdio.h>
#include <stdlib.h> /* Obsahuje popis exit() */
long double faktorial(int i)
{
if(i<0){
printf("\n\nChybny argument funkce faktorial!");
exit(1); /* Ukonceni programu, (jinak vznikne nekonecna rekurze)*/
}
return( i ? i*faktorial(i-1) : 1. );
}
int main( void )
{
int n;
printf("\nVypocet faktorialu:");
printf("\nProgram se ukonci zadanim zaporneho cisla.\n\n");
do{
printf("\nZadej cele cislo:\t");
scanf("%d",&n);
printf("Faktorial tohoto cisla je %Lf", faktorial(n));
}while(n>=0); /* Funkce main() muze byt pouzita i pro variantu
funkce faktorial, ktera program neukonci */
return 0;
}
Je užitečné si uvědomit, že algoritmy využívající rekurzivní volání funkce kladou zpravidla větší nároky na vnitřní paměť, než algoritmy, které dosahují stejného cíle bez rekurzivního volání. Na druhé straně lze rekurzivní volání funkce v některých případech použít pro jednodušší vyjádření určitého algoritmu. Následující program je určen pro nalezení největšího společného dělitele dvou celých kladných čísel čísel.
#include <stdio.h>
long int spolecny_delitel(long int i, long int j)
{
long int k;
if(i<=0||j<=0) {printf("Nektere z cisel neni kladne.");return;}
if(i<j){
k=i;i=j;j=k;
}
return( i%j ? spolecny_delitel(j,i%j): j );
}
int main( void )
{
long int i,j;
printf("\n\nNejvetsi spolecny delitel dvou celych kladnych cisel.");
printf("\nUkonceni programu - jedno z cisel neni kladne.");
while(1){
printf("\n\nZadej dve cela kladna cisla: ");
scanf("%ld%ld",&i,&j);
printf("Nejvetsi spolecny delitel cisel %ld a %ld je cislo %ld",
i,j,spolecny_delitel(i,j));
}
return 0;
}
Návratová hodnota funkce main()
Není-li hlavní funkce typu void, je možné použitím příkazu return(výraz) funkci main() přiřadit návratovou hodnotu. Přístup k této návratové hodnotě, definované po ukončení programu, závisí na operačním systému, pod kterým byl program spuštěn. Např. v operačním systému DOS je tato hodnota dostupná příkazem ERRORLEVEL. Návratovou hodnotu funkce main() lze tedy využít při práci s dávkovými soubory.
Na Linuxu je vhodným zvykem vracet hodnotu 0 z programu do operačního systému, pokud program skončil správně. Jinak se vrací nenulové číslo. Nevracet nic se považuje za nevychovanost.
/*
** program odpoved.c
** Program definuje navratovou hodnotu v zavislosti
** na zadanem znaku z klavesnice
*/
#include <stdio.h>
#include <conio.h> (1)
#define ZNAK1 'T' /* TEX */
#define znak1 't'
#define ZNAK2 'W' /* WORD */
#define znak2 'w'
#define ZNAK3 'C' /* TC++ */
#define znak3 'c'
int main( void )
{
int c;
c=getch();
if(c==ZNAK1||c==znak1) return(1);
if(c==ZNAK2||c==znak2) return(2);
if(c==ZNAK3||c==znak3) return(3);
return(4);
}
| 1 | Hlavičkový soubor <conio.h> se používal kdysi na DOSu. |
Program je možné použít v dávkovém souboru menu.bat.
@echo off
echo ************************************************************
echo ** TEX ............ T **
echo ** WORD ........... W **
echo ** TurboC++ ....... C **
echo ************************************************************
odpoved.exe
IF ERRORLEVEL 4 GOTO NC
IF ERRORLEVEL 3 GOTO C
IF ERRORLEVEL 2 GOTO WORD
IF ERRORLEVEL 1 GOTO TEX
REM Zaciname nejvyssi hodnotou protoze prikaz 'ERRORLEVEL i'
REM je povazovan za pravdivy, je-li kod, se kterym byl ukoncen
REM program vetsi nez hodnota 'i'.
:NC
echo ** Provede se BATCH pro NORTON **
REM Tady se umisti prikaz pro spusteni NC
goto end
:C
echo ** Provede se BATCH pro TurboC++ **
REM Tady se umisti prikazy pro spusteni TC++
goto end
:WORD
echo ** Provede se BATCH pro WORD **
REM Tady se umisti prikazy pro spusteni Windows a Wordu
goto end
:TEX
echo ** Provede se BATCH pro TEX **
REM Tady se umisti prikazy pro spusteni TEXu
goto end
:end
echo ************************************************************
echo ** KONEC **
echo ************************************************************
Funkce s proměnným počtem parametrů
V programovacím jazyce C existuje možnost vytvářet funkce s proměnným počtem argumentů. Příkladem funkcí s proměnným počtem argumentů jsou standardní knihovní funkce printf() a scanf(). Fakt, že funkce má proměnný počet parametrů se vyjadřuje třemi tečkami '…'.
| funkční prototyp | definice funkce: |
|---|---|
|
|
Definice funkce musí obsahovat alespoň jeden pojmenovaný argument určitého typu. V našem příkladu je n počet následujících parametrů.
Pro usnadnění práce s funkcemi o proměnném počtu argumentů byly mezi standardní knihovní funkce zařazeny funkce va_start(), va_arg(), va_end(). Funkční prototypy těchto funkcí jsou uloženy ve standardním hlavičkovém souboru <stdarg.h>, viz rovněž kapitolu 7.
V následujícím příkladu je definována funkce max() s proměnným počtem parametrů pro výpočet maxima konečné posloupnosti racionálních čísel.
/*
** Funkce s promennym poctem parametru - max(n,...)
** n - pocet prvku konecne posloupnosti,
** dalsi parametry jsou typu double.
*/
#include <stdio.h>
#include <stdarg.h> /* obsahuje funkcni prototypy funkci va_start(),
va_arg(), va_end() a definici typu va_list */
double max(int n, ...); /* to je funkcni prototyp funkce max() */
/* '...' ve funkcnim prototypu znamena neurceny pocet parametru */
int main( void )
{
double a=20., b=-10., c=30., d=-20., e=10.;
/* vyvolani funkce max() */
printf("\n\n\nMaximum z cisel %f %f\n"
"je cislo %f",a,b,max(2,a,b));
/* vyvolani funkce max() s jinym poctem parametru */
printf("\n\n\nMaximum z cisel %f %f %f %f %f\n"
"je cislo %f",a,b,c,d,e,max(5,a,b,c,d,e));
return 0;
}
double max(int n, ...)
{
int i; double amax;
va_list seznam;
/* datovy typ va_list je obsazen v hlavickovem souboru stdarg.h;
'seznam' je promenna, do ktere se ulozi seznam parametru
funkce max() */
double x; /* jeden z parametru funkce max() */
va_start(seznam, n);
/* Seznam parametru funkce max() byl ulozen do promenne 'seznam',
precetla se promenna n znamenajici pocet parametru
funkce max(), ktere nasleduji za n. */
x=va_arg(seznam,double); /* Precetl se dalsi parametr
typu double ze seznamu parametru 'seznam' funkce max(). */
for(i=0, amax=x; i<n ;i++){ /* pouziti operatoru carka */
x=va_arg(seznam,double);/* cteni dalsiho parametru typu double */
if(x>amax) amax=x;
}
va_end(seznam); /* ukonceni cteni ze seznamu argumentu */
return(amax);
}
Datové typy
V první kapitole jsme probrali základní datové typy jazyka C - datové typy char, int, float, double. Zmínili jsme se stručně i o datových modifikátorech short, long a const.
V této kapitole se budeme nejprve zabývat oblastí platnosti identifikátorů, globálními a lokálními proměnnými a dále typy ukazatel, pole, výčtový typ, struktura a union.
Globální a lokální proměnné
Program v jazyce C má obecně následující strukturu:
// globální definice a deklarace
// definice funkcí
// globální definice a deklarace
// definice funkcí
// atd.
Definice funkcí mohou obsahovat lokální definice a deklarace.
Globální proměnné
Definice proměnné je chápána kompilátorem jako definice globální proměnné, jestliže je v programu umístěna mimo funkce. Globální proměnná je přístupná ze všech funkcí, od místa definice globální proměnné do konce souboru, pokud není zastíněna lokální proměnnou.
Globální deklarace jsou deklarace proměnných, které jsou definovány v jiných souborech. Tyto deklarace jsou specifikovány použitím klíčového slova extern, proto se nazývají externí deklarace. Z hlediska každé funkce, která využívá globální proměnnou, je možné ji považovat za externí.
Příklad 2.1.1: Použití globálních proměnných v jiných souborech. Soubory hlavni.c a funkce.c mohou být kompilovány odděleně.
/* Soubor hlavni.c ***/
#include <stdio.h>
int gl; /* definice globalni promenne gl */
int main( void )
{
gl=10;
printf("\nHodnota funkce f pro x=%d je rovna %d\n",5,f(5));
return 0;
}
/* Soubor funkce.c ***/
int f(int x)
{
extern int gl; /* deklarace externi promenne */
return(gl+x);
}
Hodnota funkce f pro x=5 je rovna 15
| Pokud by globální proměnnou gl v souboru funkce.c využívalo více funkcí, bylo by možné umístit externí deklaraci na začátek souboru mimo funkce, tj. použít globální deklaraci extern. |
Kdybychom chtěli zamezit použití globální proměnné v jiných souborech než byla definována, je možné použít v její definici klíčové slovo static. Inicializace globálních proměnných se provádí právě jednou. V případě, že není globální proměnná inicializována, má implicitní hodnotu 0.
O možnosti zastínění proměnné lokální proměnnou jsme se zmínili v části 4.3.
Poznámka: Samostatný překlad jednotlivých funkcí programu, popřípadě různých souborů funkcí, lze obejít příkazem #include:
/* Soubor hlavni.c ***/
#include <stdio.h>
#include "funkce.c" /* eventuálně včetně cesty k souboru */
int gl; /* definice globalni promenne gl */
main()
{
gl=10;
printf("\nHodnota funkce f pro x=%d je rovna %d\n",5,f(5));
return;
}
Lokální proměnné
Lokálními proměnnými jsme se zabývali v části 4.3. Je-li proměnná definována uvnitř nějaké funkce (a tedy i uvnitř nějakého bloku), potom je chápána jako lokální v tomto bloku, tj. je možné ji využívat pouze v rámci tohoto bloku a z jiných částí programu není viditelná.
Implicitní paměťová třída lokálních proměnných je třída auto, tzn. že není-li řečeno jinak, při opuštění příslušného bloku (např. při ukončení funkce) proměnná zaniká a vyhrazený paměťový prostor je uvolněn pro další použití. Těmto proměnným se v jazyku C říká automatické. Odtud jsou odvozeny termíny: automatické pole, automatická struktura atd.
Pokud v definici lokální proměnné použijeme klíčové slovo static a explicitně ji tedy deklarujeme jako proměnnou této paměťové třídy, nezaniká hodnota proměnné při opuštění příslušného bloku nebo ukončení funkce, paměť je těmto proměnným vyhrazena trvale. Těmto proměnným se v jazyku C říká statické. Obdobně hovoříme o statických polích, statických strukturách apod.
Inicializace statických proměnných proběhne vždy pouze jednou, přesto že např. funkce, kde je statická proměnná definována, může být vyvolána vícekrát. Pokud nepoužijeme explicitní inicializaci, jsou statické proměnné inicializovány implicitně nulou. Automatickým proměnným musí být hodnota přiřazena, protože jinak je jejich hodnota při vstupu do bloku nedefinována. Inicializace automatických proměnných se provádí pokaždé při vstupu do bloku. ANSI norma jazyku C narozdíl od normy K&R připouští rovněž inicializaci automatických polí, podrobněji se budeme touto otázkou zabývat v odst. . (Automatická pole, která se v původním jazyku K&R nesměla inicializovat, měla však počáteční hodnotu implicitně 0).
#include <stdio.h>
main()
{ pocitadlo(); pocitadlo(); pocitadlo();
}
pocitadlo()
{ static int i=0;
printf("\nKolikrat byla funkce vyvolana? %d-krat! ",++i); return;
}
Jestliže je využívána některá lokální proměnná velice často (např. proměnná cyklu), je možné ji definovat s použitím klíčového slova register. Překladač se v tomto případě pokusí vygenerovat kód, kde by se pro příslušnou proměnnou využíval přímo některý registr procesoru, což má za následek urychlení práce s touto proměnnou.
Ukazatelé
Ukazatel (směrník, spoj, pointer) je proměnná obsahující adresu jiné proměnné.
Ukazatelé jsou mocným nástrojem jazyka C, kterým lze vytvořit velice efektivně fungující programy. Při jejich nepozorném použití lze však snadno získat i velice neočekávané výsledky.
Operátory reference a dereference
Předpokládejme, že i je proměnná typu integer a že p proměnná typu ukazatel. Operátor reference & umožňuje získat adresu proměnné i.
int i=0;
int *p;
p=&i; // v promenne p je odted adresa promenne i
tedy přiřadí adresu proměnné i do proměnné p.
V této situaci budeme říkat, že p ukazuje na proměnnou i.
p=&5; // chyba - literal 5 nema adresu
p=&(i+1); // chyba - nelze referencovat aritmeticky vyraz
Operátor dereference * naopak umožňuje získat hodnotu, která je uložena na určité adrese. Jestliže j je další proměnná typu int, pak příkaz
int i=0;
int *p = &i;
int j;
j=*p;
bude znamenat totéž co
j=i;
Operátor dereference rovněž umožňuje uložit hodnotu na určitou adresu: j=3; *p=j;
Hodnota proměnné j byla přiřazena na adresu obsaženou v proměnné p. Při tomto typu přiřazení je ale nutné, aby proměnná p byla ukazatel na typ int. Definicemi ukazatelů se budeme zabývat v dalším odstavci.
Definice proměnných typu ukazatel
Hodnotami typu ukazatel jsou adresy objektů. Každý ukazatel je spjat s typem objektu, na který může ukazovat. Význam tohoto spojení vysvětlíme v části věnované aritmetickým operacím s ukazateli, viz . Pro datový typ ukazatel není v jazyce C zavedeno klíčové slovo. Zápis int *p;
znamená, že proměnná p byla definována jako proměnná typu ukazatel na typ integer. Mnemotechnický význam tohoto zápisu je to, že *p je typu int, dereferenční operátor aplikovaný na proměnnou p tedy dává hodnotu typu int, tzn. že p ukazuje na na hodnotu typu int.
| Uvědomte si rozdíl mezi zápisy následujícího typu: |
const char *ptr; /* ukazatel na konstantni objekt (ukazatel se může měnit, sám objekt se měnit nemůže)*/
char *const ptr; /* konstantni ukazatel na objekt (ukazatel se nemůže měnit, sám objekt se měnit může)*/
char const *jmeno="Jan";
jmeno[0]='D'; /* spravne prirazeni, pointer 'jmeno' ukazuje
nyni na retezec "Dan" */
jmeno="Jana"; /* nespravne prirazeni, pointer je konstantni */
const char *objekt="OBJEKT1";
objekt="OBJEKT2";/* spravne prirazeni, byla zmenena hodnota
ukazatele; hodnotou je adresa retezce OBJEKT2 */
objekt[6]='1'; /* nespravne prirazeni, retezec, na ktery
ukazuje pointer 'objekt' nelze menit */
Při definování proměnných typu ukazatel lze rovněž využít definici s inicializací, o které jsme se zmínili v 1. kapitole.
int i, j, *p_i, *p_j; int i, j, *p_i=&i, *p_j=&j;
p_i=&i;
p_j=&j;
Oba uvedené zápisy mají stejný význam: Proměnné i, j byly definovány jako proměnné typu int a proměnné p_i, p_j jako ukazatelé na typ int. Proměnným (ukazatelům) p_i, p_j byly po řadě přiřazeny adresy proměnných i, j.
Ukazatel na typ void
Někdy je výhodné, aby ukazatel nebyl svázán s jedním datovým typem. V tomto případě se používá definice ukazatele na typ void.
int main( void )
{
int i=3,j=10;
float f=3.5,g=10.5;
void *p; /* Definice ukazatele na typ void */
printf("\ni=%2d, f=%5.1f",i,f);
/*Nasleduje prirazeni 'i=j;' vyuziva se neprimy pristup k promenne i */
p=&i; /*Pri urcení hodnoty pointeru neni pretypovani nutne */
*(int *)p=j; /*Pretypovani (int *) je nutne, prirazujeme hodnotu
typu integer na adresu, kam ukazuje pointer p. */
/*Nasleduje prirazeni 'f=g;' vyuziva se neprimy pristup k promenne f */
p=&f; /*Pri urcení hodnoty pointeru neni pretypovani nutne */
*(float *)p=g;/*Pretypovani (float *) je nutne, prirazujeme hodnotu
typu float na adresu, kam ukazuje pointer p. */
printf("\ni=%2d, f=%5.1f",i,f);
return 0;
}
|
Aritmetické operace s ukazateli
Pro ukazatele jsou definovány některé aritmetické operace. V následujících odstavcích předpokládáme, že p je ukazatel na konkrétní datový typ, tj. že není definován jako ukazatel na typ void, a že n je celé číslo.
Součet a rozdíl ukazatele a celého čísla
Výraz p+n je adresový výraz, který má hodnotu adresy n-tého prvku "za" prvkem, na který právě ukazuje p. K ukazateli p se tedy nepřičítá hodnota n, ale násobek této hodnoty a velikosti typu, na který p ukazuje.
Obdobně výraz p-n je adresový výraz, který má hodnotu adresy n-tého prvku "před" prvkem, na který právě ukazuje p.
Poznámka: Slova "za" a "před" zde samozřejmě mají své obvyklé názorné významy jen pro kladné hodnoty n. Čtenář jistě chápe, jaké hodnoty budou mít uvedené adresové výrazy pro záporné hodnoty n.
Porovnávání ukazatelů
Pro porovnání velikostí ukazatelů stejného typu p1, p2 můžeme použít relační operátory < ⇐ > >= == !=
Výrazy typu p1<p2 mají smysl pouze tehdy, jestliže p1, p2 ukazují na stejný úsek paměti, tj. např. na stejné pole. Výraz p1<p2 je nenulový jestliže hodnota p1 je menší než hodnota p2, v opačném případě má výraz hodnotu 0.
Odečítání ukazatelů
Výraz p1-p2 má smysl v případě, že p1, p2 ukazují na stejné pole dat. V tomto případě slouží ke zjištění počtu položek pole, které jsou uloženy mezi položkami, na které ukazují p1 a p2.
Operátor sizeof a aritmetické operace s ukazateli
Klíčové slovo sizeof označuje operátor, který je určen k získání velikosti zkoumaného datového typu ve slabikách (bytech).
Příklady: Předpokládejme, že a je proměnná typu int a p_d je ukazatel na typ double. Jako argument operátoru sizeof lze použít proměnnou nebo klíčové slovo označující datový typ: sizeof(a) sizeof(p_d) sizeof(*p_d) sizeof(int)
Např. první z uvedených výrazů znamená počet slabik (bytů) nutných k uložení hodnoty proměnné a, tj. hodnoty typu int, druhý výraz určuje počet slabik, nutných k uložení ukazatele na typ double. Třetí výraz je příkladem často využívané možnosti jak zjistit, kolik slabik je nutných pro uložení objektu, jehož adresa je zadaná určitým ukazatelem.
Pomocí operátoru sizeof lze výsledky některých aritmetických operací s ukazateli vyjádřit takto: p+n (char *)p+n*sizeof(*p) p-n (char *)p-n*sizeof(*p) p1-p2 ((char *)p1-(char *)p2)/sizeof(*p)
Ukazatelé a řetězcové konstanty
Řetězcová konstanta reprezentuje konstantní ukazatel, tj. adresu místa v paměti, které bylo alokováno pro její obsah.
Příklad 2. by 1 12: char *p; char *p="AHOJ!"; p="AHOJ!";
Pointer p tedy nyní ukazuje na začátek bloku paměti alokovaného pro řetězec ÄHOJ!", a je tedy možné ho použít pro přístup k této části paměti:
// příkaz: výstup:
printf("%s","AHOJ!"); // AHOJ!
printf("%s",p); // AHOJ!
for(i=0;i<2;i++)printf("%c%c",p[3],p[2]); // JOJO
Konverze %s je určená pro vstup a výstup řetězců, %c pro vstup a výstup znaků. Ke třetímu z uvedených příkladů je třeba dodat, že překladač interpretuje indexový výraz p[3] jako *(p+3). Podrobněji se budeme zabývat indexovými výrazy v části .
Ukazatelé a funkce
Výstupní parametry funkcí
V první kapitole jsme se zabývali případem, kdy parametry funkce nemění během vyvolání funkce svoji hodnotu, tj. parametry funkce jsou jejími vstupními parametry a výstup je realizován návratovou hodnotou funkce.
Důležitou vlastností ukazatelů je to, že umožňují použít část parametrů funkce jako parametry výstupní, tzn. umožňují trvale změnit hodnotu skutečného parametru funkce.
V programovacím jazyku C jsou parametry funkcí volány hodnotou. Volání odkazem v C sice neexistuje, ale s pomocí ukazatelů lze dosáhnout stejného efektu.
Volání parametrů funkce hodnotou spočívá v tom, že při vyvolání funkce se v tzv. zásobníku (část vnitřní paměti - stack) vytvoří lokální kopie pro uložení parametrů. Případné změny parametrů se týkají pouze těchto lokálních proměnných, které zaniknou s ukončením funkce.
Chceme-li aby došlo k trvalé změně hodnoty proměnné, použijeme jako parametr funkce adresu této proměnné. V zásobníku se vytvoří lokální kopie pro uložení této adresy. Tato lokální proměnná sice zaniká s ukončením příslušné funkce, ale pomocí adresy kterou obsahuje, můžeme nepřímým přístupem změnit hodnotu příslušné proměnné.
Ukažme si tento postup na jednoduchém příkladu.
suma(double a, double b, double *p_c)
{
*p_c=a+b;/* Soucet se ulozi na adresu, kterou zadava parametr p_c */
/* Nepřímý přístup k proměnné c pomocí její adresy */
return;
}
Jsou-li a,b,c proměnné typu double definované v hlavní funkci main(), pak vyvolání funkce suma() v hlavním programu může vypadat takto:
suma(a,b,&c);
#include <stdio.h>
suma(double a, double b, double *c); /* Funkcni prototyp */
/* Funkcni prototyp lze i takto: suma(double,double,double *) */
int main(void)
{
double a,b,c;
printf("\nZadej cisla a, b : ");
scanf("%lf %lf",&a,&b); /* Cteni hodnot 2 cisel double */
suma(a,b,&c); /* Vypocet souctu techto cisel */
/* 3. parametr je adresa promenne c */
printf("\n%f +%f =%f",a,b,c); /* Tisk vysledku */
return 0;
}
/* Definice funkce suma(), parametr p_c je ukazatel na typ double */
suma(double a, double b, double *p_c)
{
*p_c=a+b; /* Soucet se ulozi na adresu, kterou zadava parametr p_c */
/* Nepřímý přístup k proměnné c pomocí její adresy */
return;
}
Funkce jako parametr funkcí
Často se využívá možnosti definovat proměnnou jako ukazatel na funkci vracející nějaký typ. Např. double (*f)();
definuje proměnnou f jako ukazatel na funkci vracející typ double. Podrobněji je možné např. vymezit proměnnou g jako ukazatel na funkci vracející typ double, která má jeden parametr typu double. double (*g)(double);
Máme-li nyní definovánu nějakou funkci tohoto typu, např. funkci sin() , (její funkční prototyp je obsažen v hlavičkovém souboru math.h), je možné provést přiřazení: f=sin;
Překladač jazyka C zachází s identifikátorem funkce jako s ukazatelem na funkci, zmíněné přiřazení tedy bude znamenat, že pointry f, sin budou ukazovat na tutéž funkci. Odkaz na funkci sin() lze nyní zapsat jedním ze dvou následujících způsobů: (*f)(x) f(x)
První způsob byl zaveden K&R normou, ANSI norma připouští obě možnosti.
Přiřazení adresy funkce proměnné typu ukazatel na funkci může být provedeno při vyvolání funkce a náhradě formálního parametru skutečným parametrem. Identifikátory funkcí je možné použít jako skutečné parametry funkce.
Zapišme např. funkci která vytiskne tabulku hodnot funkce f, v intervalu [a,b] s krokem h.
tabel(double a, double b, double h, double (*f)(double x))
{ double x;
for(x=a;x<=b;x+=h){
printf("%15.5f \t %15.5f \n",x,f(x));
/* misto f(x) lze pouzit (*f)(x) */
}
return;
}
Poslední parametr funkce je definován jako ukazatel na funkci vracející typ double. Předpokládejme, že máme např. definovanou funkci polynom():
double polynom(double x)
{ return(x*x*x+x+1.);
}
Vyvolání funkce tabel v hlavním programu by tedy mohlo vypadat např. takto: tabel(0.,2.,0.1,polynom);
Zapišme nyní celý program:
#include <stdio.h>
#include <conio.h> /* Prototyp fce clrscr() - smazani monitoru */
double polynom(double x); /* Prototypy funkci polynom, tabel: */
tabel(double d, double h, double k, double (*f)(double x));
/* Funkcni prototypy lze zapsat i s nepojmenovanymi parametry takto:
double polynom(double);
tabel(double, double, double, double (*)(double)); */
main()
{ tabel(0.,2.,0.1,polynom); return;
}
tabel(double a, double b, double h, double (*f)(double x))
{ double x;
clrscr();
for(x=a;x<=b;x+=h){
printf("%15.5f \t %15.5f \n",x,f(x));
/* misto f(x) lze pouzit (*f)(x) */
} return;
}
double polynom(double x)
{ return(x*x*x+x+1.);
}
Materiál posledních dvou odstavců je použit v následujícím příkladu programu určeném pro řešení rovnice f(x) = 0 metodou půlení intervalů na intervalu [a,b]. Předpokládáme samozřejmě, že funkce f je spojitá na intervalu [a,b]. Algoritmus metody půlení intervalů bude naprogramován v podobě funkce pul(). Tato funkce určí řešení rovnice se zadanou přesností v případě, že platí f(a)f(b) ≤ 0. Funkce pul() má následující funkční prototyp:
int pul(double *p_a, double *p_b, int *p_max, double eps, double *p_x, double (*f)(double x))
Proměnné p_a, p_b jsou ukazatelé na proměnné, které na vstupu obsahují krajní body zadaného intervalu [a,b] , na výstupu krajní body intervalu, ve kterém byl v průběhu výpočtu lokalizován kořen rovnice. Proměnná p_max je ukazatel na proměnnou zadávající na vstupu maximální počet iterací, na výstupu číslo iterace, při které byla splněna požadovaná přesnost eps. Proměnná f je ukazatel na funkci, pro kterou se řeší rovnice f(x) = 0. Funkce pul() má návratovou hodnotu typu int, která je definována podle toho, jak byl ukončen výpočet kořene rovnice: pul=-1: nevhodné zadání pul=1 : nalezeno přesné řešení, uloží se na adresu zadanou proměnnou p_x , pocet provedenych iterací se uloží na adresu p_max pul=2 : nalezeno řešení se zadanou přesností, řešení leží v intervalu s krajními body na adresách p_a, p_b, počet provedených iterací je na adrese p_max pul=3 : přesnost nedosažena, vypočet ukončen po provedení max kroků
Funkce pul() je vyvolána v hlavním programu příkazem pul(&a,&b,&max,eps,&x,g);
kde g je funkce zadaná samostatnou programovou jednotkou:
double g(double x)
{ return(x*x*x+11.);
}
Celý program je tedy rozčleněn do tří programových jednotek main(),pul(), g():
#include <stdio.h>
#include <math.h>
double g(double x);
int pul(double *p_a, double *p_b, int *p_max, double eps,
double *p_x, double (*f)(double x));
/* lze pouzit i nasledujici funkcni prototypy s anonymnimi parametry:
double g(double);
int pul(double *, double *, int *, double,
double *, double (*)(double));
********************************************************************/
main()
{
double a,b,x,eps; int max,ier;
printf("\n\n\nMetoda puleni intervalu:\n");
printf("\nZadej krajni body intervalu, presnost,"
" maximalni pocet kroku");
printf("\na=");scanf("%lf",&a); printf("b=");scanf("%lf",&b);
printf("eps=");scanf("%lf",&eps); printf("max=");scanf("%d",&max);
switch(pul(&a,&b,&max,eps,&x,g)){
case 1: printf("\nPresne reseni x=%f nalezeno po %d krocich",
x,max); break;
case 2: printf("\nReseni nalezeno v intervalu (%f,%f)"
"\nPresnost splnena po %d krocich",a,b,max); break;
case 3: printf("\nReseni nalezeno v intervalu (%f,%f)"
"\nPresnost nesplnena po %d krocich",a,b,max);
break;
default:printf("\nChybne vstupni udaje!"); break;
}
return;
}
/********************************************************************
** Funkce pul() : reseni rovnice metodou puleni intervalu
** Navratove hodnoty funkce pul:
** pul=-1: NEVHODNE ZADANI
** pul=1 : PRESNE RESENI, reseni na adrese p_x,
** pocet provedenych kroku na adrese p_max
** pul=2 : RESENI SE ZADANOU PRESNOSTI,
** reseni lezi v intervalu s krajnimi body na adresach
** p_a ,p_b pocet provedenych kroku na adrese p_max
**
** pul=3 : PRESNOST NESPLNENA vypocet ukoncen po provedeni max kroku
*********************************************************************/
int pul(double *p_a, double *p_b, int *p_max, double eps, double *p_x,
double (*f)(double x)) /* lze i takto: double (*f)(double) */
{
double s,c; int i,max;
s=f(*p_a)*f(*p_b);
if(s > 0.) return(-1); /* Chybne zadani pul=-1 */
if(s == 0.){ /* Pripad: Presne reseni */
*p_x=( f(*p_a) ? *p_b : *p_a); /* Koren ulozen na adresu p_x */
*p_max=0; /* Pocet provedenych iteraci ulozen na adresu p_max */
return(1); /* Navratova hodnota pul=1 */
}
max=*p_max;
for(i=1;i<=max;i++){
if(fabs(*p_b-*p_a)<eps) {
*p_max=i; return(2);} /* Reseni s presnosti eps */
*p_x=(*p_a + *p_b)/2.; /* Novy krok metodou puleni*/
if(f(*p_x)==0) {*p_max=i; return(1);} /* Presne reseni */
if(f(*p_a)*f(*p_x)< 0.) /* Vyber noveho intervalu */
*p_b=*p_x;
else
*p_a=*p_x;
}
return(3);
}
/*******************************************************************/
double g(double x)
{ return(x*x*x+11.);
}
Pole
Každá proměnná, kterou jsme dosud v našich programech používali, nabývala v každém okamžiku pouze jedné hodnoty určitého typu. V této kapitole začínáme studovat tzv. složené (agregované) datové typy. Proměnné složených typů označují zpravidla skupinu určitých hodnot. Do této třídy patří pole, struktury a uniony.
Pole je datový typ používaný pro ukládání skupiny hodnot stejného typu. Prvky jednoho pole jsou rozlišeny tzv. indexem.
Definice pole, prvky pole
Pole je nutné definovat v programovacím jazyku C jako jiné proměnné. Definice má následující tvar:
typ_pole identifikátor_pole[počet_prvků];
typ_pole může označovat libovolný ze základních datových typů, se kterými jsme se doposud setkali kromě typu void. Později ukážeme, že typ_pole může označovat i složené datové typy. Pro zápis identifikátoru_pole platí stejná pravidla jako pro zápis identifikátorů proměnných jiných typů. počet_prvků je konstantní výraz, určující počet prvků pole. Přístup k jednotlivým prvkům lze získat výrazem identifikátor_pole[index], kde celočíselný výraz index nabývá hodnot
0, 1, …, počet_prvků-1
Např. definicí int a[5];
je v paměti alokováno místo pro 5 hodnot typu int, přístup k nim je zajištěn indexovanými výrazy a[0], a[1], a[2], a[3], a[4]
Zmíněné hodnoty (v našem případě typu int) jsou v paměti ukládány za sebou.
Je třeba si uvědomit, že překladač jazyka C neprovádí kontrolu mezí. Přiřazení a[5]=0;
je syntakticky správné a provede se i když s případnými katastrofálními následky, neboť bude přepsána část paměti, která nebyla alokována pro pole a.
Následující jednoduchý program ilustruje definici a použití jednorozměrného pole.
/*
** Program precte 5 cisel ze standardniho vstupu
** a v obracenem poradi je vytiskne.
*/
#include <stdio.h>
#define POCET 5
main()
{
int i; /* promenna cyklu */
int a[POCET]; /* definice pole typu typu int */
printf("\n\n\n Zadej %d cisel\n",POCET);
for(i=0;i<POCET;i++) /* cteni vstupnich dat v cyklu */
scanf("%d",&a[i]); /* adresa prvku pole : &a[i] */
for(i=POCET-1;i>=0;i--)
printf("\n%d",a[i]);
}
Jedna z obvyklých úloh souvisejících s jednorozměrným polem je jeho uspořádání. Uvedený program provádí uspořádání konečné posloupnosti v neklesajícím pořádku tzv. bublinkovým tříděním.
/* Program precte konecnou posloupnost racionalnich cisel
** Usporada posloupnost v neklesajicim poradku jednoduchym
** bublinkovym tridenim. Prvky usporadane posloupnosti
** vytiskne s jejich indexy.
*/
#include <stdio.h>
#define MAX 100
main()
{ int pocet;
int i,j;
double a[MAX],pomoc;
printf("\n\n\n\nZadej pocet prvku posloupnosti: ");
scanf("%d",&pocet);
if(pocet>MAX)
printf("\nTo je prilis mnoho, maximum je %d\n",MAX);
else /* pocet prvku byl programem akceptovan */
{
for(i=0;i<pocet;i++) { /* cteni prvku posloupnosti */
printf("a[%3d] : ",i+1);
scanf("%lf",&a[i]);
}
/* algoritmus jednoducheho bublinkoveho trideni : */
for(i=pocet-1;i>0;i--)
for(j=0;j<=i-1;j++)
if(a[j]>a[j+1])
{ pomoc=a[j]; a[j]=a[j+1]; a[j+1]=pomoc;
}
for(i=0;i<pocet;i++) { /* tisk usporadane posloupnosti */
printf("\na[%3d] : ",i+1);
printf("%f",a[i]);
}
} /* konec else */
}
Programovací jazyk C poskytuje také možnost použití vícerozměrných polí. Dvourozměrné pole je v jazyce C chápáno v podstatě jako "jednorozměrné pole jednorozměrných polí" atd. Definice i přístup k jednotlivým prvkům se tedy příliš neliší od jednorozměrného případu, včetně katastrofálních následků při chybné volbě indexu.
V následujícím příkladu použijeme dvourozměrné obdélníkové pole pro určení euklidovské normy obdélníkové matice.
2}.
n ai,j
m∑j = 1
Příklad 2. by 1 12: Program přečte obdélníkovou matici A = (ai,j), i = 1,m, j = 1,n ze standardního vstupu a spočítá její euklidovskou normu: ||A||E = √{∑i = 1
#include <stdio.h>
#include <math.h> /* math.h obsahuje funkcni prototyp sqrt() */
#define MAX 10
main(){
int m,n,i,j;
double a[MAX][MAX]; /* Definice dvourozmerneho pole */
double sum;
/* cteni rozmeru matice */
printf("\n\n\nZadej pocet radek a pocet sloupcu matice: ");
scanf("%d%d",&m,&n);
if(m>MAX||n>MAX){
printf("\nMaximum pro oba rozmery je %d",MAX); return;
}
/* cteni matice */
for(i=0;i<m;i++)
for(j=0;j<n;j++)
{ printf("a[%2d,%2d]: ",i+1,j+1);
scanf("%lf",&a[i][j]); /* Cteni prvku dvourozmerneho pole */
}
/* vypocet euklidovske normy matice */
for(sum=0,i=0;i<m;i++)
for(j=0;j<n;j++)
sum+=a[i][j]*a[i][j]; /* Scitani druhych mocnin prvku pole */
/* Tisk vysledku; sqrt() je funkce pro vypocet druhe odmocniny */
printf("\nEuklidovska norma matice = %f",sqrt(sum)); return;
}
Inicializace pole
Zároveň s definicí pole je možné v jazyce C provést jeho inicializaci. Např. jednorozměrné pole obsahující 7 prvků typu int může být inicializováno takto: int pole[7]={0,1,2,3,4,5,6};
Hodnota pole[0] je po takové inicializaci rovna 0, pole[1] se rovná 1 atd.
Počet prvků pole není nutné uvádět explicitně, překladač ho určí podle počtu hodnot uvedených ve složených závorkách. Definice stejného pole by tedy mohla vypadat i takto: int pole[]={0,1,2,3,4,5,6};
Poznámka:
-
Pokud je počet prvků pole definicí stanoven a je menší než počet hodnot uvedených ve složených závorkách je inicializace chybná.
-
Pokud je počet prvků pole větší, než počet hodnot uvedených v inicializaci, provede se inicializace pouze části prvků pole těmito hodnotami (začíná se prvkem s indexem 0). Ostatní prvky jsou buď inicializovány nulou - případ statického nebo globálního pole, nebo zůstanou nedefinovány - případ automatického pole, viz 1.2. (ANSI norma jazyka C povoluje inicializaci automatického pole, narozdíl od normy K&R.)
Pole typu char je možné inicializovat některým z následujících způsobů: char norma[]={'A','N','S','I',' ','C'}; char norma[]={"ANSI C"};
Při druhém způsobu inicializace, bude pole norma[] vypadat takto char norma[]={'A','N','S','I',' ','C','\0'};
tj. poslednímu prvku pole je překladačem přiřazen ukončovací znak řetězců.
Obdobným způsobem lze inicializovat i vícerozměrné pole. Následující dva způsoby inicializace obdélníkového pole tabulka jsou ekvivalentní: int tabulka[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
V definici je možné vynechat "počet řádek", "počet sloupců" vynechán být nesmí.
int tabulka[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
Obdobně lze definovat vícerozměrná pole typu char. Např. v části 4.8 jsme použili definici s inicializací dvourozměrného pole abc o velikosti 4x4 :
#define N 4
// ...
char abc[][N]={{'1','2','*','4'},
{'a','b','c','d'},
{'$','%','#','@'},
{'+','-','^','/'}};
Kdybychom chtěli inicializovat pole obsahující tyto znaky pomocí řetězců, museli bychom zvětšit délku "řádků" pole o jednu, každá "řádka" bude ukončena znakem '\0':
#define N 5
//...
char abc[][N]={{"1234"},{"abcd"},{"$%#@"},{"+-^/"}};
Ukazatelé a pole
Souvislost mezi ukazateli a poli je dána tím, že identifikátor určitého jednorozměrného pole je překladačem interpretován jako konstantní ukazatel na první prvek tohoto pole, tedy na prvek s indexem 0. Předpokládejme např., že a je následující pole typu double: double a[10];
Identifikátor a je tedy konstantní ukazatel na typ double. Z toho co jsme řekli vyplývá, že následující dvojice výrazů mají stejné hodnoty: a &a[0] *a a[0] *(a+i) a[i]
Definujeme-li teď např. ukazatel p jako ukazatel na typ double double *p;
pak po přiřazení p=a; se následující výrazy v jednotlivých řádkách rovnají: a &a[0] p &p[0] *a a[0] *p p[0] *(a+i) a[i] *(p+i) p[i]
Ukazatel p je možné měnit (narozdíl od a). Po přiřazení p++; dostaneme: a &a[0] p-1 &p[-1] *a a[0] *(p-1) p[-1] *(a+1) a[1] *p p[0] *(a+i) a[i] *(p+i-1) p[i-1]
Pro dvourozměrné pole, definované zápisem double b[10][20];
je b ukazatel na ukazatel na typ double, tj. b je ukazatel na jednorozměrná pole typu double (o délce 20 prvků), b[i] je ukazatel na typ double, tj. b[i] je pole typu double (o délce 20 prvků), b[i][j] je
typu double a platí: b &b[0] *b b[0] &b[0][0] **b *b[0] b[0][0] *(b+i) b[i] &b[i][0] *((*b+i)+j) *(b[i]+j) b[i][j]
Jako příklad můžeme použít program z části 4.1, zapsaný tentokrát pomocí pointerů.
/*
** Program precte 5 cisel ze standardniho vstupu
** a v obracenem poradi je vytiskne.
*/
#include <stdio.h>
#define POCET 5
main()
{
int a[POCET];/* definice pole typu typu int */
int *p; /* ukazatel použitý pro přístup k jednotlivým prvkům */
printf("\n\n\n Zadej %d cisel\n",POCET);
for(p=a;p<&a[POCET];p++) /* cteni vstupnich dat v cyklu */
scanf("%d",p); /* p je adresa prvku pole: &a[i] */
for(p=&a[POCET-1];p>=a;p--)
printf("\n%d",*p);
}
Pole jako parametr funkce
Budeme se zabývat následující modelovou úlohou: Jsou zadána pole a, b stejného typu. Sestavte program pro výpočet součtu a+b. Součet polí a+b budeme definovat jako pole c, jehož každý prvek se rovná součtu stejnolehlých prvků polí a a b, tj. prvků polí a a b, které mají stejné indexy jako příslušný prvek pole c. Budeme se zabývat touto úlohou postupně pro jednorozměrná, dvourozměrná a trojrozměrná pole. (Rozměrností pole zde, tak jak je obvyklé, rozumíme počet indexů.) Proceduru pro sčítání polí zapíšeme jako funkci secti() s parametry a, b a c volanou funkcí main(). V následujícím programu, rozčleněném do dvou souborů je popsaná úloha zpracovaná pro jednorozměrná pole.
/******************* soubor 1d_pole.c ******************/
#include <stdio.h>
#include <conio.h>
#define MAX 30
/* funkcni prototypy funkci precti(), tiskni() a secti(): */
precti(int, int [], const char *);
tiskni(int, int [], const char *);
secti(int, int [], int [], int []);
main()
{
int n;
int a[MAX], b[MAX], c[MAX];
clrscr();
printf("\n Zadej pocet prvku n vektoru a,b (n <= %d) : ",MAX);
scanf("%d",&n);
/* cteni a kontrolni tisk vektoru a,b: */
precti(n,a,"a"); tiskni(n,a,"a"); precti(n,b,"b"); tiskni(n,b,"b");
/* vypocet a tisk vektoru c: */
secti(n,a,b,c); /* VYVOLANI FUNKCE secti() */
tiskni(n,c,"c"); return;
}
Všimněte si, že ve funkčních prototypech není třeba udávat počet prvků jednorozměrných polí. Všimněte si také rozdílu mezi vyvoláním funkce suma(), určené pro součet hodnot dvou jednoduchých proměnných typu double, viz 3.1, a funkce secti(): suma(a,b,&c); secti(n,a,b,c);
Zápis &c ve vyvolání funkce suma() znamená adresu proměnné c, zápis c ve vyvolání funkce secti() znamená adresu prvního prvku pole c.
Definice funkcí precti(), secti(), tiskni() obsahuje další soubor. Také v hlavičkách definic jednotlivých funkcí není třeba udávat počet prvků jednorozměrných polí.
/********************* soubor secti_1d.c ***********************/
#include <stdio.h>
precti(int n, int a[], const char *s)
{
int i;
printf("\n");
for(i=0;i<n;i++){
printf("%s[%2d] : ",s,i); scanf("%d",&a[i]);
}
return;
}
tiskni(int n, int a[], const char *s)
{
int i;
printf("\nTisk vektoru %s :\n",s);
for(i=0;i<n;i++)
printf( (i%10==9 ? " %4d\n":" %4d"), a[i]);
return;
}
secti(int n, int a[], int b[], int c[])
{
int i;
for(i=0;i<n;i++)
c[i]=a[i]+b[i];
return;
}
V následujícím příkladu je analogická úloha řešena pro dvourozměrná pole. Pro správnou funkci programu může být při definici parametrů - dvourozměrných polí - ve funkčních prototypech a v hlavičkách definic jednotlivých funkcí vynechán pouze první rozměr pole, druhý musí být uveden.
/******************* soubor 2d_pole.c ******************/
#include <stdio.h>
#include <conio.h>
#define M_MAX 30
#define N_MAX 40
/* funkcni prototypy funkci precti(), tiskni() a secti(): */
precti(int, int, int [][N_MAX], const char *);
secti(int, int, int [][N_MAX], int [][N_MAX], int [][N_MAX]);
tiskni(int, int, int [][N_MAX], const char *);
main()
{
int m,n;
int a[M_MAX][N_MAX], b[M_MAX][N_MAX], c[M_MAX][N_MAX];
clrscr();
printf("\n Zadej pocet radek m poli a,b (m <= %d) : ",M_MAX);
scanf("%d",&m);
printf("\n Zadej pocet sloupcu n poli a,b (n <= %d) : ",N_MAX);
scanf("%d",&n);
precti(m,n,a,"a"); tiskni(m,n,a,"a");
precti(m,n,b,"b"); tiskni(m,n,b,"b");
secti(m,n,a,b,c);
tiskni(m,n,c,"c");
return;
}
/********************* soubor secti_2d.c ***********************/
#include <stdio.h>
#define N_MAX 40
precti(int m, int n, int a[][N_MAX], const char *s)
{
int i,j;
printf("\n");
for(i=0;i<m;i++)
for(j=0;j<n;j++){
printf("%s[%2d][%2d] : ",s,i,j); scanf("%d",&a[i][j]);
}
return;
}
tiskni(int m, int n, int a[][N_MAX], const char *s)
{
int i,j;
printf("\nTisk matice %s :\n",s);
for(i=0;i<m;i++)
for(j=0;j<n;j++)
printf( (j%10==9||j==n-1 ? " %4d\n":" %4d"), a[i][j]);
return;
}
secti(int m, int n, int a[][N_MAX], int b[][N_MAX], int c[][N_MAX])
{
int i,j;
for(i=0;i<m;i++)
for(j=0;j<n;j++)
c[i][j]=a[i][j]+b[i][j];
return;
}
Nevýhoda tohoto řešení spočívá v tom, že funkce precti(), secti(), tiskni() nejsou univerzální v tom smyslu, že konstantu N_MAX je nutné upravit podle funkce main(). V následující variantě programu je tento nedostatek odstraněn.
/******************* soubor 2dpole.c *******************/
#include <stdio.h>
#include <conio.h>
#define M_MAX 30
#define N_MAX 40
/* funkcni prototypu funkci precti(), tiskni() a secti(): */
precti(int, int, int *[], const char *);
secti(int, int, int *[], int *[], int *[]);
tiskni(int, int, int *[], const char *);
main()
{
int m,n,i;
int a[M_MAX][N_MAX], b[M_MAX][N_MAX], c[M_MAX][N_MAX];
/* definice pomocnych poli pointeru zajistujicich pristup k a,b,c */
int *ua[M_MAX], *ub[M_MAX], *uc[M_MAX];
clrscr();
printf("\n Zadej pocet radek m poli a,b (m <= %d) : ",M_MAX);
scanf("%d",&m);
printf("\n Zadej pocet sloupcu n poli a,b (n <= %d) : ",N_MAX);
scanf("%d",&n);
/* vytvoreni pristupu k prvkum pole pomoci ukazatelu */
for(i=0;i<M_MAX;i++){
ua[i]=&a[i][0]; ub[i]=&b[i][0]; uc[i]=&c[i][0];
}
/*******************************************************************/
precti(m,n,ua,"a"); tiskni(m,n,ua,"a");
precti(m,n,ub,"b"); tiskni(m,n,ub,"b");
secti(m,n,ua,ub,uc); tiskni(m,n,uc,"c"); return;
}
Analogické změny jako ve funkčních prototypech uděláme i v hlavičkách definic funkcí precti(), secti(), tiskni(). Těla těchto funkcí zůstanou nezměněna.
/********************* soubor secti2d.c ************************/
#include <stdio.h>
precti(int m, int n, int *a[], const char *s)
{ ...}
tiskni(int m, int n, int *a[], const char *s)
{ ...}
secti(int m, int n, int *a[], int *b[], int *c[])
{ ...}
Uveďme si ještě stručně obdobné řešení pro trojrozměrná pole. Namísto označení analogické M_MAX, N_MAX použijeme v trojrozměrné situaci symbolické konstanty M, N, R.
/******************* soubor 3dpole.c *******************/
#include <stdio.h>
#include <conio.h>
#define M 10
#define N 10
#define R 10
/* funkcni prototypy funkci precti(), tiskni() a secti(): */
precti(int, int, int, int **[], const char *);
tiskni(int, int, int, int **[], const char *);
secti(int, int, int, int **[], int **[], int **[]);
main()
{
int m,n,r,i,j;
int a[M][N][R], b[M][N][R], c[M][N][R];
/*** pomocna jedno- a dvourozmerna pole pointeru */
int *ua[M][N], *ub[M][N], *uc[M][N];
int **uua[M], **uub[M], **uuc[M];
/*** nasleduje cteni m,n,r */
...
/*** vytvoreni pristupu k prvkum pole pomoci ukazatelu */
for(i=0;i<M;i++){
for(j=0;j<N;j++){
ua[i][j]=&a[i][j][0]; ub[i][j]=&b[i][j][0]; uc[i][j]=&c[i][j][0];
}
}
for(i=0;i<M;i++){
uua[i]=&ua[i][0]; uub[i]=&ub[i][0]; uuc[i]=&uc[i][0];
}
/*******************************************************************/
precti(m,n,r,uua,"a"); tiskni(m,n,r,uua,"a");
precti(m,n,r,uub,"b"); tiskni(m,n,r,uub,"b");
secti(m,n,r,uua,uub,uuc); tiskni(m,n,r,uuc,"c"); return;
}
/********************* soubor secti3d.c ************************/
#include <stdio.h>
precti(int m, int n, int r, int **a[], const char *s)
{...}
tiskni(int m, int n, int r, int **a[], const char *s)
{...}
secti(int m, int n, int r, int **a[], int **b[], int **c[])
{...}
Operátor typedef
Operátor typedef je určen pro vytvoření nového označení určitého datového typu. Používá se pro zvýšení přehlednosti programu. Při definicích jednoduchých datových typů se příliš nepoužívá, v definicích ukazatelů, polí a dalších odvozených datových typů je jeho použití daleko častější.
Příklad 2. by 1 12: typedef int ROCNIK; typedef int *P_INT; typedef int POLE[10]; ROCNIK a,b,c; P_INT p1,p2,p3; POLE x,y,z;
typedef double (*FUN)(double, int); FUN f1,f2;
V prvním příkladu bylo vytvořeno nové jméno ROCNIK pro označení datového typu typu int a pomocí tohoto jména byly definovány proměnné a,b,c. Ve druhém případě byly proměnné p1,p2,p3 definovány jako ukazatelé na typ int pomocí nově vytvořeného označení P_INT tohoto typu. Ve třetím případě byly definovány proměnné x,y,z jako jednorozměrná pole typu int obsahující 10 prvků. V posledním případě byl definován typ ukazatel na funkci vracející hodnotu double s parametry typu double a int. V dalších odstavcích se zmíníme o dalších možnostech použití operátoru typedef.
Výčtový typ
Výčtový typ se často používá pro definici proměnných, které mohou nabývat pouze konečného počtu hodnot.
Příklad 2. by 1 12:
enum den{PONDELI,UTERY,STREDA,CTVRTEK,PATEK,SOBOTA,NEDELE}den1,den2;
Proměnné den1,den2 zde byly definovány jako proměnné výčtového typu den, tj. jako proměnné , které mohou nabývat jedné z uvedených hodnot: {PONDELI,UTERY,STREDA,CTVRTEK,PATEK,SOBOTA,NEDELE}
Pokud nebyl vynechán identifikátor výčtového typu, (v našem případě identifikátor den,) je možné použít ho pro definici proměnných v jiném místě programu: enum den den3, den4;
Jinou možností je zavedení nového označení datového typu operátorem typedef: enum den{PONDELI,UTERY,STREDA,CTVRTEK,PATEK,SOBOTA,NEDELE}; typedef enum den DEN;
Definici proměnných den1, den2, den3, den4 lze pomocí nově zavedeného typu DEN zapsat takto: DEN den1,den2,den3,den4;
Hodnoty uvedené ve výčtu datového typu enum jsou reprezentovány hodnotami typu int. Jednotlivým položkám jsou přiřazena celá čísla počínaje od nuly a zvyšující se o 1. V našem příkladu má tedy PONDELI hodnotu 0 a NEDELE hodnotu 6. Toto implicitní přiřazení může být změněno, jestliže přiřadíme explicitně některé položce seznamu explicitní hodnotu.
Příklad 2. by 1 12: enum krev_skupiny{NULA,A,B,AB,BA=3};
Konstanty AB a BA zde budou označovat stejné hodnoty.
Nově zavedený typ může být použit např. pro definici návratové hodnoty funkce, nebo typu parametru funkce:
Příklad 2. by 1 12: DEN zitra(DEN d) { DEN z; z=(DEN)((int)d+1)%7); /* staci ovsem z = (d+1)%7; */ return(z); }
Struktury
Struktura je odvozený datový typ charakteristický tím, že narozdíl od pole může obsahovat datové položky různých typů.
Definice struktury, položky struktury
Ukážeme si základní rysy práce se strukturami na příkladu: Definujme strukturu clovek obsahující datové položky vek, vyska, vaha: struct clovek{int vek; int vyska; double vaha;}c1,c2,c3;
Proměnné c1, c2, c3 zde byly definovány jako proměnné typu struktura clovek. Pokud nebyl vynechán v definici identifikátor struktury, (v našem případě identifikátor clovek,) je možné použít ho pro definici proměnných v jiném místě programu: struct clovek c4, c5, c6;
Jinou možností je zavedení nového označení datového typu operátorem typedef: struct clovek{int vek; int vyska; double vaha;}; typedef struct clovek CLOVEK;
Definici proměnných c1,c2,c3,c4,c5,c6 lze pomocí nově zavedeného typu CLOVEK zapsat takto: CLOVEK c1,c2,c3,c4,c5,c6;
Datový typ CLOVEK lze zavést i zápisem v obráceném pořadí: typedef struct clovek CLOVEK; struct clovek{int vek; int vyska; double vaha;};
Tyto dva řádky je také možné spojit do jednoho zápisu: typedef struct clovek{int vek; int vyska; double vaha;} CLOVEK;
Přístup k jednotlivým položkám struktury je pomocí tečkové notace:
Příklady: c1.vek=69; c1.vyska=182; c1.vaha=81.4; c2.vyska=c1.vyska;
Definujeme-li ukazatel na proměnné c1,c2 CLOVEK *p_c1=&c1, *p_c2=&c2;
lze získat přístup k jednotlivým položkám struktury c1 pomocí ukazatele na c1 takto: p_c1→vek=69;p_c1→vyska=182;p_c1→vaha=81.4;p_c2→vyska=p_c1→vyska;
V uvedených zápisech, (kde používáme ukazatel na strukturu) je výhodný pro svoji stručnost operátor → namísto operátoru . např.:
p_c1→vek=69; je stručnější než zápis (*p_c1).vek=69;
ANSI norma jazyka C, (narozdíl od K&R normy), rovněž umožňuje pracovat se strukturami jako s celkem. Následující příkaz kopíruje obsah struktury c1 do struktury c2: c2=c1;
ANSI norma jazyka C rovněž umožňuje používání tzv. bitových položek struktur. Položka je popsána jako položka typu unsigned int nebo signed int, za identifikátorem položky udáváme počet bitů, které jí chceme vyhradit. Struktura obsahující údaje o pacientovi by např. mohla obsahovat následující bitovou položku, indikující zda pacient trpí cukrovkou:
unsigned diabetes:1;
Uvedená bitová položka struktury zabírá v paměti 1 bit. Platí následující omezení: Bitová položka struktury nesmí být delší než datový typ int v dané implementaci. Za dvojtečkou tedy mohou následovat čísla 1, 2, … sizeof(int).
Struktury a pole
Pole jako položka struktury:
Definujme proměnné p1, p2 jako strukturu pacient obsahující datové položky jmeno, prijmeni, vyska, vaha, teplota, charakterizující zdravotní stav pacienta v nemocnici. Budeme předpokládat, že jméno ani příjmení pacienta neobsahuje více znaků než 30 a že jeho pobyt v nemocnici nebude delší než 100 dní.
Příklad 2. by 1 12: typedef struct pacient PACIENT; struct pacient{char jmeno[30]; char prijmeni[30]; int vyska; double vaha[101]; double teplota[101];}; PACIENT p1,p2;
Tisk příjmení pacienta p1 a jeho teploty po 14 dnech pobytu v nemocnici lze získat následujícími příkazy: i=14; printf("%s\t %d.den teplota: %f",p1.prijmeni,i,p1.teplota[i]);
Struktura může jako položku obsahovat jinou strukturu:
Příklad 2. by 1 12: typedef enum mesic MESIC; enum mesic {LEDEN=1,UNOR,BREZEN,DUBEN,KVETEN,CERVEN, CERVENEC,SRPEN,ZARI,RIJEN,LISTOPAD,PROSINEC};
typedef struct datum_narozeni DATUM_NAROZENI; struct datum_narozeni{int den; MESIC mes; int rok;}; typedef struct student STUDENT; struct student{char jmeno[30]; char prijmeni[30]; DATUM_NAROZENI datum; int rocnik;}; STUDENT Petr,Jan,Pavel,Helena,Ivana;
Pole struktur:
Využijme datového typu PACIENT, definovaného v příkladu 2.20 a definujme pole typu PACIENT: PACIENT pacienti[200];
Vytiskněme nyní příjmení 25. pacienta a jeho teplotu po 4 dnech pobytu v nemocnici: i=24; j=4; printf("%s\t %d.den teplota: %f",pacienti[i].prijmeni,j, pacienti[i].teplota[j]);
Struktury a funkce
K&R norma jazyka C umožňuje, že funkce může vracet ukazatel na strukturu a ukazatel na strukturu také může být parametrem funkce.
Podle ANSI normy jazyka C může být návratovou hodnotou funkce i struktura a struktura také může být předána funkci jako skutečný parametr.
typedef struct vector{double s[3];} VECTOR;
VECTOR v_soucin1(VECTOR a, VECTOR b)
{
VECTOR c;
c.s[0]=a.s[1]*b.s[2]-a.s[2]*b.s[1];
c.s[1]=a.s[2]*b.s[0]-a.s[0]*b.s[2];
c.s[2]=a.s[0]*b.s[1]-a.s[1]*b.s[0]; return(c);
}
#include <stdio.h>
main()
{
VECTOR a,b,c;
/* ... */
c=v_soucin1(a,b); /* vyvolani v hlavnim programu */
/* ... */
}
Tento způsob je výhodný pouze v případě, že struktury nejsou příliš veliké. Výsledek funkce je ovšem možné předat parametrem, tak jako v případě jednoduché proměnné:
typedef struct vector{double s[3];} VECTOR;
void v_soucin2(VECTOR *pa, VECTOR *pb, VECTOR *pc)
{
pc->s[0]=pa->s[1]*pb->s[2]-pa->s[2]*pb->s[1];
pc->s[1]=pa->s[2]*pb->s[0]-pa->s[0]*pb->s[2];
pc->s[2]=pa->s[0]*pb->s[1]-pa->s[1]*pb->s[0]; return;
}
#include <stdio.h>
main()
{
VECTOR a,b,c;
/* ... */
v_soucin2(&a,&b,&c); /* vyvolani v hlavnim programu */
/* ... */
}
Uniony
Union (sjednocení) je odvozený datový typ, který podobně jako struktura může obsahovat datové položky různých typů. Rozdíl mezi strukturou a unionem je v přidělení paměti. Položky struktury jsou uloženy za sebou, tj. objektu typu struct se přidělí paměť potřebná pro uložení všech položek, zatímco položky unionu jsou ukládány přes sebe, tj. objektu typu union se přidělí paměť potřebná pro nejdelší položku. Klíčové slovo je union. Při definici unionu se postupuje analogicky jako u struktur, včetně použití operátoru typedef. Analogický je i přístup k položkám unionu pomocí tečkové notace, i nepřímý přístup pomocí ukazatele na union.
Příklad 2. by 1 12: Definice unionu a přístup k jeho položkám: /* … / union znak_int(char zn; int cele) u1,u2; u1.zn=''; u1.cele=44; /* znak '*' se premaze hodnotou 44 … */
ANSI norma jazyka C umožňuje inicializaci první položky unionu:
union znak_int(char zn; int cele) u=''; / u.zn='*' */
Vstup a výstup
Programovací jazyk C nemá žádné příkazy pro vstupní a výstupní operace, jako např. FORTRAN, kde příkazy READ a WRITE jsou součástí jazyka. V této kapitole budou popsány některé standardní knihovní funkce zajišťující operace vstupu a výstupu. Funkční prototypy standardních knihovních funkcí zajišťujících vstup a výstup jsou obsaženy v hlavičkovém souboru <stdio.h>.
Soubor je v jazyce C chápán jako posloupnost slabik (bytů), která je ukončena určitou speciální kombinací slabik a má své jméno. Ukončovací kombinace - konec souboru, která už do obsahu souboru nepatří, má hodnotu symbolické konstanty EOF typu int, (protože pro ukončení posloupnosti znaků je třeba použít jinou hodnotu než znak). Je definována v hlavičkovém souboru stdin, obvykle je reprezentována hodnotou -1. V terminologii jazyka C (i operačního systému UNIX ) se zmíněná posloupnost slabik nazývá proud dat (stream) .
Standardní vstup a výstup
Jakmile je spuštěn program v jazyce C, automaticky se otevírají tři soubory: standardní vstup, standardní výstup a standardní výstup pro chybová hlášení. Tyto soubory se zpravidla označují jako stdin, stdout, stderr. Obvykle je jako standardní vstup použita klávesnice a jako standardní výstup (i pro chybová hlášení) obrazovka. V tomto odstavci se budeme zabývat funkcemi, které umožňují komunikaci programu se standardním vstupem a výstupem.
Vstup a výstup znaků
S funkcemi getchar(),putchar() jsme se setkali už v první kapitole. Funkce getchar() je určena pro vstup znaků ze standardního vstupu, funkce putchar() pro výstup znaků na standardní výstup.
Funkce mají následující funkční prototypy: int getchar(void); int putchar(int c);
Funkce getchar() nemá žádný parametr, návratovou hodnotou této funkce je kód znaku, přečtený ze standardního vstupu stdin. (Jestliže byla přečtena hodnota symbolické konstanty EOF vrací funkce tuto hodnotu.)
Funkce putchar má jako argument hodnotu typu int, zadávající kód znaku, který má být zapsán na standardní výstup stdout. Návratová hodnota funkce je kód zapsaného znaku, nebo EOF pokud při výstupu došlo k chybě.
Příklad 3. by 1 13:
/*********************************************************************
* Program kopiruje znaky ze standardniho vstupu na standardni vystup *
* Ukoncovaci "znak" souboru EOF lze zadat kombinaci 2 klaves: Ctrl+Z *
*********************************************************************/
#include <stdio.h>
main()
{ int c;
/* Testovani konce souboru */
while((c=getchar())!=EOF) /* c=getchar() musi byt v zavorce */
putchar(c);
}
Formátovaný výstup - funkce printf()
S funkcemi printf() a scanf() jsme se rovněž setkali v první a druhé kapitole. Vyložíme nyní systematicky základní možnosti použití těchto funkcí.
Funkce printf() se používá pro formátovaný výstup dat na standardní výstup stdout. Funkční prototyp má následující tvar: int printf(const char *retezec, arg1, arg2, …, argn);
Parametry arg1, arg2, … argn jsou obecně výrazy, jejichž hodnoty mají vystoupit v určitém tvaru na stdout. Funkce má proměnný počet argumentů, volání funkce ale musí obsahovat alespoň první argument retezec, tj. formátový řetězec, který obsahuje jednak libovolné znaky, jež se beze změny kopírují na výstup, jednak formátové specifikace (konverze) určující, v jakém tvaru se parametry arg1, arg2, …, argn zobrazí. Vzájemné přiřazení parametrů a konverzí je provedeno podle pořadí zleva doprava. V případě, že ve formátovém řetězci specifikujeme více konverzí, než kolik jsme zadali skutečných parametrů, vznikne chyba a nelze předpovědět co vystoupí.
Návratová hodnota funkce printf() udává počet slabik, které byly funkcí zapsány do stdout.
Příklad 3. by 1 13: printf("\n2. mocnina %d. prvku pole A je rovna cislu %f",i,A[i]*A[i]);
Znak '\n' je zde interpretován jako přechod na novou řádku, viz tabulku escape sekvencí v části . Kombinace %d, %f jsou konverze určené pro výstup hodnot po řadě typu int a double, přitom hodnota i se tiskne podle konverze %d a hodnota A[i]*A[i]) podle konverze %f.
Kdyby byl uvedený příkaz printf() umístěn v cyklu for(i=0;i<3;i++){…}
vypadal by výstup pro Ai = i, i = 0,1,2 takto: 2. mocnina 0. prvku pole A je rovna cislu 0.000000 2. mocnina 1. prvku pole A je rovna cislu 1.000000 2. mocnina 2. prvku pole A je rovna cislu 4.000000
První argument funkce printf() je, jak vyplývá z jejího funkčního prototypu, ukazatel na konstantní řetězec, viz 2.2. V následujícím příkladu je tento ukazatel zadán pomocí identifikátoru pole typu char.
#include <stdio.h>
#include <stdio.h>
main()
{ int i;
char a[][8]={{"\nAhoj,"},{" rad"},{" te"},{" zase"},{" vidim!"}};
char b[15]={"\nJa tebe taky!"};
for(i=0;i<5;i++) printf(a[i]);
printf(b); return;
}
výstup tohoto programu by vypadal takto:
Ahoj, rad te zase vidim!
Ja tebe taky!
Formátová specifikace má obecně následující tvar:
%[příznaky][šířka][.přesnost][modifikátor]konverze
Závorky [] zde označují nepovinné parametry.
Jednotlivé položky specifikace vysvětlíme v pořadí jejich důležitosti.
Konverze:
Konverze je povinný parametr, je označena jedním znakem, mezi znakem % a označením konverze mohou být umístěné další (nepovinné) parametry. Následující tabulka ukazuje, jaké konverze se používají pro výstup hodnot jednotlivých datových typů, eventuálně v jakém tvaru se zobrazí.
| konverze | typ položky seznamu odpovídající konverzi |
|---|---|
|
znak; (je-li hodnota typu int, je převedena na typ unsigned char) |
%d %i |
číslo typu signed int, desítkový (dekadický) zápis |
%u |
číslo typu unsigned int, desítkový zápis |
%o |
číslo typu unsigned int, osmičkový (oktalový) zápis |
%x %X |
číslo typu unsigned int, šestnáctkový (hexadecimální) zápis, číslice označené a,b,c,d,e,f nebo A,B,C,D,E,F |
%f |
číslo typu float, double, desetinný tvar |
%e %E |
číslo typu float, double, semilogaritmický tvar, exponent označen podle konverze e nebo E |
%g %G |
číslo typu float, double, tvar zvolen podle výhodnosti zápisu jako desetinný nebo semilogaritmický, exponent e podle konverze (v semilogaritmickém tvaru) e nebo E |
%s |
řetězec (bez ukončovacího znaku '\0') |
%p |
ukazatel; tiskne se jeho obsah nejčastěji v šestnáctkovém zápisu |
%n |
ukazatel na typ int. Na adresu na kterou ukazuje se zapíše počet znaků který byl až dosud tímto voláním zapsán na výstup, netiskne se nic. |
Modifikátor:
| h | modifikuje konverze d,i na typ signed short int, konverze u,o,x,X na typ unsigned short int |
|---|---|
l |
modifikuje konverze d,i na typ signed long int, konverze u,o,x,X na typ unsigned long int, konverze f,e,E,g,G na typ double |
L |
modifikuje konverze f,e,E,g,G na typ long double |
šířka:
| n | tiskne se alespoň n znaků, mezery se doplňují zprava nebo zleva, viz příznaky |
|---|---|
0n |
tiskne se alespoň n znaků, namísto mezer se doplňují nuly |
* |
šířka je zadána nepřímo: argument, který "je na řadě" obsahuje šířku, (musí být typu int), následuje argument, který bude vystupovat |
Přesnost:
Přesnost je dekadické číslo, které pro konverze d, i, o, u, x, X znamená minimální počet cifer na výstupu, pro konverze f, e, E, znamená počet cifer za desetinnou tečkou, pro konverze g, G znamená počet významových cifer a pro konverzi s maximální počet znaků.
Kromě toho má .* a . následující význam:
.* |
přesnost je zadána nepřímo: argument, který "je na řadě", obsahuje přesnost,(musí být typu int), následuje argument, který bude vystupovat znamená totéž co .0 |
Příznak:
- |
výsledek se zarovná doleva, zprava se doplní mezery není-li uveden, výsledek se zarovná doprava a zleva se doplní mezery nebo nuly |
+ |
číslo typu signed se vytiskne vždy se znaménkem není-li uveden vynechá se znaménko '' u kladných hodnot mezera, kladné číslo se vytiskne bez znaménka, "" bude nahrazeno mezerou |
Příklady:
Hodnota: Konverze: Výstup: Poznámka:
3.141592 %7.3 3.141 mezery se doplní zleva
3.141592 %-7.3 3.141 mezery se doplní zprava
369.24 %+11.3E +3.692E+02
Ahoj! %2s Ahoj!
Ahoj! %.2s Ah
Formátovaný vstup — funkce scanf()
Funkce scanf() se používá pro formátovaný vstup dat ze standardního vstupu stdin. Funkční prototyp má následující tvar:
int scanf(const char *retezec, arg1, arg2, ..., argn);
Parametry arg1, arg2, … argn jsou adresy proměnných, jejichž hodnoty se mají přečíst ze standardního vstupu. (Jak víme, jsou parametry funkcí volány hodnotou. Chceme-li měnit ve funkci hodnoty některých proměnných, musíme použít jejich adresy jako parametry funkce.)
Funkce má proměnný počet argumentů, volání funkce ale musí obsahovat alespoň první argument retezec, tj. formátový řetězec. Tento řetězec obsahuje formátové specifikace (konverze) určující, jak se budou jednotlivé čtené posloupnosti slabik interpretovat. Dále může formátový řetězec obsahovat bílé znaky a ostatní znaky (ASCII znaky různé od % a bílých znaků). Pokud funkce scanf() najde ve formátovém řetězci bílý znak, přečte všechny následující bílé znaky ze vstupu až po první jiný znak. Tyto bílé znaky nejsou přitom transformovány v žádnou vstupní hodnotu. Pokud najde funkce scanf () ostatní znak ve formátovém řetězci očekává, že následující znak z stdin bude s tímto znakem totožný. Tento znak bude přečten a ignorován.
Formátové specifikace mají následující tvar:
%[šířka][modifikátor]konverze
Význam parametrů modifikátor, konverze je stejný jako ve formátových specifikacích funkce printf (). Parametr šířka určuje počet znaků tzv. vstupního pole. Bude přečteno maximálně tolik znaků, kolik zadává parametr šířka. Pokud narazí funkce scanf() na bílý znak nebo na znak, který nepatří do zápisu čtené hodnoty, ukončí se čtení dříve. Na rozdíl od funkce printf(), kde lze konverze f, e, E, g, G použít pro výstup hodnot typu float i typu double, lze tyto konverze ve funkci scanf() obvykle použít pouze pro vstup hodnot typu float, zatímco pro hodnoty typu double je nutné použít tyto konverze s modifikátorem l, tedy lf, le, lE, lg, lG. Kromě toho mají konverze f, e, E, g, G na vstupu stejný význam, všechny lze je použít k přečtení čísla zapsaného v desetinném i semilogaritmickém tvaru.
Návratová hodnota funkce scanf() je počet přečtených vstupních polí.
char c; int i; float r_1; double r_2; char text[9];
...
scanf("%c%d%f%lf%8s",&c,&i,&r_1,&r_2,text);
V uvedeném příkladu je čtení řetězce omezeno na jeho prvních 8 znaků, (devátý znak je vyhrazen pro ukončovací znak '\0'). Návratová hodnota funkce by v našem příkladu byla rovna 5.
Vstup a výstup řádek
Jak jsme uvedli v předchozí části, pokud narazí funkce scanf() na bílý znak nebo na znak, který nepatří do zápisu čtené hodnoty, ukončí se čtení dříve. To poněkud komplikuje čtení řetězců obsahujících v textu mezery. Naproti tomu funkce gets(), puts() pracují s textovými řádkami jako s celky:
char *gets(char *str);
int puts(char *str);
Funkce gets() přečte řetězec znaků až do znaku '\n' tj. textovou řádku ze standardního vstupního zařízení a uloží ji do řetězce str.
Znak '\n' se neukládá a řetězec je automaticky ukončen znakem '\0'.
Návratová hodnota funkce je ukazatel na řetězec str. Pokud je řetězec prázdný, vrací funkce hodnotu symbolické konstantu NULL.
Funkce puts() vytiskne řetězec str a odřádkuje, tj. vypíše znak '\n'. Funkce vrací nezáporné číslo, v
případě že operace nemůže z nějakého důvodu proběhnout je návratová hodnota rovna EOF.
Příklady na probrané funkce a některé další uvedeme v části .
2. Vstup ze souboru a výstup do souboru
Kromě standardních, automaticky otevíraných souborů stdin, stdout, stderr, je možné otevřít a
používat další soubory. V hlavičkovém souboru stdio.h je pro práci s nimi zaveden datový typ FILE.
Identifikátor souboru je typu FILE * tj. ukazatel na typ FILE. Norma ANSI jazyka C rozeznává dva druhy souborů - textový a binární.
V textovém souboru lze knihovními funkcemi vytvářet a rozeznávat textové řádky. To je umožněno tím, že systém při zápisu automaticky některé znaky do souboru doplňuje v souladu s konvencí, která pro textové soubory v daném operačním systému platí. Při čtení textového souboru se tyto znaky naopak automaticky vyřazují. Tento režim práce s textovými soubory zajišťuje, že jejich obsah si lze prohlédnout, vytvořit nebo opravit běžným editorem.
Binární soubor není funkcemi čtení a zápisu nijak ovlivňován. To znamená, že co do binárního souboru zapíšeme, to v něm také přesně bude, a co je v binárním souboru zapsáno, to se také přesně přečte. Výhoda binárních souborů spočívá v tom, že pro uchování stejného množství informace potřebují mnohem méně prostoru než textové soubory a práce s nimi je také daleko rychlejší.
Otevření a uzavření souboru
Pro otevření a uzavření souboru se používá dvojice knihovních funkcí fopen(), fclose(). Funkce fopen () slouží k otevření existujícího souboru nebo k vytvoření nového souboru: FILE *fopen(const char *jmeno, const char *modus);
První parametr jmeno je označení souboru, který se má otevřít nebo vytvořit. Za tento parametr lze dosadit řetězec nebo pole typu char obsahující jméno souboru (eventuálně včetně disku a cesty). Druhý parametr modus určuje mód, ve kterém se má s otevíraným souborem pracovat. V následující tabulce jsou uvedeny možnosti, jak lze druhý parametr zvolit:
"r" textový soubor pro čtení "w" textový soubor pro zápis nebo pro přepsání "a" textový soubor pro připojení na konec "rb" binární soubor pro čtení "wb" binární soubor pro zápis nebo pro přepsání "ab" binární soubor pro připojení na konec "r+" textový soubor pro čtení a zápis "w+" textový soubor pro čtení, zápis nebo přepsání "a+" textový soubor pro čtení a zápis na konec "rb+" binární soubor pro čtení a zápis "wb+" binární soubor pro čtení, zápis nebo přepsání "ab+" binární soubor pro čtení a zápis na konec
Pokud lze soubor jmeno otevřít nebo vytvořit v daném módu, vrátí funkce ukazatel na typ FILE, který bude sloužit k identifikaci souboru jmeno při práci s dalšími knihovními funkcemi. Pokud se soubor nepodaří otevřít nebo vytvořit, vrátí funkce fopen() hodnotu symbolické konstanty NULL, tedy pointer, který neukazuje na žádný objekt.
Pro uzavření souboru je určena funkce fclose(): int fclose(FILE *file);
Pokud se soubor identifikovaný ukazatelem file nepodaří uzavřít, (např. nebyl-li otevřen,) vrací funkce hodnotu symbolické konstanty EOF.
FILE *soubor;
...
soubor=fopen("DATA.TXT","r");
...
fclose(soubor);
...
V uvedeném příkladu je funkcí fopen() otevřen soubor DATA.TXT jako textový soubor pro čtení.
Vyvoláním funkce fclose() je v našem příkladu soubor DATA.TXT uzavřen.
(Patří k dobrým programátorským zvykům uzavřít soubor ihned po ukončení práce s ním.)
Operace otevření a uzavření souboru nemusí ovšem z určitých důvodů proběhnout, program by proto měl testovat úspěšnost provedení těchto operací:
FILE *soubor;
...
soubor=fopen("DATA.TXT","r");
if(soubor==NULL){printf("\nChyba pri otevreni DATA.TXT");return;}
...
if(fclose(soubor)==EOF){printf("\nChyba pri uzavreni DATA.TXT");return;}
...
Základní operace s otevřeným souborem
Vstup znaků ze souboru a výstup znaků do souboru
Funkce getc(), putc() určené pro vstup znaků ze souboru a výstup znaků do souboru jsou analogické funkcím getchar() a putchar() pro standardní vstup a výstup znaků, které jsme probrali v části 1.1.
Funkce getc() je tedy určena pro vstup znaků ze souboru, funkce putc() pro výstup znaků do souboru. Uveďme pro srovnání funkční prototypy všech čtyř funkcí: int getchar(void); int getc(FILE *file); int putchar(int c); int putc(int c, FILE *file);
Funkce getc() má jediný parametr typu ukazatel na FILE identifikující soubor, ze kterého má být znak přečten, návratovou hodnotou této funkce je kód znaku, přečtený z určeného souboru. (Jestliže byla přečtena hodnota symbolické konstanty EOF vrací funkce tuto hodnotu.)
Funkce putc má jako první argument hodnotu typu int, zadávající kód znaku, který má být zapsán do souboru, který je identifikován druhým parametrem. Návratová hodnota funkce je kód zapsaného znaku, nebo EOF pokud při výstupu došlo k chybě.
Následující program využívá knihovní funkce fopen(), fclose(), getc() pro určení počtu řádek textového souboru.
#include <stdio.h>
#include <ctype.h>
/********************************************************************/
/* Program urci pocet radek a neprazdnych radek textoveho souboru */
/* (prazdna radka - radka, na ktere jsou zapsany pouze bile znaky) */
/********************************************************************/
main()
{
FILE *soubor; int c,zn=0,rd=0,rd_0=0;
/********************************************************************/
/* zn - pocet znaku na radce (bile znaky nejsou zapocitany) */
/* rd - pocet radek (radky obsahujici pouze bile znaky nejsou */
/* zapocitany) */
/* rd_0 - pocet radek (vcetne "prazdnych" radek) */
/********************************************************************/
if((soubor=fopen("TEXT","r"))==NULL){ /* test - otevreni */
printf("\nChyba pri otevreni TEXT"); return;
}
while((c=getc(soubor))!=EOF){ /* test - konec souboru */
if(c=='\n'){ /* test - konec radky */
rd_0++;
if(zn>0)rd++;
zn=0;
}
if(isspace(c)==0)zn++; /* podminka neplati pro "bily" znak */
}
rd_0++;
if(zn>0)rd++;
printf("\nPocet neprazdnych radek=%d\nPocet vsech radek=%d",rd,rd_0);
if(fclose(soubor)==EOF)printf("\nChyba pri uzavreni TEXT ");
return;
}
Uvedený program využívá knihovní funkci (přesněji, jde o předdefinované makro, viz čl. 5.1 a 7.1) isspace() k identifikaci bílých znaků. Proto je na začátku programu vložen hlavičkový soubor ctype.h příkazem #include.
Formátovaný vstup a výstup
Pro formátovaný vstup ze souboru a formátovaný výstup do souboru se používají funkce fscanf(), fprintf() analogické funkcím scanf(), printf() určeným pro formátovaný standardní vstup a výstup:
int scanf(const char *retezec, arg1, arg2, ..., argn);
int fscanf(FILE *file, const char *retezec, arg1,arg2,...,argn);
int printf(const char *retezec, arg1, arg2, ..., argn);
int fprintf(FILE *file, const char *retezec, arg1,arg2,...,argn);
Funkční prototypy funkcí fscanf(), fprintf() obsahují tedy oproti funkcím scanf(), printf() navíc parametr typu ukazatel na FILE identifikující soubor, se kterým se příslušná vstupní nebo výstupní operace provádí. Ostatní parametry mají stejný význam jako u funkcí scanf(), printf(), stejně je také definována návratová hodnota obou funkcí.
Vstup řádek ze souboru a výstup řádek do souboru
Pro vstup řádky ze souboru slouží funkce:
char *fgets(char *str, int max, FILE *file);
Funkce fgets() přečte řetězec znaků až do znaku '\n' nejvýše však max znaků ze souboru identifikovaného ukazatelem file a uloží jej do řetězce str. Znak '\n' se ukládá a řetězec je automaticky ukončen znakem '\0'. Návratová hodnota funkce je ukazatel na řetězec str. Pokud je řetězec prázdný, vrací funkce hodnotu symbolické konstanty NULL.
Funkce fputs() je určena pro zápis řetězce do souboru:
int fputs(char *str, FILE file);
Funkce puts() vytiskne řetězec str do souboru identifikovaného ukazatelem file. Řetězec neukončuje znakem '\n' ani znakem '\0'. Funkce vrací nezáporné číslo, v případě, že operace nemůže z nějakého důvodu proběhnout, je návratová hodnota rovna EOF.
/**********************************************************************/
/* Program kopiruje libovolny textovy soubor do libovolneho jineho */
/* textoveho souboru */
/**********************************************************************/
#include <stdio.h>
main()
{
char nazev[20]; int c; FILE *file_r,*file_w;
printf("\n Ktery soubor se ma kopirovat?"
"\n Zadej nazev existujiciho textoveho souboru!\n");
gets(nazev); /* cteni nazvu souboru, ktery budeme kopirovat */
file_r=fopen(nazev,"r");
if(file_r==NULL){printf("\n Chyba pri otevreni souboru pro cteni!");
return;
}
printf("\n Do ktereho souboru kopirovat?"
"\n Zadej novy odlisny nazev textoveho souboru!\n");
gets(nazev); /* cteni nazvu souboru, do ktereho ulozime kopii */
file_w=fopen(nazev,"w");
if(file_w==NULL){printf("\n Chyba pri otevreni souboru pro zapis!");
return;
}
/* Kopirovani souboru: */
/* Testovani konce souboru: */
while((c=getc(file_r))!=EOF) /* c=getc() musi byt v zavorce */
putc(c,file_w);
if((fclose(file_r)==EOF)||(fclose(file_w)==EOF))
printf("\nNektery ze souboru nelze uzavrit!");
return;
}
Pro kopírování souborů je zde použito funkcí getc(), putc(), tj. soubor je kopírován znak po znaku.
Namísto toho je možné také použít funkce fgets(), fputs(), tj. kopírování po řádkách.
V uvedeném programu by bylo třeba změnit jen část programu označenou komentářem /* Kopirovani souboru: */ a doplnit definici pole typu char, která nahradí roli proměnné c.
Program bude pracovat za předpokladu, že textové řádky neobsahují více znaků než 100:
...
char radka[101]; /* k popisum doplnime definici pole 'radka' */
...
/* Kopirovani souboru: */
while(fgets(radka,100,file_r)!=NULL) /* kopirovani po radkach */
fputs(radka,file_w);
...
Práce s binárními soubory
V tomto odstavci se budeme zabývat knihovními funkcemi, které jsou specifické pro práci s binárními soubory.
Pro čtení dat z binárního souboru se používá funkce fread(): int fread(char *kam, int delka, int pocet, FILE *file);
kde jednotlivé parametry mají následující význam:
kam - adresa v paměti, kam se bude ukládat čtený blok dat délka - délka jedné položky v bloku dat počet - počet datových položek file - proměnná typu ukazatel na FILE identifikující binární soubor
Pro zápis dat do binárního souboru se používá funkce fwrite():
int fwrite(char *odkud, int delka, int pocet, FILE *file);
kde význam formálních parametrů delka, pocet, file je stejný jako jako u funkce fread() a parametr
odkud je definován takto:
odkud - adresa v paměti, odkud se bude brát zapisovaný blok dat
Obě funkce vrací počet úspěšně přečtených resp. zapsaných položek.
Funkce fread(), fwrite() umožňují ukládat čtená data do části vnitřní paměti zadané ukazatelem kam a zapisovat data z části vnitřní paměti zadané ukazatelem odkud.
Následující funkce umožňují určit, ze které části binárního souboru budeme data číst nebo do které části binárního souboru budeme data ukládat. K tomuto účelu se používají funkce fseek() a ftell().
Funkce fseek() se používá pro nové nastavení pozice v souboru:
int fseek(FILE *file, long posun, int odkud);
kde formální parametry mají následující význam: file - proměnná typu ukazatel na FILE identifikující binární soubor posun - počet slabik (byte) od pozice v souboru dané parametrem 'odkud' odkud - místo odkud se posun provede, parametr může mít jednu ze tří hodnot: SEEK_SET - od začátku souboru SEEK_CUR - od aktuální pozice v souboru SEEK_END - od konce souboru
Návratová hodnota funkce fseek() je nula v případě úspěšného přesunu a nenulová hodnota v opačném případě.
Funkce ftell() se používá pro zjištění aktuální pozice v souboru:
long ftell(FILE *file);
Funkce vrací posunutí měřené ve slabikách (bytes) od začátku souboru.
#include <string.h>
#include <stdio.h>
#include <conio.h>
int main(void)
{
FILE *f;
char retezec[] = "Test funkci fseek() a fwrite().";
int i;
clrscr();
/* otevreme soubor pro zapis, ze ktereho je mozne i cist */
f=fopen("S.TXT", "wb+");
/* zapiseme retezec do tohoto souboru: */
fwrite(retezec,strlen(retezec),1,f);
/* nastavime "ukazovatko" na zacatek souboru */
fseek(f, 0, SEEK_SET);
do
{ /* cteme jednotlive znaky ze souboru */
i=fgetc(f);
/* vytiskneme je: */
putchar(i);
}while (i != EOF);
fclose(f); /* uzavre soubor S.TXT */
return 0;
}
Funkce strlen() se používá pro zjištění délky řetězce (bez ukončujícího znaku), viz čl. .
#include <stdio.h>
vymen_poradi(FILE *file); /* funkcni prototyp vymen_poradi() */
main()
{ FILE *file; int c,delka; long l,i,n;
file=fopen("data","rb"); /* test, zda soubor existuje */
if(file!=NULL){
printf("\nSoubor existuje - premazat?\n\t A/N\n"); c=getchar();
if( c!='A' && c!='a') return;
}
/******************** Vytvoreni testovaciho souboru ******************/
file=fopen("data","wb+");/* pokud soubor existoval, premazal se ***/
if(file==NULL) { printf("Chyba pri otevreni"); return;}
printf("\nZadej pocet polozek souboru!\n"); scanf("%ld",&n);
delka=sizeof(long);
for(i=1;i<=n;i++) fwrite(&i, delka, 1, file);
/********************* Tisk souboru data *****************************/
fseek(file,0,SEEK_SET); /* Nastaveni na zacatek souboru */
for(i=1;i<=n;i++) {fread(&l, delka, 1, file);printf(" %3ld",l);}
vymen_poradi(file);
/********************* Tisk souboru po zmene usporadani **************/
fseek(file,0,SEEK_SET); /* Nastaveni na zacatek souboru */
printf("\n\n");
for(i=1;i<=n;i++) {fread(&l, delka, 1, file);printf(" %3ld",l);}
fclose(file); /********* Uzavreni souboru ************************/
return;
}
vymen_poradi(FILE *file)
{ long j, delka=sizeof(long), pom, zac, kon, n;
fseek(file,0,SEEK_END);
n=ftell(file)/delka; /* Kolik polozek soubor obsahuje? */
for(j=0;;j++){
/* cteni 1 polozky od zacatku souboru */
fseek(file,j*delka,SEEK_SET); fread(&zac,delka,1,file);
/* cteni 1 polozky od konce souboru */
fseek(file,-(j+1)*delka,SEEK_END); fread(&kon,delka,1,file);
/* vymena prvku 'zac' a 'kon' */
if(j >= n-1-j) break;
pom=zac; zac=kon; kon=pom;
/* zapis zamenenych prvku */
fseek(file,j*delka,SEEK_SET); fwrite(&zac,delka,1,file);
fseek(file,-(j+1)*delka,SEEK_END); fwrite(&kon,delka,1,file);
} return;
}
Parametry funkce main()
Dosud jsme používali funkci main() bez parametrů, o využití návratové hodnoty funkce main() jsme se zmínili v části 5.3.
Pokud má hlavní funkce parametry, pak se obvykle označují argc, argv. Hlavička funkce main() v tomto případě vypadá takto: main(int argc, char *argv[])
Kdyby byl program např. VYPIS.EXE s hlavní funkcí uvedeného typu spuštěn z příkazové řádky instrukcí VYPIS TENTO TEXT
potom by parametry funkce main() měly následující hodnoty: argc 3 počet řetězců na příkazové řádce argv[0] řetězec udávající název programu argv[1] TENTO argv[2] TEXT
Z příkazové řádky lze zadávat i hodnoty číselných parametrů. Tyto hodnoty je ovšem nutné z příslušných řetězců argv[i] získat např. pomocí knihovních konverzních funkcí.
/***************** Program SECTI ************************************/
#include <stdlib.h> /* Obsahuje funkce pro praci s retezci */
#include <stdio.h>
main(int argc, char *argv[])
{
long L1,L2;
if(argc!=3){printf("\nVyvolani programu napr.: SECTI 1 2"
"\nProgram secte zadana dve cela cisla.");
return;
}
/* standardni funkce atol() konvertuje retezec na long : */
L1=atol(argv[1]); L2=atol(argv[2]); printf("\tSoucet = %ld",L1+L2);
return;
}
Funkční prototyp použité konverzní funkce atol() čtenář najde v čl. .
Dynamická alokace paměti
Alokace paměti je přidělení (vyhrazení) části paměti určitému objektu. Nároky na velikost paměti lze pro některé objekty specifikovat v době překladu programu, v tomto případě se používá tzv. statická alokace paměti. V době běhu programu nedochází s takto přidělenou pamětí k žádné manipulaci.
V některých případech nelze nároky na velikost paměti pro některé objekty v době překladu určit, protože tyto nároky závisí na okolnostech, které vzniknou až v době běhu programu. Alokace paměti v době běhu programu se nazývá dynamická alokace paměti. K dynamické alokaci paměti dochází v různých situacích např. při rekurzivním volání funkcí, při alokaci paměti pro lokální proměnné apod.
V této kapitole se budeme zabývat dynamickou alokací paměti v jedné její části označované jako hromada (heap). Pomocí knihovních funkcí (např. funkce malloc()) je možné dynamicky přidělit (alokovat) oblast paměti určité délky. Tato oblast paměti není pojmenována (nemá identifikátor), přístup k této oblasti paměti je zajištěn adresou, která bývá uložena v proměnné typu ukazatel. Paměť v hromadě zůstává přidělena až do jejího uvolnění (např. funkcí free()) nebo do ukončení běhu programu.
Pro práci s pamětí budeme používat funkce malloc() a free():
void *malloc(unsigned int delka);
void free(void *p);
Formální parametr delka udává počet slabik (bytes), které chceme alokovat. Funkce vrací ukazatel na typ void, který představuje adresu prvního přiděleného prvku. Ukazatel je vhodné přetypovat na ukazatel na příslušný konkrétní datový typ. V případě, že se nepodaří požadovanou paměť alokovat, vrací funkce hodnotu NULL.
Dynamicky definované pole
Nejprve se budeme zabývat dynamickou alokací paměti pro jednorozměrné pole v další části tohoto odstavce přejdeme k vícerozměrným polím.
Dynamicky definované jednorozměrné pole
...
double *d;
...
d=(double *)malloc(10*sizeof(double));/* pretypovani na (double *) */
if(d==NULL){ /* test zda alokace probehla */
printf("\nChyba pri alokaci pameti!"); return;
}
...
/* bezne pouziti d jako ukazatele a jako identifikatoru pole: */
*d=45.7; d[0]=1.1; d[9]=-60.; /* přístup k přidělené paměti */
...
free(d); /* uvolneni pameti pro dalsi pouziti */
...
Z toho co jsme uvedli v části 4.3 o souvislosti mezi ukazatelem a polem je zřejmé, že v našem příkladu lze proměnnou d používat s indexovým výrazem a že tedy je možné popsaným způsobem dynamicky definovat jednorozměrná pole. Základní operace s dynamicky definovaným polem se provádějí stejně jako s polem definovaným staticky. Uveďme nicméně dvě odlišnosti staticky a dynamicky definovaných polí:
-
Identifikátor staticky definovaného pole je konstantním ukazatelem a jeho hodnotu tedy nelze měnit.
-
Je-li např. d definováno jako v uvedeném příkladu a c takto: double c[10]; potom hodnoty sizeof(d) a sizeof(c) budou různé, přesto že příslušná pole mají stejnou velikost. V prvním případě určí operátor sizeof() velikost paměti nutné k uložení ukazatele na typ double (tj. např. 2 slabiky), ve druhém případě velikost paměti nutné k uložení 10 prvků typu double (např. 80 slabik).
V části 4.4 jsme v příkladu na sčítání jednorozměrných polí používali staticky definované jednorozměrné pole jako skutečný parametr funkce. Dynamicky definované jednorozměrné pole lze rovněž použít jako skutečný parametr funkce. Zapišme příslušný soubor 1d_pole.c s dynamicky definovanými poli.
/******************* soubor 1d_pole.c, dynamicka pole ********/
#include <stdio.h>
#include <conio.h>
/** funkcni prototypu funkci precti(), tiskni() a secti() **/
precti(int, int [], const char *);
tiskni(int, int [], const char *);
secti(int, int [], int [], int []);
main()
{
int n;
int *a, *b, *c;
clrscr();
printf("\n Zadej pocet prvku n vektoru a,b");
scanf("%d",&n);
/** dynamická definice poli a,b,c **/
a=(int *)malloc(n*sizeof(int));
b=(int *)malloc(n*sizeof(int));
c=(int *)malloc(n*sizeof(int));
/** cteni a kontrolni tisk vektoru a,b **/
precti(n,a,"a"); tiskni(n,a,"a"); precti(n,b,"b"); tiskni(n,b,"b");
/** vypocet a tisk vektoru c **/
secti(n,a,b,c); /** secteni vektoru **/
tiskni(n,c,"c"); return;
}
Soubor secti_1d obsahující definice funkcí precti(), secti(), tiskni() by zůstal i pro takto definovaná pole beze změn.
Dynamicky definovaná vícerozměrná pole
...
double **a;
/* alokujeme prostor pro 10 pointeru na typ double */
a=(double **)malloc(10*sizeof(double*));
/* a je pointer, ktery ukazuje na 1. z techto deseti pointeru */
/* 10 pametovych mist zatim zadne adresy neobsahuje */
for(i=0;i<10;i++)
a[i]=(double *)malloc(20*sizeof(double));
/* a[i] je nyni pointer ukazujici na zacatek bloku pameti,
ve kterem je misto pro 20 cisel typu double */
...
Přístup k jednotlivým prvkům tohoto dynamicky definovaného pole je stejný jako v případě staticky definovaného pole tedy např.
`+a[9][19]=10.; *(*(a+6)+7)=44.; a[10][1]=0.; a[0][20]=8.;+`
první dva příkazy jsou dobře, třetí a čtvrtý jsou chybně, protože přepisují část paměti, která pravděpodobně není vyhrazena pro pole a, což pro běh programu může mít fatální následky.
V uvedeném příkladu se jednalo o tzv. obdélníkové pole, které v každé "řádce" obsahovalo 20 prvků. Je zřejmé, že lze analogicky definovat pole s nestejnou délkou "řádek". Často se např. používá pole řetězců, tj. dvourozměrné pole typu char s nestejnou délkou "řádek".
Zcela analogicky lze alokovat paměť i pro vícerozměrná pole. V části 4.4 jsme v příkladu na sčítání trojrozměrných polí používali staticky definovaná trojrozměrná pole jako skutečné parametry funkce. Dynamicky definované vícerozměrné pole lze rovněž použít jako skutečný parametr funkce. Zapišme příslušný soubor 3dpole.c s dynamicky definovanými poli.
/******************* soubor 3dpole.c, dynamicka pole **********/
#include <stdio.h>
#include <conio.h>
/******* funkcni prototypu funkci precti(), tiskni() a secti() ****/
precti(int, int, int, int ***, const char *);
tiskni(int, int, int, int ***, const char *);
secti(int, int, int, int ***, int ***, int ***);
int main()
{
int m,n,r,i,j, ***a, ***b, ***c;
/*********** nasleduje cteni m,n,r *********/
.....
/******** dynamicka definice trojrozmernych poli a,b,c *********/
a=(int ***)malloc(m*sizeof(int **));
b=(int ***)malloc(m*sizeof(int **));
c=(int ***)malloc(m*sizeof(int **));
for(i=0;i<m;i++){
a[i]=(int **)malloc(n*sizeof(int *));
b[i]=(int **)malloc(n*sizeof(int *));
c[i]=(int **)malloc(n*sizeof(int *));
}
for(i=0;i<m;i++){
for(j=0;j<n;j++){
a[i][j]=(int *)malloc(r*sizeof(int));
b[i][j]=(int *)malloc(r*sizeof(int));
c[i][j]=(int *)malloc(r*sizeof(int));
}
}
/*******************************************************************/
precti(m,n,r,a,"a"); tiskni(m,n,r,a,"a");
precti(m,n,r,b,"b"); tiskni(m,n,r,b,"b");
secti(m,n,r,a,b,c); tiskni(m,n,r,c,"c");
return 0;
}
Funkční prototypy funkcí precti(), secti(), tiskni() lze také zapsat takto:
/******* funkcni prototypu funkci precti(), tiskni() a secti() ****/
precti(int, int, int, int **[], const char *);
tiskni(int, int, int, int **[], const char *);
secti(int, int, int, int **[], int **[], int **[]);
V souboru secti3d.c obsahující definice funkcí precti(), secti(), tiskni() není třeba dělat žádnou změnu.
Dynamicky definované datové struktury
V této závěrečné části bychom uvedli dva příklady dynamicky definovaných datových struktur: spojový seznam a binární strom. Realizace obou těchto datových struktur je v jazyce C založena na možnosti definovat strukturu obsahující jako svojí položku jeden nebo více ukazatelů na stejný datový typ, jaký sama reprezentuje. Tato položka obsahující ukazatel se nazývá dynamická položka (dynamický prvek) struktury.
Nejjednodušší takovou strukturou je následující struktura typu PRVEK:
typedef struct prvek{int hodnota; struct prvek *spoj;} PRVEK;
PRVEK p1,p2;
Spojový seznam
Spojový seznam vytvořený z prvků typu PRVEK je datová struktura obsahující posloupnost těchto prvků, která má tu vlastnost, že dynamická položka spoj každého prvku kromě posledního ukazuje na prvek následující.
Poznámka: Spojový seznam takového typu bude možné prohledávat pouze v jednou směru. Lze vytvářet i seznamy s možností prohledávání v obou směrech. Základním prvkem seznamů s možností obousměrného prohledávání je struktura obsahující dvě dynamické položky, které v seznamu ukazují na následující i předcházející prvek.
Spojový seznam je příkladem lineární datové struktury. To znamená, že ke každému prvku existuje maximálně jeden prvek následující a maximálně jeden prvek předcházející. Kromě toho je jeden prvek první a jeden poslední.
Paměť pro prvky, ze kterých je spojový seznam vytvořen lze alokovat dynamicky:
PRVEK *u_p; /* u_p je ukazatel na typ PRVEK */
...
u_p=(PRVEK *)malloc(sizeof(PRVEK));
Přístup k jednotlivým položkám struktury využívá ukazatel, který byl určen funkcí malloc():
`+u_p->hodnota u_p->spoj+`
Spojový seznam se často využívá v databázových aplikacích. Důvodem je to, že lze snadno realizovat operace typu zařazení prvku do seznamu, vyjmutí prvku ze seznamu, uspořádání seznamu apod. Další výhoda spočívá v tom, že uspořádání seznamu je možné měnit pouze pomocí hodnot ukazatelů.
Při uspořádávání seznamu tedy není nutné přepisovat jednotlivé prvky do jiné části paměti. Prvky mohou obsahovat velký objem dat a tato operace by mohla uspořádávání seznamu značně zpomalit.
#include <stdio.h>
#include <stdlib.h>
int main()
{
typedef struct prvek{int hodnota; struct prvek *spoj;} PRVEK;
int i,pocet;
PRVEK *p_f,*p_c,*p_e; /* mnemotechnika: first, current, end */
PRVEK *p_odk,*pom; /* pomocne pointery pro usporadani */
/* mnemotechnika: odkud_zacit, pomocny */
printf("\nZadej pocet prvku!"); scanf("%d",&pocet);
p_f=(PRVEK *)malloc(sizeof(PRVEK)); /* alokace pameti pro 1. prvek */
if(p_f==NULL){printf("\nMalo pameti!");return;} /* test alokace */
p_c=p_e=p_f;
p_c->hodnota=rand(); /* urceni hodnoty prvniho prvku */
for(i=2;i<=pocet;i++){ /* zadani zbyvajicich prvku */
p_e=(PRVEK *)malloc(sizeof(PRVEK)); /* alokace i-teho prvku */
if(p_e==NULL){printf("\nMalo pameti!");break;} /* test alokace */
p_c->spoj=p_e; /* pointer spoj sousedniho prvku */
p_c=p_e; p_e->hodnota=rand(); /* posunuti p_c, zadani hodnoty */
}
p_e->spoj=NULL;
for(p_c=p_f; p_c!=NULL; p_c=p_c->spoj)
printf(" \n %d",p_c->hodnota); /* tisk hodnot prvku */
/* usporadani seznamu ************************************************/
/* end posledni prvek usporadane casti seznamu (v oznaceni p_e) */
/* odk prvni prvek neusporadane casti seznamu (v oznaceni p_odk) */
for(p_e=p_f, p_odk=p_f->spoj; p_odk!=NULL; p_odk=p_e->spoj){
/* test: bude prvek nasledovat za usporadanou cast seznamu ? *****/
if(p_e->hodnota<=p_odk->hodnota){
p_e=p_odk;continue; /* end bude opet novy posledni prvek u.c.s.*/
}
/**** prvek odk neni na spravnem miste, hledame pro nej pozici ***/
p_e->spoj=p_odk->spoj; /* nejprve prvek odk vyradime ze seznamu */
/* test: umistit prvek odk pred usporadanou cast seznamu? ********/
if(p_odk->hodnota <= p_f->hodnota) {p_odk->spoj=p_f; p_f=p_odk;
} /* first bude opet novy prvni prvek */
else
/***** prvek odk patri dovnitr usporadane casti seznamu ********/
for(p_c=p_f;p_c!=NULL;p_c=p_c->spoj){
if(p_c->hodnota<=p_odk->hodnota &&
p_odk->hodnota <= p_c->spoj->hodnota){
p_odk->spoj=p_c->spoj; p_c->spoj=p_odk;
break; /* prvek byl ulozen na misto, cyklus ukoncen ***/
}
}
} /******************* konec usporadani seznamu ********************/
printf("\n\n");
for(p_c=p_f; p_c!=NULL; p_c=p_c->spoj)
printf(" \n %d",p_c->hodnota); /******** tisk hodnot prvku **********/
/******************* zruseni alokace pameti **************************/
for(p_c=p_f; p_c!=NULL; p_c=pom){ /* pomocna promenna pom se pouziva */
pom=p_c->spoj; free(p_c); /* k zapamatovani adresy */
} /* nasledujiciho objektu */
return 0;
}
Postup jsme se snažili podrobně komentovat přímo v textu programu. Uvedený program vychází z obvyklého algoritmu bublinkového třídění, viz následující příklad. Postupné výměny prvků analogické výměnám a[j-1], a[j] není třeba v algoritmu uspořádání spojového seznamu provádět. Prvek je možné umístit přímo mezi zvolené prvky změnou hodnot ukazatelů spoj.
for(i=1;i<n;i++){ /* pole indexovano od hodnoty 0 */
for(j=i;j>=1;j--){
if(a[j-1]<a[j])
break;
else{
pom=a[j-1]; a[j-1]=a[j]; a[j]=pom;/* vymena a[j], a[j-1] */
}
}
}
Binární strom
Strom je příkladem nelineární datová struktura. Slovo 'nelineární' zde znamená, že obecně neplatí, že by každý prvek musel mít maximálně jeden předcházející a maximálně jeden následující prvek. Strom je datová struktura, kde každý prvek má nejvýše jeden předcházející prvek ale může mít více prvků následujících.
Budeme se zabývat nejjednodušším případem, kdy následující prvky mohou být nejvýše dva, tj. binárním stromem. Uvažujme proto nyní strukturu se dvěma dynamickými položkami levy, prav pro ukazatele na levého a pravého následníka každého prvku. typedef struct uzel{int x; struct uzel *levy; struct uzel *prav;} UZEL;
Pro práci s binárními stromy se často užívají rekurzivní funkce. Uvedeme jeden typický příklad - funkci pro tisk hodnot všech prvků zadaného binárního stromu.
#include <stdio.h>
#include <conio.h>
typedef struct uzel {
int x;
struct uzel *levy;
struct uzel *prav;
} UZEL;
void inorder(UZEL *);/* funkcni prototyp funkce pro tisk binarniho stromu */
int main()
{
UZEL V,
L, P,
LL, LP, PL, PP;
/**********vytvoreni stromu a zadani hodnot jednotlivych prvku *******/
V.x=4; V.levy=&L; V.prav=&P;
L.x=2; L.levy=&LL; L.prav=&LP;
P.x=6; P.levy=&PL; P.prav=&PP;
LL.x=1; LL.levy=LL.prav=NULL;
LP.x=3; LP.levy=LP.prav=NULL;
PL.x=5; PL.levy=PL.prav=NULL;
PP.x=7; PP.levy=PP.prav=NULL;
clrscr();
inorder(&V); /* tisk obsahu celeho binarniho stromu */
printf("\n\n\n");
inorder(&L); /* tisk podstromu s vrcholem 'L' */
return 0;
}
void inorder(UZEL *uu)
{
if(uu!=NULL){
inorder(uu->levy); /* 1. rekurzivni odkaz */
printf("\n%d",uu->x); /* provadena operace - tisk */
inorder(uu->prav); /* 2. rekurzivni odkaz */
}
return;
}
V první části programu je vytvořen binární strom. První prvek binárního stromu je prvek V za kterým následují levý následník L a pravý následník P. Za prvkem L následují LL a LP, za prvkem P následují PL a PP. Hodnoty těchto prvků (tj. položky x) jsou zadány tak, aby vyjadřovaly pořadí, v jakém se budou hodnoty jednotlivých prvků tisknou funkcí inorder() tj. zleva doprava. Schématicky bychom mohli vyjádřit situaci takto: Označení prvků: Hodnoty prvků:
V 4
/ \ / \
L P 2 6
/ \ / \ / \ / \
LL LP PL PP 1 3 5 7
Funkce inorder() je zajímavá tím, že obsahuje dva rekurzivní odkazy. Co se děje po vyvolání funkce inorder()? Vysvětlíme podrobněji alespoň několik prvních akcí, které po vyvolání této funkce proběhnou:
-
Vyvolání funkce: inorder(&V); (Budeme v této situaci pro stručnost říkat, že funkce inorder byla vyvolána ve V.) Protože &V ≠ NULL, provede se první rekurzivní odkaz s parametrem (&V)- > levy tj. V.levy. B inární strom byl ale zadán tak, že V.levy = &L tzn. funkce inorder() je vyvolána v L .
-
Po vyvolání funkce inorder() v L je analogicky prvním rekurzivním odkazem této funkce vyvolána inorder() v LL.
-
Po vyvolání inorder() v LL je obdobně prvním rekurzivním odkazem vyvolána inorder() v LL.levy = NULL . Pro tuto hodnotu není ovšem splněna podmínka příkazu if a funkce je ukončena, řízení se předá funkci vyvolané v LL. První rekurzivní odkaz byl dokončen, vytiskne se tedy hodnota v LL tj. LL.x = 1. Následuje druhý rekurzivní odkaz, který také znamená vyvolání inorder() v NULL a funkce vyvolaná v LL je ukončena.
-
Protože je ukončena funkce vyvolaná v LL je zároveň splněn první rekurzivní odkaz funkce vyvolané v L . Na řadě je nyní tisk hodnoty L.x = 2 . Následuje druhý rekurzivní odkaz - funkce inorder() je vyvolaná v LP .
-
Po vyvolání inorder() v LP je obdobně prvním rekurzivním odkazem vyvolána inorder() v LP.levy = NULL . Pro tuto hodnotu není ovšem splněna podmínka příkazu if a funkce je ukončena, řízení se předá funkci vyvolané v LP. První rekurzivní odkaz byl dokončen, vytiskne se tedy hodnota v LP tj. LP.x = 3 . Následuje druhý rekurzivní odkaz, který také znamená vyvolání inorder() v NULL a funkce vyvolaná v LP je ukončena.
-
Protože je ukončena funkce vyvolaná v LP je zároveň splněn druhý rekurzivní odkaz funkce vyvolané v L a funkce vyvolaná v L je uzavřena. Tím byl splněn zároveň první rekurzivní odkaz funkce vyvolané v V. Na řadě je nyní tisk hodnoty V.x = 4 . Následuje druhý rekurzivní odkaz - funkce inorder() je vyvolaná v P atd.
Funkce inorder() je jedním ze tří typů funkcí, které se standardně užívají pro práci s binárními stromy. Podrobný výklad otázek, které se týkají práce s binárními stromy i s dalšími datovými strukturami najde čtenář v [].
Poznámky k předpřekladači (preprocesoru)
Předpřekladač (preprocesor) jsme zatím používali intuitivně tak, že jsme v příkladech v této publikaci prakticky vždy používali příkazy #include, kterými jsme zaváděli hlavičkové soubory např. #include<stdio.h> nebo naše uživatelské soubory, např. #include"HLAVNI.C" nebo #include"FUNKCE.C". Nyní se v závěru této publikace zmíníme o předpřekladači (obvykle se předpřekladači říká preprocesor) podrobněji.
Činnost předpřekladače se dá shrnout do několika bodů:
-
Zpracovává zdrojový text programu před použitím překladače.
-
Nekontroluje syntaktickou správnost programu
-
Provádí pouze záměnu textů, např. identifikátorů konstant za odpovídající hodnoty.
-
Vypustí ze zdrojového textu všechny komentáře.
-
Provádí výběr podmíněně překládaných částí programu.
Direktiva #define
Direktiva #define bez parametrů
Tato direktiva je velmi často užívána v jazyku C jako direktiva bez parametrů (viz čl. příkl. 6.1.) Všimněte si, že nesmíme v definici konstanty zapsat na konci středník. Ten by se totiž zapsal s uvedenou hodnotou kdykoliv, když by předpřekladač narazil na příslušný text, např. DOLNI a to by způsobilo syntaktické chyby. Můžeme samozřejmě psát
#define PI 3.1415926
ale i
#include <math.h> /* Matematicka knihovna */
#define PI (4 * atan(1.0)) /* atan je arctg */
…
Konstanty, kterým často říkáme makra, jsou-li takto definovány se nerozvinou, jsou-li uzavřeny v uvozovkách, např.
#define MAKRO past
#include <stdio.h>
int main( void )
{
printf("Toto je MAKRO");
return 0;
}
Tento program vytiskne text: Toto je MAKRO a nikoliv Toto je past, což je právě ta past.
| Používání nesrozumitelných maker činí jazyk C nesrozumitelnm a nebezpečným. Je lépe se tomu vyhnout. |
Direktiva #define s parametry
Při sestavování programů se často vyskytne případ, kdy mnohokrát používáme nějakou funkci, která je krátká, nebo při řešení diferenciálních rovnic měníme často okrajové nebo počáteční podmínky. Pak s výhodou můžeme použít direktivy s parametry. Můžeme např. psát:
#define na_treti(x) ((x)*(x)*(x))
Zdálo by se, že závorky jsou nadbytečné, není tomu tak. Kdybychom totiž napsali:
#define na_treti(x) x*x*x
pak po volání:
na_treti(a+b)
se rozvine do: a+b*a+b*a+b, což je chybné.
Ani vynechání vnějších závorek není vhodné. Např.
#define cti(c) c=getchar()
se po volání: if(cti(r) == 'b') rozvine do známé chyby:
if(r=getchar() == 'b')
Na závěr tohoto odstavce ještě upozorníme na případ, že direktiva s parametry může být někdy tak dlouhá, že se nevejde na jeden řádek. Pak ji přerušíme znakem zpětné lomítko a pokračujeme na další řádce. Např. shora uvedené makro na_treti lze zapsat:
#define na_treti(x) ((x)\
*(x)*(x))
Posledním, ale důležitým, poučením bude, že mezi makrem a první otevírací okrouhlou závorkou argumentů nesmí být mezera.
Operátory # a ##
Tyto operátory se neužívají tak často, a proto pro úsporu místa je objasníme pouze na dvou příkladech, ze kterých si čtenář význam snadno domyslí:
#define otevri_soubor(jmeno_souboru) fopen(#jmeno_souboru,"r")
main()
{/* ...*/
FILE *ms;
ms=otevri_soubor(muj_soubor);
/* ...*/}
Předpřekladač "rozepíše" toto makro na:
/*...*/
FILE *ms;
ms=fopen("muj_soubor","r");
/*...*/
#define print_var(num) printf("var" #num " = %d\n",var##num)
/*...*/
int var1,var2,var3;
/*...*/
print_var(3);
/*...*/
Zde předpřekladač rozepíše makro na:
int var1,var2,var3;
/*...*/
printf("var" "3" " = %d\n",var3);
/*...*/
Podmíněný překlad
Při ladění programů pomáhají často profesionálně sestavené ladící programy. Jejich nevýhodou je, že při snaze pokrýt co nejvíce možností jsou tyto programy někdy dost složité a mnoho programátorů proto programy píše tak, že do nich zapisují vlastní ladící tisky, které po odladění pro zvýšení přehlednosti programu ruší, přičemž při troše nepozornosti zruší víc než ladící tisky, z čehož vznikají mnohé nepříjemnosti. V jazycích C a C++ je umožněn velmi praktický tzv. podmíněný překlad, který si na několika krátkých ukázkách vysvětlíme.
Příklad 5.3: Napišme jednoduchou funkci, která bude napsána tak, aby byla přijatelná, jak pro původní jazyk C jak byl definován v [], tak pro ANSI C a C++. Využijme podmíněného překladu ve třech základních variantách.
#define K&R 0
#define na_treti(x) ((x)*(x)*(x))
#if K&R
f(x)
int x;
{return (na_treti(x)-4*x+7);}
#else
f(int x)
{ return (na_treti(x)-4*x+7);}
#endif
#undef K&R
#define na_treti(x) ((x)*(x)*(x))
#ifdef K&R
f(x)
int x;
{return (na_treti(x)-4*x+7);}
#else
f(int x)
{ return (na_treti(x)-4*x+7);}
#endif
#define K&R
#define na_treti(x) ((x)*(x)*(x))
#ifndef K&R
f(x)
int x;
{return (na_treti(x)-4*x+7);}
#else
f(int x)
{ return (na_treti(x)-4*x+7);}
#endif
Vazba na systém UNIX (Linux)
V této kapitole se seznámíme se základními příkazy operačního systému UNIX, aby čtenář, který je zvyklý pracovat s operačním systémem DOS, se rychle orientoval i v tomto široce užívaném systému, který je ostatně z velké části napsán přímo v jazyku C. Naučíme se překládat zdrojové programy psané v jazyku C a sestavovat je do pracovních programů. Poté se zmíníme o původním jazyku C, označovaným nyní často jako K&R C podle autorů jazyka Kerrighana a Ritchieho, který je popsán v dnes již klasické knize [], a v kterém byl operační systém z převážné části napsán. Pak se naučíme pracovat s tzv. vi editorem, který je v UNIXu běžně používán. A nakonec se zmíníme o vstupech a výstupech nízké úrovně read a write.
Některé základní příkazy operačního systému UNIX
Budeme postupovat tak, že uvedeme jméno příkazu a jeho funkci. Nebudeme zabíhat do detailů, protože to jednak není cílem těchto skript a jednak se příkazy v různých systémech mírně liší. Čtenář si podle této hrubé kostry, upřesní použití sám.
-
password
Příkaz mění heslo uživatele. Uživatel je dotázán na staré a nové heslo. Po úspěšném zadání starého, je mu přiděleno nové. Může být odmítnuto, je-li příliš krátké nebo, podle názoru systému, příliš jednoduché. -
who
Zobrazí identifikace přihlášených uživatelů do systému. Příkaz who am I identifikuje přímo uživatele, který se táže. -
date
Zobrazí systémový čas a datum v pořadí: datum čas. -
cat
Příkaz spojuje soubory s jejich následujícím výpisem.
Použití:cat soubor_1
Příkaz vypíše soubor soubor_1 na obrazovce;
příkaz `cat soubor_1 soubor_2 > soubor_3 `
spojí soubor_1 a soubor_2 a zapíše do souboru_3. Bylo-li v souboru něco uloženo před použitím tohoto příkazu, je to zničeno. -
more
Vypisuje soubor po obrazovkách.
Použití: more soubor
Je možné též použít při příkazu cat např. cat soubor_1 soubor_2 > soubor_3|more
Tento příkaz nám umožní pohodlné prohlížení souboru_3, i když je delší než jedna obrazovka. -
clear
Příkaz maže obrazovku. -
pwd
Příkaz vypíše pracovní (tj. právě otevřený) adresář. -
cd
Příkaz změní pracovní adresář. Např.
cd /home
změní stávající pracovní adresář na adresář /home, je-li to přípustné nebo možné. -
ls
Provádí výpis souborů v pracovním adresáři. Napíšeme-li např.
ls -l
Realizuje výpis jmen souborů s podrobnější informací. Uvedeme-li parametr -a vypíše se i seznam tzv. tečkovaných souborů, které se normálně nezobrazují.
Často v systému existuje ještě příkaz l. Pak platí, že pro podrobný výpis se používá příkazu l a pro stručný ls. Na tomto místě také upozorňujeme na znakya?, které mají tentýž význam jako v systému DOS. Např.ls tvypíše všechny soubory začínající písmenemt. Příkazls t???vypíše všechna jména čtyřpísmenových souborů začínajících písmenemt. -
rm
Příkaz odstraní soubor z adresáře. Např. příkaz
rm soubor_1
odstraní z pracovního adresáře soubor_1. -
mv
Příkaz přejmenuje soubor. Např. příkaz
mv soubor_1 soubor_2
přejmenuje soubor_1 na soubor_2. Existoval-li nějaký soubor_2 je jeho původní obsah zničen. -
cp
Příkaz kopíruje soubory.
cp soubor_1 soubor_2
Provede kopii souboru soubor_1 do souboru soubor_2. -
man
Tento příkaz vyvolá anglicky psaný manuál o systému; u slova man se musí napsat jméno souboru nebo příkazu (což je v UNIXu obvykle totéž), který nás zajímá, např.
man vi
nám poskytne podrobné informace o textovém editoru vi.
Toto jsou základní příkazy, s kterými čtenář při prvém seznamování se systémem vystačí. Může se mu však stát, že při sebevětší opatrnosti, udělá takovou logickou chybu v programu, že se program tzv. a uživatel musí proces nějak ukončit. K tomu mu mohou posloužit ještě dva příkazy: -
ps
Příkaz zobrazí stav procesu (nebo procesů), které právě v systému probíhají. Např.
ps -t3
zobrazí stav procesu na terminálu tty3 (uživatel se dozví o typu terminálu např. z příkazu who am I).
Další parametry, které může čtenář vyzkoušet jsou:-tp1(např. pro terminál typu ttyp1),-gzobrazí všechny procesy,-lby měl zobrazit podrobný výpis.
Nejdůležitější, co však v daném případě uživatel potřebuje, je číslo zacykleného procesu, které se vždy dozví jak ze stručného, tak podrobného výpisu.
Toto číslo procesu použije v dalším příkazu, který zacyklený proces ukončí. -
kill
Zapíšeme-li např.kill -9 4287ukončíme proces s číslem procesu4287. Je samozřejmé, že příkazpsa příkazkill, musíme v takovém případě použít z jiného terminálu, než na kterém došlo k zacyklení procesu.
Proto je dobré příkaz who am I použít okamžitě po přihlášení se do systému.
Překlad zdrojových souborů
V tomto článku se zmíníme o překladu zdrojových souborů a sestavení pracovního programu.
Představme si modelový případ, kdy máme vytvořeny nejprve dva programové soubory radek.c a funkce.c (zdrojové programy v jazyku C mají standardně koncovku c), které chceme přeložit a uložit jako soubory. To můžeme provést příkazem:
cc -c radek.c funkce.c
Vzniknou soubory radek.o a funkce.o
Nyní např. editorem vi připravíme další zdrojový soubor, např. muj_pra.c, který chceme přeložit a sestavit se soubory radek.o a funkce.o do pracovního programu. Napíšeme
cc muj_pra.c radek.o funkce.o
Pracovní program, protože jsme nestanovili jinak, se standardně jmenuje a.out. Chceme-li nazvat program program jinak, např. mp, napíšeme:
cc muj_pra.c radek.o funkce.o -o mp
Program se pak jmenuje mp a tak může být vyvolán. Např. příkaz:
mp < muj_vst >muj_vys
zajistí vyvolání programu, přičemž vstup do programu je ze souboru muj_vst a výstup je do souboru muj_vys. Potřebujeme-li vyvolat např. matematickou knihovnu (v operačních systémech UNIX má obvykle označení lm), píšeme ji až za soubory, které ji při sestavení potřebují, nejlépe úplně na konci příkazu. Kdybychom tedy využívali matematickou knihovnu ve shora uvedeném programu, psali bychom:
cc muj_pra.c radek.o funkce.o -o mp -lm
Nic nám nebrání provést překlad a sestavení všech souborů najednou. Můžeme tedy psát:
cc muj_pra.c radek.c funkce.c -o mp -lm
Je samozřejmé, že můžeme pracovat i s direktivou #include. Zápis již tak jednoduchého příkazu pro překlad se ještě zjednoduší.
Hlavní rozdíly mezi jazykem ANSI a K&R
V tomto článku se zmíníme o rozdílech mezi standardem jazyka a jazykem původním, o kterém se nám nechce ani psát jako o dialektu, i když vznikem normy jazyka tomu tak je. Jen velice stručně z historie.
-
Pro implementaci systému UNIX se autoři (Ritchie D., Thompson K.) rozhodli použít programovací jazyk dostatečně efektivní z hlediska strojového kódu, ale nezávislý na konkrétním procesoru.
-
Nejprve zvažovali použít jazyk BCPL (od Richardse M.), později však Thompson navrhl variantu zvanou jazyk B.
-
Jazyk B byl sice dostatečně efektivní, ale nikoliv dostatečně obecný, a proto Ritchie ve snaze zvýšit obecnost navrhl jazyk C, v kterém je napsána převážná část systému.
-
Jazyk se dále vyvíjel a v roce 1978 napsali Kernighan a Ritchie knihu [], která představovala starší standard jazyka.
-
V roce 1984 vzniká první verze normy ANSI C a v roce 1990 vyšla norma ANSI C pod označením X3J11/90-013.
-
Vzniká řada rozšíření jazyka, ale žádná nedosáhla takového rozšíření jako jazyk C, s výjimkou jazyka C umožňující objektově orientované programování. Jazyk C vlastně označuje, jak čtenář tohoto skripta ví, jazyk C+1. Proto jazyk C++ a ne jazyk C+.
O hlavních rozdílech mezi ANSI C a K&R C jsme se již zmínili na různých místech v předchozích kapitolách. Nyní je nejprve vyjmenujeme v přehledu podle důležitosti a pak si hlavní rozdíly ukážeme na příkladech. Je třeba si jen uvědomit, že přechod mezi K&R C a ANSI C je díky mnoha implementacím neostrý.
-
Formální parametry se nedeklarují přímo v závorkách za identifikátorem funkce, ale nejprve se vyjmenují v závorkách seznamu a po ukončení seznamu se deklarují jejich typy.
Tyto deklarace jsou uvedeny před složenou závorkou, která určuje tělo (operační část) funkce.
Tato skutečnost je dosti podstatná.
Je-li to totiž realizováno takto, ztrácí se možnost kontroly a eventuálního přetypování skutečných parametrů.
Tato možnost v ANSI C je v případě, že uvádíme prototypy, což (narozdíl od jazyka C++) ovšem nemusíme. -
Automatické pole se nesmí inicializovat.
-
Nelze pracovat se strukturami jako s celkem, tedy máme-li dvě struktury a a b téhož typu, nelze psát
a=b. -
Struktura nesmí být návratovou hodnotou funkce a struktura nesmí být předána funkci jako skutečný parametr.
-
Chybí slovní symbol
void. To znamená, že není možné dát překladači informaci, že některá funkce nevrací hodnotu, ale je tím, čemu se v jiných programovacích jazycích říká vlastní procedura nebo podprogram. -
Chybí slovní symbol
const. To znamená, že konstanty se musí definovat direktivou#define. -
Výčtový typ (slovní symbol
enum) není zaveden. -
Někdy nebývají deklarovány standardní konstanty
NULLaEOFa musíme je definovat sami.
V případě NULL to není problém, protože stačí např.
#define NULL 0
nebo místo NULL používáme číslici 0, ale v případě EOF je třeba zjistit ekvivalent v daném systému, obvykle to bývá -1 nebo 1.
Tyto problémy by ale mohly nastat jen u starších verzí jazyka.
-
Nejsou definovány operátory
a#(viz 2). -
Není definováno unární
+tzn. že+1, respektive+ase chápe jako0+1respektive0+a. -
Při konverzích dochází k automatickému převodu
floatnadouble.
Příklad: Sestavme program pro tisk tabulky Fahrenheit - Celsius. Pro přepočet použijte vzorce
\(C = \frac{59}{(F-32)}\).
Tabulku tiskněme ve tvaru:
Stupne Fahrenheita Stupne Celsia
dddd ddd.dd
dddd ddd.dd
... ...
kde d je číslice, znaménko minus nebo prázdný znak.
/*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius
pro fahr = 0, 20,..., 300 */
int main(void)
{
/* Definice konstant pro dolni a horni mez a krok zmeny */
const int DOLNI=20,HORNI=300,KROK=20;
float fahr=DOLNI; /* Definice promenne fahr s pocatecnim prirazenim; */
float cels; /* Definice promenne, ktera bude nabyvat hodnot stupnu
Celsia
Tisk zahlavi tabulky */
printf("\n Stupne Fahrenheita Stupne Celsia\n");
/* Prikaz while, ktery se bude provadet dokud fahr bude mensi nebo
rovno 300 */
while(fahr<=HORNI)
{
cels=5./9.*(fahr-32);
printf( "\n %4.0f %5.2f",fahr,cels);
fahr=fahr+KROK;
}
return 0;
}
/*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius
pro fahr = 0, 20,..., 300 */
#define DOLNI 20
#define HORNI 300
#define KROK 20
main()
{
float fahr=DOLNI; /* Definice promenne fahr s pocatecnim
prirazenim;*/
float cels; /* Definice promenne, ktera bude nabyvat hodnot stupnu
Celsia
Tisk zahlavi tabulky */
printf("\n Stupne Fahrenheita Stupne Celsia\n");
/*Prikaz while, ktery se bude provadet dokud fahr bude mensi nebo
rovno 300*/
while(fahr<=HORNI)
{
cels=5./9.*(fahr-32);
printf( "\n %4.0f %5.2f",fahr,cels);
fahr=fahr+KROK;
}
}
Příklad: Zapišme program pro výpočet ∫01[(sin(x))/( x)] Simpsonovou metodou v ANSI C a K&R C
#include <stdio.h>
#include <math.h> /* Matematicka knihovna - nazev zavisi na
implementaci jazyka */
#include "integral.c"
/****************************************************************
* Soubor s nazvem hlavint.c vyuziva soubor integral.c *
****************************************************************/
main()
{
int i,m,j;double g(double),integral(double,double,int,FUK);
printf("zadej pocet vypoctu, zakladni deleni\n");
scanf("%d %d",&j,&m);
for(i=1;i<=j;i++)
printf("Deleni intervalu: %d integral = %f\n",m*i,
integral(0.,1.5,m*i,g));
return 0;
}
double g(double x)
{
if (x==0)
return(1);
else
return(sin(x)/x);
}
/****************************************************************
* Soubor integral.c, ktery realizuje numericky vypocet integralu*
* Numericka metoda: Simpsonova formule *
****************************************************************/
typedef double (*FUK)(double);
double integral(double a, double b, int n, FUK f)
/* FUK f je totez jako bychom psali: double(*f)(double)) */
{
double s; double h=(b-a)/n; int k,i;
k=1;s=f(a)+f(b);
for(i=1;i<n;i++)
{
s+=(k+3)*f(a+i*h);
k=-k;
}
return(h/3*s);
}
#include <stdio.h>
#include <math.h>
#include "integral.c"
/****************************************************************
* Soubor s nazvem hlavikr.c vyuziva soubor intkr.c *
****************************************************************/
main()
{
int i,m,j;double g(),integral();
printf("zadej pocet vypoctu, zakladni deleni\n");
scanf("%d %d",&j,&m);
for(i=1;i<=j;i++)
printf("Deleni intervalu: %d integral = %f\n",
m*i,integral(0.,1.,m*i,g));
/* Pri tonto zpusobu zapisu bez moznosti zadani prototypu, je
neprijemnou chybou napr. zapis: integral(0,1,m*i,g), protoze
parametry 0 a 1 se prenesou jako celociselne, ale procedura
integral je povazuje za double */
return 0;
}
double g(x)
double x; /* !!!!!! Toto je hlavni rozdil !!!!!! */
{
if (x==0)
return(1);
else
return(sin(x)/x);
}
/****************************************************************
* Soubor intkr.c, ktery realizuje numericky vypocet integralu *
* Numericka metoda: Simpsonova formule *
****************************************************************/
typedef double (*FUK)(double);
double integral(a,b,n,f)
double a,b;int n;FUK f; /* !!!!!! Toto je hlavni rozdil !!!!!! */
{
double s; double h=(b-a)/n; int k,i;
k=1;s=f(a)+f(b);
for(i=1;i<n;i++){
s+=(k+3)*f(a+i*h);
k=-k;
}
return(h/3*s);
}
Editor vi
Editor vi je nejrozšířenějším editorem v systémech UNIX. Tento editor není tak přátelský k uživateli jako mnohé textové editory známé z prostředí užívaných v systému DOS, ale je schopen spolupráce s libovolným terminálem. Seznámíme se s ním jen do té míry, abychom byli schopni zapisovat soubory a provádět jejich opravy a úpravy, i když někdy ne optimálně. Má-li čtenář zájem se s tímto editorem seznámit podrobněji, nalezne informace v uživatelském manuálu příslušného systému UNIX nebo přímo na obrazovce terminálu pomocí příkazu man vi.
Předpokládejme, že chceme vytvořit nový soubor soub_1, pak napíšeme: vi soub_1
Pokud soubor neexistoval je obrazovka v 1. sloupci vyplněna znaky ~ a kurzor je na prvním sloupci prvního řádku. Nemůžeme zapisovat, pokud nepřejdeme do tzv. vkládacího režimu stiskem tlačítka s písmenem i (insert) nebo a ( append) bez následného stisku tlačítka enter. Od tohoto okamžiku můžeme psát. Vkládání ukončíme stiskem tlačítka esc. Zápis na disk lze realizovat např. stiskem ZZ, nebo stiskem :x a stiskem enter. Chceme-li soubor přejmenovat, např. na sou_2, zapíšeme :x sou_2 a stiskneme enter. Neuvedli jsme si, že jsme-li v tzv. příkazovém režimu (po stisku :) píšeme do spodní části obrazovky a příkazy tam zapsané ukončujeme vždy stiskem enter. Tuto skutečnost již nebudeme dále zdůrazňovat. Nechceme-li soubor uložit na disk a editaci chceme ukončit, stiskneme :q!. Kdybychom si chtěli soubor jen prohlédnout a neudělali bychom v něm žádné změny, stačí ukončit prohlížení zápisem :q.
V textu se standardně můžeme pohybovat, nejsme-li ve vkládacím režimu tlačítky h vlevo, l doprava, k, nahoru a j dolů, při správném nastavení terminálu se můžeme pohybovat též kurzorovými šipkami, což je názornější. Ve vkládacím režimu pohybujeme kurzorem pouze šipkami. Nejsme-li ve vkládacím režimu můžeme nalézt řetězec znaků, např. if(lad) pomocí zápisu /if(lad)enter na následující výskyty řetězce se dostaneme stiskem n (následující) a N ( předcházející).
Nejsme-li ve vkládacím režimu, můžeme vymazat kterýkoliv znak tím, že na něj přemístíme kurzor a stiskneme tlačítko x. Můžeme tak rušit libovolně dlouhý text, chceme-li vymazat právě jeden znak stiskneme r a zapíšeme správný znak, eventuálně R - pak se přejde do přepisovacího režimu až do stisku tlačítka esc. Chceme-li zrušit celou řádku stiskneme dd. Stiskneme-li u, rušíme poslední změnu, což nám při chybě může ušetřit mnoho práce.
Jsme-li někde na řádku a nejsme ve vkládacím režimu, můžeme se rychle dostat na konec řádky stiskem $ a na začátek řádky stiskem | . Stiskneme-li A, dostaneme se na konec řádky a zároveň přejdeme do vkládacího režimu a k téže změně režimu dojde, přejdeme-li na začátek řádky stiskem I. Chceme-li se pohybovat po řádce rychleji než po znacích, umožní nám stisk w posun o slovo dopředu a b posun o slovo zpět. Téhož, ale s ignorováním interpunkce, dosáhneme stisky W nebo B.
Nejsme-li ve vkládacím režimu, můžeme listovat souborem po obrazovkách: dopředu o obrazovku stiskem ctr F a zpět stiskem ctr B. Chceme-li docílit pohyb o půl obrazovky dopředu, stiskneme ctr D a dozadu ctr U.
Před povely lze přidat číslo: např. 5x zruší pět znaků, 3dd vymaže tři řádky, 3w přeskočí dvě slova apod. Mezi číslo a povel posunu je možno vložit písmeno d, např. dW zruší slovo, 5dW zruší pět slov.
Velice užitečný je přesun části textu na jiné místo. Postup:
-
požadovanou část textu zrušíme,
-
přesuneme kurzor tam, kam potřebujeme,
-
stiskneme písmeno p (vlož za kurzor) nebo P (vlož před kurzor).
Při kopírování postupujeme podobně, jen místo příkazu d, použijeme příkazu y (příkaz yw vybere slovo, příkaz y3w vybere tři slova, příkaz 6yy vybere šest řádek apod.). Postup lze stručně popsat takto:
-
kurzorem najedeme na začátek kopírovaného textu,
-
požadovanou část kopírovaného textu označíme příkazem y,
-
přesuneme kurzor tam, kam potřebujeme,
-
stiskneme písmeno p (vlož za kurzor) nebo P (vlož před kurzor).
Chceme-li uvolnit řádku pod kurzorem, stiskneme o, chceme-li uvolnit řádku nad kurzorem, stiskneme písmeno O.
Chceme-li vyměnit řetězce znaků, které se mohou vyskytovat od řádků n1 do řádku n2 můžeme psát :n1,n2 s/hledaný_řetězec/řetězec_nahrazující/
Při takto zapsaném příkazu se provede náhrada pouze prvního výskytu na řádku. Chceme-li nahradit všechny výskyty na řádku, zapíšeme na konec příkazu písmeno g. Tedy příkaz :20,200 s/muj program/tvuj program/g
provede záměnu všech řetězců muj program řetězcem tvuj program od dvacátého do dvoustého řádku souboru včetně.
Mohli bychom si vysvětlit, jak lze udávat řádky, ale místo toho doporučíme čtenáři, aby si ve svém domovském adresáři zavedl tečkovaný soubor (tyto soubory jsou běžně nevypsatelné příkazy ls nebo l, neudáme-li parametr -a) s názvem .exrc, ve kterém zapíše vi editorem jediný řádek ve tvaru: set nu smd
Tím se zajistí, že řádky souborů, které se editují editorem budou číslovány a v pravém dolním rohu obrazovky se bude zobrazovat režim, v kterém se editor právě nalézá. Zkratka nu lze též psát number a akronym smd lze psát showmode, což je pro čtenáře znalého základů angličtiny dostatečně vysvětlující. 5. Vstupy a výstupy nízké úrovně V tomto článku se zmíníme o operacích vstupu a výstupu nízké úrovně pomocí nichž je možné implementovat části standardních knihoven. Byly implementovány v původním K&R C, ale jsou k dispozici i v jazycích C v jiných operačních systémech.
Logická čísla
V operačním systému UNIX se vstup a výstup realizuje čtením a zapisováním do souborů, protože všechna přídavná zařízení, včetně uživatelského terminálu, se považují za soubory. Veškerá komunikace mezi programem a přídavnými zařízeními se tak uskutečňuje jediným rozhraním.
Před čtením nebo zapisováním do souboru je obecně nutné informovat systém tzv. otevřením souboru. Systém prověří, existuje-li takový soubor a máme-li právo přístupu. Je-li všechno v pořádku, předá systém programu kladné celé číslo, které nazveme logickým číslem souboru. Kdykoliv se pak nad souborem provádí nějaká operace, identifikuje se soubor tímto číslem a nikoliv jménem souboru (podobně je tomu ve v jazyku Fortran např. v příkazech read(1,…) a write(2,…)).
Když interpret systémových příkazů, kterému se v UNIXu říká shell spouští program, otevře tři soubory s logickými čísly 0, 1 a 2, které se po řadě nazývají standardní vstup, standardní výstup a standardní chybový výstup. Všechny jsou obyčejně spojeny s terminálem. Nehodí-li se uživateli toto pevné spojení na terminál, může přesměrovat vstupy a výstupy pomocí znaků < a > do souboru nebo ze souboru, jak jsme si již ukázali v čl. 2. V tomto případě shell změní přiřazení pro logická čísla 0, resp. 1 na soubory muj_vst, resp. muj_vys. Chybová hlášení se zapisují na terminál.
Vstup a výstup nízké úrovně
Celý vstup a výstup se realizuje pomocí dvou příkazů (funkcí), které se nazývají read a write. Prvním argumentem je logické číslo souboru, druhým argumentem je vyrovnávací paměť z které data přicházejí nebo odcházejí. Třetí argument je počet slabik (bytes), které je třeba přenést. Volání mají např. tvar:
cti_n_sl = read(lc,vp,n);
zapis_n_sl = write(lc,vp,n);
Příkazy vrací počet skutečně přenesených slabik. Při čtení se může vrátit menší počet slabik, než se požadovalo. Je-li hodnota funkce read rovna nule, znamená to konec souboru a tt>-1 signalizuje nějakou chybu. Při zápisu se vrací počet požadovaných slabik. Když se hodnota funkce write tomuto počtu nerovná, znamená to chybu.
Počet slabik, které se čtou nebo zapisují může být libovolný. Často užívané hodnoty jsou 1 (pro čtení po znacích (bez vyrovnávací paměti), 512, 1024 apod. (délka fyzických bloků přídavných zařízení).
Napišme nyní dva jednoduché programy.
Příklad 6.3: Zapišme program, který bude kopírovat soubory.
V systému UNIX bude tento program kopírovat "cokoliv kamkoliv", protože vstup a výstup můžeme přesměrovat na libovolný soubor (a zařízení).
Příklad 6.4: Použijme příkazy read a write k implementaci funkcí vyšší úrovně m_getchar a m_putchar, které jsou totožné se standardními getchar a putchar
#include <stdio.h>
#define MASKA 0377 /*Zabezpeci kladnou velikost znaku */
m_getchar() /* Vstup jedineho znaku */
{
char c; /* c musi byt definovano jako znak, protoze read prijima
ukazatel na znak */
return ((read(0,&c,1)>0) ? c & MASKA : EOF); /* Podmineny vyraz,
EOF není totiz znak */
}
m_putchar(int c) /* Vystup jedineho znaku */
{
return(write(1,&c,1));
}
/* Hlavni program pro testovani m_getchar a m_putchar */
int main( void )
{
int c;
while((c=m_getchar())!=EOF)
{
m_putchar(c);
}
return 0;
}
Použití příkazů open(), create(), close(), unlink()
Chceme-li použít jiné soubory než náhradní vstupní, výstupní a chybový soubor, musíme je otevřít explicitně. Existují dvě volání systému: open (otevření souboru) a creat (vytvoření souboru). Volání open se podobá funkci fopen, kterou jsme si vysvětlili dříve v kapitole 2.1 s tou výjimkou, že namísto ukazatele na soubor vrací logické číslo souboru.
/*...*/
int lc,vstvys;char[15]
/*...*/
lc=open(jmeno_souboru, vstvys);
Parametr vstvys má hodnotu 0, resp. 1 pro vstup, resp. pro výstup.
Nastane-li nějaká chyba, vrací open hodnotu -1, jinak vrací platnou hodnotu logického čísla.
lc=creat(jmeno_souboru,ochrana_souboru);
Podařilo-li se vytvořit soubor se jménem jmeno_souboru, vrací se hodnota logického čísla, jinak se vrací hodnota -1. Existuje-li již soubor, je zkrácen na nulovou délku.
Je-li soubor úplně nový, vytvoří ho creat s režimem ochrany.
V UNIXu je se systémem spojených devět bitů informací o ochranách, které řídí přístupová práva pro čtení, zápis a použití souboru pro vlastníka, vlastníkův tým a ostatní.
Vhodné je použít trojciferné osmičkové číslo.
Např. číslo 0755 (tj. dvojkově: 111 101 101) povoluje vše vlastníku souboru a čtení a použití souboru skupině i ostatním.
Číslo 0774 (tj. dvojkově: 111 111 100) povoluje vše vlastníku a skupině a pouze čtení pro ostatní.
/* Definice m_cp, ktery je zjednodusenou verzi prikazu cp kopirujici
jeden soubor do druheho. Ve skutecnem prikazu cp je mozne, aby
druhym argumentem byl adresar */
#define NULA 0
#define VYRPAM 512 /* Vyrovnavaci pamet */
#define OCHRANA 744 /* Vlastnik smi vse; skupina a ostatni jen cist */
main(poc_arg,arg)
int poc_arg; char *arg[];
{
int f1,f2,n,chyba();
char vp[VYRPAM];
if(poc_arg!=3)
chyba("Pouziti: m_cp odkud kam",NULA);
if((f1=open(arg[1],0))==-1)
chyba("m_cp: nelze otevrit %s",arg[1]);
if((f2=creat(arg[2],OCHRANA))==-1)
chyba("m_cp: nelze vytvorit %s",arg[2]);
while((n=read(f1,vp,VYRPAM))>0)
if(write(f2,vp,n)!=n)
chyba("m_cp: chyba pri zapisu",NULA);
exit(0);
}
chyba(s1,s2) /* chybova zprava a ukonceni prikazu */
char *s1,*s2;
{
printf(s1,s2);
printf("\n");
exit(1);
}
Počet souborů, které mohou být v programu současně otevřené, bývá omezený a závisí na konkrétním systému (15 až 20 je standardní). Z toho ovšem vyplývá, že program, který pracuje s mnoha soubory, musí používat logická čísla opakovaně. Funkce close ruší spojení mezi logickým číslem a otevřeným souborem a uvolňuje logické číslo pro spojení s jiným souborem. Při ukončení programu se všechny otevřené soubory automaticky uzavřou.
Zmiňme se ještě o příkazu (funkci) exit, která je v příkladu použita. Argument funkce exit je k dispozici procesu, který daný proces vyvolal, aby program, který tento program vyvolal jako podproces mohl testovat, skončil-li úspěšně nebo s chybou. Podle konvence hodnota 0 signalizuje, že vše je v pořádku a různé nenulové hodnoty signalizují nenormální situace.
Příkaz unlink(jmeno_souboru) vyřadí soubor jmeno_souboru z evidence systému správy souborů.
Přímý přístup — funkce seek() a lseek()
Vstupní a výstupní operace nad souborem jsou v jazyku C sekvenční, tzn. že každý příkaz read nebo write se provede na tom místě souboru, které je bezprostředně za tím místem, kde se provedla operace předcházející. Do souboru však můžeme zapisovat nebo číst v libovolném pořadí. Umožňuje
to příkaz (funkce) lseek. Umožňuje pohyb po souboru bez čtení nebo zápisu. Např. zápis:
lseek(lc,rel_adresa,pocatek);
zajistí, že běžná pozice v souboru s logickým číslem lc se posune na běžnou pozici rel_adresa - často užívaný (a trochu matoucí) anglický název je offset -, která je určena parametrem pocatek. Proměnná rel_adresa je typu long, lc a pocatek jsou typu int. Hodnotou pocatek může být 0, resp. 1, resp. 2, které určují, že rel_adresa se vztahuje k začátku, resp. k běžné pozici, resp. ke konci souboru. Příkaz
lseek(lc,0L,2);
vyhledá před zápisem do souboru jeho konec. Kdežto návrat na začátek, tj. převinutí je
lseek(lc,0L,0);
konstantu 0L lze psát i (long)0.
#define VELIKOST 512
get(lc,bp,vp)
int lc;long bp;char *vp;
/*****************************************************************
* Funkce pocita a vraci pocet slabik v souboru cislo lc od bezne *
* pozice bp. Ukazatel vp ukazuje na pocatek vyrovnavaci pameti, *
* urychlujici celou operaci *
*****************************************************************/
{
long n=0,m;
lseek(lc,bp,0); /* Prejdi na pocatek */
while((m=read(lc,vp,VELIKOST))>0)
n=n+m;
return(n);
}
main(int parg, char *arg[])
/****************************************************************
* Hlavni program testujici funkci get. Sectou se vsechny slabiky*
* v souboru zapsanem v arg[1] *
****************************************************************/
{
int lc;char vyr_pam[VELIKOST];
if (parg !=2)
printf("Pouziti: jmeno_programu jmeno_souboru_jehoz_znaky_pocitame\n");
else
{
lc=open(arg[1],0);
printf("\nPocet znaku v souboru %s je %d\n",arg[1],get(lc,0L,vyr_pam));
}
}
Ve starších verzích UNIXu se příkaz lseek značí seek. Má tutéž funkci, jen parametr rel_adresa není typu long int, ale int.
Knihovny funkcí standardu ANSI
V této kapitole uvádíme přehled často používaných knihovních funkcí standardu ANSI C. Funkce jsou rozděleny podle oblasti jejich použití. Vždy je uveden název, stručná charakteristika a úplný funkční prototyp.
Rozpoznávání skupin znaků
Všechny funkce vrací 0 jako FALSE a nenulovou hodnotu - obvykle tentýž znak jako TRUE.
| jméno | popis | prototyp |
|---|---|---|
isalnum |
znak je alfanumerický ['A'- 'Z', 'a'-'z', '0'-'9'] |
int isalnum(int); |
isalpha |
znak je písmeno ['A'- 'Z', 'a'-'z'] |
int isalpha(int); |
iscntrl |
znak je řídící [0x01 - 0x1f, 0x7f] |
int iscntrl(int); |
isdigit |
znak je číslice ['0' - '9'] |
int isdigit(int); |
isgraph |
znak je viditelný [0x21 - 0x7e] |
int isgraph(int); |
islower |
znak je malé písmeno ['a' - 'z'] |
int islower(int); |
isprint |
znak lze vytisknout [0x20 - 0x7e] |
int isprint(int); |
ispunct |
znak je interpunkční znak [0x21 - 0x2f, 0x3a - 0x40, 0x5b - 0x60, 0x7b - 0x7e] |
int ispunct(int); |
isspace |
Znak je "bílý" znak [0x09 - 0x0d, 0x20] |
int isspace(int); |
isupper |
znak je velké písmeno [ 'A' - 'Z'] |
int isupper(int); |
isxdigit |
znak je hexadecimální číslice ['0' - '9', 'a' - 'z', 'A' - 'Z'] |
int isxdigit(int); |
Konverzní funkce
| jméno | popis | prototyp |
|---|---|---|
atof |
řetězec na double |
double atof(const char *s); |
atoi |
řetězec na int |
int atoi(const char *s); |
atol |
řetězec na long int |
long atol(const char *s); |
strtod |
řetězec na double |
double strtod(const char *s, char **endptr); |
strtol |
řetězec na long int |
long strtol(const char *s, char **endptr, int radix); |
strtoul |
řetězec na unsigned long int |
unsigned long strtoul(const char *s, char **endptr, int radix); |
tolower |
velká písmena na malá, jiné znaky nezměněny |
int tolower(int); |
toupper |
malá písmena na velká, jiné znaky nezměněny |
int toupper(int); |
Souborově orientované funkce
#include <stdio.h>
V některých funkčních prototypech následujících funkcí se používá typ size_t:
size_t - je definován tak, aby byl shodný s typem návratové hodnoty operátoru sizeof, tj. jako některý z typů unsigned typedef ui-type size_t);
| jméno | popis | prototyp |
|---|---|---|
clearerr |
nuluje indikaci chyb |
void clearerr(FILE *file); |
fclose |
uzavírá soubor |
int fclose(FILE *file); |
feof |
test konce souboru |
int feof(FILE *file); |
ferror |
zjišťuje chyby při práci se souborem |
int ferror(FILE *file); |
fflush |
zapisuje obsah vyrovnávacího bufferu do paměti |
int fflush(FILE *file); |
fgetc |
čte znak ze souboru |
int fgetc(FILE *file); |
fgetpos |
vrací aktuální pozici ukazatele v souboru |
int fgetpos(FILE *file, fpos_t *pos); |
fgets |
čte řetězec maximální délky n ze souboru |
char *fgets(char *s, int n, FILE *file); |
fopen |
otevření souboru |
FILE *fopen(const char *filename, const char *mode); |
fprintf |
formátovaný zápis do souboru |
int fprintf(FILE *file, const char *format, [,arg, …]); |
fputc |
zápis znaku do souboru |
int fputc(int c, FILE *file); |
fputs |
výstup řetězce do souboru s odřádkováním |
int fputs(const char *s, FILE *file); |
fread |
čte blok dat ze souboru |
size_t fread(void *ptr, size_t size, size_t n, FILE *file); |
freopen |
sdružuje nový soubor s již otevřeným souborem, výhodné pro přesměrování |
FILE *freopen(const char *fname, const char *mode, FILE *file); |
fscanf |
formátované čtení ze souboru |
int fscanf(FILE *file, const char *format, [,address, …]); |
fseek |
nastavuje novou pozici ukazatele v souboru |
int fseek(FILE *file, long offset, int whence); |
fsetpos |
nastavuje novou pozici ukazatele v souboru |
int fsetpos(FILE *file, const fpos_t *pos); |
ftell |
vrací aktuální pozici ukazatele v souboru |
long ftell(FILE *file); |
fwrite |
zapisuje blok dat do souboru |
size_t fwrite(void *ptr, size_t size, size_t n, FILE *file); |
getc |
čte znak ze souboru |
int getc(FILE *file); |
getchar |
čte znak z stdin |
int getchar(void); |
gets |
čte řetězec z stdin |
char *gets(char *s); |
perror |
výpis chybového hlášení na stderr |
void perror(const char *s); |
printf |
formátovaný zápis do souboru stdout |
int printf(const char *format, [,arg, …]); |
putc |
zápis znaku do souboru |
int putc(int c, FILE *file); |
putchar |
zápis znaku do souboru stdout |
int putchar(int c); |
puts |
zápis řetězce do souboru stdout |
int puts(const char *s); |
remove |
ruší soubor specifikovaný jeho jménem |
int remove(const char *fname); |
rename |
přejmenovává soubor |
int rename(const char *oldname, const char *newname); |
rewind |
umísťuje ukazatel souboru na začátek souboru |
void rewind(FILE *file); |
scanf |
formátované čtení ze souboru stdin |
int scanf(const char *format, [,address, …]); |
setbuf |
přiřazuje souboru vyrovnávací paměť |
void setbuf(FILE *file, char *buf); |
setvbuf |
přiřazuje souboru vyrovnávací paměť |
int setvbuf(FILE *file, char *buf, int type, size_t size); |
sprintf |
formátovaný zápis do řetězce |
int sprintf(char *buf, const char *format[,arg,…]); |
sscanf |
formátované čtení ze řetězce |
int sscanf(const char *buf, const char *format[,address,…]); |
strerror |
vrací ukazatel na řetězec chybového hlášení |
char *strerror(int errnum); |
tmpfile |
otevírá dočasný pracovní soubor v binárním režimu |
FILE *tmpfile(void); |
tmpnam |
vytváří dosud nepoužité jméno souboru |
char *tmpnam(char *sptr); |
ungetc |
vrací znak zpět do vstupního souboru |
int ungetc(int c, FILE *file); |
vfprintf |
zapisuje formátovaný výstup do souboru |
int vfprintf(FILE *file, const char *format, va_list arglist); |
vprintf |
zapisuje formátovaný výstup do souboru stdout |
int vprintf(const char *format, va_list arglist); |
vsprintf |
zapisuje formátovaný výstup do řetězce buf |
int vsprintf(char *buf, const char *format, va_list arglist); |
Práce s řetězci a bloky v paměti
#include <string.h>
| jméno | popis | prototyp |
|---|---|---|
memchr |
hledá v n bytech znak c |
void *memchr(const void *s, int c, size_t n); |
memcmp |
porovnání obsahů pamětí |
int memcmp(const void *s1, const void *s2, size_t n); |
memcpy |
kopírování obsahu paměti |
void *memcpy(void *dest, const void *src, size_t n); |
memmove |
kopíruje blok n slabik byte |
void *memmove(void *dest, const void *src, size_t n); |
memset |
vyplnění obsahu paměti konstantou |
void *memset(void *s, int c, size_t n); |
strcat |
sloučení dvou řetězců |
char *strcat(char *dest, const char *scr,); |
strchr |
hledá první výskyt daného znaku v řetězci |
char *strchr(const char *s, int c); |
strcmp |
porovná dva řetězce |
int strcmp(const char *s1, const char *s2); |
strcoll |
porovná dva řetězce |
int strcoll(char *s1, char *s2); |
strcpy |
kopírování jednoho řetězce do druhého |
char *strcpy(char *dest, const char *src); |
strerror |
vytváří zakázkové chybové hlášení |
char *strerror(int errnum); |
strlen |
zjištění délky řetězce (bez ukončujícího znaku '\0') |
size_t strlen(const char *s); |
strncat |
připojuje nejvýše n znaků k řetězci dest |
char *strncat(char *dest, const char *scr, size_t n); |
strncmp |
porovná nejvýše n znaků jednoho řetězce s částí druhého |
int strncmp(const char *s1, const char *s2, size_t n); |
strncpy |
kopírování nejvýše n znaků řetězce scr do řetězce dest, přičemž v případě potřeby dest ořezává nebo doplňuje znaky '\0' |
char *strncpy(char *dest, const char *src, size_t n); |
strpbrk |
prohledává řetězec s1 a hledá první výskyt libovolného znaku ze řetězce s2 |
char *strpbrk(const char *s1, const char *s2); |
strrchr |
prohledává řetězec s1 a hledá poslední výskyt daného znaku |
char *strrchr(const char *s, int c); |
strspn |
vrací délku počátečního úseku s1, který se zcela shoduje s s2 |
size_t strspn(const char *s1, const char *s2); |
strstr |
hledá v řetězci s1 výskyt daného podřetězce s2 |
char *strstr(const char *s1, const char *s2); |
strtok |
dělí s1 na podřetězce v místech, kde se vyskytují znaky z s2 |
char *strtok(char *s1, const char *s2); |
strxfrm |
transformuje úsek řetězce - kopíruje nejvýše n slabik (byte) z s1 do s2 |
size_t strxfrm(char *s1, char *s2, size_t n ); |
Matematické funkce
#include <limits.h>
#include <stdlib.h>
#include <math.h>
| jméno | popis | prototyp |
|---|---|---|
abs |
absolutní hodnota celého čísla |
int abs(int x); |
acos |
arkus kosinus |
double acos(double x); |
asin |
arkus sinus |
double asin(double x); |
atan |
arkus tangens |
double atan(double x); |
atan2 |
arkus tangens hodnoty y/x |
double atan2(double y, double x); |
ceil |
zaokrouhluje nahoru |
double ceil(double x); |
cos |
funkce kosinus |
double cos(double x); |
cosh |
funkce hyperbolický kosinus |
double cosh(double x); |
div |
dělí dvě celá čísla, vrací podíl a zbytek ve struktuře div_t |
div_t div(int numer, int denom); |
exp |
exponenciální funkce při základu e |
double exp(double x); |
fabs |
absolutní hodnota reálného čísla |
double fabs(double x); |
floor |
zaokrouhluje dolů |
double floor(double x); |
fmod |
počítá x modulo y , zbytek podílu x/y |
double fmod(double x); |
frexp |
rozděluje číslo double na mantisu a exponent: x = f*2i , kde f ∈ [1/2,1), kde f je návratová hodnota funkce a i je uloženo v řetězci pexp |
double frexp(double x, int *pexp); |
labs |
absolutní hodnota long int |
long int labs(long int x); |
ldexp |
hodnota funkce x 2i |
double ldexp(double x, int i); |
ldiv |
Dělí dvě čísla long int, vrací podíl a zbytek |
ldiv_t ldiv(long int numer, long int denom); |
log |
přirozený logaritmus x |
double log(double x); |
log10 |
dekadický logaritmus x |
double log10(double x); |
modf |
rozkládá číslo typu double na celočíselnou a zlomkovou část, celočíselná je definována funkcí celá část [x] |
double modf(double x, double *i); |
pow |
počítá xy |
double pow(double x, double y); |
rand |
Generuje pseudonáhodné číslo v rozsahu 0 až 215-1 |
int rand(void); |
sin |
funkce sinus |
double sin(double); |
sinh |
funkce hyperbolický sinus |
double sinh(double); |
sqrt |
druhá odmocnina z čísla x |
double sqrt(double); |
srand |
inicializuje generátor náhodných čísel |
void srand(unsigned seed); |
tan |
funkce tangens |
double tan(double x); |
tanh |
funkce hyperbolický tangens |
double tanh(double x); |
Práce s dynamickou pamětí
#include <stdlib.h>
| jméno | popis | prototyp |
|---|---|---|
calloc |
alokuje operační paměť pro n položek (alokovanou paměť vynuluje) |
void *calloc(size_t n, size_t size); |
free |
uvolňuje alokovaný blok operační paměti |
void free(void *block); |
malloc |
alokace operační paměti |
void *malloc(size_t size); |
realloc |
změna velikosti paměťového bloku |
void *realloc(void *block, size_t size); |
Práce s datem a s časem
V některých funkčních prototypech následujících funkcí se používají datové typy clock_t, time_t definované v hlavičkovém souboru time.h.
| jméno | popis | prototyp |
|---|---|---|
clock |
procesorový čas, který uplynul od začátku programu |
clock_t clock(void); |
ctime |
konvertuje datum a čas na řetězec |
char *ctime(const time_t *time); |
difftime |
počítá rozdíl mezi dvěma časy získanými pomocí time |
double difftime(time_t t2, time_t t1); |
gmtime |
konvertuje datum a čas na typ struct tm |
struct tm *gmtime(const time_t *timer); |
localtime |
provádí korekci času pro časové pásmo |
struct tm *localtime(const time_t *timer); |
mktime |
konvertuje čas z typu struct tm na kalendářový formát |
time_t mktime(struct tm *t); |
strftime |
formátuje čas pro výstup |
size_t _cdecl strftime(char *s, size_t max, const char *fmt, const struct tm *t); |
time |
vrací aktuální počet sekund od 1.1.1970 |
time_t time(time_t *timer); |
Práce s proměnným počtem parametrů
| jméno | popis | prototyp |
|---|---|---|
va_arg |
nastavení dalšího argumentu z proměnného seznamu argumentů |
type va_arg(va_list ap, type); |
va_end |
nastavení posledního argumentu z proměnného seznamu argumentů |
void va_end(va_list ap); |
va_start |
nastavení prvního argumentu z proměnného seznamu argumentů |
void va_start(va_list ap, lastfix); |
Řízení procesů a některé další funkce
| jméno | popis | prototyp |
|---|---|---|
abort |
abnormálně ukončuje program |
void abort(void); |
assert |
otestuje zadanou podmínku a popřípadě přeruší program |
void assert(int test); |
atexit |
registrace funkce, která se provede po skončení main() |
int atexit(atexit_t func); |
exit |
ukončení programu |
void exit(int status); |
longjmp |
provádí nelokální goto |
void longjmp(jmp_buf jmpb, int retval); |
setjmp |
připravuje nelokální goto |
int setjmp(jmp_buf jmpb); |
localeconv |
vrací ukazatel na strukturu aktuálního státu (lokality), informace jak je zapisován čas, datum, … |
struct lconv *localeconv(void); |
setlocale |
nastavuje lokalitu |
char *setlocale(int category, char *locale); |
bsearch |
binární vyhledávání v poli |
void bsearch(const void *key, const void *base, size_t nelem, size_t width, int (*fcmp)(const void, const void*)); |
signal |
specifikuje akce, které obsluhují signály |
void (*signal(int sig, void (*func)(int))(int); |
Literatura
[1] Herout, P: Učebnice jazyka C. České Budějovice, KOPP, 1992
[2] Barclay K.A.: ANSI C. Prentice-Hall, New York, 1990.
[3] Kernighan, B.W.-Ritchie, D.M.: Programovací jazyk C. Bratislava, Praha, Alfa, SNTL, 1988; slovenský překlad originálu The C Programming Language. Englewood Cliffs, Prentice-Hall, 1978
[4] Plauger P.J.,Brodie J.: Standard C. Microsoft Press, New York, rok vydání neuveden.
[5] Richta K.-Brůha, I.: Programovací jazyk C. Praha, Ediční středisko ČVUT, 1991
[6] Tywoniak J.: Unix pro začátečníky, 1 -10. Bajt, 1992-93
[7] Virius M.: Programovací jazyky C/C++. Praha, G-Comp, 1992
[8] Wirth N.: Algorithms + Data Structures = Programs. Prentice-Hall, Englewood Cliffs, New Jersey, 1975. (Slovenský překlad EVT ALFA 1988)