Štruktúry
štruktúrované údajové typy, používateľom definované údajové typy, zoznamy údajov, binárne súbory, načítavanie a ukladanie štruktúrovaných údajov, serializácia, deserializácia
Záznam z prednášky
Úvod
(slide) Na predmete Základy algoritmizácie a programovania ste sa venovali najmä tzv. jednoduchým alebo ináč nazývaným aj primitívnym údajovým typom. Výlučne s nimi si však do konca života nevystačíme, pretože svet a problémy, ktoré budete riešiť, nebudú vždy také jednoduché. A dnes si ukážeme prečo.
Predstavte si napríklad, že vo svojom programe potrebujete opísať vlastnosti nejakého auta alebo osoby. Čo všetko by vás zaujímalo? (nechať študentov menovať jednotlivé vlastnosti a zapisovať si ich do súboru)
Ak by sme tieto vlastnosti mali reprezentovať v jazyku C, aké údajové typy by mali? (k vymenovaným vlastnostiam začať písať údajové typy)
Máme k dispozícii niekoľko vlastností, pomocou ktorých vieme opísať nejakú osobu. Čo ak by sme však potrebovali opísať viac ako jednu osobu? Čo ak by sme potrebovali vytvoriť informačný systém, ktorý bude pracovať s tisíckami takýchto osôb? Čo ak budeme potrebovať funkciu, ktorá spracuje informácie o konkrétnej osobe?
(slide) Jazyk C umožňuje pracovať s tzv. štruktúrovanými typmi údajov, resp. štruktúrami. Význam je podobný ako tomu bolo v jazyku Pascal, kde sa na tento účel používal údajový typ
record
.Podobný prístup je možné vidieť napr. aj v relačných databázach, kde entita (tabuľka) reprezentuje štruktúrovaný údajový typ a konkrétny riadok v tabuľke hovorí o konkrétnom objekte, resp. zázname.
Vytvorenie štruktúrovaného údajového typu
(slide) Na vytvorenie štruktúrovaného údajového typu (štruktúry) sa používa kľúčové slovo
struct
nasledovne:struct person { char name[10]; char surname[20]; char sex; // M, F int age; };
Poznámka
Zamyslime sa však nad reprezentáciou veku osoby - je dobré ho udržiavať ako celé číslo? Ak ho totiž budeme udržiavať v takejto forme, musíme ho v pravidelných intervaloch aktualizovať. A otázkou je, že kedy? Znie celkom logicky, že by to mohlo byť 1. januára, ale človek, ktorý má narodeniny až 31. decembra, bude o rok starší už podstatne skôr.
Ak chceme mať teda presnú informáciu o veku, toto nie je správne riešenie, ako sa popasovať s týmto problémom. Keď budete mať v druhom ročníku databázové systémy, takéto riešenie pre reprezentáciu údajov budete považovať za porušnie tzv. 3. normálnej formy, pretože sa jedná o tzv. dopočítavaný údaj.
My sa k reprezentácii dátumu a času v počítači vrátime v niektorej z bududúcich prednášok. Tentokrát sa tým proste len nebudeme zaoberať.
Tým sme vytvorili štruktúru s názvom
person
. Ak chceme vytvoriť premennú, ktorá bude daného údajového typu, zadeklarujeme ju v tvare:struct person john;
Kľúčové slovo typedef
(slide) V jazyku C sa môžete stretnúť s kľúčovým slovom
typedef
, pomocou ktorého je možné zadefinovať nové meno pre niektorý už existujúci údajový typ.(slide) Jeho použitie si môžeme ilustrovať na nasledujúcom príklade:
typedef unsigned char BYTE;
Následne v kóde viem zadeklarovať premennú
byte
tak, že bude typuBYTE
:; BYTE byte
Tento zápis je ekvivalentný so zápisom:
unsigned char byte;
Ak by sme teda chceli vytvoriť nový údajový typ na základe práve vytvorenej štruktúry
Person
, mohol by vyzerať nasledovne:typedef struct { char name[10]; char surname[20]; char sex; // M, F int age; } PERSON;
Následne je možné zadeklarovať novú premennú, ktorá bude typu
PERSON
:; PERSON john
Upozornenie
Aj keď jazyk C tento spôsob podporuje, môžete sa stretnúť s
rozličnými odporúčaniami ohľadom používania tohto spôsobu. Napríklad
vývojári linuxového jadra to majú vyslovene zakázané a pokiaľ
vytvárajú vlastné štruktúrované údajové typy, nepoužívajú kľúčové slovo
typedef
. Rovnako sa budeme správať aj my - v našich
projektoch nebudeme používať kľúčové slovo
typedef
.
Typ size_t
(slide) S niektorými údajovými typmi, ktoré boli vytvorené pomocou kľúčového slova
typedef
, sa je možné stretnúť bežne v knižniciach jazyka. Ak sa pozriete napr. na typ návratovej hodnoty funkciestrlen()
z knižnicestring.h
(napr. pomocouman strlen
), tak zistíte, že sa jedná o typsize_t
a nie o typint
, ako sme si doteraz mysleli a aj používali. Čo za typ je tedasize_t
?Tento údajový typ je zadefinovaný v hlavičkovom súbore
stddef.h
(man stddef.h
). A je pri ňom uvedené, že:Unsigned integer type of the result of the
sizeof
operator.Takže sa jedná o kladné celé číslo a jeho použitie je vhodné všade tam, kde sa pracuje s dĺžkou alebo indexom, napr. pri iterovaní pomocou cyklu
for
.Použitie tohto údajového typu si ilustrujeme v rámci dnešných príkladov. A hneď ho viem použiť aj pri reprezentácii roku osoby v štruktúre
struct person
, ktorý je rozhodne celým kladným číslom:struct person { char name[10]; char surname[20]; char sex; // M, F size_t age; };
Upozornenie
Pri tejto príležitosti si však treba dať pozor pri formátovaní údajov
v prípade použitia typu size_t
vo formátovacích reťazcoch
funkcií ako printf()
a scanf()
. Používajte
preto modifikátor z
:
%zu
- preunsigned decimal
%zd
- presigned decimal
Veľkosť štruktúrovaného údajového typu
(slide) Ako je to s množstvom pamäti, ktoré štruktúrované údajové typy zaberajú v pamäti? Koľko miesta v pamati teda bude zaberať premenná, ktorá bude typu
struct person
?(slide) Množstvo bytov, ktoré zaberá štruktúra v pamäti, je možné zistiť ako súčet veľkostí jednotlivých jej členov. Ak teda chcem zistiť, koľko bytov v pamäti zaberá štruktúra
person
, vložíme do programu tieto riadky:("%ld + %ld + %ld + %zu = %ld\n", printfsizeof john.name, sizeof john.surname, sizeof john.sex, sizeof john.age, sizeof john.name + sizeof john.surname + sizeof john.sex + sizeof john.age ); // 10 + 20 + 1 + 8 = 39
Operátor
sizeof()
samozrejme môžeme použiť na celú štruktúru, čím získame veľkosť štruktúry v pamäti použitím jedného operátorasizeof()
:("Size is: %zu\n", sizeof(struct person)); printf// Size is: 40
Po spustení programu sa však na obrazovku vypíše hodnota 40 miesto očakávanej hodnoty 39. Ako je to možné?
Obsah štruktúry je totiž v pamäti zarovnaný na veľkosť deliteľnú číslom 4. Ak chceme toto správanie potlačiť, pridáme k štruktúre atribút
packed
:struct person { char name[10]; char surname[20]; char sex; // M, F size_t age; } __attribute__((packed));
Tentokrát sa po spustení a použití operátora
sizeof()
nad celou štruktúroustruct person
naozaj zobrazí hodnota 39.
Inicializácia štruktúry
(slide) Inicializovať premennú typu štruktúra je možné podobne ako jednorozmerné pole - vymenovaním hodnôt pre konkrétnych členov štruktúry v rovnakom poradí, ako je štruktúra definovaná:
struct person sherlock = { "sherlock", "holmes", 'M', 33 };
Môžeme však použiť aj tzv. Designated initialisers, kde je možné podobne ako v poli selektívne inicializovať konkrétne prvky. Na poradí v tomto prípade nezáleží:
struct person john = { .sex = 'M', .age = 42, .surname = "smith", .name = "john" };
Prístup k položkám štruktúry
Pre prístup ku konkrétnym položkám štruktúry budeme používať operátor “
.
”. Ak teda chceme nastaviť premennejjohn
typustruct person
pohlavie, zabezpečíme to zápisom:.sex = 'M'; john
Podobne môžeme pristupovať aj ku ostatným položkám štruktúry. Vytvoríme teda novú premennú typu
struct person
a miesto jej priamej inicializácie naplníme jej položky hodnotami postupne. Nakoniec ich vypíšeme na obrazovku:struct person bruce; .sex = 'M'; bruce(bruce.name,"bruce"); strcpy(bruce.surname, "wayne"); sprintf ("%s %s is %c\n", printf.name, bruce.surname, bruce.sex); bruce
Štruktúra ako parameter funkcie
Keďže však chceme vypisovať obsah aj iných premenných, ktoré sú typu
struct person
, pripravíme si za týmto účelom funkciu s menomprint_person()
. Táto funkcia dostane jeden parameter typu tejto štruktúry a vypíše na obrazovku jej hodnoty:void print_person(struct person person){ ("%s %s (%c) is %zu years old.\n", printf.name, person.surname, person.sex, person.age person); }
V kóde ju potom môžeme použiť nasledovne:
(john); print_person
Ako je možné vidieť zo zápisu, štruktúrované údajové typy sa odovzdávajú funkcii adresou podobne, ako tomu bolo v prípade polí.
Zoznam osôb
Vráťme sa však k pôvodnému zámeru - chceme uchovávať informácie o viacerých ľuďoch - nie len o jednom človeku. Ak by sme teda chceli naraz uchovať informácie o viacerých ľuďoch, čo by sme za tým účelom vedeli s výhodou použiť?
Pre vytvorenie zoznamu osôb použijeme jednorozmerné pole typu
struct person
:#include <stdio.h> #include <string.h> // structure representing a person struct person { char name[10]; char surname[20]; char sex; size_t age; }; void print_person(struct person person){ ("%s %s (%c) is %zu years old.\n", printf.name, person.surname, person.sex, person.age person); } int main(){ // read the nr of persons ("Enter the number of persons: "); printfsize_t size; ("%zu", &size); scanf // declare list of persons struct person list[size]; // populate list for(size_t i = 0; i < size; i++){ ("Enter %zu person: ", i); printf("%s %s %c %zu", scanf[i].name, list[i].surname, &list[i].sex, list&list[i].age ); } // traverse the list for(size_t i = 0; i < size; i++){ ("%zu: ", i); printf(list[i]); print_person} }
Aby sme nemuseli stále dookola pridávať osoby znova a znova, môžeme si zoznam pripraviť bokom do súboru (list.txt) a následne presmerovať vstup do nášho programu:
$ ./person < list.txt
Perzistencia dát
(slide) Teraz sa pozrime na to, ako zabezpečiť trvácnosť našich údajov. Našim cieľom bude zabezpečiť, aby sa údaje nestratili a nepoškodili ani po vypnutí a opätovnom zapnutí aplikácie. Budeme ich teda ukladať a následne načítavať zo súboru.
Základnú prácu so súbormi už ovládate zo zimného semestra. Viete načítavať a ukladať textové súbory. My sa teraz naučíme pracovať s binárnymi.
Ak by sme mali tieto údaje ukladať ako textové súbory, pravdepodobne by sme došli ku niektorej schéme, ako napr.
(slide) My však nebudeme ukladať údaje textovo, ale binárne
(slide) Na čítanie a zápis budeme používať dve funkcie:
fread()
afwrite()
.Obe funkcie vrátia počet prečítaných alebo zapísaných údajov. Pozor - nie je to počet prečítaných/zapísaných bytov, ale počet prvkov.
To, či bol počas čítania dosiahnutý koniec súboru, je možné zistiť z tohto počtu (je rovný 0) alebo pomocou funkcie
feof()
.Poďme sa teda pozrieť na to, ako uložiť najprv jednu osobu a potom aj všetky osoby na disk.
Uloženie binárnych údajov na disk
Uloženie jedného prvku
Najprv sa pokúsme uložiť na disk len jednu osobu, napr. prvú v zozname:
FILE* fp = fopen("person.bin", "wb"); (&list[0], sizeof list[0], 1, fp); fwrite// fwrite(&list[0], sizeof(struct person), 1, fp); (fp); fclose
Keď sa pozrieme na disk, uvidíme tam súbor s názvom
person.bin
. Keď sa do neho pozrieme, síce budeme viacmenej schopní dešifrovať uložené údaje (tie textové), ale okrem nich tam bude kopec ďalších nezobraziteľných znakov.Poznámka
Pre prezeranie binárnych súborov je možné s výhodou použiť hexa editory. Jeden z nich je priamo integrovaný napr. aj v správcovi súborov Midnight Commander, ktorý viete spustiť priamo z príkazového riadku príkazom
mcview
. Hexa režim následne zapnete klávesomF4
.Pozrime sa však aj na výslednú veľkosť uloženého súboru. Prečo je tá veľkosť taká aká je? Na disku uložené údaje zaberajú presne toľko miesta, koľko zaberá štruktúra
struct person
v pamäti.
Uloženie celého zoznamu
Teraz sa však pokúsme uložiť celý zoznam. Uložíme ho pomocou nasledujúceho fragmentu kódu:
FILE* fp = fopen("list.bin", "wb"); for(size_t i = 0; i < size; i++){ (&list[i], sizeof list[i], 1, fp); fwrite} (fp); fclose
V tomto prípade sme ukladali obsah zoznamu položku po položke. V princípe by bolo možné uložiť aj obsah celého poľa naraz týmto volaním:
(list, sizeof list[0], size, fp); fwrite
To však nemusí byť vždy praktické, nakoľko ukladanie prebieha po blokoch. A my sme povedali, že uloženie prebehne jedenkrát v bloku veľkom
sizeof(struct person) * size
.Pozrime sa opäť na veľkosť súboru - je možné ju aspoň odhadnúť pred samotným uložením? (veľkosť 1 štruktúry * počet položiek)
Funkcia pre uloženie celého zoznamu
Fragment kódu na uloženie zoznamu do súboru môžeme osamostatniť a vytvoriť z neho funkciu. Nazveme ju
save_list()
a môže vyzerať nasledovne:void save_list(struct person list[], const size_t size, const char* path){ // open file for writing FILE* fp = fopen(path, "wb"); // write size first (&size, sizeof size, 1, fp); fwrite // write list for(size_t i = 0; i < size; i++){ (&list[i], sizeof list[i], 1, fp); fwrite} // close file (fp); fclose}
Serializácia
(slide) Procesu ukladania dát na disk sa zvykne hovoriť aj serializácia. Serializáciu môžeme voľne definovať ako
proces prekladu údajov (objektov) do formátu, v ktorom môžeme tento údaj uložiť (napr. do pamäte alebo na disk alebo ich môžeme preniesť po sieti).
Načítanie binárnych údajov z disku
- Teraz sa pozrime na to, ako tieto uložené údaje prečítať z disku a
uložiť ich do premennej typu
struct person
ako aj do poľa tohto typu.
Načítanie jedného prvku
Začneme nahratím jednej osoby z disku - zo súboru
person.bin
a následne necháme tieto údaje vypísať na obrazovku:struct person person; FILE* fp = fopen("person.bin", "rb"); (&person, sizeof person, 1, fp); fread(fp); fclose (person); print_person
Ak by sme ako súbor, z ktorého budeme osobu načítavať, použili
list.bin
, výsledok sa nezmení - aj zo súboru, kde sa nachádza viac osôb dôjde k prečítaniu práve jednej osoby z jeho začiatku.
Načítanie celého zoznamu
Teraz sa však pokúsme načítať celý uložený zoznam osôb. Postup bude vyzerať dosť podobne, ako tomu bolo v prípade jeho ukladania - bude prebiehať kus po kuse alebo osoba po osobe:
// load struct person list[10]; FILE* fp = fopen("list.bin", "rb"); for(size_t i = 0; i < 10; i++){ (&list[i], sizeof list[0], 1, fp); fread} (fp); fclose // traverse the list for(size_t i = 0; i < 10; i++){ ("%zu: ", i); printf(list[i]); print_person}
To je síce pekné, ale celé to má jeden vážny nedostatok - čo ak potrebujeme načítať iný počet dát, ako je 10? Ak chceme tento problém vyriešiť, musíme tento údaj o počte položiek uložiť do výsledného súboru tiež. A to ešte predtým, ako začneme ukladať samotné údaje.
Najprv teda upravíme kód, ktorý ukladá zoznam osôb. V rámci úpravy ešte pred samotnými osobami uložíme informáciu o tom, koľko osôb je v súbore uložených:
FILE* fp = fopen("list.bin", "wb"); (&size, sizeof size, 1, fp); fwritefor(size_t i = 0; i < size; i++){ (&list[i], sizeof list[i], 1, fp); fwrite} (fp); fclose
Následne upravíme aj kód deserializácie, v ktorom najprv načítame počet položiek a následne načítame potrebný počet položiek:
// open file for reading FILE* fp = fopen("list.bin", "rb"); // read the size int size; (&size, sizeof size, 1, fp); fread // create the array of size elements struct person list[size]; // read the elements for(size_t i = 0; i < size; i++){ (&list[i], sizeof list[i], 1, fp); fread} (fp); fclose // traverse the list for(size_t i = 0; i < size; i++){ ("%zu: ", i); printf(list[i]); print_person}
Funkcia pre načítanie celého zoznamu
- Takúto funkciu aktuálne nevieme spraviť efektívne, ak chceme vytvoriť zoznam len s takým množstvom prvkov, ktorý je uložený. Musíme dynamicky alokovať.
Deserializácia
- (slide) Procesu extrakcie údajov z disku sa hovorí aj deserializácia.
Záver
Dnes sme sa teda pozreli na štruktúry a štruktúrované údajové typy v jazyku C. Ukázali sme si ich základné použitie, vytvorili sme zoznam, ktorý obsahoval položky typu štruktúra a ukázali sme si aj proces serializácie a deserializácie takýchto údajov.
Nabudúce budeme v téme pokračovať a pozrieme sa, ako triediť a vyhľadávať v zoznamoch, ktoré obsahujú štruktúrované údaje.