[Cvičení 10] [Obsah] [Cvičení 12]

Cvičení 11


Témata


Bitové operátory

Bitové operace patří k operacím nižší úrovně, manipulují totiž s přímo s jednotlivými bity operandů. Využití najdou zejména při programování řídicích aplikací pro jednočipové mikropočítače, kdy je práce s jednotlivými bity nezbytná. V běžných aplikacích je třeba pracovat s bitovými operacemi „opatrně“; často je třeba znát vnitřní reprezentaci dat a chování programu tak nemusí být stejné na všech platformách (překladačích, procesorech, operačních systémech).

V jazyce C existuje šest bitových operátorů. Přehled uvádí tabulka 1:

OperátorOperace
&Bitový součin (AND)
|Bitový součet (OR)
^Exlusivní bitový součet - výlučné NEBO (XOR)
~Bitová negace
<<Bitový posuv vlevo
>>Bitový posuv vpravo

Tabulka 1: Bitové operátory

Funkci bitových operátorů ukážeme na příkladě:

unsigned char  a = 0x85; /* 133 desitkove, 10000101 dvojkove */
unsigned char  b = 0x46; /*  70 desitkove, 01000110 dvojkove */
unsigned char  c,d,e,f,g,h;

c = a & b;
d = a | b;
e = a ^ b;
f = ~ a;
g = a << 2;
h = b >> 3;
Výsledky operací součinu, součtu, exlusivního součtu a negace jsou zřejmé:

& (AND)  | (OR)  ^ (XOR)  ~ (NOT)
a =10000101  a =10000101  a =10000101  a =10000101
b =01000110  b =01000110  b =01000110   
c =00000100  d =11000111  e =11000011  f =01111010 

Operace posuvu (vlevo, resp. vpravo) mají následující syntaxi: op << n, resp. op >> n. Provedou posuv operandu op o n bitů vlevo (vpravo). Na uvolněná místa se nasouvají nuly, nenulové bity „vypadávají“. V uvedeném příkladě, hodnoty proměnných g, h budou mít hodnotu

g =00010100  h =00001000
Operaci logického součinu můžeme využít při zjišťování, zda jsou určité bity ve slově nastaveny pomocí tzv. masky. Chceme zjistit, zda např. bit 5. řádu proměnné x je nastaven. Hodnota 25=32 = 0x20 (20 je zapsána v šestnáctkové soustavě) je maska, která ma nastaven na hodnotu 1 právě bit 5. řádu. Zjištění spočívá v testu na nenulovou hodnotu bitového součinu masky a proměnné x:
unsigned char x;
unsigned char maska;
...
maska = 0x20;
if (x & maska != 0) ...
else ...
Pokud neznáme předem, který bit máme testovat (je dán např. hodnotou proměnné n), vyrobíme masku pomocí operace posuvu doleva čísla 1:
unsigned char x;
unsigned char maska;
int n; // v proměnné n je uloženo, který bit máme testovat
...
maska = (1 << n);
if (x & maska != 0) ...
else ...

Struktury a uniony

Struktury

Pole je homogenní datový typ, tedy všechny položky pole jsou stejné. V případě struktur jde o heterogenní datový typ. Struktura tedy obsahuje několik položek různých datových typů (mohou být samozřejmě i stejného datového typu), které ovšem spolu logicky souvisejí. Struktura umožňuje „společné“ pojmenování všech sdružených údajů, s nimiž se potom pohodlněji pracuje.

Struktura je obecně definována následujícím zápisem:

struct [jméno struktury]
{
   typ jméno_položky; 
   typ jméno_položky;
   ...
} proměnná;
Struktura je uvedena klíčovým slovem struct. Pojmenování struktury jménem je nepovinné. V bloku definic položek struktury se uvádějí jednotlivé položky struktury.

Ukážeme se definici proměnné bod typu struktura, která má dvě položky (x-ovou a y-ovou souřadnici):

struct
{
  float x;
  float y;
} bod;
K položkám struktury přistupujeme pomocí tečkové notace, tedy položku x a y proměnné bod nastavíme takto: bod.x = 3.5; bod.y = 4;

V tomto případě ale nemůžeme využít definici struktury na jiném místě kódu, např. jako parametr funkce. Pokud bychom strukturu pojmenovali:

struct Bod
{
  float x;
  float y;  
};
můžeme pak deklarovat proměnnou, např. A takto struct Bod A;. Nevýhodou je, že musíme psát stále klíčové slovo struct. Uživatelsky příjemnější je definovat uživatelský typ pomocí klíčového slova typedef:
typedef struct
{
  float x;
  float y;
} TBod;
Proměnné typu TBod deklarujeme jako každé jiné proměnné, již bez klíčového slova struct:
TBod b1,b2;

Statické pole struktur deklarujeme TBod pole_bodu[10], k položkám pole přistupujeme pole_bodu[0].x = 3;

Je možné deklarovat také ukazatel na strukturu a paměť pro data alokovat dynamicky:

TBod *b3;

b3 = (TBod*)malloc(sizeof(TBod));
...
free(b3); /* uvolnění paměti */
K položkám bodu b3 přitupujeme standardně pomocí „*“ a „.“: *b3.x = 4;. Programátoři v jazyce C používají častěji jiný zápis: b3 -> x = 4;, tedy pokud je nějaká proměnná ukazatel na strukturu, pro přístup k položkám zapisují místo hvězdičky a tečky „šipku“ (znaky minus a větší zapsané bez mezery); oba zápisy *b3.x = 4 a b3 -> x = 4 jsou v jazyce C ekvivalentní.

Strukturu můžeme předat funkci jako parametr, předává se buď struktura sama nebo ukazatel na strukturu (což je výhodnější z hlediska paměťových nároků, pokud struktura zabírá více paměti; nezapomeňme, že v případě předání struktury se na zásobník předává kopie dat). Definici funkce, která počítá vzdálenost bodu od počátku, je v tabulce 3. Tabulka ukazuje 2 funkce, u první se předává jako parametr samotná struktura, u druhé ukazatel na strukturu.

Předání samotné struktury Předání ukazatele na strukturu
double vzdalenost1(TBod b)
{
  return sqrt (b.x*b.x + b.y*b.y);
}
double vzdalenost2(TBod *b)
{
  return sqrt (b->x*b->x + b->y*b->y);

Tabulka 3: Předávání struktur funkcím

Uniony, bitová pole

Prezentace o sjednocení (uniony) a bitových polích si můžete stáhnout: uniony.ppt.


Parametry hlavního programu

Operační systémy umožňují předávat programu při jeho spouštění parametry (argumenty) hlavního programu. Např. notepad.exe text.txt představuje spuštění programu notepad (poznámkový blok) s jedním parametrem - jménem souboru, který má být automaticky otevřen.

Abychom v hlavním programu v jazyce C mohli přistupovat k parametrům, musíme deklarovat hlavičku hlavního programu takto:

int main(int argc, char **argv)
{
 
  ...
  return 0;
}
nebo
int main(int argc, char *argv[])
{
 
  ...
  return 0;
}
Hodnota argc nese počet parametrů na příkazovém řádku. Tento počet je včetně jména spouštěného programu! Pokud bychom programovali poznámkový blok v jazyce C a spouštěli jej notepad.exe text.txt, měl by parametr argc hodnotu 2. V případě spouštění fiktivního programu progr se dvěma parametry, progr nadpis 12, má argc hodnotu 3.

Parametr argv je ukazatel na pole řetězů, které představují parametry příkazové řádky, včetně názvu programu. Tedy, argv[0] je ukazatel na řetězec s názvem spouštěného programu, který může být včetně cesty! Dále, argv[1] je řetězec s prvním parametem, argv[2] je řetězec s druhým parametrem atd.

Vrátíme-li se k příkladu s poznámkovým blokem a uvažujeme-li standardní instalaci systému Windows, pak argv[0] je řetězec C:\WINDOWS\notepad.exe, argv[1] má hodnotu text.txt.
Ve druhém příkladu s programem prog obsahuje argv[0] název programu včetně cesty, argv[1] obsahuje řetězec nadpis a argv[2] řetězec (!) 12.


Návratový kód hlavního programu

Hlavní program v úvodu kapitoly je deklarován jako funkce s návratovou hodnotou typu int (hlavní program může vracet pouze celé číslo). Každý program může při ukončení předat operačnímu systému (vrátit) číslo, tzv. návratový kód. Platí nepsané pravidlo, že hodnota 0 znamená úspěšné ukončení programu, nenulová hodnota chybu. Význam nenulových návratových hodnot není dán a záleží jen na programátorovi, jak hodnoty využije (měl by vše popsat v dokumentaci programu).

Princip se využívá zejména v dávkových souborech (typu .bat, skriptech v UNIXu/Linuxu), kdy po skončení programu je možné větvit algoritmus dávky právě podle návratového kódu (test proměnné errorlevel) - viz předmět SSS.

Principiálně vypadá programování návratových kódu takto:

int main(int argc, char **argv)
{
  kód;
  if (chyba) return 1;
  kód;
  return 0;
}
Pokud programátor nehodlá využívat parametry hlavního programu, ani vracet hodnotu, může deklarovat hlavní program jako proceduru bez parametrů:
void main(void)
{
  ...
}

Úloha 11.1

Napište jednoduchou kalkulačku. Program s názvem kalk bude mít povinně 3 parametry: číslo operátor, číslo, např. kalk 3.5 + 2. Na obrazovku pak vypíše výsledek operace. Uvažujte tyto operátory: + - * /. Ošetřete chybu dělení nulou a správný počet parametrů programu. Není-li počet argumentů správný, vypište chybové hlášení ve tvaru:
Spouštění: kalk číslo operátor číslo
Jméno programu kalk získejte z argumentu argv[0], odstraňte cestu.
(Návod: číselné hodnoty z řetězců převádějte pomocí funkce sscanf, jméno programu nekopírujte - najděte poslední výskyt znaku '\\' a při tisku jména programu předejte ukazatel na další znak.)

Polotovar:

Dev C++: kalkp.dev, kalkp.c
CodeBlocks: kalkp.cbp, kalkp.c

Řešení:

Dev C++:kalk.dev, kalk.c
CodeBlocks:kalk.cbp, kalk.c


Definice uživatelského typu

Pomocí klíčového slova typedef lze definovat vlastní typ. Částečně jsme danou problematiku již probírali. Nyní si ji zopakujeme a rozšíříme.

Obecně má definice typu podobu:

typedef definice_typu identifikátor_typu;
Jako první příklad uvedeme vytvoření de facto aliasu typu float:
typedef float TPrvek;
Definovali jsme nový typ TPrvek, který je evivalentem typu float. Čtenář si možná klade otázku, proč jsme uvedli tento jednoduchý příklad, jenž se může zdát nesmyslným. Nesmyslný příklad není, naopak, ukazuje možnost, jak psát programy odolnější proti chybám. Představme si, že zpracováváme dynamické pole prvků typu int. V kódu se určitě vyskytují tyto řádky:
int main()
{
  int *pole;
  ...
  pole = (int *)malloc(n*sizeof(int));
  ...
}
Pokud se rozhodneme pracovat s prvky typu float, musíme přepsat typ int všude tam, kde se s prvky pole pracuje (zde na dvou místech kódu - v deklaraci pole a při volání funkce malloc). Je nebezpečí, že někde v kódu zapomeneme typy zaměnit nebo zaměníme omylem typy v té části kódu, která se prvků pole vůbec netýká (např. hlavičky procedur a funkcí).

Pokud nadefinujeme uživatelský typ a dodržujeme důsledně jeho použití v kódu všude tam, kde se pracuje s polem, stačí pak upravit pouze definici typu:

typedef int TPrvek;

int main()
{
  TPrvek *pole;
  ...
  pole = (TPrvek *)malloc(n*sizeof(TPrvek));
  ...
}
Ukážeme si ještě dva příklady definice typů:
typedef int* TPint; // typ TPint je typ ukazatel na int, proměnnou pi typu ukazatel pak deklarujeme: TPint pi;
typedef float TPoledeset[10]; // TPoledeset je typ pole o deseti prvcích typu float, vlastní pole A pak deklarujeme: TPoledeset A;
Poznámka: Bývá zvykem, že identifikátory uživatelsky definovaných typů programátoři začínají písmenem T (jako typ)

Výčtový datový typ

Výčtový (enumerační) datový typ je typ, jehož obor hodnot je dán (jak již název napovídá) výčtem těchto hodnot. Hodnoty jsou specifikovány symbolickými jmény. Nadefinujme výčtový typ reprezentující barvy:
typedef  enum { CERVENA, MODRA, ZELENA, BILA, CERNA } TBarvy;
Obsahem proměnné typu TBarva jsou tedy hodnoty CERVENA, MODRA, ZELENA, BILA nebo CERNA.

Proměnnou barva_auta typu TBarvy nadeklarujeme standardně:

TBarvy barva_auta;
V kódu pracujeme se proměnnými výčtového typy a symbolickými hodnotami běžně jako s jinými standardními datovými typy , např.:
barva_auta = CERVENA;
if (barva_auta == CERVENA) ...
V jazyce C jsou hodnoty výčtového datového typu interně reprezentovány celými čísly počínaje 0, tj. v našem příkladě s barvami je hodnota CERVENA reprezentována 0, MODRA 1 atd. Při definici typu máme možnost ovlivnit vnitřní reprezentaci přiřazením hodnot:
typedef  enum { CERVENA, MODRA=5, ZELENA, BILA=10, CERNA } TBarvy;
V tomto případě je hodnota CERVENA vnitřně reprezentována 0, MODRA 5, ZELENA 6, BILA 10, CERNA 11. Překladač přiřazení nekontroluje, takže v jazyce C je bez problémů přeložitelná tato definice:
typedef  enum { CERVENA, MODRA=5, ZELENA, BILA=3, CERNA, SEDA } TBarvy;
Zde ale mají symbolické hodnoty MODRA a SEDA přiřazeny interní reprezentaci číslem 5, nejsou tedy v kódu rozlišitelné!

Poznámka: V jazycích se silnou typovou kontrolou (např. Pascal) je dovolenou používat v kódu pouze symbolická jména hodnot výčtového typu. Jazyk C je jazyk se slabou typovou kontrolou, kdy se provádějí kontroly pouze na úrovni vnitřní reprezentace. Je tedy možné zapsat do kódu přiřazení barva_auta = 2; Používat tento způsob se zásadně nedoporučuje, protože snižuje přehlednost a čitelnost kódu!


[Cvičení 10] [Obsah] [Cvičení 12]