ZÁKLADY PROGRAMOVÁNÍ V C

Podobné dokumenty
Základy programování 2 KMI/ZP2

IUJCE 07/08 Přednáška č. 6

Pointery II. Jan Hnilica Počítačové modelování 17

Př. další použití pointerů

Pokročilé programování v jazyce C pro chemiky (C3220) Operátory new a delete, virtuální metody

Základy programování (IZP)

Základy programování (IZP)

Střední škola pedagogická, hotelnictví a služeb, Litoměříce, příspěvková organizace

Pole stručný úvod do začátku, podrobně později - zatím statická pole (ne dynamicky) - číslují se od 0

Práce s pamětí a předávání parametrů. Úvod do programování 1

Více o konstruktorech a destruktorech

Základy programování (IZP)

Programování v C++, 2. cvičení

Práce s polem a pamětí

Lineární spojový seznam (úvod do dynamických datových struktur)

6. lekce Úvod do jazyka C knihovny datové typy, definice proměnných základní struktura programu a jeho editace Miroslav Jílek

ZÁKLADY PROGRAMOVÁNÍ V C

Ukazatel (Pointer) jako datový typ - proměnné jsou umístěny v paměti na určitém místě (adrese) a zabírají určitý prostor (počet bytů), který je daný

for (int i = 0; i < sizeof(hodnoty) / sizeof(int); i++) { cout<<hodonoty[i]<< endl; } cin.get(); return 0; }

Ukazatele a pole. Chceme-li vyplnit celé pole nulami, použijeme prázdný inicializátor: 207 Čárka na konci seznamu inicializátorů

IUJCE 07/08 Přednáška č. 4. v paměti neexistuje. v paměti existuje

- dělají se také pomocí #define - podobné (použitím) funkcím - předpřipravená jsou např. v ctype.h. - jak na vlastní makro:

Operační systémy. Cvičení 4: Programování v C pod Unixem

Ukazatele, dynamická alokace

Základy programování (IZP)

Čtvrtek 8. prosince. Pascal - opakování základů. Struktura programu:

Správné vytvoření a otevření textového souboru pro čtení a zápis představuje

8. lekce Úvod do jazyka C 3. část Základní příkazy jazyka C Miroslav Jílek

Programování v jazyce C pro chemiky (C2160) 3. Příkaz switch, příkaz cyklu for, operátory ++ a --, pole

ZPRO v "C" Ing. Vít Hanousek. verze 0.3

Konstruktory a destruktory

Pole a Funkce. Úvod do programování 1 Tomáš Kühr

1. lekce. do souboru main.c uložíme následující kód a pomocí F9 ho zkompilujeme a spustíme:

2 Datové typy v jazyce C

1. lekce. do souboru main.c uložíme následující kód a pomocí F9 ho zkompilujeme a spustíme:

Aplikace Embedded systémů v Mechatronice. Michal Bastl A2/713a

Michal Krátký. Úvod do programovacích jazyků (Java), 2006/2007

Strukturu lze funkci předat: (pole[i])+j. switch(výraz) velikost ukazatele

8 Třídy, objekty, metody, předávání argumentů metod

Střední škola pedagogická, hotelnictví a služeb, Litoměříce, příspěvková organizace

Struktura programu v době běhu

11a Dynamické dvourozměrné pole (obdobně vícerozměrné)

int ii char [16] double dd název adresa / proměnná N = nevyužito xxx xxx xxx N xxx xxx N xxx N

Odvozené a strukturované typy dat

7 Formátovaný výstup, třídy, objekty, pole, chyby v programech

Pokročilé programování v jazyce C pro chemiky (C3220) Třídy v C++

Programování v jazyce C a C++

Správa paměti. doc. Ing. Miroslav Beneš, Ph.D. katedra informatiky FEI VŠB-TUO A-1007 /

Mělká a hluboká kopie

Základy programování (IZP)

Algoritmizace a programování

1. D Y N A M I C K É DAT O V É STRUKTUR Y

Práce s binárními soubory. Základy programování 2 Tomáš Kühr

Distanční opora předmětu: Programování v jazyce C Tématický blok č. 6: Dynamická alokace paměti, typové konstrukce Autor: RNDr. Jan Lánský, Ph.D.

4. Typ ukazatel, strukturované datové typy

Výhody a nevýhody jednotlivých reprezentací jsou shrnuty na konci kapitoly.

Ukazatele #2, dynamická alokace paměti

Programujeme v softwaru Statistica

Obsah. Předmluva 13 Zpětná vazba od čtenářů 14 Zdrojové kódy ke knize 15 Errata 15

Dynamická vícerozměrná pole. Základy programování 2 Tomáš Kühr

Programování v C++ 2, 4. cvičení

C++ přetěžování funkcí a operátorů. Jan Hnilica Počítačové modelování 19

Algoritmizace prostorových úloh

Pole a kolekce. v C#, Javě a C++

Výčtový typ strana 67

Programovanie v jazyku C - to chce dynamiku

Základní datové struktury

Dynamická alokace paměti

int t1, t2, t3, t4, t5, t6, t7, prumer; t1=sys.readint();... t7=sys.readint(); prume pru r = r = ( 1+t 1+t t3+ t3+ t4 t5+ t5+ +t7 +t7 )/ ;

Např.: // v hlavičkovém souboru nebo na začátku // programu (pod include): typedef struct { char jmeno[20]; char prijmeni[20]; int rok_nar; } CLOVEK;

Vícerozměrná pole. Úvod do programování 2 Tomáš Kühr

Algoritmizace a programování

Michal Krátký. Úvod do programovacích jazyků (Java), 2006/2007

Algoritmizace a programování

Programování v jazyce C a C++

Vícerozměrná pole. Inicializace pole

Operační systémy. Cvičení 3: Programování v C pod Unixem

Funkce pokročilé možnosti. Úvod do programování 2 Tomáš Kühr

Úvod do jazyka C. Ing. Jan Fikejz (KST, FEI) 28. prosince Fakulta elektrotechniky a informatiky Katedra softwarových technologií

PB071 Programování v jazyce C Jaro 2017

PB071 Programování v jazyce C Jaro 2015

Programování v C++ 1, 1. cvičení

Spojová implementace lineárních datových struktur

DUM 06 téma: Tvorba makra pomocí VBA

Pokročilé programování v jazyce C pro chemiky (C3220) Pokročilá témata jazyka C++

Strukturované typy a ukazatele. Úvod do programování 1 Tomáš Kühr

Úvod do jazyka C. Ing. Jan Fikejz (KST, FEI) Fakulta elektrotechniky a informatiky Katedra softwarových technologií

Paměť počítače. alg2 1

BI-PA1 Programování a algoritmizace 1, ZS Katedra teoretické informatiky

Algoritmizace prostorových úloh

Program převod z desítkové na dvojkovou soustavu: /* Prevod desitkove na binarni */ #include <stdio.h>

Funkce, intuitivní chápání složitosti

VÝUKOVÝ MATERIÁL. Bratislavská 2166, Varnsdorf, IČO: tel Číslo projektu

Algoritmizace a programování

for (i = 0, j = 5; i < 10; i++) { // tělo cyklu }

Tabulka symbolů. Vazba (binding) Vazba - příklad. Deklarace a definice. Miroslav Beneš Dušan Kolář

MAXScript výukový kurz

Anotace. Pointery. Martin Pergel,

Základy C++ I. Jan Hnilica Počítačové modelování 18

Připravil: David Procházka. Vertex Buffer Objects

Transkript:

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 Práce s pamětí 1 1.1 Adresy a ukazatele............................... 1 1.2 Pole a pointery, pointerová aritmetika.................... 2 1.3 Dva druhy paměti............................... 3 1.4 Předávání parametrů funkce odkazem.................... 8 1.5 Další funkce pro práci s pamětí........................ 9 1.6 Vícerozměrná pole.............................. 10 1.7 Pointery a struktury.............................. 13 ii

1 1 Práce s pamětí 1.1 Adresy a ukazatele Programátor může při práci v C přímo přistupovat do paměti, kterou má program k dispozici. Paměť si můžeme představit jako posloupnost paměťových buněk, každá o velikosti 1 bytu, které jsou naskládány za sebou a očíslovány (podobně jako indexy u pole). Pořadové číslo bytu budeme považovat za jeho adresu v paměti. Adresu lze uložit do proměnné. Takové proměnné říkáme ukazatel nebo (anglicky) pointer. O adrese, která je hodnotou pointeru, říkáme, že na ni pointer ukazuje (případně, že ukazuje na objekt, který se na dané adrese nachází.) K pointeru se váže i informace o typu. Slouží ke správné interpretaci paměti, na kterou pointer ukazuje. Jednotlivé byty vícebytových objektů jsou v paměti uloženy za sebou a pointer vždy obsahuje adresu prvního bytu takového objektu. Podle typu lze určit, kolik bytů je potřeba přečíst a jak je interpretovat. Řekněme například, že máme pointer na int (pokud je k dané adrese asociován typ, mluvíme o pointeru na daný typ). Potom víme, že k získání hodnoty typu int, jejíž první byte ja adrese obsažené daným pointerem, je nutno přečíst sizeof(int) bytů a interpretovat je jako celé číslo. Poznamenejme, že někdy používáme slovo pointer místo slova ukazatel. Pokud například funkce vrací adresu, řekneme, že vrací pointer. Jestli se jedná o adresu nebo o proměnnou, jejíž hodnotou je adresa, poznáme vždy z kontextu. Pointer definujeme následovně. typ *jmeno = init; Inicializaci lze vynechat. Znak * se váže ke jménu pointeru, nikoliv k typu. Například v následujícím kódu je a proměnná typu int, kdežto ptr pointer na int. int a, *ptr; K základním úkonům s adresami slouží unární operátory adresy & a dereference *. Pomocí operátoru adresy lze získat adresu proměnné, která je jeho argumentem. (získáme tak adresu, na které je hodnota proměnné uložena v paměti). Pomocí operátoru dereference lze přistupovat k objektu na adrese, která je jeho argumentem. Více v následujícím příkladu. 1 int a = 3, b, *ptr1, *ptr2; 2 3 /* ukazatel ptr ukazuje na promennou a 4 (obsahuje jeji adresu) */ 5 ptr1 = &a; 6 7 /* hodnota b bude 5 8 (*ptr1 je 3, protoze a je 3)*/ 9 b = *ptr1 + 2; 10 11 /* ptr1 a ptr2 ukazuji na stejne misto */

2 12 ptr2 = ptr1; 13 14 /* zmenime hodnotu na miste, kam ukazuje ptr2, tedy 15 hodnotu promenne a */ 16 *ptr2 = 5; 17 18 /* hodnota b bude 8 19 predeslym prikazem jsme do a ulozili 5 */ 20 b = a + 3 V předchozím příkladě získáme pomocí operátoru adresy adresu, na které je umístěna hodnota proměnné a, a přiřadíme ji do pointeru ptr1 (řádek 5). Na řádku 9 aplikujeme operátor dereference na ptr1. Protože ptr1 ukazuje na místo, kde je uložena proměnná a, máme tak přístup k její hodnotě a výsledkem aplikace operátoru je právě hodnota proměnné a. Dopadne to tedy stejně, jako bychom místo *ptr1 napsali a. Proto poté do b přiřadíme hodnotu 5. Na řádku 12 přiřadíme do ptr2 hodnotu ptr1. Oba pointery tak ukazují na stejné místo v paměti (tam, kde je hodnota a). Na řádku 16 použijeme operátor dereference na ptr2 a pomocí operátoru přiřazení nastavíme hodnotu na 5. Vidíme tedy, že výsledek operátoru dereference lze použít jako r-value pro získání hodnoty v paměti i jako l-value pro změnu hodnoty v paměti, na kterou pointer ukazuje. Řádek 16 by tedy šel přepsat jako a = 5;. 1.2 Pole a pointery, pointerová aritmetika Pokud použijeme jméno pole 1 v kódu, ve většině případů toto jméno zdegeneruje na konstantní pointer, který je stejného typu jako prvky pole a který ukazuje na první byte prvního prvku pole. To, že je pointer konstantní, znamená, že mu nelze přiřadit jinou hodnotu. Degenerací myslíme to, že ztratíme informaci o velikosti pole a známe pouze jeho začátek. Jméno pole nezdegeneruje pouze při několika málo použitích, jedním z nich je při použití operátoru sizeof v místě kódu, které se nachází v bloku, kde bylo pole deklarováno. Bezpečné je předpokládat, že jméno zdegeneruje na pointer všude. int arr[] = 0,1,2,3,4,5; int arr2[] = 1,2,3; /* v sizeof(arr), arr nezdegeneruje */ int size = sizeof(arr)/sizeof(arr[1]); /* tady arr zdegeneruje */ int *ptr = arr; /* tady zdegeneruji obe jmena, ale prirazeni nefunguje, protoze arr je konstantni pointer */ arr = arr2; Pointerů lze využít při práci s polem, lze k tomu využít operací pointerové aritmetiky. 1 připomeňme, že tím myslíme pole, které je alokováno na zásobníku a nikoliv na halde (vysvetlení v další kapitole), podle ANSI standardu musíme velikost takového pole zadat tak, aby byla známa v době překladu.

3 K adrese lze přičíst, nebo od ní lze odečíst, nezáporné celé číslo Výsledkem přičtení čísla c k pointeru ptr (v kódu lze zapsat jako ptr + c) je adresa, kterou získáme tak, že k adrese uložené v ptr přičteme c krát velikost typu ptr. Například, pokud je ptr typu int, přičteme k adrese c * sizeof(int). Adresu tedy neposunujeme o c bytů, ale o tolik bytů, aby se mezi ptr a výslednou adresu vešlo c hodnot typu int. Odečítání funguje analogicky, pouze opačným směrem. Lze spočítat rozdíl adres stejného typu. Výsledkem je počet prvků daného typu, které se mezi dvě dané adresy vejdou. Adresy lze porovnávat stejně jako čísla. K přístupu k jednotlivým prvkům pole můžeme výhodně využít pointerové aritmetiky a operátoru dereference. Je potřeba si dávat pozor na to, že operátor dereference má vyšší prioritu než aritmetické operátory. int arr[] = 1,2,3; /* totez jako arr[0] = 5, *arr je totez jako *(arr + 0) */ *arr = 5 /* totez jako arr[1] = 5;*/ *(arr + 1) = 5; /* spatne: *arr je totez jako arr[0], a tedy prikaz je arr[0] + 1 = 2; */ *arr + 1 = 5 Pro ukázku přepíšeme s pomocí pointerové aritmetiky funkci, jejímž úkolem je obrátit pořadí prvků v poli. void otoc_pole(int *p, int velikost) int i; for(i=0; i<velikost/2; i++) int foo = *(p + i); *(p + i) = *(p + velikost - 1 - i); *(p + velikost - 1 - i) = foo; Toho, že jméno pole je pointerem, jsme využili i při specifikaci argumentu předaného funkci. Pokud chceme pole předat jako argument, lze místo int array[] použít int *array. Dokonce bych to doporučil, při předání jména pole funkci, toto jméno stejně zdegeneruje na pointer. 1.3 Dva druhy paměti Paměť, kterou program během svého běhu používá, je na začátku nejdříve přidělena (také říkáme alokována) a když už není potřeba, tak je uvolněna (také říkáme dealokována). Například při definici proměnné je potřeba pro tuto proměnnou alokovat pamět, protože potřebujeme hodnotu proměnné někde udržovat. V momentě, kdy proměnnou už nepotřebujeme,

4 například je to lokální proměnná ve funkci a funkce už skončila, tak paměť uvolněna. Uvolňovat nepotřebnou pamět je důležité, paměť počítače je totiž omezený zdroj, o který se musí dělit mnoho běžících programů. Program, který opakovaně alokuje paměť a neuvolňuje ji, by mohl nakonec vyčerpat celou paměť počítače (kdyby jej operační systém neukončil). Paměť přidělenou programu lze rozdělit na dvě skupiny. Na pamět na zásobníku a na pamět na heapu. (Heap je anglické slovo pro hromadu nebo haldu 2.) Přidělování a uvolňování paměti na zásobníku funguje z pohledu programátora automaticky, u paměti na heapu se o přidělování i uvolňování paměti stará programátor sám. Princip alokace a dealokace paměti na zásobníku je následující (pro jednoduchost se dopustíme několika nepřesností 3 ). V momentě zavolání funkce je alokována paměť pro všechny lokální proměnné (tj. proměnné definované v těle volané funkce, včetně polí) 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ů. To je důvod, proč se argumenty předávají funkci tzv. hodnotou (viz kapitola o předávání argumentů hodnotou z minulého dílu semináře). Pokud v těle funkce měníme její argument, změna proběhne v kopii skutečně předaného argumentu. Všechna paměť je uvolněna v momentě, kdy funkce skončí. Představme si situaci, kdy funkce main volá funkci A, která volá funkci B a poté C. Postup alokací a dealokací je následující: alokace pro main, alokace pro A, alokace pro B, dealokace pro B, alokace pro C, dealokace pro C, dealokace pro A, dealokace pro main. Vidíme tedy, že dealokace jsou prováděny v opačném pořadí než alokace: jako první je dealokována pamět funkce volané jako poslední. Odtud jméno zásobník: náboje jsou ze zásobníku pistole stříleny v opačném pořadí, než v jakém jsou do něj vkládány. Na zásobníku jsou také alokovány i globální proměnné. Jsou alokovány při spuštění programu a dealokovány během ukončení programu. Jedním z omezení paměti na zásobníku je, že velikost paměti, kterou je třeba alokovat, je nutno znát už v době překladu programu. 4 Na zásobníku proto nelze uchovávat pole, jehož velikost v době překladu programu nevíme Druhým omezením je, že takové pole nelze vrátit jako návratovou hodnotu funkce. Přesněji řečeno, lze sice vrátit pointer ukazující na začátek pole, ovšem samotné pole nutně leží v paměti, která bude dealokována. 5 Přístup k prvkům takového pole může způsobit nedefinované chování programu či jeho havárii. Obě omezení demonstruje následující příklad. /* navratovy typ funkce: adresa typu int */ int *vrat_pole(int velikost) /* nasledujici radek je SPATNE! velikost pole neni konstanta */ int array[velikost]; /* kod nejak naplni pole */ /* SPATNE! array lezi v pameti, ktera bude po skonceni funkce dealokovana */ 2 V textu se držíme anglicismů pointer a heap. V programátorské hantýrce už to jsou ustálené výrazy, které jsou používány častěji než jejich české protějšky. 3 Přesný popis se dozvíte v kurzu o operačních systémech 4 v ANSI standardu, v novějších standardech to jde. Nebezpečí, že přeteče zásobník je ale vyšší. 5 Přesněji: může být použita a přepsána během volání dalších funkcí, je to pamět, kterou bychom neměli číst a už vůbec ne do ní zapisovat.

5 return array; Pro paměť na heapu žádná podobná omezení nejsou. Programátor může kdykoliv požádat o přidělení paměti o velikosti, kterou požaduje. V momentě, kdy už paměť nepotřebuje, požádá o její uvolnění. Přidělování i uvolňování paměti probíhá pomocí funkcí ze standardní knihovny (soubor stdlib.h). Pro přidělení paměti lze využít funkci void *malloc(size_t velikost); Funkce malloc vrací adresu void prvního bytu přidělené paměti. Paměť je přidělena v kuse, všechny její byty jsou za sebou, jako je tomu například u pole. Pokud se alokace paměti nezdaří, vrací malloc adresu 0. To je adresa rezervovaná za specifickým účelem: pokud obsahuje pointer adresu 0, znamená to, že neukazuje na smysluplná data. Argumentem funkce malloc je velikost paměti, kterou chceme alokovat, v bytech. Argument je typu size_t, který slouží pro reprezentaci velikosti objektů v paměti. Je to neznaménkový celočíselný typ, který je na většině platforem roven typu unsigned int nebo typu unsigned long. Výsledek typu size_t vrací například operátor sizeof. Klíčové slovo void znamená, že pointer nenese žádné informace o tom, jak lze s pamětí na daném místě pracovat. S takovým pointerem tedy nelze provádět žádné operace (např. aritmetiku, dereferenci). Abychom mohli paměť rozumně používat, je třeba pointer přetypovat. To lze provést buď implicitně (např. pomocí přiřazení) nebo explicitně. Explicitní přetypování provedeme tak, že před hodnotu, kterou chceme přetypovat, napíšeme výraz (novy_typ). Viz následující příklad. int i = 5; float f; /* f bude mit hodnotu 1, vysledek deleni je typu int */ f = i/4; /* f bude mit hodnotu 1.25, protoze hodnotu i pretypujeme na float a vysledek deleni je typu float */ f = ((float) i)/4; Pokud programátor přetypovává pointer, obvykle je to změna typu z void na nějaký jiný typ, nebo opačně. Pokud je void pointer argumentem funkce (respektive jejím návratovým typem), ve většině případů to znamená, že funkce je schopna pracovat s adresami všech typů (respektive o typu musí rozhodnout až programátor, jako je tomu u malloc). Pomocí malloc můžeme snadno opravit funkci vrat_pole z příkladu nahoře. Paměť alokovaná pomocí malloc není po skončení funkce automaticky uvolněna a navíc lze její velikost určit až při běhu programu, nikoliv při jeho překladu. int *vrat_pole(int velikost) /* array je pointer na prvni byte pole */ int *array = malloc(sizeof(int) * velikost) /* kod nejak naplni pole */

6 return array; Pole, které obsahuje velikost prvků typu int, má v paměti sizeof(int) * velikost bytů. Proto voláme funkci malloc právě s tímto argumentem. Vrácenou adresu poté přiřadíme do pointeru array (a implicitně ji přetypujeme na int). Pomocí array nyní můžeme s pamětí pracovat jako s polem (viz pointerová aritmetika a vztah mezi pointery a poly). Pointer také můžeme vrátit, protože pole nebude po skončení funkce dealokováno. Pro alokaci paměti existuje alternativní funkce void *calloc(size_t polozky, size_t velikost); Rozdíl oproti malloc je v tom, že všechny byty alokované paměti jsou nastaveny na nulu. Prvním argumentem funkce calloc je počet položek, které chceme alokovat, druhým je velikost jedné položky. Nepotřebnou paměť uvolňujeme pomocí funkce void free(void *adresa); Jediným argumentem funkce je adresa začátku paměti, kterou chceme uvolnit. Uvolňovaná paměť musí být alokována některou z funkcí na alokaci paměti. Pokud je argumentem adresa jiné paměti (například lokální proměnné) nebo je paměť již dealokována, je chování funkce nedefinováno (program většinou havaruje). Poslední funkcí pro alokaci a dealokaci paměti je funkce, díky které můžeme změnit velikost alokované paměti. void *realloc(void *adresa, size_t velikost); Prvním argumentem je adresa paměti, jejíž velikost chceme změnit, druhým je její nová velikost v bytech. Funkce vrací adresu začátku paměti se změněnou velikostí. Tato adresa se může lišit od adresy předané jako argument, při změně velikosti totiž může být potřeba obsah paměti na předané adrese přesunout. Obsah paměti se při volání realloc nezmění, ale pokud velikost zmenšujeme, je paměť uvolněna od konce (jsou uvolněny byty s většími adresami). Funkci lze předat jen adresu paměti předtím alokované pomocí některé funkce pro alokaci paměti, v opačném případě je chování nedefinováno. Funkce pro práci s pamětí si ukážeme na dalším příkladu. Vytvoříme datovou strukturu, do které půjdou ukládat celá čísla, a struktura se bude sama zvětšovat tak, aby se do ní všechna čísla vešla. Jako základ takové struktury vezmeme pole, k němu si zapamatujeme jeho velikost (proměnná velikost) a počet v něm vložených prvků (proměnná hlava). Čísla budeme do pole ukládat od začátku, první číslo vložíme na index 0, druhé na index 1 atd. Všimněme si, že hlava je indexem prvního prázdného políčka. Pro vložení čísla tedy stačí zapsat vkládané číslo na index hlava a tento index o jedna zvětšit. Při vkládání si ovšem musíme dát pozor na situaci, kdy je hlava větší nebo rovna velikosti pole. V takové situaci bychom totiž zapisovali mimo pole. V případě, že hlava je rovna velikosti pole, je pole již plné a je nutné ho zvětšit pomocí funkce realloc. Podrobnosti v následujícím kódu. #include <stdlib.h> #include <stdio.h> Program 1.1: Zvětšovací pole

7 /* globalni promenne pro pole, jeho velikost a pocet vlozenych prvku */ int *data; int velikost; int hlava; /* alokuje pocatecni pamet pro data, argument je pocet prvku */ void init(int v) /* pamet pro v prvku typu int */ data = malloc(sizeof(int) * v); velikost = v; hlava = 0; /* uvolni strukturu z pameti */ void uvolni() free(data); /* prida cislo p do struktury */ void pridej(int p) /* pole data uz je plne, musim jej zvetsit */ if(velikost == hlava) velikost = velikost * 2; data = realloc(data, sizeof(int) * velikost); /* pridam prvek p */ data[hlava] = prvek; hlava += 1; int main() int i; /* inicializuji na 4 prvky*/ init(4); /* pridam 100 prvku */ for(i=0; i<100; i++) pridej(i); /* uvolnim z pameti */ uvolni(); return 0; K pochopení principu fungování programu doporučuji čtenáři, aby si doprogramoval funkci, která vypíše všechny prvky ve struktuře, tj. vypíše všechny prvky v poli data s inde-

8 xem menším než hlava a proměnné velikost a hlava, a tutofunkci pak zavolá po každém přidání. Úkoly 1. Naprogramujte funkci pro spojení dvou textových řetězců. Argumenty funkce jsou dva řetězce ke spojení, funkce vrátí nový řetězec (jako ukazatel). 2. Funkci, která bere jako argument pole čísel, a vrátí pole jejich druhých mocnin (tedy prvek na indexu i vráceného pole bude mít hodnotu druhé mocniny prvku na indexu i vstupního pole). 3. V programu zvětšovací pole nahraďte použití globální proměnných pomocí strukturovaného typu. 4. K programu zvětšovací pole doprogramujte funkci, která zjistí, zda se číslo předané jí jako argument nachází v datové struktuře. 5. K programu zvětšovací pole dopište funkci, která smaže prvek předaný jí jako argument z datové struktury. 6. Upravte funkci pro mazání prvku z předchozího příkladu tak, aby když je počet prvků ve struktuře menší než polovina jeho velikosti, zmenšila paměť alokovanou pro pole data na polovinu. 7. K programu zvětšovací pole dopište funkci pro uvolnění datové struktury z paměti. 1.4 Předávání parametrů funkce odkazem V minulém semestru jsme zmínili, že argumenty jsou funkci předávány hodnotou, a v předchozích kapitolách jsme vysvětlili, proč tomu tak je. To ovšem neznamená, že nelze naprogramovat funkci, která umí změnit svůj argument. Pokud funkci předáme jako parametr pole, může funkce měnit prvky tohoto pole. Jak už víme, je to tak proto, že jméno pole je pointer ukazující na první prvek pole. Ve skutečnosti tedy nepředáváme funkci jako argument pole, ale jeho adresu. Adresa samotná 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. Právě popsaný princip je obecný a lze jej aplikovat i na jiné argumenty než pole. 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áme odkazem. Například pokud chceme, aby funkce mohla měnit hodnotu proměnné typu int, nepředáváme jí argument typu int, ale pointer na int. V těle funkce pak k hodnotě proměnné přistupujeme pomocí operátoru dereference. Blíže následující příklad. Funkce deleni počítá celočíselný podíl prvních dvou argumentů. Zbytek po dělení je přiřazen do třetího argumentu, který je předán odkazem. Je nutné nezapomenout, že při volání funkce musíme předat adresu argumentu, nikoliv argument samotný (stačí použít operátor reference). /* treti argument je pointer typu int*/ int deleni(int a, int b, int *r) /* do r priradim zbytek po deleni */

9 *r = a % b; return a/b. int main() int x, y; /* po provedeni nasledujiciho radku: x je rovno 2, y je rovno 3. Treti argument je adresa promenne y */ x = deleni(13, 5, &y); return 0; 1.5 Další funkce pro práci s pamětí Ve standardní knihovně (hlavička string.h) se nachází další užitečné funkce pro práci s pamětí. První z nich je void *memcpy(void *mem1, void *mem2, size_t n); Funkce zkopíruje n bytů z adresy mem2 na adresu mem1. Jako návratovou hodnotu vrací mem1. Pokud se zdrojová a cílová paměť překrývají, není chování funkce definováno. Na adrese mem1 již musí být alokována dostatečně velká paměť, tedy alespoň n bytů. Současně s tím, musí být na adrese mem2 dostatečně mnoho bytů pro čtení. V opačném případě je chování funkce nedefinováno a může vést k havárii programu. Funkci lze využít například při kopírování pole. /* kopirovani pole: predpokladejme, ze foo a bar jsou pole typu int o m prvcich, chceme zkopirovat obsah bar do foo */ /* nasledujici je spatne, kopirujeme pouze adresu! */ foo = bar; /* kopie cyklem */ int i; for(i=0; i<m; i+=1) foo[i] = bar[i]; /* kopie funkci memcpy */ memcpy(foo, bar, sizeof(int) * m); Funkce memmove funguje stejně jako memcpy, ovšem s tím rozdílem, že kopírování proběhne správně i v případě, že se zdrojova a cílová paměť překrývají. /* posouvani pameti: predpokladejme, ze foo je pole typu int o m prvcich. prvky s indexem vetsim nez nula posuneme na index o 1 mensi */ /* pomoci cyklu */

10 int i; for(i=1; i<m; i+=1) foo[i-1] = foo[i]; /* pomoci memmove*/ memmove(foo, foo+1, sizeof(int) * (m - 1)); Funkce memset s hlavičkou void *memset(void *mem, int c, size_t n) slouží k nastavení všech bytů v paměti na adrese mem na hodnotu danou parametrem c. Parametr n udává počet bytů, které budou nastaveny. Funkce vrací adresu mem. /* vynulovani pole: predpokladejme, ze foo je pole typu int o m prvcich */ /* vynulovani cyklem */ int i; for(i=0; i<m; i+=1) foo[i] = 0; /* pomoci memset */ memset(foo, 0, sizeof(int) * m); 1.6 Vícerozměrná pole Pole, která jsme je viděli dosud, byla jednorozměrná. Pole, jehož prvky jsou jednorozměrná pole, je dvourozměrná. Pokud bychom si jednorozměrné pole představili jako sekvenci prvků zapsaných za sebou na jeden řádek, dvourozměrné pole by pak odpovídalo zápisu prvků do tabulky. K prvkům ve dvourozměrném poli přistupujume pomocí dvou indexů, jeden index určuje, ve kterém jednorozměrném poli se prvek vyskytuje, a druhý index je indexem v tomto jednorozměrném poli. Představíme-li si pole jako tabulku, pak jeden index určuje řádek a druhý index sloupec tabulky, a společně tak jednoznačně určují umístění prvku v tabulce. Myšlenku rozšíření jednorozměrného pole na dvourozměrné lze analogicky aplikovat na rozšíření pro více dimenzí. Třírozměrné pole je jednorozměrné pole jehož prvky jsou dvourozměrná pole, čtyřrozměrné pole je jednorozměrné pole, jehož prvky jsou třírozměrná pole atd. Vícerozměrná pole definujeme následovně. typ jmeno[v1]...[vn] Definice je tedy podobná definici jednorozměrného pole, rozdíl je v tom, že musíme specifikovat velikosti všech dimenzí daného pole. Velikosti se píší pro každou dimenzi zvlášť do hranatých závorek. /* definice dvou a ctyrrozmerneho pole */ int foo[2][3]; int bar[2][2][6][8]; Definice s inicializací vypadá následovně.

11 typ jmeno[v1]...[vn] = i1,... in ; Na pravé straně je seznam v1 výrazů, každý z nich je inicializací pole o jeden rozměr menšího než pole aktuálně inicializované (je to výraz, který bychom napsali na pravou stranu, kdybychom inicializovali pole o jedna menší dimenze). int qux[4] = 1,2,3,4; int foo[2][3] = 1, 2, 3, 4, 5, 6; int bar[2][3][4] = 1,2,3,4,5,6,7,8,9,1,2,3, 4,5,6,7,8,9,0,1,2,3,4,5; K prvku vícerozměrného pole přistupujeme pomocí specifikování potřebného počtu indexů, tedy například /* prvek pole foo */ foo[0][2] = 3; /* prvek pole bar*/ bar[1][1][3] = 12; Příklad Pomocí dvourozměrného pole lze v programu reprezentovat vzdálenosti mezi místy na mapě. Řekněmě, že máme na mapě 10 zajímavých míst. Abychom s nimi mohli v programu pracovat, přiřadíme jim různá čísla z intervalu 0 až 9. Číslo přiřazené danému místu budeme brát jako reprezentaci tohoto místa v programu. Vzdálenosti mezi místy můžeme ukládat do dvourozměrného pole o velikosti 10 10 a to tak, že na místě daném indexy i a j bude vzdálenost mezi místy i a j, pokud se z místa i lze do místa j dostat, jinak tam bude 1. Cestu, během které navštívíme některá místa na mapě, lze v programu reprezentovat jako jednorozměrné pole čísel míst, uspořádané za sebou v pořadí, ve kterém místa navštívíme. Tedy například pole, které obsahuje postupně čísla 0, 3 a 2 reprezentuje cestu z místa 0 přes místo 3 do místa 2. V následujícím příkladu napíšeme funkci pro výpočet délky cesty na mapě. Funkce bere 3 argumenty: pole vzdáleností mezi místy, cestu a počet míst na ní. Vrátí buď délku cesty nebo, v případě že zadaná cesta neexistuje, vrátí -1. Zadaná cesta neexistuje, pokud neexistuje cesta mezi dvěma místy, které na zadané cestě sousedí. double delka(double p[][10], int cesta[], int pocet) double r = 0; int i; /* jedna iterace: z~mista cesta[i-1] do mista cesta[i] */ for(i=1; i<pocet; i++) int m1 = cesta[i-1]; int m2 = cesta[i]; if(p[m1][m2] == -1) return -1; r += p[m1][m2];

12 return r; Všimněte si, že u argumentu p musíme specifikovat velikost druhé dimenze pole. Pokud je vícerozměrné pole argumentem funkce, můžeme vynechat pouze velikost první dimenze, všechny ostatní velikosti musíme uvést (to souvisí s tím, že v paměti jsou vícerozměrná pole reprezentována jako jednorozměrná, o tom se dočtete o kousek dál). K dvourozměrnému poli alokovanému na heapu můžeme přistupovat pomocí indexů stejně jako k poli alokovanému na zásobníku. Oba typy pole se liší ve způsobu alokace a uvolnění z paměti. U pole na zásobníku se alokace a dealokace děje automaticky, kdežto pro pole na heapu musí programátor alokovat paměť sám, stejně tak jako ji uvolnit. Připomeňme, že dvourozměrné pole je pole, jehož prvky jsou jiná jednorozměrná pole a také, že k poli lze přistupovat pomocí pointeru ukazujícím na jeho první prvek. Postup při alokaci dvourozměrného pole o rozměrech m, n je proto následující: 1. Alokujeme pole pointerů o m prvcích. 2. Alokujeme m jednorozměrných polí o n prvcích, přičemž pointery na tyto pole uložíme do pole alokovaného v prvním kroku. /* alokace pole o rozmerech m, n*/ int i; /* 1) jednorozmerne pole pointeru s m prvky */ int **pole2d = malloc(m * sizeof(int *)); /* 2) m jednorozmernych poli s~n prvky */ for(i=0; i<m; i+=1) pole2d[i] = malloc(n * sizeof(int)); Na předchozím příkladu si všimněme, že pointer může ukazovat na jiný pointer. V předchozím příkladu ukazuje pole2d na pointer, který ukazuje na int. Obsahuje tedy adresu, na které je v paměti uložená jiná adresa (a můžeme tedy říci, že typem, na který ukazuje, je pointer na int). Při deklaraci takového pointeru musíme použít dvakrát znak *. Podobná pravidla, jaká platí pro dvojitý pointer pole2d, platí pro další vícenásobné pointery. Například při definici trojitého pointeru potřebujeme tři znaky *. Dvourozměrné pole dealokujeme tak, že uvedený postup obrátíme. Nejdříve uvolníme m jednorozměrných polí a až poté uvolníme pole pointerů. /* uvolneni pole o rozmerech m, n*/ int i; /* 1) uvolnime m jednorozmernych poli */ for(i=0; i<m; i+=1) free(pole2d[i]); /* 2) uvolnime pole pointeru */ free(pole2d);

13 Dvourozměrné pole lze reprezentovat pomocí jednorozměrného pole 6. Použijeme-li analogii mezi dvourozměrným polem a tabulkou, kde první rozměr identifikujeme s řádky tabulky a druhý rozměr se sloupci tabulky, pak stačí naskládat řádky tabulky za sebe. Dvourozměrné pole o dimenzích m, n lze tedy reprezentovat polem s m n prvky, přičemž prvek na indexech i, j ve dvourozměrném poli se nachází na indexu n i + j v jednorozměrném poli. Výhoda této reprezentace je v jednoduší alokaci a dealokaci, nevýhoda pak v tom, že k prvkům takto reprezentovaného dvourozměrného pole již nelze přistupovat pomocí zadání indexů i, j v hranatých závorkách za jménem pole. /* reprezentace jednorozmernym polem */ /* alokace */ int *pole2d = malloc(sizeof(int) * m * n); /* pristup k prvku na indexech i, j */ pole2d[i * n + j] = 12; /* dealokace */ free(pole2d); Úkoly 1. Vytvořte funkci, beroucí celočíselné argumenty m a n, která alokuje a vrátí pole o rozměrech m,n obsahující tabulku násobků, tj. prvek na indexech i, j bude roven i j. Poté funkci přepište s použitím reprezentace jednorozměrným polem. 2. Pomocí dvourozměrného pole lze reprezentovat hrací pole při piškvorkách (např. prázdné políčko je 0, křížek je 1, kolečko je 2). Napište funkci, která prohledá toto dvourozměrné pole a vrátí nejdelší souvislou posloupnost křížků nebo koleček: (a) na řádku, (b) ve sloupci, (c) diagonálně. 1.7 Pointery a struktury Víme, že ve struktuře lze mít položku, která je také strukturou, ovšem nikoliv tou právě definovanou. Někdy ovšem ve struktuře položku, která je stejného typu jako právě definovaná struktura potřebujeme. Tomuto požadavku můžeme vyhovět tak, že do struktury zahrneme pointer typu právě definované struktury. Celou situaci si můžeme ukázat na příkladu lineárního seznamu. Lineární seznam (dále již jen seznam) lze implementovat tak, že si zavedeme následující strukturu (pro seznam obsahující celá čísla). typedef struct _node int data; struct _node *next; node; 6 Pomocí jednorozměrného pole lze reprezentovat všechna vícerozměrná pole, ukážeme si to pro dvourozměrná, pro ostatní počty rozměrů je postup analogický

14 Při definici struktury _node můžeme už v těle struktury definovat jako položku ukazatel next ukazující na právě definovanou strukturu (tedy na typ struct _node). Poté pouze pomocí typedef typ pojmenujeme node. Každému číslu v seznamu bude odpovídat jedna struktura node, jejíž položka data bude obsahovat dané číslo. Říkejme takové struktuře uzel. Celý seznam si budeme v programu pamatovat jako pointer na první uzel, položka next první uzlu bude ukazovat na druhý uzel atd. Hodnota položky next posledního uzlu bude 0. Pro ukázku si uvedeme, jak by mohla vypadat funkce pro přidání prvku na začátek seznamu a vypsání všech prvků seznamu, další funkce si čtenář může naprogramovat jako cvičení. node *add(node **list, int key) node *new = malloc(sizeof(node)); new->data = key; new->next = *list; *list = new; return new; void print_list(node *list) /* nasledujici je idiom prochazeni seznamu */ while(list) printf("%i ",list->data); list = list->next; printf("\n"); int main() /* test */ node *list=0; int i; for(i=0;i<10;i++) add(&list,i); print_list(list); return 0; Protože funkce add mění začátek seznamu, je potřeba tuto informaci (tj. novou adresu prvního uzlu) dát vědět vně funkce. V příkladu jsme to udělali pomocí předání argumentu od- 0

15 kazem (argument tedy musí být pointer na pointer na node, uvnitř funkce pak musíme dereferencovat). Druhou možností by bylo adresu na nový první uzel vrátit jako návratovou hodnotu. Funkce print_list je příkladem použití idiomu pro procházení seznamu: pomocí list = list->next posuneme pointer list na další prvek; toto opakujeme, dokud se nedostaneme na konec seznamu, což poznáme tak, že se list rovná 0. V kódu si také všimněme, že pokud máme pointer na strukturu, přistupujeme k jejím položkám pomocí šipky (tvořené pomlčkou a většítkem) a nikoliv pomocí tečky. Úkoly 1. Naprogramujte funkci, která vytvoří kopii seznamu předaného jí jako argument. Funkce musí fungovat tak, že pokud změníme kopii, původní seznam se nezmění. 2. Naprogramujte funkci, které ze seznamu smaže první uzel obsahující číslo předané funkci jako parametr. Funkce daný uzel neuvolní z paměti, pouze ho odstraní ze seznamu a vrátí pointer na něj jako svou návratovou hodnotu. Pokud seznam uzel s daným číslem neobsahuje, vrátí funkce 0. 3. Naprogramujte funkci, která vrátí ukazatel na n-tý uzel v seznamu počítáno od konce. Pokud seznam nemá dostatečný počet prvků, vrátí funkce 0.