10.1. Uživatelský datový typ
10.2. Výčtový typ
10.3. Typ struktura
10.4. Typ union
10.5. Bitová pole
10.6. Klasifikace typů v C
Dosud jsme se seznámili jen se základními datovými typy. To jest takovými typy, které vyhovují pro jednoduché výpočty či (programové) zpracování textu. Tyto typy máme v C přímo k dispozici (jsou součástí normy jazyka). Také známe preprocesor a víme, že použitím symbolických konstant program zpřehledníme. V této kapitole si ukážeme tvorbu a použití takových strukturovaných datových typů, jaké nám přináší život. Také si ukážeme definici výčtových typů, která umožní hodnoty nejen pojmenovat, ale provádět při překladu i jejich typovou kontrolu. Na úvod kapitoly si necháváme popis definice vlastních datových typů s prakticky libovolnou strukturou.
10.1. Uživatelský datový typ
Vyšší programovací jazyk má k dispozici takové základní
datové typy, které pokryjí většinu potřeb. S jejich pomocí můžeme vytvářet
rozsáhlejší homogenní datové struktury, jako například pole, či
heterogenní, jako strukturu či unii (struct
a union
). Programátor
ovšem musí mít k dispozici mechanismus, kterým si vytvoří datový typ podle
svých potřeb. Tento mechanismus se nazývá typedef
a jeho syntaxe je na
první pohled velmi jednoduchá:
typedef;
Po klíčovém slově typedef
následuje definice typu type definition
.
Poté je novému typu určen identifikátor identifier
.
Skalní zastánce nějakého datového typu či jazyka si spojením maker a uživatelských typů může C přetvořit k obrazu svému:
/*********************/ /* soubor typedef0.c */ /*********************/ #includeint main() { typedef float real; real x = 2.5, y = 2.0; printf("%5.1f * %5.1f = %5.1fn", x, y, x * y); return 0; }
Získá tím ovšem jistotu, že jeho programy bude číst pouze on sám.
Složitější typové deklarace
Jestliže se uvedené jednoduché typové konstrukce1 prakticky nepoužívají, podívejme se naopak na některé konstrukce složitější. Nejprve si shrňme definice, které bychom měli zvládnout poměrně snadno:
deklarace | typ identifikátoru jméno |
---|---|
typ jméno; | typ |
typ jméno[]; | (otevřené) pole typu |
typ jméno[3]; | pole (pevné velikosti) tří položek typu (jméno[0], jméno[1],jméno[2]) |
typ *jméno; | ukazatel na typ |
typ *jméno[]; | (otevřené) pole ukazatelů na typ |
typ *(jméno[]); | (otevřené) pole ukazatelů na typ |
typ (*jméno)[]; | ukazatel na (otevřené) pole typu |
typ jméno(); | funkce vracející hodnotu typu |
typ *jméno(); | funkce vracející ukazatel na hodnotu typu |
typ *(jméno()); | funkce vracející ukazatel na hodnotu typu |
typ (*jméno)(); | ukazatel na funkci vracející typ2 |
Může se stát, že si u některých definic přestáváme být jisti. Uvedeme si proto zásady, podle nichž musíme pro správnou interpretaci definice postupovat. Obecně se držíme postupu zevnitř ven. Podrobněji lze zásady shrnout do čtyř kroků:
- Začněme u identifikátoru a hledejme vpravo kulaté nebo hranaté závorky (jsou-li nějaké).
- Interpretujme tyto závorky a hledejme vlevo hvězdičku.
- Pokud narazíme na pravou závorku (libovolného stupně vnoření), vraťme se a aplikujme pravidla 1 a 2 pro vše mezi závorkami.
- Aplikujme specifikaci typu.
Celý postup si ukážeme na příkladu:
char *( *( *var) ()) [10]; 7 6 4 2 1 3 5
Označené kroky nám říkají:
- Identifikátor
var
je deklarován jako - ukazatel na
- funkci vracející
- ukazatel na
- pole 10 prvků, které jsou
- ukazateli na
- hodnoty typu
char
.
Raději další příklad. Tentokrát již popíšeme jen výsledek. Jednotlivé kroky nebudeme značit:
unsigned int *( * const *name[5][10]) (void);
Identifikátor name
je dvourozměrným polem o celkem 50 prvcích. Prvky tohoto
pole jsou ukazateli na ukazatele, které jsou konstantní. Tyto konstantní ukazatele
ukazují na typ funkce, která nemá argumenty a vrací ukazatel na hodnotu typu
unsigned int
.
Následující funkce vrací ukazatel na pole tří hodnot typu double
:
double ( *var (double (*)[3])) [3];
Její argument, stejně jako návratová hodnota, je ukazatel na pole tří prvků typu
double
.
Argument předchozí funkce je konstrukce, která se nazývá abstraktní
deklarace. Obecně se jedná o deklaraci bez identifikátoru. Deklarace obsahuje jeden či
více ukazatelů, polí nebo modifikací funkcí. Pro zjednodušení a
zpřehlednění abstraktních deklarací se používá konstrukce typedef
.
Abstraktní deklarace my nás neměly zaskočit ani v případě, kdy
typedef
použito není. Raději si několik abstraktních deklarací uvedeme:
int * | ukazatel na typ int |
int *[3] | pole tří ukazatelů na int |
int (*)[5] | ukazatel na pole pěti prvků typu int |
int *() | funkce bez specifikace argumentů vracející ukazatel na int |
int (*) (void) | ukazatel na funkci nemající argumenty vracející int |
int (*const []) (unsigned int, ..) | ukazatel na nespecifikovaný počet konstantních ukazatelů na funkce, z nichž každá má první argument unsigned int a nespecifikovaný počet dalších argumentů |
10.2. Výčtový typ
Výčtový typ nám umožňuje definovat konstanty výčtového typu. To je výhodné například v okamžiku, kdy přiřadíme hodnotě výčtového typu identifikátor. Pak se ve zdrojovém textu nesetkáme například s hodnotou 13, či dokonce 0x0d, ale například CR, či Enter. Takový text je mnohem čitelnější. Jestliže později zjistíme, že je třeba hodnotu změnit, nemusíme v textu vyhledávat řetězec 13, který se navíc může vyskytovat i jako podřetězec řady jiných řetězců, ale na jediném místě změníme hodnotu výčtové konstanty. Že se jedná o klasické konstanty (či dokonce konstantní makra), které jsme poznali prakticky na začátku textu? Téměř to tak vypadá, ale výčtové konstanty mohou mít navíc pojmenován typ, který reprezentuje všechny jeho výčtové hodnoty. I tím se zvýší přehlednost3.
Podívejme se nejprve na příklad. Naším úkolem je zpracovat stisknuté klávesy na standardní 101tlačítkové klávesnici PC-AT. Pokud bychom do jednotlivých větví umístili pro porovnávání celočíselné konstanty, zřejme bychom sami brzy ztratili přehled. Použijeme-li výčtové konstanty, je situace zcela jiná. Ostatně podívejme:
/************************************************/ /* soubor enum_use.c */ /* definice a naznak pouziti vyctovych konstant */ /* pro jednoduchy editor */ /************************************************/ typedef enum { Back = 8, Tab = 9, Esc = 27, Enter = 13, Down = 0x0150, Left = 0x014b, Right = 0x014d, Up = 0x0148, NUL = 0x0103, Shift_Tab = 0x010f, Del = 0x0153, End = 0x014f, Home = 0x0147, Ins = 0x0152, PgDn = 0x0151, PgUp = 0x0149 } key_t; ... int znak; ... else if ((znak == Left) || (znak == Back)) ... else if (znak == Enter) ... else if (znak == Esc) ... else if ... ...
Je zřejmé, že výpis zdrojového textu je krácen. Jde nám o ukázku. Přestože je zřejmě průhledná, podívejme se na syntaxi definice výčtového typu:
enum [] { [= ], ...} [var_list];
Klíčové slovo enum
definici uvádí. Nepovinné označení
type_tag
umožňuje pojmenování hodnot výčtového typu bez použití
konstrukce typedef
. Poté následuje seznam výčtových konstant ve
složených závorkách. Na závěr definice můžeme (nepovinný parametr) přímo
uvést proměnné4,
které mohou definovaných výčtových hodnot nabývat. Vraťme se ještě k
obsahu bloku. Seznam identifikátorů je důležitý i pořadím jejich definice. Pokud nepoužijeme
nepovinnou konstrukci =
, je první výčtové konstantě přiřazena
hodnota nula. Následník pak má hodnotu o jedničku vyšší než předchůdce.
Jak jsme si ovšem ukázali v příkladu, můžeme přiřadit i první konstantě hodnotu jinou
než nulovou, rovněž může mít následník hodnotu nesouvisející s předchůdcem. Tak
mohou vzniknout "díry" v číslování, případně i synonyma. Díky
této možnosti (příklad nás jistě přesvědčil, že je užitečná) nemůže překladač
kontrolovat, zdali nabývá proměnná hodnoty korektní, či nikoliv. To je přijatelná
cena, kterou platíme za popsané možnosti.
Poznamenejme, že hodnoty výčtových typů nelze posílat na výstup ve tvaru, v jakém jsme je definovali. Můžeme je zobrazit pouze jako odpovídající celočíselné ekvivalenty. Obdobně je můžeme číst ze vstupu. Výčtové konstanty se tedy ve své textové podobě nacházejí pouze ve zdrojovém tvaru programu. Přeložený program pracuje již jen číselnými hodnotami výčtových konstant.
Vrátíme-li se k příkladu, povšimneme si skutečnosti, že nepoužíváme
type_tag
. Tato možnost byla nutná ještě před zavedením konstrukce
typedef
. Dnes je obvyklejší pracovat naznačeným stylem. Přinejmenším
pokaždé při deklaraci argumentů ušetříme ono klíčové slovo enum
.
10.3. Typ struktura
Dosud jsme v C obvykle vystačili se základními datovými typy. Realita, kterou se ve svých programech často neuměle pokoušíme popsat, zřejmě tuto jednoduchost postrádá. Nezřídka se setkáváme se skutečnostmi, k jejichž popisu potřebujeme více souvisejících údajů. Programátor navíc dodá, že různého typu. Užitečnou možností je konstrukce, která takovou konstrukci dovolí a pro její snadné další použití i pojmenuje. Směřujeme k definici struktury. Její korektní syntaktický předpis je následující:
struct [] { [ ] ; [ ] ; ... } [ ] ;
Konstrukci uvádí klíčové slovo struct
. Následuje nepovinné
pojmenování struct type name
, které jako v případě
výčtového typu obvykle nepoužíváme. Zůstalo zachováno spíše kvůli
starší K&R definici C. Následuje blok definic položek struktury. Po něm opět můžeme
definovat proměnné nově definovaného typu. Položky jsou odděleny středníkem. Jsou
popsány identifikátorem typu type
, následovaným jedním nebo
více identifikátory prvků struktury variablename
. Ty jsou navzájem odděleny
čárkami.
Pro přístup k prvkům struktury používáme selektor struktury (záznamu) .
(Je jím tečka). Tu umístíme mezi identifikátory proměnné typu struktura a
identifikátor položky, s níž chceme pracovat. V případě, kdy máme ukazatel na strukturu,
použijeme místo hvězdičky a nezbytných5 závorek raději operátor ->
.
Podívejme se na příklad. Definujeme v něm nové typy complex
a
vyrobek
. S použitím druhého z nich definujeme další typ zbozi
.
Typ zbozi
představuje pole mající POLOZEK_ZBOZI
prvků, každý z nich
je typu vyrobek
. Typ vyrobek
je struktura, sdružující položky
ev_cislo
typu int
, nazev
typu znakové pole délky
ZNAKU_NAZEV+1
6
. Teprve takové definice nových typů, někdy se jim říká
uživatelské, používáme při deklaraci proměnných.
typedef struct {float re, im;} complex; typedef struct { int ev_cislo; char nazev[ZNAKU_NAZEV + 1]; int na_sklade; float cena; } vyrobek; typedef vyrobek zbozi[POLOZEK_ZBOZI];
Syntaxe struct
sice nabízí snadnější definice proměnných
použitých v programu, otázkou zní, jak čitelné by pak bylo například
deklarování argumentu nějaké funkce jako ukazatel na typ zboží. Jinak řečeno, konstrukci
struktury pomocí typedef
oceníme spíše u rozsáhlejších
zdrojových textů. U jednoúčelových krátkých programů se obvykle na eleganci
příliš nehledí.
Dále se podívejme na přiřazení hodnoty strukturované proměnné při její definici.
vyrobek *ppolozky, a = {8765, "nazev zbozi na sklade", 100, 123.99};
Konstrukce značně připomíná obdobnou inicializaci pole. Zde jsou navíc jednotlivé prvky různých typů.
Následují ukázky přiřazení hodnot prvkům struktury. Nejzajímavější je srovnání přístupu do struktury přes ukazatel. Čitelnost zavedení odlišného operátoru v tomto případě je zřejmá. Můžeme porovnat s druhou variantou uvedenou jako komentář:
ppolozky->ev_cislo = 1; /* (*ppolozky).ev_cislo = 1; */
Nyní se podívejme na souvislý zdrojový text.
/************************/ /* soubor struct01.c */ /* ukazka struct */ /************************/ #include#include #define ZNAKU_NAZEV 25 #define POLOZEK_ZBOZI 10 #define FORMAT_VYROBEK "cislo:%5d pocet:%5d cena:%10.2f nazev:%sn" typedef struct {float re, im;} complex; typedef struct { int ev_cislo; char nazev[ZNAKU_NAZEV + 1]; int na_sklade; float cena; } vyrobek; typedef vyrobek zbozi[POLOZEK_ZBOZI]; int main(void) { complex cislo, im_jednotka = {0, 1}; zbozi polozky; vyrobek *ppolozky, a = {8765, "nazev zbozi na sklade", 100, 123.99}; cislo.re = 12.3456; cislo.im = -987.654; polozky[0].ev_cislo = 0; strcpy(polozky[0].nazev, "polozka cislo 0"); polozky[0].na_sklade = 20; polozky[0].cena = 45.15; ppolozky = polozky + 1; ppolozky->ev_cislo = 1; /* (*ppolozky).ev_cislo = 1; */ strcpy(ppolozky->nazev, "polozka cislo 1"); ppolozky->na_sklade = 123; ppolozky->cena = 9945.15; printf("re = %10.5f im = %10.5fn", im_jednotka.re, im_jednotka.im); printf("re = %10.5f im = %10.5fn", cislo.re, cislo.im); printf(FORMAT_VYROBEK, a.ev_cislo, a.na_sklade, a.cena, a.nazev); printf(FORMAT_VYROBEK, polozky[0].ev_cislo, polozky[0].na_sklade, polozky[0].cena, polozky[0].nazev); printf(FORMAT_VYROBEK, ppolozky->ev_cislo, ppolozky->na_sklade, ppolozky->cena, ppolozky->nazev); return 0; }
Tento výstup získáme spuštěním programu.
re = 0.00000 im = 1.00000 re = 12.34560 im = -987.65399 cislo: 8765 pocet: 100 cena: 123.99 nazev:nazev zbozi na sklade cislo: 0 pocet: 20 cena: 45.15 nazev:polozka cislo 0 cislo: 1 pocet: 123 cena: 9945.15 nazev:polozka cislo 1
O užitečnosti struktur nás dále přesvědčí detailní pohled na typ, který jsme
dosud používali, aniž bychom si jej blíže popsali. Je to typ FILE
. Jeho definice v
hlavičkovém souboru STDIO.H
je:
typedef struct { short level; unsigned flags; char fd; unsigned char hold; short bsize; unsigned char *buffer, *curp; unsigned istemp; short token; } FILE;
Pokud se rozpomeneme na vše, co jsme se dosud o proudech dozvěděli, naznačí nám
některé identifikátory, k jakému účelu jsou nezbytné. Výhoda definice
FILE
spočívá mimo jiné i v tom, že jsme tento datový typ běžně
používali, aniž bychom měli ponětí o jeho definici. O implementaci souvisejících
funkcí, majících FILE *
jako jeden ze svých argumentů či jako
návratový typ, ani nemluvě.
Pro základní použití struktur již máme dostatečné informace. Intuitivně jsme schopni odhadnout, jak přistupovat k prvku struktury, který je rovněž strukturou (prostě umístíme mezi identifikátory prvků další tečku).
Problém nastane v okamžiku, kdy potřebujeme definovat dvě struktury, které spolu navzájem souvisejí. Přesněji řečeno, jedna obsahuje prvek typu té druhé. A naopak. Pravdou sice je, že se nejedná o častou situaci, nicméně se můžeme podívat na použití neúplné deklarace. Nebudeme si vymýšlet nějaké příliš smysluplné struktury. Princip je následující:
struct A; /* incomplete */ struct B {struct A *pa}; struct A {struct B *pb};
Vidíme, že u neúplné deklarace určíme identifikátoru A
třídu struct
. V těle struktury B
se ovšem může vyskytovat pouze ukazatel na
takto neúplně deklarovanou strukturu A
. Její velikost totiž ještě není
známa7.
10.4. Typ union
Syntakticky vypadá konstrukce union
následovně:
union [] { ; ... } [ ] ;
Již na první pohled je velmi podobná strukturám. S jedním podstatným rozdílem, který není zřejmý ze syntaxe, ale je dán sémantikou. Z položek unie lze používat v jednom okamžiku pouze jednu. Ostatní mají nedefinovanou hodnotu. Realizace je jednoduchá. Paměťové místo vyhrazené pro unii je tak veliké, aby obsáhlo jedinou (paměťově největší) položku. Tím je zajištěno splnění vlastností unie. Překladač C ponechává na programátorovi, pracuje-li s prvkem unie, který je určen správně8 či nikoliv. Ostatně v okamžiku překladu nejsou potřebné údaje stejně k dispozici.
Každý z prvků unie začíná na jejím začátku. Můžeme si představit, že paměťově delší prvky překrývají ty kratší. Této skutečnosti můžeme někdy využít. Nevíme-li, jakého typu bude návratový argument, definujeme unii mající položky všech požadovaných typů. Dalším argumentem předáme informaci o skutečném typu hodnoty. Pak podle ní provedeme přístup k správnému členu unie9.
10.5. Bitová pole
K současným trendům programování patří i oprostění se od nutnosti šetřit každým bajtem, v extrémních případech až bitem, paměti. Plýtvání zdroji (pamětí, diskovou kapacitou, komunikací) je stále častěji skutečností. Přesto jsou i dnes oblasti, v nichž je úsporné uložení dat ne-li nezbytné, tedy alespoň vhodné. Ano, jedná se mimo jiné o operační systémy. Jednou z možností, jak úsporně využít paměť, jsou právě bitová pole.
Bitové pole je celé číslo, umístěné na určeném počtu bitů. Tyto bity tvoří souvislou oblast paměti. Bitové pole může obsahovat více celočíselných položek. Můžeme vytvořit bitové pole tří tříd:
- prosté bitové pole;
- bitové pole se znaménkem;
- bitové pole bez znaménka.
Bitová pole můžeme deklarovat pouze jako členy struktury či unie. Výraz,
který napíšeme za identifikátorem položky a dvoutečkou, představuje velikost pole v
bitech. Nemůžeme definovat přenositelné bitové pole, které je rozsáhlejší
než typ int
.
Způsob umístění jednotlivých položek deklarace do celočíselného typu je implementačně závislý.
Podívejme se nyní na příklad bitového pole. Jak můžeme z předchozího textu
usuzovat, je spjat s konkrétním operačním systémem. V hlavičkovém souboru
IO.H
překladače BC3.1 je definována struktura ftime
, která popisuje datum a
čas vzniku (poslední modifikace) souboru v OS MS-DOS (řekněme včetně verze 6):
struct ftime { unsigned ft_tsec : 5; /* Two seconds */ unsigned ft_min : 6; /* Minutes */ unsigned ft_hour : 5; /* Hours */ unsigned ft_day : 5; /* Days */ unsigned ft_month : 4; /* Months */ unsigned ft_year : 7; /* Year - 1980 */ };
Prvky struktury jsou bitová pole. Výsledkem je výborné využití 32 bitů. Jediným omezením je skutečnost, že sekundy jsou uloženy v pěti bitech a pro rok zůstává bitů sedm. Jinak řečeno, pro sekundy můžeme použít 32 hodnot. Proto jsou uloženy zaokrouhleny na násobek dvou. Rok je uložen jako hodnota, kterou musíme přičíst k počátku, roku 1980. Umístění jednotlivých položek v bitovém poli ukazuje tabulka (z důvodů umístění na stránce je rozdělena do dvou částí):
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
ft_hour | ft_min | ft_sec | |||||||||||||
hodiny | minuty | sekundy/2 |
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
ft_year | ft_month | ft_day | |||||||||||||
rok - 1980 | měsíc | den |
Jelikož se snažíme nepoužívat systémově závislé funkce, nebudeme si uvádět příklad použití bitových polí. Výše uvedená ukázka nám postačí.
10.6. Klasifikace typů v C
V této kapitole jsme dokončili výklad typů, které máme v C k dispozici. Proto jsme zahrnuli obrázek, který typy v C klasifikuje. Pro větší výrazové schopnosti angličtiny, umožňující popsat na nenší ploše potřebné skutečnosti, jsme v tomto obrázku nepoužili české termíny.
1 Prakticky se jedná o přejmenování.
2 Při definicích (*jméno)[]
a (*jméno)()
jsou závorky
nezbytné, v případě *(jméno[])
jsou nadbytečné - zvyšují
pouze čitelnost.
3 A tedy i bezpečnost. Přehledný program neskrývá myšlenku použitím fint a
nejasných konstrukcí. Někdy snad není tak efektivní (i když i o tom lze pochybovat,
přirozeně musíme brát případ od případu), ale zřejmě bude obsahovat méně chyb.
Pokud tam přece jen nějaké budou (Murphy ...), pravděpodobně je dříve odhalíme.
4 Jedná se tedy o definici proměnných, neboť jim nejen deklarujeme typ, ale přidělujeme jim i
paměťové místo.
5 Nevíme-li proč, je na čase nalistovat tabulku s prioritou a asociativitou operátorů.
6 Tedy řetězec délky ZNAKU_NAZEV. Znak, který je v poli navíc, je zarážkou
řetězce.
7 Velikost nutná pro uložení ukazatele ovšem známa je. Proto je taková
konstrukce možná.
8 Přesněji, jehož hodnota je definována.
9 Méně čitelné řešení předá vždy argument nejdelšího typu a
poté jej podle potřeby přetypuje.
petr.saloun@ vsb.cz
DOWNLOAD & Odkazy
- Domovská stránka autora - http://www.cs.vsb.cz/saloun/publik/c4zelenace/
- Další užitečné zdroje týkající se tématu - index.html
- Obsah kurzu - index.html