Kapitola 11. Struktury a uživatelské typy dat

Obsah

11.1. Uživatelský datový typ
11.2. Složitější typové deklarace
11.3. Výčtový typ
11.4. Typ struktura
11.5. Typ unie
11.6. Bitová pole
11.7. Opakování

Časová náročnost
Časová náročnost kapitoly: 1 hodina 20 minut

V této části 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.

Potřebné znalosti
Potřebné znalosti
K zvládnutí obsahu této kapitoly je nutné mít znalosti z kapitoly 2 až 10 - umět psát i složitějsí programy, chápat problematiku souborů (textových i binárních), umět do nich zapisovat a číst z nich data.

11.1. Uživatelský datový typ

Časová náročnost
Časová náročnost: 12 minut

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). Je však nutné musí mít k dispozici mechanismus, kterým si vytvoří datový typ podle svých potřeb. Tento mechanismus se nazývá typedef a má následující syntaxi:

Důležité
typedef definice typu identifikátor;

Po klíčovém slově typedef následuje definice typu definice typu. Poté je novému typu určen identifikátor identifikátor.

A hned si ukažme několik příkladů. Nejprve si vytvořme nové jméno smallint, což bude jméno pro signed char a bude použito pro deklaraci i.

src/typedef1.c
Příklad 11.1.
/******************************
*	typedef1.c
*	19.03.2002
******************************/

#include <stdio.h>

typedef signed char smallint;

int main(void)
{
 smallint i;
 
 for(i=0; i<10; i++)
	 printf("%d ", i);
	
 return 0;
}

Další příklad je založen na kopírování řetězců. Pomocí konstrukce typedef vytvoříme nový datový typ string. Tím si zajistíme, že hvězdičky při deklaraci argumentů zmizí.

src/typedef2.c
Příklad 11.2.
/******************************
*	typedef2.c
*	19.03.2002
******************************/

#include <stdio.h>

#define	SIZE 80

typedef char * string;

void strcpy1(string d, string s)
{
 while ((*d++ = *s++) != 0) ;
}

void strcpy2(string d, string s)
{
 int i = 0;
 while ((d[i] = s[i]) != 0)
   i++;
}

int main()
{
 string s1 = "prvni (1.) retezec",
        s2 = "druhy (2.) retezec";
 char   d1[SIZE],
        d2[SIZE];
 strcpy1(d1, s1);
 strcpy2(d2, s2);
 printf("d1[]:%s\n", d1);
 printf("d2[]:%s\n", d2);
 return 0;
}

Musíme si však uvědomit dvě základní věci:

Důležité
  1. Typedef nezpůsobí deaktivaci původního jména.
  2. Můžeme použít několik příkazů typedef pro vytvoření několika nových jmen pro stejný typ.

Jednoduchý příklad
Příklad:

typedef int vyska;
typedef vyska delka;
typedef delka hloubka;

hloubka d;

Proměnná d je stále typu int.

11.2. Složitější typové deklarace

Časová náročnost
Časová náročnost: 15 minut

Nejprve si shrňme definice, které bychom už měli zvládnout.

Důležité

Tabulka 11.1.

DeklaraceVýznam
typ jméno;typ
typ jméno[];(otevřené) pole typu typ
typ jméno[3];pole (pevné velikosti) tří položek typu typ (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
typ jméno();funkce vracející hodnotu typu typ
typ *jméno();funkce vracející ukazatel na hodnotu typu typ
typ *(jméno());funkce vracející ukazatel na hodnotu typu typ
typ (*jméno)();ukazatel na funkci, vracející typ

Přesto se může stát, že si u některých definic přestáváme být jisti. Při deklaracích typ (*jméno)[]; a typ (*jméno)(); jsou závorky nezbytné, ale v případě typ *(jméno[]); jsou závorky naopak nadbytečné - zvyšují pouze čitelnost.

Důležité Zásady pro správnou interpretaci definic:
  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 jedna a dva pro vše mezi závorkami
  4. aplikujme specifikaci typu

Tento postup si nejlépe vysvětlíme na příkladu.

Jednoduchý příklad
Příklad:

char  *(  *(  *var)  ()) [10];

A podle popsaných zásad postupujeme následovně:

  1. identifikátor var je deklarován jako
  2. ukazatel na
  3. funkci vracející
  4. ukazatel na
  5. pole deseti prvků, které jsou
  6. ukazateli na
  7. hodnoty typu char

Další příklad, zde již popíšeme pouze výsledek.

Jednoduchý příklad
Příklad:

double  (  *var (double (*)[3])) [3]

Tato funkce vrací ukazatel na pole tří hodnot typu double. 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.

Na závěr této části si uveďme několik příkladů abstraktních definic.

Jednoduchý příklad
Příklad:
DeklaraceVýznam
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ů

11.3. Výčtový typ

Časová náročnost
Časová náročnost: 12 minut

V jazyku C můžeme definovat seznam pojmenovaných celočíselných konstant, nazvaných výčet (enumerace). Tyto konstanty lze pak použít všude, kde lze zadat celé číslo. Pro definici výčtového typu použijeme tento obecný formát:

Důležité
enum jméno-výčtu {seznam-položek} seznam-proměnných

Kde:

  • enum - je klíčové slovo, zahajující definici hodnot výčtového typu
  • jméno-výčtu - je nepovinná, dnes již nepožívaná "visačka" ve stylu K&R
  • {seznam-položek} - je seznam konstant výčtového typu s možnou explicitně přiřazenou hodnotou, jinak nabývá první konstanta výčtového typu hodnoty nula, druhá hodnoty jedna, ..., každý následník má hodnotu o jedničku vyšší, než jeho předchůdce
  • seznam-proměnných - je nepovinný seznam proměnných daného typu enum

Jednoduchý příklad
Příklad:

enum {cervena, zelena, zluta} barva;

Výčtový typ se zde skládá z konstant cervena, zelena a zluta. Byla vytvořena jedna proměnná se jménem barva. Protože překladač přiřazuje konstantám výčtu celočíselnou hodnotu počínaje od nuly je cervena rovna 0, zelena rovna 1 a zluta rovna 2. Toto standardní přiřazení hodnot překladačem však můžeme změnit zadáním explicitní hodnoty konstanty.

Jednoduchý příklad
Příklad:

enum {cervena, zelena=9, zluta} barva;

Je cervena zase rovna 0, zelena je rovna 9 a zluta je rovna 10.

Výčet je v podstatě celočíselný typ a výčtová proměnná může obsahovat libovolnou celočíselnou hodnotu, nejen tu, která je definována výčtem. Kvůli zachování přehlednosti bychom však měli používat výčtové proměnné jen pro uchování hodnot definovaných jejich výčtovým typem.

Hlavním důvodem použití výčtových typů je napomoci vytváření samo vysvětlujícího a přehlednějšího zápisu programu.

Následující krátký program vytváří výčtový typ sestávající se z některých názvů částí počítače. Přiřadí comp hodnotu CPU (což je vlastně 1) a zobrazí ji.

src/enum.c
Příklad 11.3.
/******************************
*	enum.c
*	19.03.2002
******************************/

#include <stdio.h>

enum {klavesnice, CPU, monitor, tiskarna} comp;

int main(void)
{
 comp = CPU;
 printf("%d", comp);
 
 return 0;
}

Důležité Všimněte si, ž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í celá čísla. 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 s číselnými hodnotami výčtových konstant.

11.4. Typ struktura

Časová náročnost
Časová náročnost: 20 minut

Struktura je sdružený datový typ, který je složen ze dvou, nebo více proměnných nazvaných prvky struktury. Na rozdíl od pole, ve kterém jsou všechny prvky stejného typu, může mít každý prvek struktury svůj vlastní typ, který se může lišit od ostatních typů.

Struktury se v jazyku C definují podle následujícího obecného formátu:

Důležité
struct jméno-typu
{
 typ prvek1[, prvek1, ....];
 typ prvek2[, prvek2, ....];
 typ prvek3[, prvek3, ....];
  .
  .
  .
 typ prvekN[, prvekN, ....];
} seznam-proměnných;  

Kde:

  • struct - říká překladači že se jedná o strukturovaný datový typ.
  • jméno-typu - nepovinné pojmenování typu, které jako v případě výčtového typu nepoužíváme a zůstalo zachováno kvůli starší K&R definici.
  • typ prvek1[, prvek1, ....]; - blok definic položek struktury. Položky jsou odděleny středníkem. Jsou popsány identifikátorem typu, následovaným jedním, nebo více identifikátory prvků struktury. Ty jsou navzájem odděleny čárkami. Nic nám nebrání v tom, uvést místo jednoduchého identifikátoru pole položek určeného typu. Toto pole je pak součástí struktury stejně, jako ostatní položky struktury.
  • seznam-proměnných; - proměnné nově definovaného typu.

Pro přístup k prvkům struktury používáme selektor struktury (záznamu) . "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ých závorek raději operátor ->.

Informace ve struktuře mají většinou nějaký logický vztah. Strukturu bychom mohli například používat pro uchování adresy osoby.

Jednoduchý příklad
Příklad:

struct 
{
 char jmeno[20];
 char prijmeni[30];
 char ulice[30];
 int cislo_popisne;
 char mesto[30];
 unsigned char psc;
} adresa;

Přiřazení hodnot může vypadat následovně:

adresa.jmeno="Petr";
adresa.prijmeno="Novak";
adresa.cislo_popisne=158;

Následující příklad nám předvede některé ze způsobů práce s prvky struktury.

src/struct_1.c
Příklad 11.4.
/******************************
*	struct_1.c
*	19.03.2002
******************************/

#include <stdio.h>
 
struct
{
 int i;
 double d;
 char str[80];
} s;

int main(void)
{
 printf("Zadejte cele cislo: ");
 scanf("%d", &s.i);
 printf("Zadejte desetinne cislo: ");
 scanf("%lf", &s.d);
 printf("Zadejte retezec: ");
 scanf("%s", &s.str);
 
 printf("Bylo zadano: \n");
 printf("%d %lf %s", s.i, s.d, s.str);

 return 0;
}

Další příklad nám ukáže, že jména prvků struktury nebudou v konfliktu s jinými proměnnými používající stejná jména. Proto se taky na obrazovku vypíše 10 100 101. Proměnná i a strukturovaný prvek i nemají mezi sebou žádný vztah.

src/struct_2.c
Příklad 11.5.
/******************************
*	struct_2.c
*	19.03.2002
******************************/

#include <stdio.h>

struct
{
 int i;
 int j;
} s;

int main(void)
{
 int i;

 i = 10;
 s.i = 100;
 s.j = 101;
 printf("%d %d %d", i, s.i, s.j);
 
 return 0;
}

Syntaxe typu FILE

typedef struct {
  short          level;
  unsigned       flags;
  char           fd;
  unsigned char  hold;
  short          bsize;
  unsigned char *buffer, *curp;
  unsigned       istemp;
  short          token;
} FILE;

Typ FILE je jeden z typů, které již používáme dlouho dobu. Jeho definice je v hlavičkovém souboru stdio.h

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.

Jednoduchý příklad
Příklad:

struct A;                /* neuplna */
struct B {struct A *pa};
struct A {struct B *pb}; /* dokonceni */

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áma. Velikost nutná pro uložení ukazatele ovšem známa je. Proto je taková konstrukce možná.

11.5. Typ unie

Časová náročnost
Časová náročnost: 5 minut

Syntaxe:

Důležité
union [<union type name>] {
  <type> <variable names> ;
  ...
} [<union variables>] ;

Již na první pohled je unie 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. Každý z prvků unie totiž 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 unie. Méně čitelné řešení předá vždy argument nejdelšího typu a poté jej podle potřeby přetypuje.

11.6. Bitová pole

Časová náročnost
Časová náročnost: 10 minut

Jazyk C má zvláštní druh struktury nazvaný bitové pole. Bitové pole se skládá z jednoho nebo více bitů. Pomocí bitového pole lze přistupovat prostřednictvím jména k jednomu nebo více bitům v bytu nebo ve slově. Pro definici bitového pole použijeme tento obecný formát:

Důležité
typ jméno: délka;

Položka typ je buď int nebo unsigned. Zadáte-li bitové pole se znaménkem, pak je nejvyšší bit považován za znaménkový bit. Počet bitů v poli udává položka délka. Všimněte si, že je jméno bitového pole odděleno od od délky pole v bitech dvojtečkou.

Bitová pole se hodí, když potřebujeme natěsnat informace do co nejmenšího místa. Zde je například struktura, která používá bitové pole pro uložení inventárních informací.

Jednoduchý příklad
Příklad:

struct
{
 unsigned department: 3 /* max. 7 oddeleni */
 unsigned instock: 1 /* 1 je na sklade, 0 neni na sklade */
 unsigned ordered: 1 /* 1 je objednana, 0 neni objednana */
 unsigned time: 3 /* doba vedeni objednavky v mesicich */
} inv[MAX_ITEM];

V tomto případě lze použít jeden byte k uložení informací o inventární položce, které by bez bitových polí zabraly normálně 4 byty. S bitovým pole se pracuje stejně jako s jinými prvky struktury.

Například následující příkaz přiřadí do bitového pole department v 10. položce pole inv hodnotu 3.

Jednoduchý příklad
Příklad:
inv[9].department = 3;

Následující příkaz zjišťuje, zda je 5. položka na skladě:

Jednoduchý příklad
Příklad:
if(!inv[4].instock) 
   printf("Neni na sklade");
else
   printf("Je na sklade");
   

11.7. Opakování

Cvičení
Cvičení

Úkol k textu

Zadání 1)

Ukažte jak udělat u UL nové jméno pro unsigned long. Napište krátký program, který deklaruje proměnnou pomocí UL, přiřadí jí hodnotu a vypíše hodnotu proměnné na obrazovku.
Řešení

Úkol k textu

Zadání 2)

Co je špatně na tomto zápisu?
typedef balance float;
Řešení

Úkol k textu

Zadání 3)

Vytvořte výčtový typ obsahující dny v týdnu.
Řešení

Úkol k textu

Zadání 4)

Je tento úsek programu správný? Pokud ne, proč?
enum auta {Ford, Chrysler, GM} vyrobce;

vyrobce = GM;
printf("Auto vyrobil: %s",vyrobce);
Řešení

Úkol k textu

Zadání 5)

Co je špatně na tomto kousku programu?
stuct 
{
 int i;
 long l;
 char str[80];
} s;
.
.
.
i = 10;
.
Řešení

Úkol k textu

Zadání 6)

Co je struktura a co je unie?
Řešení

Úkol k textu

Zadání 7)

Co je špatně na tomto úseku programu?
struct
{
 int a;
 char b;
 float c;
} myvar, *p;

p = &myvar;
p.a = 10;
Řešení
test11.swf
Test
Kliknutím na ikonu spustíte test.