ZÁKLADY PROGRAMOVÁNÍ V C poznámky pro Základy programování 2 Petr Osička
i Tento text je doplňkovým učebním textem k semináři Základy programování 2 vyučovanému na Katedře informatiky Přírodovědecké fakulty Univerzity Palackého v Olomouci. Nedělá si ambice být úplnou učebnicí programování v jazyce C, pouze zhruba shrnuje, co se říkalo na semináři. Podrobné informace o jazyce C lze najít jinde, například v literatuře na niž se odkazuji na stránce předmětu. Ke čtení textu je potřeba znalost programování v rozsahu kurzu Základy programování 1. Materiál pravděpodobně obsahuje chyby. Prosím laskavého čtenáře, aby mě o chybách, na které narazí, informoval. Petr Osička Olomouc, jaro 2018
Obsah 1 Adresování paměti 1 1.1 Pointer, operátory reference a dereference.................. 1 1.2 Degenerace pole a pointerova aritmetika................... 3 1.3 Pointery a struktury.............................. 5 1.4 Předávání argumentů funkce odkazem.................... 7 1.5 Pointery typu void............................... 7 1.6 Úkoly..................................... 9 2 Alokace a dealokace paměti 10 2.1 Automatická alokace a dealokace....................... 10 2.2 Manuální alokace a dealokace......................... 11 2.3 Další funkce pro práci s pamětí........................ 15 2.4 Úkoly..................................... 16 ii
1 1 Adresování paměti 1.1 Pointer, operátory reference a dereference K paměti přistupujeme jako k posloupnosti míst o velikosti jednoho bajtu. Tato posloupnost je očíslována od nuly podobně jako jsou indexována pole. Index místa je jeho adresou. Vše s čím v programu pracujeme je někde v paměti. Je to velmi přirozené, hodnoty proměnných, hodnoty v polích, hodnoty předané funkcím jako argumenty, konstanty apod. si program musí někde pamatovat. Pokud je velikost dané hodnoty více než 1 bajt, ke uložena jako posloupnost za sebou jdoucích bajtů (je tedy v kuse ), v takovém případě je její adresou adresa prvního bajtu této posloupnosti. Díky implementaci virtuální paměti v operačním systému, má běžicí program (který není jádrem os) přístup k celému adresnímu rozsahu na dané platformě. Adresní rozsah je typicky dán velikostí slova na dané platformě. Například pro amd-64 je velikost slova 64 bitů, pomocí kterých můžeme uložit 2 6 4 různých adres. Při psaní programu se ovšem o to, na jaké konkrétní adresy hodnoty budeme ukládat, nestaráme (to zajišťuje operační systém a alokátor paměti). Můžeme ovšem: zjistit, na jaké adresa je již existující hodnota uložena zjistit, jaká hodnota je uložena na konkrétní adrese Pro uložení adresy existují v C speciální typy, ke každému existujícímu typu x existuje typ adresa hodnoty typu x. Například k typu int existuje typu adresa hodnoty typu int. Proměnným takového typu (a často samotným adresám) říkáme pointer, nebo česky ukazatel. Hovoříme například o pointeru na int, či ukazateli na int. Pointer definujeme následujícím způsobem. typ *jmeno_promenne; Znak * určuje, že se jedná o pointer. Váže ke jménu proměnné, nikoliv k samotnému typu, a píše se kamkoliv mezi typ a jméno proměnné. Chceme-li například vytvořit pointer na int, pak to provedeme následujícím způsobem int *pt; Chceme-li vytvořit více pointerů na jednom řádku, musíme použít * u každého jména pointeru, jinak vytvoříme obyčejnou proměnnou. * promenne pt a pt2 jsou pointery na int, * promenna h je obycejna promenna typu int. int *pt, *pt2, h; Typ pointer na typ x označujeme jako x *. Typ pointer na int je tedy označován jako int *. Toto označení typu lze použít na místech, na kterých bychom použili jakéholiv jiného
2 typu, tedy například při specifikaci typu argumentu funkce, typu návratové hodnoty funkce apod. Velikost pointeru je typicky rovna velikosti slova dané architektury. printf("sizeof(int *): %lu, sizeof(char *): %lu\n", sizeof(int *), sizeof(char *)); K pointeru se váží dvě informace: adresa místa v paměti a informace o typu na který pointer ukazuje. Díky informaci o typu víme, jak danou pamět interpretovat jako hodnotu a kolik bajtů hodnota zabírá. Pokud máme například pointer na int, pak víme že zabírá sizeof(int) bajtů. Je nutno si uvědomit, je že hodnota pointeru je také uložena v paměti. Už víme, že pointer na typ x je typu x *. Na tento typ také můžeme vytvořit pointer, bude to tedy pointer na pointer na x. Označení typu takového pointeru je x **. Pokračováním tohoto argumentu můžeme vytvořit pointer s libovolným počtem *. Všechny definice v následujícím příkladě jsou korektní. int a; int *pt; int **pt2; K základní práci s adresami slouží unární operátor reference (někdy nazýván také jako operátor adresy) & a operátor dereference *. Operátor & umožňuje získat adresu, na které je uložen nějaký objekt (například proměnná). Operátor * umožní přístup k hodnotě, která uložena na dané adrese, takže můžeme hodnotu číst i měnit. Oba operátory dodržují typ: pokud je objekt typu x, operátor & vrací pointer na typ x; operátor *, jehož argumentem je pointer na typ x, umožnuje práci s hodnotou typu x. Více v následujícím příkladu int a = 2; int b = 5; ziskame adresy promennych a,b int *pt_a = &a; int *pt_b = &b; pt_c ukazuje take na a int *pt_c = pt_a; printf("pt_a: na adrese %.8X je hodnota %i\n", pt_a, *pt_a); printf("pt_c: na adrese %.8X je hodnota %i\n", pt_c, *pt_c); * zmenime hodnotu promenne a tak, ze zapiseme na adresu (pointer pt_c), * na ktere je ulozena jeji hodnota. K hodnote promenne b * pristupujeme take pomoci adresy (pointer pt_b). * Prirazeni je ekvivalentni radku * a = 120 + b; *pt_c = 120 + *pt_b; * Z vypisu by melo byt jasne, ze jsme zmenili hodnotu promenne a tak, * ze jsme prepsali
3 printf("--- po prirazeni --- \n"); printf("pt_a: na adrese %.8X je hodnota %i\n", pt_a, *pt_a); printf("pt_c: na adrese %.8X je hodnota %i\n", pt_c, *pt_c); printf("hodnota a je %i\n", a); Je možné, aby pointer obsahoval adresu místa v paměti, které nebylo programu přidělěno (říkáme mu pak neplatný pointer). Dereferencování takového pointeru vede k nedefinovanému chování programu, velmi často dokonce k jeho havárii. V následujícím příkladu se pokoušíme dereferencovat neinicializovaný pointer. int *pt; následujici radek program pravdepodobne shodi printf("%i\n", *pt); Abychom se vyhnuli dereferencování neplatného pointeru, bylo by pěkné mít možnost otestovat, jestli je pointer neplatný. S obecnou hodnotou pointeru to bohužel nelze. Existují ovšem dvě specifické hodnoty, které reprezentují neplatný pointer a které lze testovat. int *pt_1 = 0; int *pt_2 = NULL; test platnosti pointeru pt_1 if (!pt_1) printf("pointer pt_1 obsahujici adresu %.8X je neplatny!\n", pt_1); test platnosti pointeru pt_2 if (!pt_2) printf("pointer pt_2 obsahujici adresu %.8X je neplatny!\n", pt_2); Může se stát, že se NULL ve skutečnosti nerovná 0, nicméně na testování platnosti to nemá žádný vliv (navenek se NULL tváří, jako by bylo rovno 0). 1.2 Degenerace pole a pointerova aritmetika Pole a pointery jsou si v C velmi blízké. Připomeňme, že pokud deklarujeme pole, program si v témže bloku pamatuje jeho velikost. Můžeme tedy provést následující. int array[10]; spocitame pocet prvku pole * sizeof(array) vrati velikost pole v bajtech!!! * sizeof(array[0]) vrati velikost jednoho prvku v bajtech printf("pole ma velikost %lu bajtu a obsahuje %i prvku \n", sizeof(array), sizeof(array)/sizeof(array[0]));
4 Program vypíše správnou velikost pole v bajtech i počet jeho prvků. Pokud bychom se ovšem pokusili o stejnou věc ve funkci, jíž bylo pole předáno jako argument, bude se program chovat jinak. void test(int array[]) * Tady sizeof(array) vrati velikost pointeru!!! * Tim padem nasledujici radek vypise nespravne hodnoty. printf("pole ma velikost %lu bajtu a obsahuje %i prvku \n", sizeof(array), sizeof(array)/sizeof(array[0])); int main() int array[10]; funkce(array); return 0; Při překladu předchozího programu překladač pravděpodně vypíše varování, že použití sizeof(array) ve funkci test vrátí velikost int *. Důvodem pro toto chování je mechanismus, kterému říkáme degenerování pole na pointer. Jméno pole je totiž konstantní pointer na typ prvků pole (je-li pole typu x, pak jeho jméno je pointer na x). Konstantní znamená, že jeho hodnotu nemůžeme změnit. K degeneraci pole na pointer dochází i při předání pole jako argumentu funkce. Následující hlavičky funkcí jsou tedy ekvivalentní. void test(int array[]); void test(int *array); Degeneraci pole na pointer ilustruje i následující příklad. int array[10] = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; int a = 15; pt obsahuje adresu prvního bajtu prvního prvku pole array int *pt = array; int *pt2 = &a; zadny z nasledujich tri radku nelze provest, pointer array je konstantni array = pt2; array = &a; array = 0; vypiseme prvni prvek pole printf("%i\n", *pt); Díky tomu, že pole degeneruje na pointer správného typu, můžeme dereferencí tohoto poin-
5 teru získat první prvek pole, jak vidíme na posledním řádku předchozího příkladu. Protože pole je v paměti v souvislém bloku, lze adresu prvku na indexu i získat tak, že adresu začátku pole zvětšíme o i krát velikost jednoho prvku pole. Tím se v paměti posuneme ze začátku pole o i prvků. V programu počet bajtů, o které je nutno se posunout nemusíme počítat, aritmetické operace s pointery to udělají za nás. K pointeru lze totiž přičíst, nebo od něj odečíst, celé kladné číslo. Výsledkem přičtení čísla c k pointeru pt je pointer, který získáme zvětšením pt o c-krát velikost typu, na který pt ukazuje. Pokud by byl například pt pointer na int, tak bychom pt zvětšili o c-krát sizeof(int). Odečítání celého čísla funguje analogicky. Pointerové aritmetiky můžeme využít k přístupu k jednotlivým prvkům pole, pokud máme k dispozici pointer na začátek pole. Dobře si prostudujte následující program a text, který vypíše. void print_array_info(int array[], int size) int i; vypiseme index, adresu a hodnotu kazdeho prvku pole for(i=0; i<size; i++) printf("index: %i \t address: %.8X \t value: %i\n", i, array+i, *(array+i)); int main () int array[5] = 1,2,3,4,5; print_array_info(array, 5); return 0; Vidíme, že k prvku na indexu i v poli array se můžeme dostat i pomocí výpočtu adresy tohoto prvku pomocí pointerové aritmetiky a poté použitím operátoru dereference. Následující dva výrazy jsou tedy ekvivalentní. array[i] *(array + i) Operátor dereference má vyšší prioritu než operátor sčítání, musíme proto použít závorky. Další aritmetickou operací s pointery je rozdíl dvou pointerů stejného typu. Rozdíl pt1 - pt2 je roven počtu hodnot daného typu, které se za sebe vejdou do paměti mezi pt2 a pt1. Dále lze adresy porovnávat pomocí operátorů porovnání, chování těchto operátorů je zde stejné jako u čísel. 1.3 Pointery a struktury Máme-li pointer na strukturu, můžeme k položkám této struktury přistupovat pomocí kombinace operátoru dereference a tečky. Předpokládejme, že máme definovánu následující strukturu. struct my_structure int item_a;
6 double item_b; ; typedef struct my_structure my_structure; Potom k položkám můžeme přistoupit následovně my_structure data = 10, 3.2; my_structure *pt = &data; pristoupime k polozkam struktury data pomoci pointeru pt int a = (*pt).item_a; (*pt).item_b = 29.6; Pro kombinaci použití operátoru dereference a tečky k přístupu k položkám struktury přes pointer existuje v C alternativní syntaxe pomocí operátoru ->. Poslední dva řádky bychom tedy mohli nahradit pomocí int a = pt->item_a; pt->item_b = 29.6; Pokud přiřazujeme do instance struktury jinou její instanci, dojde ke zkopírovaní všech položek struktury. Stejně tak tomu je, i když přiřazujeme dereferenci pointeru. my_structure data = 10, 3.2; my_structure *pt = &data; * tady dojde ke zkopirovani hodnot polozek promenne data * do polozek promenne copy my_structure copy = *pt; tato zmena se neprojevi v promenne data copy.item_a = 123; Struktura může jako položky obsahovat jiné struktury, ovšem ne samu sebe. Je to proto, že struktura by pak měla nekonečnou velikost. Struktura ovšem může obsahovat pointer na samu sebe, pointer má totiž konečnou velikost. struct _node int data; struct _node *next; ; Pokud předáváme funkci jako argument strukturu, je vhodné předávat ji pomocí pointeru. Uvnitř funkce máme k položkám struktury přístup v obou případech (tj. při předání hodnotou i odkazem). Použijeme-li pointer vyhneme se při volání funkce kopírování potenciálně velkého kusu paměti, kterou při volání předáme jako argument. misto teto definice funkce double fce (my_structure a)
7 pristup k polozkam double result = a.item_a + a.item_b; return result; pouzijeme radsi tuto definici funkce double fce (my_structure *a) pristup k polozkam double result = a->item_a + a->item_b; return result; 1.4 Předávání argumentů funkce odkazem Pokud chceme, aby funkce měnila hodnotu proměnné předané jí jako argument, nepředáváme funkci hodnotu této proměnné, ale její adresu. Říkáme, že argument předáváme odkazem. Samotná adresa je předána hodnotou, to ale nebrání v tom, aby funkce k paměti na této adrese přistupovala a měnila její obsah. Příkladem takového chování je předání pole jako argumentu funkce. Jak jsme diskutovali v předchozích kapitolách, ve skutečnosti funkci předáváme adresu pole. Následující příklad ukazuje jednoduchou funkci, které předáváme třetí argument odkazem. int division(int a, int b, int *remainder) int result = a / b; na adresu remainder ulozime zbytek po deleni *remainder = a % b; return result; int main() int x, y; x = division(13, 5, &y); * v promenne x je 2 * v promenne y je 3 1.5 Pointery typu void Speciálním typem pointeru je void pointer, jehož typové označení je void *. Slouží k uložení adresy bez informace o typu. Void pointer nelze dereferencovat. Void pointer je možno přiřadit pointeru jakéhokoliv typu a naopak, pointer jakéhokoliv typu lze přiřadit void pointeru,
8 bez nutnosti provádět explicitní přetypování. Void pointer je užitečný například v případě, kdy chceme naprogramovat funkci pracující s pamětí, u které není potřeba znalost o typu. Příkladem takových funkcí jsou například memcpy a malloc (podrobnosti uvidíme v další kapitole). Void pointery mají i další použití, ale to na tomto místě nebudeme rozvádět. Jako příklad použití void pointerů si napíšeme funkci, která vytiskne nějakou část paměti hexadecimálně po bajtech. Připomeňme, že k zachycení obsahu jednoho bajtu potřebujeme dvouciferné hexadecimální číslo. #include <stdio.h> argumenty * mem - adresa pameti, kterou chceme vytisknout * size - pocet bajtu, ktere chceme vytisknout * bytes_per_row - pocet bajtu, ktere chceme vytisknout na jeden radek void dump_memory_on_screen(void *mem, unsigned int size, int bytes_per_row) tady implicitne pretypujeme void pointer na unsigned char unsigned char *bytes = mem; int i; for(i=0; i<size; i+=1) if (i && (i % bytes_per_row == 0)) printf("\n"); tisk obsahu pameti hexadecimalne printf("%.2x ", *bytes); posunu pointer o jeden bajt bytes += 1; printf("\n"); typedef struct int a; int b; float c; my_struct; int main() int a = 394301208; double b = 4302.30103; my_struct array[3] = 1,2,1.5, 3,7,12.2, 1,1,55.4; printf("dumping integer %i\n", a); dump_memory_on_screen(&a, sizeof(a), 8);
9 printf("\ndumping double %f\n", b); dump_memory_on_screen(&b, sizeof(b), 8); printf("\ndumping array of structs\n"); dump_memory_on_screen(array, sizeof(my_struct) * 3, 8); return 0; 1.6 Úkoly 1. Vyzkoušejte všechny příklady kódu z tého kapitoly. Kde to je potřeba, dopište vlastní kód. 2. Naprogramujte vlastní verzi knihovní funkce strcmp. K přístupu k jednotlivým znakům řetězců používejte pointerů a pointerové aritmetiky. Detaily o funkci strcmp najděte v referenční příručce. 3. Naprogramujte funkci, která pro dané pole čísel (můžete zvolit libovolný číselný typ) spočítá průměr a medián. Jeden z výsledků funkce vrátí odkazem. 4. Napište funkce pro převod čísel typu int mezi little endian na big endian reprezentací (viz stránka na wikipedii). 5. Implementujte vlastní verzi knihovní funkce memcpy s hlavičkou void *my_memcpy(void *src, void *dest, unsigned long n); Funkce zkopíruje n za sebou jdoucích bajtů začínajících na adrese src na místo začínající na adrese dest. Funkce vrátí pointer dest. Příklad použití int array[6] = 1, 2, 3, 4, 5, 6; int destination[6] = 0, 0, 0, 0, 0, 0; nasledujici radek zkopiruje obsah pole array do pole destination my_memcpy(array, destination, 6 * sizeof(int));
2 Alokace a dealokace paměti Paměť, kterou běžící program využívá, je mu nejdřívě přidělena (alokována). Pokud jí již není potřeba, je paměť uvolněna. Podle použitého mechanismu přidělování a uvolněnování můžeme paměť rozdělit na dva typy. Na paměť, která je přidělována a uvolňována automaticky, a na paměť, jejíž přidělení a uvolnění musí programátor provést manuálně. 2.1 Automatická alokace a dealokace Paměť je automaticky přidělena a uvolněna pro následující objekty: globální a lokální proměnné a pole všech typů (včetně struktur!) statické proměnné a pole ve funkcích (o statických proměnných níže) všech typů Paměť pro globální a statické proměnné a pole je přidělena na začátku běhu programu, předtím než začne běh funkce main. Tato paměť je po přidělení automaticky vynulována. K uvolnění paměti globálních a statických proměnných a polí dochází až na konci běhu programu. Připomeňme, že když program skončí, operační systém uvolní všechnu paměť, kterou program používal, i tu, která není uvolňována automaticky. Lokální proměnné a pole ve funkcích podléhají jinému automatickému mechanismu alokace a uvolňování. V momentu zavolání funkce přidělena paměť pro všechny lokální proměnné, a pro argumenty funkce. Do paměti odpovídající argumentům funkce jsou překopírovány hodnoty skutečně předaných argumentů. Všechna paměť je uvolněna v okamžiku, kdy funkce skončí. Pokud z funkce vracíme adresu (nebo ji přiřazujeme proměnné předané funkci odkazem), měli bychom si být vědomi toho, že pamět na místě, na které adresa ukazuje, bude po skončení funkce uvolněna 1. Následující funkce jsou tedy špatně. int *make_array_up_to_five() int array[10] = 1, 2, 3, 4, 5; chyba, po skonceni funkce je pamet uvolnena return array; int *bar (int c) int k = c * 2; chyba, po skonceni funkce je pamet uvolnena return &k; 1 fyzicky být uvolněna nemusí, nicméně za uvolněnou ji musíme považovat 10
11 Proměnnou v těle funkce lze z vyjmout z režimu automatické alokace a dealokace lokálních proměnných a vložit ji do režimu automatické alokace a dealokace globálních proměnných. Dělá se to tak, že proměnnou definujeme jako static. static typ jmeno = pocatecni_hodnota; Pokud proměnnou při definici neinicializujeme, je inicializována na 0, protože paměť, která je alokována pro globální a statické proměnné, je po alokaci vynulována. Statickou proměnnou můžeme inicializovat pouze konstantní hodnotou vyjádřenou literálem (tedy nikoliv hodnotou proměnné, hodnotou získanou zavoláním funkce apod.) V následujícím příkladě vidíme funkci, která počítá kolikrát byla zavolána. void counting() static int called = 0; called += 1; printf("pocet zavolani: %i \n"); int main() int i; vypisi se cisla 1 az 10 for(i = 0; i < 10; i+=1) counting(); return 0; 2.2 Manuální alokace a dealokace void *malloc(size_t memory_size); void *calloc(size_t n, size_t unit_size); void free(void *ptr); void realloc(void *ptr, size_t memory_size); Funkce malloc (jméno je zkratkou sousloví memory allocation) slouží k alokaci paměti. Jejím jediným argumentem je počet bajtů, které chceme alokovat. Funkce alokuje souvislý blok paměti a vrátí adresu jeho prvního bajtu. Pokud se alokace paměti z nějakého důvodu nepovede, vrací funkce hodnotu 0. Není definováno, co alokovaná paměť obsahuje. Pro alokaci paměti slouží i funkce calloc. Bere dva argumenty, počet položek, které chceme alokovat a velikost jedné položky v bajtech. Funkce alokuje blok paměti o velikosti odpovídající součinu argumentů a vrátí adresu jeho prvního bajtu. Pokud se alokace nezdaří, vrací calloc hodnotu 0. Všechny bajty alokované paměti jsou nastaveny na hodnotu 0. Funkce free slouží k uvolnění paměti, kterou jsme předtím manuálně alokovali (a ještě
12 jsme ji neuvolnili). Tuto pamět předáváme funkci pomocí adresy jejího prvníhé bajtu. Pokud je argument jiná adresa není chování funkce definováno (program může havarovat, dostat se do nekonzistentního stavu a podobně). Funkce realloc slouží ke změně velikosti alokované paměti. Jejími argumenty jsou adresa již (manuálně) alokované paměti a nová velikost. Funkce provede alokace a dealokace nutné pro změnu velikosti paměti a vrátí adresu začátku daného bloku paměti. Tato adresa se může lišit od adresy předané funkci jako argument, může totiž být potřeba paměť přesunout. Pokud pomocí realloc paměť zmenšujeme, je uvolně od konce (jsou uvolněny bajtů s vyššími adresami). Pokud pomocí realloc paměť zvětšujeme, může se stát, je že ji realloc alokuje na novém místě a obsah původní paměti tam překopíruje a paměť na původním místě je uvolněna. Pokud se alokace na novém místě nezdaří, původní paměť uvolněna není a realloc vrací 0. Následuje několik příkladů použítí výše uvedených funkcí. alokujeme pole o 10 prvcich int *p = malloc(10* sizeof(int)); nasledujici pouziti reallocu neosetruje situaci, kdy je nutno pamet presunout a nepovede se nova alokace, to by vedlo ke dvema chybam 1) p bude rovno 0 a dal v programu se p muzeme pokusit dereferencovat 2) puvodni pamet nebude uvolne a ztratime na ni adresu -> memory leak p = realloc(p, 20 * sizeof(int)); spravne musime realloc pouzit nasledovne int *tmp = realloc(p, 20*sizeof(int)); if (!tmp) tady se rozhodneme, co provest, pokud se alokace nezdari else p = tmp; funkce vytvori pole obsahujici cisla z intervalu a b int *make_interval(int a, int b) int size = b - a; int i;
13 int *ret = 0; if (size <= 0) return 0; ret = malloc(size * sizeof(int)); if (!ret) return 0; for(i = 0; i < size; i+= 1) ret[i] = a + i; return ret; bag ~ multimnozina prvku pridavani a odebirani prvku testovani poctu vyskytu daneho prvku #define BAG_START_SIZE 100 typedef struct int *data; int size; int head; bag; bag *create_bag () pamet pro structuru bag bag *result = malloc(sizeof(bag)); ; if(!result) return 0; pamet pro pole pro uchovavani prvku result->data = malloc(sizeof(int) * BAG_START_SIZE); if(!result->data) free(result); return 0; result->size = BAG_START_SIZE; result->head = 0; return result; vraci 1, kdyz se pridani povede vraci 0, kdyz se pridani nepovede
14 int add_to_bag (bag *b, int element) if (b->head == b->size) int *tmp; b->size *= 2; tmp = realloc(b->data, b->size * sizeof(int)); if(!tmp) return 0; b->data = tmp; b->data[b->head] = element; b->head += 1; return 1; void delete_from_bag (bag *b, int element) int i; hledam prvek for(i = 0; i < b->head; i += 1) if(b->data[i] == element) break; pokud jsem prvek nasel, tak jej smazu vymenou s poslednim prvekm a snizenim head o 1 if(i!= b->head) b->data[i] = b->data[b->head-1]; b->head -= 1; int occurence_in_bag (bag *b, int element) int i; int occurences = 0; for(i=0; i<b->head; i+=1) if (b->data[i] == element) occurences += 1; return occurences;
15 void destroy_bag(bag *b) if (!b) return; if (b->data) free(b-data); free(b); 2.3 Další funkce pro práci s pamětí Ve standardní knihovně se nachází další užitečné funkce pro práci s pamětí. Pro příklad uvedeme tři z nich, další lze najít v referenční příručce ke standardní knihovně. void *memcpy(void *mem1, void *mem2, size_t n); Funkce memcpy zkopíruje n bajtů z adresy mem2 na adresu mem1 a vrátí mem1. Pokud se zdrojová a cílová paměť překrývají, není chování funkce definováno. Funkce předpokládá, že má přístup k potřebné paměti: na adrese mem1 musí být alokována dostatečně velká paměť a na adrese mem2 musí být dostatečně mnoho bajtů pro čtení. V opačném případě je chování funkce nedefinováno a může vést k havárii programu. Příkladem použítí funkce je kopírování pole. kopirovani pole: predpokladejme, ze source a target jsou pole typu int o size prvcich. kopie prvku cyklem int i; for(i=0; i<m; i+=1) target[i] = source[i]; kopie pomoci memcpy memcpy(foo, bar, sizeof(int) * m); Druhou funkcí je memmove. void *memmove(void *mem1, void *mem2, size_t n); memmove funguje obdobně jako memcpy. Rozdíl je v tom, že kopírování proběhne správně i v případě, kdy se zdrojová a cílová pamět překrývají. Funkce memset nastaví obsah všech bajtů paměti na požadovanou hodnotu. void *memset(void *mem, int c, size_t n); Jako parametry se funkci předává adresa začátku paměti, hodnota, na kterou chceme bajty nastavit, a počet bajtů, které chceme nastavit. Cílová hodnota se předává funkci jako int, ve funkci je přetypována na unsigned char. Funkce vrací pointer mem. Často se využívá například k vynulování paměti.
16 vynulovani pole: predpokladame, ze array je pole o m prvcich vynulovani cyklem int i; for(i=0; i<m; i+=1) array[i] = 0; vynulovani pomoci memset memset(array, 0, sizeof(int) * m); 2.4 Úkoly 1. Upravte funkci delete_from_bag tak, se velikost struktury v paměti zmenšovala vždy, když je struktura zaplněna z méně než jedné čtvrtiny, současně však musí mít kapacitu nejméně BAG_START_SIZE. 2. Implementujte funkci s hlavičkou char *string_concatenate(char *s1, char *s2); která vytvoří nový řetězec spojením řetězců s1 a s2. Ve funkci využijte standardní funkci memcpy. 3. Implementujte vlastní verzi knihovní funkce memmove. Například tedy funkci s hlavičkou void *my_memmove(void *src, void *dest, unsigned int n); která přesune n bajtů paměti z adresy src na adresu dest. Funkce musí fungovat korektně i pokud se zdrojový a cílový blok paměti překrývají! 4. V podobném duchu, jako je implementována datová struktura bag v příkladu z textu implementujte zásobník a frontu (stačí, budeme-li do nich ukládat int).