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.
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 <type definition> <identifier> ;
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:
/**************/ /*
TYPEDEF0.C
*/ /**************/ #include <stdio.h> int main() { typedef float real; real x = 2.5, y = 2.0; printf("%5.1f * %5.1f = %5.1f\n", x, y, x * y); return 0; }
Získá tím ovšem jistotu, že jeho programy bude číst pouze on sám.
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ů:
Celý postup si ukážeme na příkladu:
char *( *( *var) ()) [10];
7 6 4 2 1 3 5
Označené kroky nám říkají:
var
je deklarován jako 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-ti 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ů |
Výčtový typ nám umožňuje definovet 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í 101 tlačí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 [<type_tag>] {<constant_name> [=
<value>], ...} [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 = <value>
, 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
.
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 [<struct type name>] {
[<type> <variable-name[, variable-name, ...]>] ;
[<type> <variable-name[, variable-name, ...]>] ;
...
} [<structure variables>] ;
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 <stdio.h> #include <string.h> #define ZNAKU_NAZEV 25 #define POLOZEK_ZBOZI 10 #define FORMAT_VYROBEK "cislo:%5d pocet:%5d cena:%10.2f nazev:%s\n" 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.5f\n", im_jednotka.re, im_jednotka.im); printf("re = %10.5f im = %10.5f\n", 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 souvisí. 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.
Syntakticky vypadá konstrukce union
následovně:
union [<union type name>] {
<type> <variable names> ;
...
} [<union variables>] ;
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í vlastnosí 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.
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:
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čí.
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.
Výsvětlivky:
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.
Název: | Programování v jazyce C |
Autor: | Petr Šaloun |
Do HTML převedl: | Kristian Wiglasz |
Poslední úprava: | 20.11.1996 |