Predchozi kapitola

Obsah

Konec

Priloha A

10. Odvozene a strukturovane typy dat

10.1. Uzivatelsky datovy typ.

Slozitejsi typove deklarace

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.

10.1. Uzivatelsky datovy typ.zpet

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.

Slozitejsi typove deklaracezpet

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:

  1. Zacneme u identifikatoru a hledejme vpravo kulate nebo hranate zivorky (jsou-li nejake).
  2. Interpretujme tyto zavorky a hledejme vlevo hvezdicku.
  3. Pokud narazime na pravou zavorku (libovolneho stupne vnoreni), vratme se a aplikujme pravidla 1 a 2 pro vse mezi zavorkami.
  4. Aplikujme specifikaci typu.

Cely postup si ukazeme na prikladu:

char *( *( *var) ()) [10];
7 6 4 2 1 3 5

Oznacene kroky nam rikaji:

  1. Identifikator var je deklarovan jako
  2. ukazatel na
  3. funkci vracejici
  4. ukazatel na
  5. pole 10-ti prvku, ktere jsou
  6. ukazateli na
  7. hodnoty typu 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

10.2. Vyctovy typ.zpet

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.

10.3. Typ struktura.zpet

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+16. 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.

10.4. Typ union.zpet

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.

10.5. Bitova pole.zpet

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:

  1. proste bitove pole
  2. bitove pole se znamenkem
  3. bitove pole bez znamenka

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.

10.6. Klasifikace typu v Czpet

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.


Predchozi kapitola

Obsah

Zacatek

Priloha A


Nazev: Programovani v jazyce C
Autor: Petr Saloun
Do HTML prevedl: Kristian Wiglasz
Posledni uprava: 03.12.1996