#include <stdio.h>
int main()
{
("Ahoj svete!!\n");
printfreturn 0;
}
V této kapitole jsou informace o základních prvních jazyka, aby šlo psát aspoň elementární programy. Tyto informace budou později v kurzu rozšířeny.
Proměnnou definujeme následovně.
; // bez inicializace
typ identifier= init; // s inicializací typ identifier
Při definici je nutné specifikovat typ proměnné pomocí klíčového slova pro něj (o typech níže). Inicializace je nepovinná. Při inicializaci platí pravidla jako při provádění operátoru přiřazení (viz dále). Přístup k neinicializované proměnné vede k nedefinovanému chování.
int x;
int y = 10;
("%i\n", x); // nedefinované chování, přístup k neinicializované proměnné
printf("%i\n", y); printf
C je jazyk s lexikálním rozsahem platnosti proměnných. Proměnná existuje od místa své definice do konce bloku, ve kterém je definována. Pokud je v nějakém bloku použita proměnná (např. součást výrazu atd.), je hledána v aktuálním bloku a poté v blocích nadřazených. Hledání končí na úrovni souboru, pokud tam není proměnná nalezena, vyhlásí překladač chybu. Proměnné, které jsou definovány na úrovni souboru označujeme jako globální. Ve stejném bloku nemohou být definovány dvě proměnné stejného jména.
Blok je část kódu uzavřená mezi { }
, které si
odpovídají: Bloky lze vytvářet jenom tak, aby se zanořovali, otevírací i
uzavírací závorka bloku musí být uvnitř nadřazeného bloku. Takto bloky
přirozeně vytváří stromovou hierarchii, jejím kořenen je úroveň
samotného souboru, tj. obsah souboru mimo všechny bloky.
Důsledkem mechanismu vyhledávání proměnných je jejich shadowing.
int x = 10;
{
int x = 100;
("%i\n", x); // vytiskne 100
printf}
("%i\n", x); // vytiskne 10 printf
Funkci definujeme následujícím způsobem.
(parameter_list)
type_of_return_value identifier {
// body
}
První řádek je tzv. hlavička funkce, kde je postupně typ návratové
hodnoty funkce, jméno funkce a v závorkách seznam argumentů funkce. To
je čárkami oddělená posloupnost dvojic typ id
,
specifikujích typ argumentů a jejich jména. Příkaz
return val;
ukončí vykonávání funkce a jako návratovou
hodnotu vrátí hodnotu výrazu val
. Pokud chceme funkci bez
návratové hodnoty, napíšeme na místo typu návratové hodnoty klíčové
slovo void
a používáme return
bez
val
, nebo jej nepoužijeme vůbec a funkce končí, když kód
doběhne na konec těla funkce.
int sum(int a, int b)
{
return a + b;
}
void print_numbers(int a, int b, int c)
{
("%i %i %i\n", a, b, c);
printf}
Funkci voláme následovně.
(list_of_values) identifier
Tady je identifier
jméno funkce a
list_of_values
je čárkami oddělený seznam výrazů. Ty se
vyhodnotí a výsledky těchto vyhodnocení se přířadí argumentům funkce
(hodnoty se kopírují, argumenty se předávají hodnotou). Pokud má funkce
návratovou hodnotu, je výsledkem vyhodnocení volání funkce hodnota,
kterou funkce vrátí.
int z = sum(1 + 4, 7);
("%i\n", z); // vytiskne 12
printf
(z, z, 3); // vytiskne 12 12 3 print_numbers
Pole je celými čísly indexovaná kolekce hodnot stejného typu. Indexujeme od 0.
Pole lze definovat následujícími způsoby
[no_els];
type a[no_els] = init;
type b[] = init; type c
Výraz type
je typ prvků, a, b, c
jsou jména
polí, no_els
je počet prvků pole: toto musí být hodnota
známá v době překladu, například literál. Výraz init
je
inicializační výraz pro pole.
{ val_0, val_1, ..., val_n }
Výrazy val_
se vyhodnotí a výsledky vyhodnocení se po
řadě přiřadí na indexy 0, 1, …. Pokud je počet výrazů menší než velikost
pole, jsou zbylé hodnoty vynulovány. Další možností je
{ [0] = val_0, [3] = val_3, [5] = val_5 }
Výrazy v hranatých závorkách jsou indexy, na které se při
inicializaci zapíše hodnota na pravé straně =
. Prvky na
indexech, které v inicializačním výrazu nejsou, jsou vynulovány.
Pokud v definici pole není uveden počet prvků, je převzat z inicializačního výrazu.
K jednotlivým prvků pole přistupujeme pomocí indexu zapsaného do hranatých závorek za jméno pole.
Pole si nenese informaci o své velikosti a jazyk nekontroluje, zda při přístupu k poli používáme korektní index (v rozsahu 0 až velikost pole - 1). Přístup k poli pomocí neplatného indexu vede k nedefinovanému chování.
Pole lze předat jako argument funkci, současně s ním je ovšem nutné předat i jeho velikost.
/* n je velikost pole a */
int find_max(int a[], int n)
{
int max = a[0];
for (int i = 1; i < n; i += 1)
{
if (a[i] > max)
{
= a[i];
max }
}
return max;
}
K tisku budeme používat funkci printf
ze standardní
knihovny. Její hlavička je
int printf(format_string, values)
Argument format_string
je řetězec (viz níže). Tento
řetězec může obsahovat speciální instrukce pro tisk, tyto instrukce jsou
ve tvaru %instr
, kde instr
je vlastní
instrukce. Každé instrukci ve format_string
odpovídá ve
values
(to je seznam výrazů oddělených čárkami) jedna
hodnota, která se pomocí dané instrukce vytiskne (bráno zleva doprava).
Instrukce se většinou týkají toho, jaký typ hodnoty se tiskne,
podrobnosti lze nalézt v manuálu ke standardní knihovně. Základní
informace jsou v příkladu níže.
("hello"); // žádné speciální instrukce, vytiskne se hello
printf("%i", -2); // instrukce %i -- tisk celého znaménkového čísla
printf("%u", 15); // instrukce %u -- tisk celého bezznaménkového čísla
printf
instrukce popis-------------------
%c znak
%s řetězec
%f číslo s desetinnou čárkou
Jazyk C je staticky typovaný (typy se kontrolují při překladu) a slabě typovaný (děje se mnoho implicitních typových konverzí).
Pro celá čísla máme char
, short
,
int
, long
, long long
. Tyto typy
mají ještě bezznaménkovou variantu uvozenou klíčovým slovem
unsigned
. Pro čísla s desetinnou čárkou máme typy
float
, double
, long double
.
Standard jazyku zaručuje minimální velikosti a rozsahy hodnot pro jednotlivých typů a dále
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
a
sizeof(float) <= sizeof(double) <= sizeof(long double)
Skutečné velikosti se liší podle překladače a platformy. Například na 64 bitovém unixovém OS s překladačem gcc je
typ sizeof(typ)
------------------------
char 1
short 2
int 4
long 8
long long 8
float 4
double 8
long double 16
------------------------
přičemž celá čísla jsou typicky uložena ve dvojkové (doplňkové) soustavě a čísla s desetinnou čárkou podle IEEE 754 standardu.
Velikost typů lze zjistit pomocí sizeof(typ)
.
Typ char
je používán uložení znaků podle ASCII tabulky
(která znaky kóduje pomocí čísel). Znakové literály jsou mezi apostrofy.
Některé speciální znaky je nutné v literálech zapisovat pomocí
backquotování, například znak nového řádku je \n
.
/*
x a y níže se rovnají
*/
char x = 'A'; // znakový literál
char y = 65; // numerický literál
Řetězec v C je uložen v poli typu char
. Přitom znaky
čteme od začátku pole až po hodnotu 0. Poslední znak řetězce je tak znak
na indexu o jedna menším, než na kterém je hodnota 0 (je to znak
'\0'
). Pole, ve kterém je řetězec uložen, tak musí být
alespoň o jedna větší, než samotný řetězec (potřebujeme jedno místo pro
hodnotu 0), může být ovšem větší. Řetězcové literály píšeme do
uvozovek.
char string[] = "hellope";
/*
pole je velikosti 8
a[0] je znak 'h'
a[1] je znak 'e
...
a[6] je znak 'e'
a[7] je znak '\0' (hodnota 0)
*/
Jazyk C nemá typ pro pravdivostní hodnotu. Celočíselná hodnota 0 je nepravda, ostatní hodnoty jsou pravda.
Alias vytvoříme typu následujím způsobem.
typedef def_expr;
Výraz def_expr
je ve tvaru definice proměnné, k typu v
něm vytvoříme alias tvořený identifikátorem proměnné v něm. V
následujícím příkladu vytvoříme k typu int
alias
number
.
typedef int number;
Položky různých typů lze spojit do strukturovaného typu, který vytvoříme
struct type_id
{
;
item_1;
item_2...
;
item_n};
kde jednotlivé položky item_1
až item_n
jsou zápisy ve formě type item_id
, type
je tu
typ položky a item_id
je jméno položky. Nově vytvořený typ
je struct type_id
. Ke kratšímu pojmenování lze použít
alias, je možný i následující zápis.
struct type_id
{
;
item_1;
item_2...
;
item_n} type_id;
Tímto vytvoříme typ pojmenovaný jenom type_id
. (Pokud
ponecháme první výskyt type_id
, vytvoříme typy
struct type_id
a type_id
současně; první
výskyt type_id
jde vynechat, potom vytvoříme pouze
type_id
.)
Inicializační výraz strukturovaného typu vytvoříme následovně.
{ init_1, init_2, ..., init_n }
Výrazy init_1
až init_n
jsou inicializační
výrazy jednotlivých položek. Pokud některé položky vynecháme, jsou
vynulovány. Alternativně lze použít zápis
{
.item_id_1 = init_1,
.item_id_2 = init_2,
...
.item_id_n = init_n,
}
přičemž lze opět vynechat některé položky. Chceme-li vynulovat všechny položky, lze použít následující výraz.
{ 0 }
Pro přístup k jednotlivým položkám použijeme operátoru .
(tečka).
.item_id var
Příklad
/*
Příklad ukazuje použití struktury pro reprezentaci
vektoru na dvourozměrném euklidovkém prostoru.
Funkce počítají skalární součin a násobení skalárem.
*/
typedef struct
{
float x;
float y;
} vec2f;
float dot_product(vec2f a, vec2f b)
{
return a.x * b.x + a.y * b.y;
}
(vec2f a, float b)
vec2f product{
return (vec2f){b * a.x, b * a.y};
}
int main()
{
/* inicializace */
= { .x = 0.3, .y = 0.5 };
vec2f u = { .x = 1.2, .y = 10.5 };
vec2f v
/* zápis do položek */
.x += 10;
u.y = 6;
v
/* volání funkcí */
float dp = dot_product(u, v);
= product(v, 4.1);
vec2f p
return 0;
}
Hodnotami výčtového typu je množina pojmenovaných celočíselných konstant. Typ spolu s konstantami vytvoříme následovně
enum type_id
{
,
id_1,
id_2,
id_3...
id_n};
Vytvoříme typ enum type_id
(pro kratšího aliasu lze jako
u strukturovaného typu použít typedef
). Jména jednotlivých
konstant jsou id_1, id_2, ...
, jejich hodnoty jsou po řadě
0,1,2 ...
. Konstantám lze v definici typu přiřadit
explicitní hodnoty.
enum type_id
{
= value_1,
id_1 ,
id_2= value_3,
id_3 ...
id_n};
Přiřazení hodnot není povinné. Pokud nemá konstanta v definici přiřazenu hodnotu, je její hodnota o jedna větší, než hodnota předchozí konstanty. Pokud nemá hodnotu přiřazenu první konstanta, je její hodnota 0.
Typické využití je pro určování druhů entit (například chyb, tokenů apod).
/*
Následující enum lze použit pro určení typu chyby,
která se může stát při zpracování souboru.
*/
typedef enum
{
,
FE_NONE,
FE_NOT_FOUND,
FE_PERMISSION_DENIED,
FE_BAD_FORMAT} file_error;
/*
První konstantou je FE_NONE pro žádnou chybu, toto je výhodný
trik, protože FE_NONE má hodnotu 0, která je jediná nepravda,
ostatní položky mají hodnotu indikující pravdu.
*/
= process_file(filename);
file_error err if (err)
{
// tady ošetříme chybu
}
/*
V následujícím výčtovém typu má poslední konstanta
hodnotu, která se rovná počtu konstant. Toho lze
použít pro vytvoření pole, kde jednotlivá políčka
indexujeme pomocí konstant.
*/
typedef enum
{
,
TT_OPERATOR,
TT_IDENTIFICATOR,
TT_BRACE,
TT_TOKEN_TYPE_COUNT} token_type;
int last_token_offsets[TT_TOKEN_TYPE_COUNT];
[TT_BRACE] = 10; last_token_offsets
Jména konstant jsou globální (pro soubor, ve kterém jsou definována), na začátek těchto jmen většinou dáváme zkratku jména typu, abychom zmenšili pravděpodobnost konfliktu jména konstanty se jménem nečeho jiného. Jména konstant jsou často kapitálkami.
Definice unionu je podobná definici strukturovaného typu (včetně
aliasu pomocí typedef
)
union type_id
{
,
item_1,
item_2...
};
Všechny položky (hodnoty) unionu jsou ovšem v paměti začínají na jednom místě a překrývají se tak (velikost unionu je pak velikostí jeho největší položky, je to úspora místa). Za platnou tak můžeme považovat pouze hodnotu jedné položky. Lze využít při potřebě šetřit pamět, spolu s výčtovým typem pro reprezentovat jednu hodnotu, která může být různých typů a vytvořit obdobu tzv. tagged unionu.
typedef enum
{
,
RT_INT,
RT_STRING,
RT_FLOAT
} result_type;
typedef struct
{
;
result_type typeunion
{
int int_value;
char *string;
float float_value;
} value;
} tagged;
/*
při zpracování hodnoty typu tagged potom nejdříve zkontrolujeme
položky type a podle ní víme, kterou položku z unionu value máme
zpracovat.
*/
= function_that_returns_tagged();
tagged x if (x.type == RT_INT)
{
// zpracujeme int
(x.value.int_value);
process_int}
else if (x.type == RT_STRING)
{
// zpracujeme string
(x.value.string);
process_string}
else
{
// zpracujeme float
(x.value.float_value);
process_float}
Operátory (= akce, výpočet) aplikujeme na operandy (= data), a získáme tím nějaký výsledek (= data). Ve složitějších výrazech se operátory vyhodnocují v pořadí podle priority, toto pořadí lze změnit uzávorkováním. Operátory se stejnou prioritou jsou vyhodnocování podle associativity (zleva či zprava), opět to jde změnit pomocí uzávorkování.
Operátor je počítán z hodnot stejného typu a téhož typu je i výsledek. Pokud je v kódu operátor aplikován na hodnoty různých typů, dojde k implicitním typovým konverzím (tj. změnám typů) operandů směrem k většímu typu (jsou to type promotions).
Toto se řídí následujícími pravidly (aplikovanými seshora dolů):
Je-li jeden operand neceločíselného typu.
long double
, je druhý konvertován
na long double
.double
, je druhý konvertován na
double
.float
, je druhý konvertován na
float
Jsou-li oba operandy celočíselné.
Typy menší než int
(znaménkové a bezeznaménkové
verze char
a short
) jsou přetypovány na
int
, pokud int
dokáže reprezentovat všechny
hodnoty původního typu. Jinak jsou přetypovány na
unsigned int
.
Operand menšího typu je převeden na větší typ, který je schopen reprezentovat jeho hodnoty. Typy jsou uspořádány do následujícího pořadí.
unsigned long long > long long > unsigned long > long > unsigned int > int
Níže je tabulka aritmetických operátorů.
Operátor Arita Popis-----------------------------------------------------------------------
- 1 Prefixový unární operátor, mění znaménko
+,-,*,/ 2 Binární operátory, klasické aritmetické operace
% 2 Binární operátor, zbytek po celočíselném dělení
Priorita a associativita se řídí stejnými pravidly jako v matematice.
Slouží k přiřazení hodnoty na nějaké zapisovatelné místo (například do proměnné) Syntax je následující.
= rvalue lvalue
Výraz lvalue
musí označovat místo, kam se dá zapisovat.
rvalue
je výraz, který se vyhodnotí, a výsledek je zapsán
do lvalue
. Dochází k implicitní konverzi typu
rvalue
na typ lvalue
. Tato konverze může být
ztrátová: například dojde k zaokrouhlování, ztrátě desetinné části
čísla, zachování pouze spodních bajtů celého čísla. (Některé konverze
typů jsou zakázány, operátor provádí pouze konverze mezi kompatibilními
typy, detaily lze nalézt ve standardu/manuálu.) Výsledkem je hodnota
rvalue
po přiřazení.
Operátor přiřazení lze kombinovat s binárními operátory, jako jej v
následujícím příkladu kombinujeme operátor +
.
int x = 10;
/* místo */
= x + 3;
x /* můžeme psát */
+= 3; x
Následující operátory porovnávají číselné typy (nebo adresy, o tom dále). Výsledek je 1 pokud je porovnání pravdivé, nebo 0 jinak. Operátory jsou následující.
<, >, <=, >=, ==
Test rovnosti je tvořen dvěma rovnítky, nikoliv jedním. Jedno rovnítko je operátor přiřazení.
Operátory pro konjunkci (log. spojka a), disjunkci (log. spojka nebo) a negaci.
Operátor Popis--------------------------------------
&& konjunkce
|| disjunkce
! negace, unární, prefixový
--------------------------------------
Operátory konjunkce a disjunkce jsou asociativní zleva a vyhodnocují se líně. Uveďme si to na příkladu konjunkce.
/*
Uvažme následující konjunkci
*/
&& y && z && w
x
/*
Díky asociativitě zleva je to vlastně
*/
((x && y) && z) && w
/*
Pokud je x == 1, y == 1, z == 0, w == 1, probíhá výpočet následovně:
1. x && y dá výsledek 1
2. 1 && z dá výsledek 0, výpočet hodnoty výrazu končí s výsledkem 0
V tomto vyhodnocení líné, 0 && z už se nepočítá.
Líného vyhodnocení lze s výhodou využít, pokud je ve výrazu
(pred && op)
výraz pred kontrolou toho, jestli můžeme výraz op bezpečně vyhodnotit.
Pokud se totiž pred vyhodnotí na 0, výraz op už se nevyhodnocuje.
*/
Bitové operátory aplikujeme na celočíselné operandy. Umožnují pracovat s jednotlivými bity těchto operandů. Jedná se logické operátory aplikované po bitech, nebo bitové posuvy. Operátory si vysvětlíme na následujících příkladech.
Předpokládejme, že x == 100
a y == 50
jsou
jednobajtová čísla (typu unsigned char
). Ve dvojkové
soustavě máme
== 0 1 1 0 0 1 0 0
x == 0 0 1 1 0 0 1 0 y
Potom máme
konjunkce po bitech0 1 1 0 0 1 0 0
x 0 0 1 1 0 0 1 0
y -----------------------
& y 0 0 1 0 0 0 0 0
x
disjunkce po bitech 0 1 1 0 0 1 0 0
x 0 0 1 1 0 0 1 0
y -----------------------
| y 0 1 1 1 0 1 1 0
x
nonekvivalence po bitech0 1 1 0 0 1 0 0
x 0 0 1 1 0 0 1 0
y -----------------------
^ y 0 1 0 1 0 1 1 0
x
negace po bitech0 1 1 0 0 1 0 0
x -----------------------
~x 1 0 0 1 1 0 1 1
, tady o 2 bity, lze o obecný počet bitů
bitový posuv doleva(zprava nahrazujem prázná místa 0)
0 1 1 0 0 1 0 0
x -----------------------
<< 2 1 0 0 1 0 0 0 0
x
2 bity, lze o obecný počet bitů
bitový posuv doprava o (zleva nahrazujeme prázdná místa 0 pro unsigned a 1 pro signed typy)
0 1 1 0 0 1 0 0
x -----------------------
>> 2 0 0 0 1 1 0 0 1 x
Na bitové operátory se aplikují typové konverze jak u aritmetických operátorů. U operátorů posuvu musí být druhý operand nezáporný.
if (tval)
{
// blok při pravidvém tval
}
else
{
// blok při nepravdivém tval
}
Větev else
je nepovinná. Pokud blok obsahuje pouze jeden
příkaz, lze závorky vynechat. Při vynechávání závorek bloků, nemusí být
na první pohled patrné, ke kterému if
patří dané
else
: toto je dáno pravidlem, že patří k nejbližššímu
if
, které připadá v úvahu (tj. není obsazeno jiným
else
podle struktury bloků). Toto pravidlo se zhusta
používá v konstrukci známé jako žebřík.
if (tval_1)
{
// tval_1 je pravda
}
else if (tval_2)
{
// tval_1 je nepravda
// tval_2 je pravda
}
else if (tval_3)
{
// tval_1 a tval_2 jsou nepravda
// tval_3 je pravda
}
else
{
// tval_1, tval_2, tval_3 jsou nepravda
}
Není špatným zvykem psát závorky pro bloky skoro vždy (mimo speciálních idiomů, například žebříku).
Dalším příkazem pro větvení je switch
. Tuto konstrukci
si nastudujte z literatury jako cvičení.
Základní cyklus.
while (tval)
{
// tělo
}
Běh je následující
tval
, pokud není pravdivý, cyklus
končíKrokovací cyklus.
for (iter; tval; incr)
{
// tělo
}
Běh je následující
iter
. Ta může
obsahovat i definici proměnné, tato proměnná je potom platná jenom v
těle cyklu (uchovává si ale hodnotu mezi jednotlivými iteracemi).tval
. Pokud je pravdivá, provede se
tělo cyklu, jinak cyklus skončí.incr
a přechází se k bodu 2.Části iter
, tval
i incr
lze
vynechat (středníky musí zůstat), pokuch chybí tval
je brán
jako pravdivý.
Cyklus s alespoň jednou iterací.
do
{
// tělo
} while (tval);
Podobné cyklu while
, pouze vyhodnocujeme
tval
na konci těla cyklu, nikoliv na začátku. V důsledku
toho tělo cyklu provede vždy aspoň jednou.
/*
Příklad: výpis čísel od 0 do n (n už se netiskne)
*/
int n = 10;
{
int i = 0;
while(i < n)
{
("%i\n", i);
printf+= 1;
i }
}
{
for(int i = 0; i < n; i += 1)
{
("%i\n", i);
printf}
}
{ // nefunguje pro n == 0 !
int i = 0;
do
{
("%i\n", i);
printf+= 1;
i } while(i < n);
}
Vykonávání libovolného cyklu lze přerušit provedením příkazu
break
. Ukončit provádění aktuální iterace cyklu a skočit
dopředu na konec těla cyklu lze příkazem continue
.
Pokud se v úkolech vyskytují čísla, vhodně zvolte typ. Většinou stačí
int
, případně float
. Můžete předpokládat, že
funkce dostanou správné argumenty: pokud je například v úkolu napsáno,
že argumentem funkce má být přirozené číslo, můžete pro ně zvolit typ
int
a přitom ve funkci nekontrolovat, že argument je
nezáporný. Nemusíte volit unsigned int
.
/* funkce zjišťující, jestli je n prvočíslo */
int is_prime(int n)
{
/* následující test není potřeba */
if (n <= 0)
{
// a very bad error !!!!
}
}