Jste zde

Programování v jazyce C - 10. Odvozené a strukturované typy dat

V dnešním pokračování seriálu o výuce programování v jazyce C se podíváme na tvorbu a použití

strukturovaných datových typů. 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.

10.1. Uživatelský datový typ

Složitější typové deklarace

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 */
/*********************/
#include 
int 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:

deklaracetyp 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ů:

  1. Začněme u identifikátoru a hledejme vpravo kulaté nebo hranaté závorky (jsou-li nějaké).
  2. Interpretujme tyto závorky a hledejme vlevo hvězdičku.
  3. 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.
  4. 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í:

  1. Identifikátor var je deklarován jako
  2. ukazatel na
  3. funkci vracející
  4. ukazatel na
  5. pole 10 prvků, které jsou
  6. ukazateli na
  7. 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 variable­name. 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+16. 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:

  1. prosté bitové pole;
  2. bitové pole se znaménkem;
  3. 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.

RNDr. Petr Šaloun, Ph.D.
petr.saloun@ vsb.cz

DOWNLOAD & Odkazy

Hodnocení článku: