18. Ukazatele II
  1. Odkazy jsou speciálním typem ukazatelů, které umožňují pracovat s ukazateli jako s normálními objekty. Odkazy jsou deklarovány pomocí operátoru &. Např.

  2. mojeStruktura* ukStrukt = new mojeStruktura;
    mojeStruktura& odkaz = *ukStrukt;
    odkaz.X = 100;
    Povšimněte si, že při přístupu ke složkám struktury jsou použity přímé selektory složky. V reálných programech obvykle není zapotřebí udržovat při použití odkazu ukazatel na strukturu a předchozí zápis lze zkrátit takto:
    mojeStruktura& odkaz = *new mojeStruktura;
    odkaz.X = 100;
    Nyní se opět vrátíme k naší konzolové aplikaci adresáře našich známých. Řešení z konce předchozí kapitoly můžeme změnit takto:
    #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 {
        adresar& zaznam = *seznam[index];
        cout << "Jméno: ";
        cin.getline(zaznam.jmeno, sizeof(zaznam.jmeno)-1);
        cout << "Příjmení: ";
        cin.getline(zaznam.prijmeni, sizeof(zaznam.prijmeni)-1);
        cout << "Ulice: ";
        cin.getline(zaznam.ulice, sizeof(zaznam.ulice)-1);
        cout << "Město: ";
        cin.getline(zaznam.mesto, sizeof(zaznam.mesto)-1);
        cout << "Psč: ";
        char buff[10];
        cin.getline(buff, sizeof(buff)-1);
        zaznam.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 opět zobrazeny červeně. Povšimněte si deklarace odkazu na strukturu adresar. Při každém průchodu cyklem je odkazu přiřazen jiný objekt (následující prvek v poli). Pro přístup k prvkům struktury nyní používáme operátor přímého selektoru složky. Jak uvidíme pozdějí, odkaz umožňuje chápat ukazatel jako objekt. Výsledkem používání odkazu dostáváme kratší a čitelnější kód.
    Přestože odkazy jsou preferované před ukazateli, není tomu tak vždy. Odkazy nelze použít ve všech případech. Např. odkaz nemůže být deklarován bez přiřazení hodnoty. Musí být inicializován při deklaraci. Následující kód způsobí chybu překladu:
    mojeStruktura* ukStruktura = new mojeStruktura;
    mojeStruktura& odkaz;
    odkaz = *ukStruktura;
    odkaz.X = 100;
    Další problém s odkazem je ten, že jej nelze nastavit na NULL nebo 0, což u ukazalele lze.
  3. Parametry u funkcí jsme zatím předávali hodnotou (v předchozí kapitole byla ukázka předávání parametrů odkazem). V případě struktur a tříd je ale lepší předávat tyto objekty odkazem. Odkazem může být předán libovolný objekt (standardní datové typy jako je int nebo char a také instance struktur nebo tříd). Při předávání parametru funkce hodnotou je vytvořena kopie objektu a funkce pracuje s touto kopií. Když předáváme parametr odkazem, pak je předán ukazatel na objekt a ne objekt samotný. To má dvě výhody. Objekt předaný funkci může být funkcí modifikován a předávání odkazem eliminuje režijní náklady na vytvoření kopie objektu.

  4. Možnost modifikace objektu je důležitým aspektem při předávání parametrů odkazem. Podívejte se na následující kód:
    void Inkrementace(int& xPos, int& yPos)
    {
      xPos++;
      yPos++;
    }
    int x = 20;
    int y = 40;
    Inkrementace(x, y);
    // x je nyní rovno 21 a y 41
    Povšimněte si, že po návratu z funkce jsou oba předané parametry zvětšeny o 1. To je z důvodu modifikace aktuálního objektu funkcí prostřednictvím ukazatele (nezapomeňte, že odkaz je typ ukazatele).
    Funkce může vracet pouze jednu hodnotu. Pomocí parametrů předávaných odkazem, můžeme dosáhnout efektu návratu více hodnot. Funkce stále vrací pouze jednu hodnotu, ale objekty předané odkazem mohou být aktualizovány a funkce tak zdánlivě vrací více hodnot.
    Dalším důvodem pro předávání parametrů odkazem je eliminace režijních nákladů spojených s vytvářením kopie objektů při volání funkce. Při používání standardních datových typů jsou režijní náklady na vytvoření kopie zanedbatelné. Při práci se strukturami a třídami mohou být značné. Struktury v libovolné situaci lze předat odkazem (viz následující ukázka):
    void nejakaFunkce(mojeStruktura& s)
    {
      // uděláme něco se s
      return;
    }
    mojeStruktura mojeStr;
    ....
    nejakaFunkce(mojeStr);
    Použití odkazu umožňuje modifikaci objektu předaného funkci. Někdy se ale můžeme dostat do situace, kdy této modifikaci chceme zabránit.
  5. Jestliže proměnnou deklarujeme s klíčovým slovem const, pak nemůžeme změnit její hodnotu. Stejné řešení lze použít i při předávání parametru funkcí odkazem a udělat tak konstantní objekt:

  6. void nejakaFunkce(const mojeStruktura& s)
    {
      // uděláme něco se s
      return;
    }
    mojeStruktura mojeStr;
    ....
    nejakaFunkce(mojeStr);
    Nyní můžeme funkci předávat objekt a nemusíme se obávat, že jej funkce změní. Pokus o modifikaci konstantního objektu uvnitř funkce způsobí při překladu chybu. Např.
    void nejakaFunkce(const mojeStruktura& s)
    {
      s.slozka = 100;    // chyba, konstantní objekt nelze modifikovat
      return;
    }
    Takovýto objekt je konstantní pouze uvnitř funkce. Před voláním funkce a po návratu z funkce jej lze modifikovat (pokud není původně deklarován jako konstantní).
    Když si máme vybrat, zda parametr budeme předávat odkazem nebo ukazatelem, pak většinou dáváme přednost předávání odkazem. Při předávání znakového pole je ale výhodnější použít předávání parametru ukazatelem (ukazatel na znakové pole a jméno pole jsou zaměnitelné).
  7. Změňte naši aplikaci s adresářem našich známých tak, že u funkce zobrazZaznam budete strukturu předávat odkazem.
  8. Při vytváření a rušení dynamických proměnných se používají operátory new a delete. S operátorem new jsme se již seznámili. K uvolnění paměti používáme operátor delete.

  9. Jak již bylo uvedeno dříve, paměť můžeme alokovat lokálně (v zásobníku) nebo dynamicky (v hromadě). Následující ukázka kódu alokuje dvě pole. Jedno je alokováno v zásobníku (lokální alokace) a druhé v hromadě (dynamická alokace):
    char buff[80];
    char* velkyBuff = new char[4096];
    V prvním případě velikost alokované paměti je malá a není podstatné, zda bude použit zásobník nebo hromada. V druhém případě se jedná o velké pole a je tedy vhodné jej alokovat v hromadě (šetříme místem v zásobníku). V případě polí (řetězec je pole typu char), dynamické a lokální instance jsou zaměnitelné. Tzn. používají stejnou syntaxi:
    strcpy(buff, "Ahoj");
    strcpy(velkyBuff, "Hodně dlouhý řetězec ......");
    // a později
    strcpy(velkyBuff, buff);
    Jméno pole bez operátoru indexace ukazuje na začátek pole. Ukazatel také ukazuje na začátek pole a je tedy jedno zda použijeme jméno pole nebo ukazatel na pole.
    Pokud operátor new nemůže alokovat požadovanou paměť, pak vrací NULL. To můžeme testovat např. takto:
    char* buff = new char[1024];
    if (buff) strcpy(buff, "Nějaký text");
    else SignalizaceChyby();
    Jestliže alokujeme velmi velkou oblast paměti nebo alokujeme paměť v kritické oblasti programu, pak je vhodné testovat ukazatel na přípustnost (úspěšnost alokace). Běžné alokace lze ponechat bez testování.
  10. Všechna alokovaná paměť musí být dealokována (uvolněna), když ji již nepotřebujeme. U lokálních objektů to probíhá automaticky. Při použití dynamické alokace, je za uvolňování zodpovědný programátor a k uvolňování paměti používá operátor delete. Všechna volání new mají své odpovídající delete. Operátory new a delete tvoří dvojici.

  11. Použití operátoru delete je snadné:
    nejakyObjekt* mujObjekt = new nejakyObjekt;
    // nějaká činnost s objektem
    delete mujObjekt;        // objekt je zrušen
    Při používání samotného operátoru delete nejsou žádné problémy, ale je několik věcí spojených s ukazateli a delete, na které musíme dávat pozor. Za prvé nesmíme zrušit ukazatel (přesněji řečeno objekt na který ukazatel ukazuje), který již byl zrušen (vznikne chyba přístupu a řada dalších problémů). Za druhé můžeme zrušit ukazatel, který byl nastaven na 0.
    Někdy deklarujeme ukazatel pro případ, kdy by mohl být použit, ale nevíme jistě zda v dané instanci našeho programu byl skutečně použit. Např. můžeme mít objekt, který je vytvářen, když uživatel provede jistou volbu v nabídce. Pokud uživatel nikdy tuto volbu neprovede, pak objekt nebude vytvořen. Problémem je, že v případě vytvoření objektu je zapotřebí zrušit ukazatel a nerušit jej, když objekt nebyl vytvořen. Zrušením neinicializovaného ukazatele si způsobíme problémy, protože nevíme na kterou část paměti ukazuje.
    Jak již bylo uvedeno dříve je vhodné inicializovat ukazatel nulou, pokud jej přímo neinicializujeme normální hodnotou. To je ze dvou důvodů vhodný způsob. První důvod byl popsán výše; neinicializovaný ukazatel obsahuje náhodnou hodnotu, což je nežádoucí. Druhým důvodem je to, že zrušení ukazatele NULL je přípustné (můžeme zrušit tento ukazatel a nemusíme se zabývat tím, zda jej uživatel použil):
    ukazatel* nekdy = 0;
    // možné použití nekdy
    delete nekdy;
    Zrušení ukazatele v tomto případě je v pořádku a ať již ukazatel ukazuje na objekt nebo je NULL.
    Můžeme se také dostat do situace, kdy vytvoříme objekt v jedné části programu a zrušíme jej v jiné části programu. Přitom část kódu rušící objekt nemusí být nikdy provedena. V tomto případě by bylo vhodné zrušit objekt při ukončení programu (v některých případech ale objekt již mohl být zrušen). K zabránění dvojnásobného zrušení je vhodné ukazatel po zrušení nastavit na NULL (nebo 0):
    ukazatel* vytvoren = new ukazatel;
    // později
    delete vytvoren;
    vytvoren = 0;
    Pokud nyní použijeme na tento objekt dvakrát operátor delete, pak se nic nestane, neboť není chybou zrušit ukazatel NULL.
    Jinou možností, jak zabránit problému dvojnásobného zrušení je testování ukazatele na nenulovou hodnotu před použitím delete:
    if (nekdy) delete nekdy;
    To ale také předpokládá nastavení ukazatele na 0 při jeho zrušení.
    Pokud při dynamickém vytvoření objektu použijeme odkaz, pak syntaxe rušení je jiná. Následující příklad ukazuje tento problém:
    mojeStruktura& odkaz = *new mojeStruktura;
    odkaz.X = 100;
    delete &odkaz;
    Vidíme, že rušení ukazatele v případě odkazu vyžaduje použít adresový operátor. Odkaz nelze nastavit na 0 a tedy musíme být opatrní a nerušit odkaz dvakrát.
  12. Když se vrátíme k našemu programu adresáře našich známých, tak vidíme, že program je chybný (ve verzi, kde pracujeme s ukazateli a verzi s odkazy). Dynamicky alokovaná paměť není uvolňována. Vytvořené pole struktur alokovaných v hromadě není nikdy uvolněno z paměti. Na konec našeho programu (za řádek getch();) je nutno vložit:

  13. for (int i = 0; i < 3; i++)
      delete seznam[i];
    Nyní již máme spráný program. Projdeme pole ukazatelů a zrušíme každý z nich samostatně.
  14. Když voláme new k vytvoření pole, pak můžeme použít verzi new[] tohoto operátoru. Není důležité znát jak toto pracuje, ale je nutno vědět jak probíhá rušení dynamicky alokovaných polí. Použijeme jednu z předchozích ukázek, na jejíž konec přidáme příkaz delete[]:

  15. char buff[80];
    char* velkyBuff = new char[4096];
    strcpy(buff, "Ahoj");
    strcpy(velkyBuff, "Hodně dlouhý řetězec ......");
    // a později
    delete[] velkyBuff;
    Je zde použit operátor delete[]. Nebudeme se zde zabývat technickým popisem, ale můžeme si být jisti, že všechny prvky pole jsou zrušeny. 
  16. Následující konzolová aplikace deklaruje dvourozměrné pole dynamicky. V našem případě má pole tři řádky a pět sloupců, ale jeho rozměry můžeme snadno modifikovat. Dvourozměrné pole je tvořeno jednorozměrným polem s ukazateli na jednotlivé řádky, tj. s ukazateli opět na jednorozměrná pole.

  17. #include <iostream.h>
    #include <conio.h>
    void display(long double **);
    void de_allocate(long double **);
    int m = 3;                               // Počet řádků.
    int n = 5;                               // Počet sloupců.
    int main(int argc, char **argv) {
       long double **data;
       data = new long double*[m];           // Krok 1: Nastavení řádků.
       for (int j = 0; j < m; j++)
          data[j] = new long double[n];      // Krok 2: Nastavení sloupců
       for (int i = 0; i < m; i++)
          for (int j = 0; j < n; j++)
              data[i][j] = i + j;            // Inicializace pole
       display(data);
       de_allocate(data);
       getch();
       return 0;
    }
    void display(long double **data) {
       for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++)
                 cout << data[i][j] << " ";
           cout << "\n" << endl;
           }
       }
    void de_allocate(long double **data) {
       for (int i = 0; i < m;  i++)
           delete[] data[i];                 // Krok 1: Zrušení sloupců
       delete[] data;                        // Krok 2: Zrušení řádků
    }
    Snažte se pochopit, jak tento program pracuje. Změňte tento program tak, aby rozměry pole bylo možno zadat až při spuštění programu.
  18. Hlavičkový soubor ctype.h obsahuje řadu funkcí testujících znaky (isalnum, isalpha, isdigit, islower a další) a funkcí převádějících znaky (toascii, tolower, toupper, atd.). Seznamte se s jejich používáním (pomocí nápovědy).

Základní pravidla pro používání ukazatelů a dynamické alokace paměti:

18. Ukazatele II