9. DYNAMICKÉ DÁTOVÉ ŠTRUKTÚRY 9.1. Dynamická alokácia pamäte. 9.2. Zoznam. 9.3. Pole ukazovateľov. Dynamické dátové štruktúry predstavujú akýsi protiklad ku statickým dátovým štruktúram. Povedzme si teda najprv niečo o nich. Porovnaním ich vlastností lepšie vyniknú prednosti tých či oných pre isté triedy úloh. Statické dáta (SD) majú svoju veľkosť presne a nemenne určenú v okamihu prekladu zdrojového textu. Preto sa s nimi pracuje jednoduchšie. Odpadá možnosť práce s nepridelenou pamäťou. Potiaľ klady. Teraz zápory. Nech náš program potrebuje viac či menej dát, nedá sa s tým nič robiť. Preto často radšej volíme SD trochu naddimenzované, aby náš program zvládol "väčšinu" úloh, ktorých triedu je schopný riešiť. Pre príklady nemusíme chodiť ďaleko. Ak napíšeme program pre prácu s maticami postavený na SD, musíme určiť ich dimenziu. Dimenzia 3 x 3 asi nepostačí, tak radšej siahneme k dimenzii 10 x 10, alebo radšej k 20 x 20. A budeme mať dobrý pocit, že naša úloha vyrieši všetko. Pocit je pochopiteľne mylný. Úloha si neporadí už s maticou 21 x 21. Naviac kladieme na OS vždy rovnaké, a to väčšinou premrštené, požiadavky na operačnú pamäť. V jednoúlohovom systéme to obvykle nevadí. Vo viacúlohovom, nieto ešte viacuživateľskom prostredí je takéto plytvanie takmer neodpustiteľné. SD sú pochopiteľne statické v zmysle veľkosti pamäte, ktorú majú pridelenú. Ich hodnoty sa behom programu môžu meniť. Dynamické dáta (DD) nemajú veľkosť pri preklade určenú. Pri preklade sú vytvorené iba premenné vhodného typu ukazovateľ na, ktoré nám za chodu slúžia ako pevné body pre prácu s DD. O požadovaný pamäťový priestor však musíme požiadať OS. Ten našu žiadosť uspokojí, alebo nie. Rozhodne však žiadame vždy len toľko pamäte, koľko sa za chodu ukázalo byť potrebné. Popravde povedané, požadujeme jej trošku viac. Ta trocha naviac je daná jednak nutnou réžiou, jednak našimi schopnosťami. Záleží len na nás, ako vhodné dynamické dátové štruktúry vyberieme (alebo si obľúbime). Pochopiteľne vznikajú mnohé nebezpečia, najmä pri zabudnutí požiadania o pamäť a zápise do nepridelených priestorov. DD majú ešte jednu prednosť oproti SD. Pokiaľ požiadavky na DD vystupujú v na sebe nezávislých častiach programu, môže pamäťová oblasť byť menšia, než prostý súčet týchto požiadavkou. Akonáhle totiž končí časť programu, vráti pridelenú časť DD späť. Naopak nový úsek programu podľa potreby o pamäť požiada. DD teda znižujú pamäťové nároky z dvoch dôvodov. Jednak preto, že požadujú toľko pamäte, koľko je potrebné. Jednak vďaka možnému viacnásobnému používaniu pridelenej dynamickej pamäti. SD sú umiestnené v dátovom segmente. Klasický program je totiž rozdelený na dátový segment a kódový segment. Kódový segment obsahuje kód, ktorý prekladač vytvorí na základe nášho zdrojového textu. Tento kód by sa za chodu programu teda nemal meniť. Nemennosť kódového segmentu
operačné systémy obvykle kontrolujú a zaručujú. MS-DOS však nie. Práve z toho plynú prakticky isté havárie dokonca celého OS pri chybnej práci s ukazovateľmi. Dátový segment je opäť vytvorený prekladačom. Sú v ňom umiestnené všetky statické dáta programu. Táto časť programu sa behom chodu programu mení. Okrem kódového a dátového segmentu prideľuje pri spustení programu OS ešte isté prostredie. To má buď nejakú štandardnú veľkosť, alebo je táto veľkosť určená na základe požiadavkou zavádzaného programu. Tu je umiestnený zásobník. O ňom vieme, že je potrebný pri predávaní argumentov funkciám, návratové hodnoty od funkcií a tiež pre nájdenie návratovej adresy po ukončení funkcie. Táto návratová adresa je do zásobníka zapísaná pri volaní funkcie. Zásobník však spravidla neobsadí celú zostatkovú pamäť, ktorú má program (proces) k dispozícii. Časť pridelenej pamäte zostáva voľná. Tej sa spravidla hovorí hromada či halda (heap). Jej veľkosť môžeme pri tvorbe programu určiť. A práve halda predstavuje oblasť pamäte, do ktorej sa umiestňujú DD. 9.1. Dynamická alokácia pamäte. Aby časti programu mohli ako žiadať o pridelenie dynamickej pamäte, tak už nepotrebnú pamäť vracať, musí existovať aspoň základná programová podpora. Tu si však nebudeme vytvárať sami. Je definovaná ANSI normou jazyka a teda ju dostávame spolu s prekladačom. Súhrne sa programová podpora pre dynamickú alokáciu pamäte nazýva podľa oblasti, ktorej sa prideľovanie týka, správca haldy (heap manager). Súčasťou správcu haldy sú nielen potrebné funkcie, ale aj dátové štruktúry. Ako užívateľa nás pochopiteľne zaujímajú predovšetkým funkcie správcu haldy. Popíšme si teda niektoré z nich. Náš výber je určený predovšetkým ich prenositeľnosťou. Deklarácie funkcií správcu haldy sú umiestnené v alloc.h, prípadne v stdlib.h. void *malloc(size_t size); predstavuje požiadavku o pridelenie súvislého bloku pamäte o veľkosti size. Ak je úspešný, dostávame ukazovateľ na jeho začiatok, inak NULL. void *calloc(size_t nitems, size_t size); ako predchádzajúca s tým, že naša požiadavka je rozložená na nitems položiek, každá o size bytoch. Naviac je pridelená pamäť vyplnená nulami. void free(void *block); je naopak vrátenie skôr alokovanej pamäte, na ktorú ukazuje block. void *realloc(void *block, size_t size); umožňuje zmeniť veľkosť alokovanej pamäte, na ktorú ukazuje block na novú veľkosť určenú hodnotou size. V prípade potreby (požiadavka je väčšia, než pôvodný blok) je obsah pôvodného bloku prekopírovaný. Vracia ukazovateľ na nový blok.
UNIX a MS-DOS definujú pre dynamickú zmenu veľkosti haldy dvojicu funkcií. Prvá z nich, int brk(void *addr); nastaví hranicu haldy programu na hodnotu danú addr. Druhá z dvojice, void *sbrk(int incr); umožní zvýšiť túto hodnotu o incr bajtov. MS-DOS pridáva ešte funkciu, ktorá vracia počet voľných bajtov na hromade (v závislosti na pamäťovom modely vracia unsigned int alebo unsigned long): unsigned coreleft(void); Krátku ukážku pridelenia a vrátenia pamäte pre reťazec predstavujú dve nasledujúce funkcie. Rozsiahlejšie a úplné programy sú súčasťou každej z nasledujúcich podkapitol. char *newstr(char *p) register char *t; t = malloc(strlen(p) + 1); strcpy(t, p); return t; void freestr(char *p) free(p); Prvá funkcia vytvorí na hromade dostatočný priestor a nakopíruje doň predávaný reťazec. Ukazovateľ na túto kópiu vracia ako svoju funkčnú hodnotu. Druhá funkcia prevádza činnosť opačnú. Oblasť určenú ukazovateľom vráti správcovi haldy. Detailné vlastnosti správcu haldy môžu byť rôzne. Čomu sa však obvykle nevyhneme je situácia nazvaná segmentácia haldy. Ak nie sú úseky z haldy vrátené v opačnom poradí, než boli prideľované (LIFO), vzniknú na hromade striedavo úseky voľnej a obsadenej pamäte. Celková veľkosť volnej pamäte, daná súčtom všetkých voľných úsekov, je potom väčšia než veľkosť najväčšieho súvislého voľného bloku. Môže potom nastať situácia, kedy voľnej pamäte je síce dosť, ale naša požiadavka na jej pridelenie nie je uspokojená, pretože nie je k dispozícii dostatočne veľká súvislá oblasť. 9.2. Zoznam. Je dynamická dátová štruktúra, ktorá medzi svojimi členmi obsahuje okrem dátovej časti aj väzobný člen, ktorý ukazuje na nasledujúci prvok zoznamu, alebo NULL, ak je posledným prvkom. Dátové minimum, ktoré pre správu zoznamu potrebujeme, je ukazovateľ na jeho prvý prvok. Popísaný zoznam sa rovnako nazýva jednosmerný. Postupne totiž môžeme prejsť od začiatku zoznamu až na jeho koniec, nie však naopak. Existuje ešte jeden variant zoznamu, majúci väzobné členy dva. Jeden ukazuje opäť na následníka, druhý na predchodcu.
Ukážku použitia zoznamu prináša nasledujúca úloha (a program). Majme za úlohu spočítať početnosť výskytu všetkých slov vo vstupnom texte. Pre jednoduchosť budeme rozlišovať malé a veľké písmena. Na záver vytlačíme tabuľku, v ktorej bude uvedená početnosť výskytu a reťazec, predstavujúci slovo. Za slová považuje program reťazce písmen a číslic. Túto skutočnosť však môžeme meniť úpravou vlastností funkcie odstran_neslova. Mená funkcií aj premenných v programe sú volené s ohľadom na ich maximálnu popisnosť. Vstupom môže byť súbor, ak je zadaný ako 1. argument príkazového riadku. Inak je vstupom štandardný vstup. Jednosmerný zoznam Obrázok odpovedá dátovej štruktúre vytvorenej v programe. Hodnoty sú iba ilustratívne. Jediný nedynamický objekt zoznamu je ukazovateľ prvy na začiatok zoznamu. Ostatné objekty sú dynamické. V obrázku je táto skutočnosť odlíšená tvarom rohov obdĺžnikov. Obrázok môže byť aj návodom, ako dynamické objekty rušiť. Táto činnosť totiž v programe nie je obsiahnutá. Rozhodne je zrejmé, že musíme uvolniť ako pamäť vyčlenenú pre hodnoty typu struct_info, tak aj pamäť, ktorú obsadzujú reťazce, na ktoré je zo štruktúry ukazované. /****************************************************************/ /* subor SLOVA.C */ /* prevedie nacitanie vstupneho textu, jeho rozlozenie na slova */ /* a z tychto slov vytvori zoznam s ich pocetnostou vyskytu. */ /* Na zaver vytlaci slova a ich pocetnost. */ /****************************************************************/ #include <stdio.h> #include <string.h> #include <alloc.h> #include <ctype.h> #include <conio.h> #define DLZKA_RIADKU 500 #define PAGE 24 /* typedef struct info; */ typedef struct struct_info int pocet; char *slovo; struct struct_info *dalsi; info; void odstran_neslova(char *ptr) /* odstrani medzery, tabelatory a znaky CR, LF zo zaciatku retazca */ char *pom; pom = ptr;
if (strlen(ptr) == 0) return; while ((*pom!= '\0') && ((*pom == 0x20) (*pom == '\t') (*pom == '\n') (*pom == '\r') (!isalnum(*pom)))) pom++; strcpy(ptr, pom); /* void odstran_neslova(char *ptr) */ void vrat_slovo(char *r, char *s) int i = 0; while ((r[i]!= 0x0) && (isalnum(r[i]) (r[i] == '_'))) i++; /* while */ if (i == 0) *s = 0x0; else strncpy(s, r, i); s[i] = 0x0; strcpy(r, r + i); /* void vrat_slovo(char *r, char *s) */ void pridaj_slovo(info **prvy, char *s) info *prvok; prvok = *prvy; while (prvok!= NULL) if (strcmp(s, prvok->slovo) == 0) (prvok->pocet)++; return; prvok = prvok->dalsi; prvok = malloc(sizeof(info)); prvok->slovo = malloc(strlen(s) + 1); strcpy(prvok->slovo, s); prvok->pocet = 1; prvok->dalsi = *prvy; *prvy = prvok; /* void pridaj_slovo(info **prvy, char *s) */ void vytlac(info *prvy) int vytlacene = 0; while (prvy!= NULL) printf("%4d..%s\n", prvy->pocet, prvy->slovo); prvy = prvy->dalsi; vytlacene ++; if ((vytlacene % PAGE) == 0) printf("pre pokracovanie stlac klavesu"); getch(); printf("\n"); /* void vytlac(info *prvy) */ int main(int argc, char **argv)
info *prvy = NULL; char riadok[dlzka_riadku + 1], slovo[dlzka_riadku + 1]; FILE *f; if (argc!= 1) f = fopen(argv[1], "rt"); else f = stdin; if (f == NULL) return(1); while (fgets(riadok, DLZKA_RIADKU, f)!= NULL) odstran_neslova(riadok); while (strlen(riadok)!= 0) vrat_slovo(riadok, slovo); odstran_neslova(riadok); pridaj_slovo(&prvy, slovo); vytlac(prvy); return 0; /* int main(int argc, char **argv) */ 9.3. Pole ukazovateľov. Veľmi pohodlnou dynamickou dátovou štruktúrou je dynamické pole ukazovateľov. Princíp je jednoduchý. Požiadame o pamäť pre dostatočne veľké pole ukazovateľov. Ďalej už pracujeme rovnako, ako by pole bolo statické (až na ten ukazovateľ na pole naviac). K jednotlivým prvkom môžeme pristupovať pomocou indexu. Nesmieme zabudnúť, že prvky sú ukazovatele, a že teda musíme pamäť, na ktorú ukazujú, tiež alokovať. Vďaka funkcii realloc() máme možnosť ľahko meniť počet prvkov nášho poľa ukazovateľov. Naviac s tým, že naše dynamické dáta svoju adresu nemenia a funkcia realloc() prenesie správne (stále platné) adresy do nového (väčšieho) bloku ukazovateľov. My k nim pristupujeme pomocou rovnakej premennej, indexy všetkých prvkov zostavajú rovnaké, len ich pribudlo. Skvelé. Ukážková úloha, ktorú riešime, načíta vstupné riadky textu, ukladá ich na hromadu s prístupom pomocou poľa ukazovateľov a nakoniec riadku vytlačí v rovnakom poradí, v akom boli načítané. Hodnoty START a PRIRASTOK sú úmyselne volené malé, aby sa ukázala možnosť realokácie poľa aj pri zadávaní vstupu z klávesnice. Pre rozsiahlejšie pokusy doporučujeme presmerovať vstup. /************************************************/ /* subor STR_ARRY.C */ /* zo standardneho vstupu cita riadky, alokuje */ /* pre ne pamat a ukazovatele na tuto kopiu */ /* nacitaneho riadku uklada do pola ukazovatelov */ /* pole ukazovatelov ma pociatocnu velkost, ktora */ /* sa vsak v pripade potreby zmeni - realloc() */ /* po ukonceni vstupu nacitane riadky vytlaci */ /************************************************/ /********************************/ /* obvykle navratove hodnoty: */ /* O.K. 0 */ /* error!0 (casto -1)*/
/********************************/ #include <stdio.h> #include <alloc.h> #include <string.h> #define LADENIE #define START 2 #define PRIRASTOK 1 #define DLZKA_RIADKU 100 typedef char * typedef retazec * retazec; pole_retazcov; #if defined(ladenie) void volno(void) printf("\nje volnych %10lu bajtov\n", coreleft()); /* LARGE */ /* void volno(void) */ void uvolni(pole_retazcov *p_r, int pocet) int i; for (i = 0; i < pocet; i++) free((*p_r)[i]); free(*p_r); /* void uvolni(pole_retazcov *p_r, int pocet) */ #endif /* defined(ladenie) */ int alokacia(pole_retazcov *p_r, int pocet) *p_r = malloc(pocet * sizeof(retazec)); return (*p_r == NULL)? -1 : 0; /* int alokacia(pole_retazcov *p_r, int pocet) */ int re_alokacia(pole_retazcov *p_r, int novy_pocet) pole_retazcov pom; if (*p_r == NULL) if (alokacia(p_r, novy_pocet)) return -1; /* chyba */ pom = realloc(*p_r, sizeof(retazec) * novy_pocet); if (pom == NULL) return -1; else *p_r = pom; return 0; /* int re_alokacia(pole_retazcov *p_r, int novy_pocet) */ int pridaj_riadok(pole_retazcov *p_r, retazec s, int index) int dlzka = strlen(s) + 1; if (((*p_r)[index] = malloc(dlzka)) == NULL) return -1; strcpy((*p_r)[index], s);
return 0; /* int pridaj_riadok(pole_retazcov *p_r, retazec s, int index) */ int citaj_a_pridavaj(pole_retazcov *p_r, int *pocet, int *alokovane, int prir) char riadok[dlzka_riadku], *pom; puts("zadavaj retazce, posledny CTRL-Z na novom riadku"); do if ((pom = gets(riadok))!= NULL) if (*pocet + 1 > *alokovane) if (re_alokacia(p_r, *alokovane + prir)) puts("nedostatok pamate"); return -1; *alokovane += prir; if (pridaj_riadok(p_r, riadok, *pocet)) puts("nedostatok pamate"); return 1; (*pocet)++; while (pom!= NULL); return 0; /*int citaj_a_pridavaj(pole_retazcov *p_r, int *pocet, int *alokovane, int prir)*/ void zobrazuj(pole_retazcov p_r, int pocet) while (pocet--) puts(*p_r++); /* void zobrazuj(pole_retazcov p_r, int pocet) */ int main(void) int pocet = 0, alokovane = START, prirastok = PRIRASTOK; pole_retazcov p_ret = NULL; #if defined(ladenie) volno(); #endif /* defined(ladenie) */ if (alokacia(&p_ret, alokovane)) puts("nedostatok pamate"); return 1; if (citaj_a_pridavaj(&p_ret, &pocet, &alokovane, prirastok)) return 1; zobrazuj(p_ret, pocet); #if defined(ladenie) uvolni(&p_ret, pocet); volno();
#endif /* defined(ladenie) */ return 0; /* int main(void) */ V programe je funkcia volno() definovaná v závislosti na skutočnosti, či je definované makro LADENIE. Ide teda o podmienený preklad. Prečo sme ho zavádzali je ľahké. Vo fázy ladenia potrebujeme mať istotu, že pamäť, ktorú sme alokovaly, neskôr správne vraciame. Akonáhle je program odladený, nie je táto pomocná funkcia potrebná. Nemusíme ale vnášať chyby tým, že ju aj jej volanie bezhlavo zmažeme. Výhodnejšie je, premyslieť si takúto situáciu už pri návrhu programu. Potom ju, rovnako ako v príklade, budeme prekladať podmienene. Poznamenajme, že obe popísané dynamické dátové štruktúry môžeme modifikovať pridaním ďalších funkcií na zásobník, frontu,.... Pre podrobnejší popis dynamických dátových štruktúr doporučujeme skvelé dielo profesora Niclausa Wirtha Algoritmy a štruktúry údajov. Predchádzajúca kapitola Obsah Začiatok Nasledujúca kapitola