17. Ukazatele I
  1. Ukazatele dělíme do dvou základních kategorií: ukazatele na data (objekty) a ukazatele na funkce. Oba dva typy ukazatelů jsou speciální objekty, které obsahují adresy v paměti. Oba dva typy ukazatelů mají různé vlastnosti, účely použití a pravidla zacházení. Obecně řečeno, ukazatelé na funkce se používají pro přístup k funkcím a pro předávání funkcí jako parametrů jiným funkcím. S těmito ukazateli není možné provádět žádné aritmetické operace. Ukazatele na objekty můžeme inkrementovat a dekrementovat tak, jak je to při prohlížení polí nebo složitějších struktur potřeba.

  2. Na ukazatel na funkci se můžeme dívat jako na adresu, kde je uchován proveditelný kód funkce, tj. na adresu, kam je při volání funkce předáno řízení. Ukazatel na funkci má typ "ukazatel na funkci s určitým počtem a typy parametrů a vracející jistý typ" (v jazyce C na parametrech nezáleží). Např. po deklaraci
    void (*fce)(int);
    je fce ukazatel na funkci s parametrem typu int, která nic nevrací.
  3. Následující konzolová aplikace ukazuje použití ukazatele na funkci. Program se nejprve zeptá, zda se má provádět sčítání nebo násobení. Podle této odpovědi vloží do proměnné operace ukazatel na funkci sečti nebo na funkci nasob. Dále zadáme dvě čísla, která se použijí jako parametry vybrané funkce.

  4. int secti(int a, int b)
    {
      return a+b;
    }
    int nasob(int a, int b)
    {
      return a*b;
    }
    int main(int argc, char **argv)
    {
      int (*operace) (int, int);
      int x, y, volba;
      cout << "Budeme Sčítat (1) nebo Násobit (2) ?";
      do {
        cin >> volba;
      } while (volba != 1 && volba != 2);
      if (volba == 1) operace = secti;
      if (volba == 2) operace = nasob;
      cout << "Zadej dvě celá čísla: ";
      cin >> x >> y;
      cout << "Výsledek je " << operace(x, y) << endl;;
      getch();
      return 0;
    }
    S použitím ukazatele na funkci vytvořte program vypisující tabulku malé násobilky nebo obdobnou tabulku sčítání. Výpis celé tabulky řešte jako funkci (ukazatel na funkci provádějící operaci předávejte jako parametr funkce).
  5. Většinou se budeme ale zabývat ukazateli na data. Ukazatel na data musí být deklarován tak, aby ukazoval na nějaký konkrétní typ, a to i v případě, že tento typ je void (což vlastně znamená ukazatel na cokoliv). Je-li typ libovolný předdefinovaný nebo uživatelem definovaný typ (včetně void), potom deklarace

  6. typ *ptr;            // pozor - neinicializovaný ukazatel
    deklaruje objekt ptr typu "ukazatel na typ". Než začneme ukazatel používat, musíme jej nejprve inicializovat. Prázdný ukazatel (ukazatel, který na nic neukazuje) by měl obsahovat adresu, u které je zaručeno, že se bude lišit ode všech platných adres v daném programu (adresa 0). Pro lepší čitelnost programů je tato adresa (prázdný ukazatel) označena symbolickou konstantou NULL (je definována např. v stdlib.h). Všechny ukazatele je možné testovat na rovnost či nerovnost s hodnotou NULL.
    Ukazatel typu "ukazatel na void" nesmí být zaměňován s prázdným ukazatelem. Při přiřazování hodnot ukazatelů je nutno, aby ukazatelé byly stejného typu, nebo aby jeden z nich byl typu "ukazatel na void". Jinak je nutno provést přetypování.
  7. Podívejme se na příklad. Předpokládejme, že máme pole prvků typu int. Jednotlivé prvky pole můžeme zpřístupňovat pomocí operátoru indexace. To již známe z dřívějška.

  8. int pole[] = {5, 10, 15, 20, 25};
    int promenna = pole[3];        // hodnota 20
    Totéž můžeme provést pomocí ukazatele:
    int pole[] = {5, 10, 15, 20, 25};
    int* ptr = pole;
    int promenna = ptr[3];
    V tomto příkladě adresa paměti začátku pole je přiřazena ukazateli ptr. Tento ukazatel je ukazatelem na datový typ int (při deklaraci je použit symbol *). Můžeme deklarovat ukazatel na libovolný celočíselný datový typ, stejně jako na objekty (struktury nebo třídy). Po přiřazení, ukazatel obsahuje paměťovou adresu začátku pole a tedy ukazuje na pole (jméno proměnné pole použité bez operátoru indexace, vrací adresu prvního prvku pole).
    V tomto případě můžeme pro přístup k poli používat ukazatel i jméno pole. U dynamických objektů je možno použít pouze ukazatel (viz dále).
  9. Aritmetika ukazatelů je omezena na sčítání, odčítání a porovnávání ukazatelů. Aritmetické operace na ukazatelích typu "ukazatel na typ" berou v úvahu délku typu typ, tzn. počet slabik potřebných na uchování objektu typu typ. Při provádění aritmetických operací se předpokládá, že ukazatel ukazuje do pole objektů. Je-li tedy ukazatel deklarován jako ukazatel na objekt typu typ, potom přičtení celočíselné hodnoty k tomuto ukazateli jej posune o tento počet objektů typu typ. Má-li typ typ velikost 10 slabik, potom přičtením hodnoty 5 k tomuto ukazateli jej posuneme o 50 slabik v paměti dále. Rozdílem dvou ukazatelů je počet prvků pole, které navzájem oddělují tyto dva ukazatele. Rozdíl ukazatelů má smysl pouze v případě, kdy oba ukazují do stejného pole.

  10. Hodnotu ukazatele je možné převést na hodnotu jiného typu ukazatele za pomocí mechanismu přetypování:
    char *str;
    int *ip;
    str = (char *) ip;
    Obecně platí, že operátor přetypování (typ *) převádí daný ukazatel na typ "ukazatel na typ".
  11. Všechny příklady konzolových aplikací, se kterými jsme se zatím seznámili používají lokální alokaci objektů. Tzn. paměť požadovaná pro proměnnou nebo objekt je získána ze zásobníku programu. Všechnu paměť, kterou program potřebuje pro lokální proměnné, volání funkcí apod. bere ze zásobníku. Tato paměť je alokována, když je zapotřebí a uvolňována, když již zapotřebí není. To obvykle nastává, když program vstupuje do funkce nebo jiného lokálního bloku kódu. Paměť pro všechny lokální proměnné funkce je alokována při vstupu do funkce. Při výstupu z funkce, je všechna paměť alokovaná funkcí uvolněna. Toto probíhá automaticky a nemáme možnost určit jak a kdy paměť bude uvolněna.

  12. Lokální alokace má výhody a nevýhody. Výhodou je, že paměť může být v zásobníku alokována velmi rychle. Nevýhodou je, že zásobník má pevnou velikost a nemůže být změněn za běhu programu. Pokud náš běžící program přečerpá kapacitu zásobníku, pak je program ukončen chybou.
    Pro proměnné standardních datových typů a malá pole je lokální alokace vhodným řešením. Když ale začneme používat velká pole, struktury a třídy, pak budeme potřebovat dynamickou alokaci v hromadě.
    Hromada zahrnuje všechnu volnou operační paměť počítače a volné místo na disku (použité pro odkládací soubory). Velikost této paměti bývá obvykle asi 100 Mslabik. Hromada je tedy podstatně větší než zásobník. S dynamicky alokovanou pamětí se ale hůře pracuje a je to také pomalejší. Neobejdeme se ale bez ní.
    V předchozích kapitolách jsme se zabývali konzolovou aplikací udržující adresář našich známých. Při lokální alokaci struktury jsme používali příkazy:
    adresar zaznam;
    strcpy(zaznam.jmeno = "Karel");
    strcpy(zaznam.prijmeni = "Novak");
    // atd.
    Při dynamické alokaci používáme operátor new a v tomto případě bychom zapsali:
    adresar* zaznam;
    zaznam = new adresar;
    strcpy(zaznam->jmeno = "Karel");
    strcpy(zaznam->prijmeni = "Novak");
    // atd.
    První řádek deklaruje ukazatel na strukturu adresar. Další řádek inicializuje tento ukazatel vytvořením nové dynamické instance struktury adresar. Takto vytváříme a zpřístupňujeme objekty. V dalších příkazech vidíme nahrazení operátoru přímého selektoru složky (.) operátorem nepřímého selektoru složky (->).
    Dynamicky vytvářené pole struktur vyžaduje více práce. V lokální verzi použijeme např.
    adresar seznam[3];
    seznam[0].pcs = 53002;
    zatímco v dynamické verzi je nutmo postupovat takto:
    adresar* seznam[3];
    for (int i = 0; i < 3; i++)
      seznam[i] = new adresar;
    seznam[0]->pcs = 53002;
    Vidíme, že musíme vytvořit novou instanci struktury samostatně pro každý prvek pole. Přístup k datovým složkám pole provádíme operátorem indexace a operátorem nepřímého selektoru složky.
  13. Operátory & a * jsou operátory odkazu (reference) a dereference. Např.

  14. &typový_výraz
    převádí typový_výraz na ukazatel na typový_výraz. Všimněte si, že identifikátory některých objektů (např. jména funkcí a jména polí) jsou v jistém kontextu automaticky převedeny na typ "ukazatel na objekt". Operátor & je možné s takovýmito objekty používat, ale jeho výskyt je vlastně zbytečný (a matoucí). Operátor & říká překladači "Dej mě adresu proměnné a ne obsah proměnné".
    Ve výrazu
    * typový_výraz
    musí být operand typový_výraz typu "ukazatel na typ", kde typ je libovolný typ. Výsledek dereference je typ typ.
  15. Pokuste se určit co dělají následující příkazy:

  16. int i, *ukazatel = &i;
    cin >> i;
    cout << ukazatel << endl << *ukazatel << endl;
  17. Předpokládejte deklarace:

  18. int promenna;
    int *ukazatel = &promenna;
    int **ukazatel_na_ukazatel = &ukazatel;
    Co provádějí následující příkazy:
    **ukazatel_na_ukazatel = 10; *ukazatel = 10;
    promenna = 10; *ukazatel_na_ukazatel = &promenna;
  19. Neinicializovaný ukazatel obsahuje, stejně jako jiná neinicializovaná proměnná, náhodnou hodnotu. Pokus o použití neinicializovaného ukazatele může způsobit havárii programu. V mnoha případech provádíme současnou deklaraci a inicializaci ukazatele. Např.

  20. adresar* pom = 0;
    Pokud se pokusíme použít ukazatel NULL (ukazatel nastavený na NULL nebo nulu), je automaticky procesorem detekován pokus o nedovolný přístup k paměti (program je ukončen, ale nemohou nastat různé náhodné chyby).
    Povšimněte si ještě zápisu operátoru * (přesněji řečeno používání mezer před a za *). Je jedno zda zapisujeme
    int* i;
    int *i;
    int * i;
    Všechny tyto zápisy jsou ekvivalentní a je jedno, kterou možnost zvolíme. Je ale vhodné jednu z těchto možností si vybrat a dodržovat ji.
  21. Zadání 4 z kapitoly 15 nyní změníme na použití dynamické alokace. Náš program se změní takto (hlavičkový soubor zůstane beze změny):

  22. #include <iostream.h>
    #include <conio.h>
    #include <stdlib.h>
    #pragma hdrstop
    #include "structur.h"
    //---------------------------------------------------------------------
    #pragma argsused
    void zobrazZaznam(int, adresar adrZaz);
    int main(int argc, char **argv)
    {
      adresar* seznam[3];
      for (int i = 0; i < 3; i++)
        seznam[i] = new adresar;
      cout << endl;
      int index = 0;
      do {
        cout << "Jméno: ";
        cin.getline(seznam[index]->jmeno, sizeof(seznam[index]->jmeno)-1);
        cout << "Příjmení: ";
        cin.getline(seznam[index]->prijmeni,
                    sizeof(seznam[index]->prijmeni)-1);
        cout << "Ulice: ";
        cin.getline(seznam[index]->ulice, sizeof(seznam[index]->ulice)-1);
        cout << "Město: ";
        cin.getline(seznam[index]->mesto, sizeof(seznam[index]->mesto)-1);
        cout << "Psč: ";
        char buff[10];
        cin.getline(buff, sizeof(buff)-1);
        seznam[index]->psc = atoi(buff);
        index++;
        cout << endl;
      } while (index < 3);
      clrscr();
      for(int i = 0; i < 3; i++) {
        zobrazZaznam(i, *seznam[i]);
      }
      cout << "Zadej číslo záznamu: ";
      int zaz;
      do {
        zaz = getch();
        zaz -= 49;
      } while (zaz < 0 || zaz > 2);
      adresar pom = *seznam[zaz];
      clrscr();
      zobrazZaznam(zaz, pom);
      getch();
      return 0;
    }
    void zobrazZaznam(int cis, adresar adrZaz)
    {
      cout << "Záznam " << (cis + 1) << ":" << endl;
      cout << "Jméno: " << adrZaz.jmeno << " " << adrZaz.prijmeni << endl;
      cout << "Adresa: " << adrZaz.ulice << endl;
      cout << "        " << adrZaz.mesto << endl;
      cout << "        " << adrZaz.psc << endl << endl;
    }
    Změněné řádky jsou v předchozím výpisu zobrazeny červeně. Je deklarováno pole ukazatelů, pro každý prvek pole je vytvořena instance, operátory přímého selektoru složky jsou v cyklu nahrazeny operátory nepřímého selektoru složky a dvakrát byl použit operátor dereference. Funkce zobrazZaznam zůstala beze změny.
    Toto řešení není ideální, bude ještě vylepšeno.
  23. Parametry funkcí v jazyku C jsou volány hodnotou. To znemožňuje funkci měnit hodnotu parametru tak, aby změněná hodnota byla použitelná mimo funkci, tzn. jazyk C nepoužívá parametry volané odkazem. Tento problém lze vyřešit pomocí ukazatelů. Funkci předáme jako parametr adresu proměnné, s jejiž hodnotou pak budeme pracovat. Např. následující funkce zaměňuje hodnoty svých parametrů:

  24. void zamen(int *a, int *b)
    {
      int c = *a;
      *a = *b;
      *b = *c;
    }
    Funkci pak vyvoláme např. takto:
    int x = 10, y = 20;
    zamen(&x, &y);
    Upravte tuto funkci tak, aby výměna prvků proběhla pouze když a > b a aby funkční hodnota informovala zda k výměně došlo. Vyzkoušejte v nějakém programu.
  25. Hlavičkový soubor stdlib.h obsahuje řadu různých funkcí. Jsou zde např. funkce provádějící celočíselné dělení a určující maximální a minimální hodnotu ze dvou hodnot (div, ldiv, max a min). Dále tu jsou funkce provádějící převody typů (atoi, atol, ecvt, strtod a řada dalších). Jsou zde také funkce pro generování náhodných čísel, s kterými jsme se již seznámili. Seznamte se s těmito funkcemi pomocí ukázkových programů v nápovědě.

Nové pojmy:


 
17. Ukazatele I