10.1. Uzivatelsky datovy typ.
10.2. Vyctovy typ.
10.3. Typ struktura.
10.4. Typ union.
10.5. Bitova pole.
10.6. Klasifikace typu v C
Dosud jsme se seznamili jen se zakladnimi datovymi typy.
To jest takovymi typy, ktere vyhovuji pro jednoduche
vypocty ci (programove) zpracovani textu. Tyto typy mame v
C primo k dispozici (jsou soucasti normy jazyka). Take
zname preprocesor a vime, ze pouzitim symbolickych konstant
program zprehlednime. V teto kapitole si ukazeme tvorbu a
pouziti takovych strukturovanych datovych typu, jake nam
prinasi zivot. Take si ukazeme definici vyctovych
typu, ktera umozni hodnoty nejen pojmenovat, ale provadet
pri prekladu i jejich typovou kontrolu. Na uvod kapitoly si
nechavame popis definice vlastnich datovych typu s prakticky
libovolnou strukturou.
Vyssi programovaci jazyk ma k dispozici takove
zakladni datove typy, ktere pokryji vetsinu potreb. S
jejich pomoci muzeme vytvaret rozsahlejsi homogenni
datove struktury, jako napriklad pole, ci heterogenni, jako
strukturu ci unii (struct
a union
).
Programator ovsem musi mit k dispozici mechanismus, kterym
si vytvori datovy typ podle svych potreb. Tento mechanismus
se nazyva typedef
a jeho syntaxe je na prvni
pohled velmi jednoducha:
typedef <type definition> <identifier> ;
Po klicovem slove typedef
nasleduje definice
typu type definition
. Pote je novemu typu urcen
identifikator identifier
.
Skalni zastance nejakeho datoveho typu ci jazyka si spojenim maker a uzivatelskych typu muze C pretvorit k obrazu svemu:
/**************/ /*
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; }
Ziska tim ovsem jistotu, ze jeho programy bude cist pouze on sam.
Jestlize se uvedene jednoduche typove
konstrukce1
prakticky nepouzivaji, podivejme se naopak na nektere
konstrukce slozitejsi. Nejprve si shrnme definice, ktere
bychom meli zvladnout pomerne snadno:
deklarace | typ identifikatoru jmeno |
---|---|
typ jmeno; | typ |
typ jmeno[]; | (otevrene) pole typu |
typ jmeno[3]; | pole (pevne velikosti) tri polozek typu (jmeno[0], jmeno[1],jmeno[2]) |
typ *jmeno; | ukazatel na typ |
typ *jmeno[]; | (otevrene) pole ukazatelu na typ |
typ *(jmeno[]); | (otevrene) pole ukazatelu na typ |
typ (*jmeno)[]; | ukazatel na (otevrene) pole typu |
typ jmeno(); | funkce vracejici hodnotu typu |
typ *jmeno(); | funkce vracejici ukazatel na hodnotu typu |
typ *(jmeno()); | funkce vracejici ukazatel na hodnotu typu |
typ (*jmeno)(); | ukazatel na funkci, vracejici typ2 |
Muze se stat, ze si u nekterych definic prestavame byt jisti. Uvedeme si proto zasady, podle nichz musime pro spravnou interpretaci definice postupovat. Obecne se drzime postupu zevnitr ven. Podrobneji lze zasady shrnout do ctyr kroku:
Cely postup si ukazeme na prikladu:
char *( *( *var) ()) [10];
7 6 4 2 1 3 5
Oznacene kroky nam rikaji:
var
je deklarovan jako char
. Radeji dalsi priklad. Tentokrat jiz popiseme jen vysledek. Jednotlive kroky nebudeme znacit:
unsigned int *( * const *name[5][10]) (void);
Identifikator name
je dvourozmernym polem o
celkem 50-ti prvcich. Prvky tohoto pole jsou ukazateli na
ukazatele, ktere jsou konstantni. Tyto konstantni ukazatele
ukazuji na typ funkce, ktera nema argumenty a vraci ukazatel
na hodnotu typu unsigned int
.
Nasledujici funkce vraci ukazatel na pole tri hodnot
typu double
:
double ( *var (double (*)[3])) [3];
Jeji argument, stejne jako navratova hodnota, je ukazatel
na pole tri prvku typu double
.
Argument predchozi funkce je konstrukce, ktera se nazyva abstraktni
deklarace. Obecne se jedna o deklaraci bez
identifikatoru. Deklarace obsahuje jeden ci vice ukazatelu,
poli nebo modifikaci funkci. Pro zjednoduseni a
zprehledneni abstraktnich deklaraci se pouziva konstrukce typedef
.
Abstraktni deklarace my nas nemely zaskocit
ani v pripade, kdy typedef
pouzito neni.
Radeji si nekolik abstraktnich deklaraci uvedeme:
int * | ukazatel na typ int |
int *[3] | pole tri ukazatelu na int |
int (*)[5] | ukazatel na pole peti prvku typu int |
int *() | funkce bez specifikace argumentu vracejici ukazatel na int |
int (*) (void) | ukazatel na funkci nemajici argumenty vracejici int |
int (*const []) (unsigned int, ..) | ukazatel na nespecifikovany pocet konstantnich ukazatelu na funkce, z nichz kazda ma prvni argument unsigned int a nespecifikovany pocet dalsich argumentu |
Vyctovy typ nam umoznuje definovet konstanty
vyctoveho typu. To je vyhodne napriklad v okamziku, kdy
priradime hodnote vyctoveho typu identifikator. Pak se ve
zdrojovem textu nesetkame napriklad s hodnotou 13, ci
dokonce 0x0d, ale napriklad CR, ci Enter. Takovy text je
mnohem citelnejsi. Jestlize pozdeji zjistime, ze je
treba hodnotu zmenit, nemusime v textu vyhledavat retezec
13, ktery se navic muze vyskytovat i jako podretezec rady
jinych retezcu, ale na jedinem miste zmenime hodnotu
vyctove konstanty. Ze se jedna o klasicke konstanty (ci
dokonce konstantni makra), ktere jsme poznali prakticky na
zacatku textu? Temer to tak vypada, ale vyctove
konstanty mohou mit navic pojmenovan typ, ktery reprezentuje
vsechny jeho vyctove hodnoty. I tim se zvysi prehlednost3.
Podivejme se nejprve na priklad. Nasim ukolem je zpracovat stisknute klavesy na standardni 101 tlacitkove klavesnici PC-AT. Pokud bychom do jednotlivych vetvi umistili pro porovnavani celociselne konstanty, zrejme bychom sami brzy ztratili prehled. Pouzijeme-li vyctove konstanty, je situace zcela jina. Ostatne podivejme:
/************************************************/ /* 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 zrejme, ze vypis zdrojoveho textu je kracen. Jde nam o ukazku. Presto, ze je zrejme pruhledna, podivejme se na syntaxi definice vyctoveho typu:
enum [<type_tag>] {<constant_name> [=
<value>], ...} [var_list];
Klicove slovo enum
definici uvadi.
Nepovinne oznaceni type_tag
umoznuje
pojmenovani hodnot vyctoveho typu bez pouziti konstrukce typedef
.
Pote nasleduje seznam vyctovych konstant ve slozenych
zavorkach. Na zaver definice muzeme (nepovinny parametr)
primo uvest promenne4, ktere mohou definovanych vyctovych hodnot
nabyvat. Vratme se jeste k obsahu bloku. Seznam
identifikatoru je dulezity i poradim jejich definice.
Pokud nepouzijeme nepovinnou konstrukci = <value>
,
je prvni vyctove konstante prirazena hodnota nula.
Naslednik pak ma hodnotu o jednicku vyssi, nez
predchudce. Jak jsme si ovsem ukazali v prikladu, muzeme
priradit i prvni konstante hodnotu jinou, nez nulovou,
rovnez muze mit naslednik hodnotu nesouvisejici s
predchudcem. Tak mohou vzniknout "diry" v
cislovani, pripadne i synonyma. Diky teto moznosti
(priklad nas jiste presvedcil, ze je uzitecna),
nemuze prekladac kontrolovat, zdali nabyva promenna
hodnoty korektni ci nikoliv. To je prijatelna cena, kterou
platime za popsane moznosti.
Poznamenejme, ze hodnoty vyctovych typu nelze posilat na vystup ve tvaru, v jakem jsme je definovali. Muzeme je zobrazit pouze jako odpovidajici celociselne ekvivalenty. Obdobne je muzeme cist ze vstupu. Vyctove konstanty se tedy ve sve textove podobe nachazeji pouze ve zdrojovem tvaru programu. Prelozeny program pracuje jiz jen ciselnymi hodnotami vyctovych konstant.
Vratime-li se k prikladu, povsimneme si skutecnosti, ze
nepouzivame type_tag
. Tato moznost byla nutna
jeste pred zavedenim konstrukce typedef
. Dnes je
obvyklejsi pracovat naznacenym stylem. Prinejmensim
pokazde pri deklaraci argumentu usetrime ono klicove
slovo enum
.
Dosud jsme v C obvykle vystacili se zakladnimi datovymi typy. Realita, kterou se ve svych programech casto neumele pokousime popsat, zrejme tuto jednoduchost postrada. Nezridka se setkavame se skutecnostmi, k jejichz popisu potrebujeme vice souvisejicich udaju. Programator navic doda, ze ruzneho typu. Uzitecnou moznosti je konstrukce, ktera takovou konstrukci dovoli a pro jeji snadne dalsi pouziti i pojmenuje. Smerujeme k definici struktury. Jeji korektni syntakticky predpis je nasledujici:
struct [<struct type name>] {
[<type> <variable-name[, variable-name, ...]>] ;
[<type> <variable-name[, variable-name, ...]>] ;
...
} [<structure variables>] ;
Konstrukci uvadi klicove slovo struct
.
Nasleduje nepovinne pojmenovani structtypename
,
ktere jako v pripade vyctoveho typu obvykle
nepouzivame. Zustalo zachovano spise kvuli starsi
K&R definici C. Nasleduje blok definic polozek struktury.
Po nem opet muzeme definovat promenne nove definovaneho
typu. Polozky jsou oddeleny strednikem. Jsou popsany
identifikatorem typu type
, nasledovanym jednim,
nebo vice identifikatory prvku struktury variablename
.
Ty jsou navzajem oddeleny carkami.
Pro pristup k prvkum struktury pouzivame selektor
struktury (zaznamu) .
(je jim tecka). Tu
umistime mezi identifikatory promenne typu struktura a
identifikator polozky, s niz chceme pracovat. V pripade,
kdy mame ukazatel na strukturu, pouzijeme misto hvezdicky a
nezbytnych5
zavorek radeji operator ->
.
Podivejme se na priklad. Definujeme v nem nove typy complex
a vyrobek
. S pouzitim druheho z nich definujeme
dalsi typ zbozi
. Typ zbozi
predstavuje pole majici POLOZEK_ZBOZI
prvku,
kazdy z nich je typu vyrobek
. Typ vyrobek
je struktura, sdruzujici polozky ev_cislo
typu int
, nazev
typu znakove pole delky ZNAKU_NAZEV+1
6
.
Teprve takove definice novych typu, nekdy se jim rika
uzivatelske, pouzivame pri deklaraci promennych.
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 nabizi snadnejsi
definice promennych pouzitych v programu, otazkou zni, jak
citelne by pak bylo napriklad deklarovani argumentu
nejake funkce jako ukazatel na typ zbozi. Jinak receno,
Konstrukci struktury pomoci typedef
ocenime
spise u rozsahlejsich zdrojovych textu. U
jednoucelovych kratkych programu se obvykle na eleganci
prilis nehledi.
Dale se podivejme na prirazeni hodnoty strukturovane promenne pri jeji definici.
vyrobek *ppolozky,
a = {8765, "nazev zbozi na sklade", 100, 123.99};
Konstrukce znacne pripomina obdobnou inicializaci pole. Zde jsou navic jednotlive prvky ruznych typu.
Nasleduji ukazky prirazeni hodnot prvkum struktury. Nejzajimavejsi je srovnani pristupu do struktury pres ukazatel. Citelnost zavedeni odlisneho operatoru v tomto pripade je zrejma. Muzeme porovnat s druhou variantou uvedenou jako komentar:
ppolozky->ev_cislo = 1;
/* (*ppolozky).ev_cislo = 1; */
Nyni se podivejme na souvisly zdrojovy 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 vystup ziskame spustenim 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 uzitecnosti struktur nas dale presvedci detailni
pohled na typ, ktery jsme dosud pouzivali, aniz bychom si jej
blize popsali. Je to typ FILE
. Jeho definice v
hlavickovem 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 vse, co jsme se dosud o proudech
dozvedeli, naznaci nam nektere identifikatory, k jakemu
ucelu jsou nezbytne. Vyhoda definice FILE
spociva mimo jine i v tom, ze jsme tento datovy typ
bezne pouzivali, aniz bychom meli poneti o jeho
definici. O implementaci souvisejicich funkci, majicich FILE
*
jako jeden ze svych argumentu ci jako navratovy
typ, ani nemluve.
Pro zakladni pouziti struktur jiz mame dostatecne informace. Intuitivne jsme schopni odhadnout, jak pristupovat k prvku struktury, ktery je rovnez strukturou (proste umistime mezi identifikatory prvku dalsi tecku).
Problem nastane v okamziku, kdy potrebujeme definovat dve
struktury, ktere spolu navzajem souvisi. Presneji receno,
jedna obsahuje prvek typu te druhe. A naopak. Pravdou sice, je,
ze se nejedna o castou situaci, nicmene se muzeme podivat
na pouziti neuplne deklarace. Nebudeme si
vymyslet nejake prilis smysluplne struktury. Princip je
nasledujici:
struct A; /* incomplete */
struct B {struct A *pa};
struct A {struct B *pb};
Vidime, ze u neuplne deklarace urcime identifikatoru A
tridu struct
. V tele struktury B
se
ovsem muze vyskytovat pouze ukazatel na takto neuplne
deklarovanou strukturu A
. Jeji velikost totiz
jeste neni znama7.
Syntakticky vypada konstrukce union
nasledovne:
union [<union type name>] {
<type> <variable names> ;
...
} [<union variables>] ;
Jiz na prvni pohled je velmi podobna strukturam. S jednim podstatnym rozdilem, ktery neni zrejmy ze syntaxe, ale je dan semantikou. Z polozek unie lze pouzivat v jednom okamziku pouze jednu. Ostatni maji nedefinovanou hodnotu. Realizace je jednoducha. Pametove misto, vyhrazene pro unii je tak velike, aby obsahlo jedinou (pametove nejvetsi) polozku. Tim je zajisteno splneni vlastnosi unie. Prekladac C ponechava na programatorovi, pracuje-li s prvkem unie, ktery je urcen spravne8 ci nikoliv. Ostatne, v okamziku prekladu nejsou potrebne udaje stejne k dispozici.
Kazdy z prvku unie zacina na jejim zacatku. Muzeme
si predstavit, ze pametove delsi prvky prekryvaji ty
kratsi. Teto skutecnosti muzeme nekdy vyuzit.
Nevime-li, jakeho typu bude navratovy argument, definujeme
unii, majici polozky vsech pozadovanych typu. Dalsim
argumentem predame informaci o skutecnem typu hodnoty. Pak
podle ni provedeme pristup k spravnemu clenu unie9.
K soucasnym trendum programovani patri i oprosteni se od nutnosti setrit kazdym bajtem, v extremnich pripadech az bitem, pameti. Plytvani zdroji (pameti, diskovou kapacitou, komunikaci) je stale casteji skutecnosti. Presto jsou i dnes oblasti, v nichz je usporne ulozeni dat ne-li nezbytne, tedy alespon vhodne. Ano, jedna se mimo jine o operacni systemy. Jednou z moznosti, jak usporne vyuzit pamet jsou prave bitova pole.
Bitove pole je cele cislo, umistene na urcenem poctu bitu. Tyto bity tvori souvislou oblast pameti. Bitove pole muze obsahovat vice celociselnych polozek. Muzeme vytvorit bitove pole tri trid:
Bitova pole muzeme deklarovat pouze jako cleny struktury
ci unie. Vyraz, ktery napiseme za
identifikatorem polozky a dvouteckou, predstavuje velikost
pole v bitech. Nemuzeme definovat prenositelne bitove pole,
ktere je rozsahlejsi, nez typ int
.
Zpusob umisteni jednotlivych polozek deklarace do celociselneho typu je implementacne zavisly.
Podivejme se nyni na priklad bitoveho pole. Jak muzeme
z predchoziho textu usuzovat, je spjat s konkretnim
operacnim systemem. V hlavickovem souboru IO.H
prekladace BC3.1 je definovana struktura ftime
,
ktera popisuje datum a cas vzniku (posledni modifikace)
souboru v OS MS-DOS (rekneme vcetne 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 bitova pole. Vysledkem je
vyborne vyuziti 32 bitu. Jedinym omezenim je skutecnost,
ze sekundy jsou ulozeny v peti bitech a pro rok zustava
bitu sedm. Jinak receno, pro sekundy muzeme pouzit 32
hodnot. Proto jsou ulozeny zaokrouhleny na nasobek dvou. Rok je
ulozen jako hodnota, kterou musime pricist k pocatku, roku
1980. Umisteni jednotlivych polozek v bitovem poli ukazuje
tabulka (z duvodu umisteni na strance je rozdelena do dvou
casti):
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 | mesic | den |
Jelikoz se snazime nepouzivat systemove zavisle funkce, nebudeme si uvadet priklad pouziti bitovych poli. Vyse uvedena ukazka nam postaci.
V teto kapitole jsme dokoncili vyklad typu, ktere mame v
C k dispozici. Proto jsme zahrnuli obrazek, ktery typy v C
klasifikuje. Pro vetsi vyrazove schopnosti anglictiny,
umoznujici popsat na nensi plose potrebne skutecnosti,
jsme v tomto obrazku nepouzili ceske terminy.
Vysvetlivky:
1 Prakticky se jedna o
prejmenovani.
2 Pri definicich (*jmeno)[]
a (*jmeno)()
jsou zavorky nezbytne, v pripade *(jmeno[])
jsou nadbytecne - zvysuji pouze citelnost.
3 A tedy i bezpecnost.
Prehledny program neskryva myslenku pouzitim fint a
nejasnych konstrukci. Nekdy snad neni tak efektivni (i kdyz
i o tom lze pochybovat, prirozene musime brat pripad od
pripadu), ale zrejme bude obsahovat mene chyb. Pokud tam
prece jen nejake budou (Murphy ...), pravdepodobne je
drive odhalime.
4 Jedna se tedy o definici
promennych, nebot jim nejen deklarujeme typ, ale pridelujeme
jim i pametove misto.
5 Nevime-li proc, je na case
nalistovat tabulku s prioritou a asociativitou operatoru.
6 Tedy retezec delky
ZNAKU_NAZEV. Znak, ktery je v poli navic, je zarazkou
retezce.
7 Velikost nutna pro ulozeni
ukazatele ovsem znama je. Proto je takova konstrukce mozna.
8 Presneji, jehoz hodnota je
definovana.
9 Mene citelne reseni
preda vzdy argument nejdelsiho typu a pote jej podle
potreby pretypuje.
Nazev: | Programovani v jazyce C |
Autor: | Petr Saloun |
Do HTML prevedl: | Kristian Wiglasz |
Posledni uprava: | 03.12.1996 |