19. Dynamické proměnné
  1. Následující část programu přečte textový soubor a řádky textu uloží v operační paměti. Předpokládáme, že soubor obsahuje maximálně 1000 řádků textu (řádky textu nebudou delší než 99 znaků) a pro uložení řádku rezervujeme pouze potřebný počet slabik. Na závěr programu jsou použité dynamické proměnné uvolněny.

  2. char radek[100];
    char *radky[1000];
    FILE *soubor;
    int i, n = 0;
    if ((soubor = fopen("SOUBOR.TXT", "rt")) == NULL) {
      cout << "Soubor nelze otevřít" << endl;
      return 1;
    }
    while (fgets(radek, sizeof(radek), soubor) != NULL) {
      radky[n] = new char[strlen(radek) + 1];
      strcpy(radky[n++], radek);
    }
    for (i = 0; i < n; i++) delete[] radky[i];
    Změňte tento program tak, aby bylo možno specifikovat čtený soubor a aby obsah souboru uložený v paměti byl vypsán na obrazovku.
  3. V předchozím zadání jsme pro uložení ukazatelů na jednotlivé řádky textu použili statické pole (velikost pole je v našem případě omezena na 1000 řádků). Abychom toto omezení obešli použijeme spojový seznam s prvky

  4. struct prvek {
      prvek *dalsi;
      char radek[100];
    }
    Položka dalsi ukazuje na další prvek seznamu (další řádek textu) a položka radek obsahuje vlastní text řádku. Abychom se spojovým seznamem mohli pracovat, musíme mít statickou proměnnou, která ukazuje na začátek seznamu. Jelikož nově přečtené řádky přidáváme na konec seznamu je vhodné mít i ukazatel na konec seznamu.
    Následuje část programu vytvářející spojový seznam řádků a část uvolňující seznam.
    char radek[100];
    struct prvek {
      prvek *dalsi;
      char radek[100];
    } *zacatek = NULL, *konec, *pom;
    FILE *soubor;
    if ((soubor = fopen("SOUBOR.TXT", "rt")) == NULL) {
      cout << "Soubor nelze otevřít" << endl;
      return 1;
    }
    while (fgets(radek, sizeof(radek), soubor) != NULL) {
      pom = (prvek *)new char[strlen(radek)+sizeof(prvek *) + 1];
      strcpy(pom->radek, radek);
      if (zacatek == NULL) zacatek = pom;
        else konec->dalsi = pom;
      konec = pom;
      konec->dalsi = NULL;
    }

    while (zacatek->dalsi != NULL) {
      pom = zacatek;
      zacatek = zacatek->dalsi;
      delete[] pom;
    }
    Doplňte tento program o výpis v paměti uloženého obsahu souboru.


  5. C++ umožňuje pracovat s funkcemi, které mají stejné jméno ale různé parametry. Funkce, které sdílejí společné jméno, se nazývají překryté funkce. Předpokládejme, že máme funkci nasob, která násobí dva celočíselné parametry a vrací výsledek násobení. Můžeme ale také potřebovat podobnou funkci, která násobí dvě reálná čísla. V C bylo nutno používat různé funkce:

  6. int nasobInt(int cislo1, int cislo2);
    float nasobFloat(float cislo1, float cislo2);
    short nasobShort(short cislo1, short cislo2);
    V C++ ale můžeme pracovat s překrývajícími se funkcemi. Naše funkce tedy budou mít tyto prototypy:
    int nasob(int cislo1, int cislo2);
    float nasob(float cislo1, float cislo2);
    short nasob(short cislo1, short cislo2);
    Všechny funkce mají stejné jméno. Překladač pak určí volanou funkci na základě předaných parametrů. Např.
    float x = 1.5;
    float y = 17.2;
    float vysledek = nasob(x, y);
    Překladač vidí dva parametry typu float, které jsou předávány funkci nasob a volá funkci nasob s dvěma parametry typu float. Pokud parametry budou typu int, pak bude použita verze funkce s parametry typu int.
    Volaná funkce je tedy určena seznamem parametrů (každá deklarace překrývající funkce musí mít jiný seznam parametrů; tj. počet parametrů a typy parametrů). Nelze vytvořit překryté funkce, které se budou lišit pouze vrácenou hodnotou.
    Překladač rozlišuje překryté funkce pomocí procesu nazvaného komolení jmen. Komolení jmen spočívá v zakódování počtu a typu parametrů v interním jméně funkce, které je udržováno překladačem.
  7. Používání překrytých funkcí vyžaduje použití správných datových typů při volání funkce. Při převodu datového typu používáme přetypování, tj. převod jednoho datového typu na jiný. Např.

  8. float x = (float)10 * 5.5;
    V tomto případě přetypování říká překladači "udělej z čísla 10 typ float". Druhý operand je automaticky interpretován jako typ float, protože obsahuje desetinnou tečku. Podívejte se ještě na následující příklad:
    int cele = 5;
    float realne = 10.5;
    float vysledek = nasob(cele, realne);
    V tomto případě překladač generuje chybu, protože nemůže nalézt funkci nasob s prvním parametem typu int a druhým parametrem typu float. Podobnou chybu bude generovat i v případě příkazu:
    int vysledek = nasob(10, 10);
    Zde překladač nedokáže určit jakého typu jsou použité číselné konstanty. V této situaci máme dvě možnosti. Konstanty před voláním funkce přiřadíme do proměnných nebo provedeme při volání přetypování. První možnost ukazuje následující kód:
    int x = 10;
    int y = 10;
    int vysledek = nasob(x, y);
    a druhou možnost kód:
    int vysledek = nasob((int)10, (int)10);
    Nyní překladač ví, že má tyto konstanty brát jako typ int. Přetypování lze také použít k dočasné změně jednoho datového typu na jiný. Např.
    int cele = 5;
    float realne = 10.5;
    float vysledek = nasob((float)cele, realne);
    V tomto případě je první parametr převeden na typ float a překladač je již schopen najít požadovanou funkci.
  9. Funkce implementující implicitní parametry může vypadat takto:

  10. // deklarace - parametr "poprve" má implicitní hodnotu false
    void Redraw(bool poprve = false);
    // definice
    void Redraw(bool poprve)
    {
      if (poprve){
        // kód pro první kreslení
      }
      // kód kreslení
    }
    Tuto funkci můžeme volat s nebo bez parametru. Pokud je při volání funkce parametr uveden, pak funkce se chová jako normální funkce. Pokud při volání funkce parametr není uveden, pak je automaticky použita implicitní hodnota parametru. Pro náš příklad jsou následující dva řádky kódu identické:
    Redraw();
    Redraw(false);
    Když parametr má implicitní hodnotu, pak při volání funkce může být vynechán. V jedné funkci mohou být také implicitní i neimplicitní parametry:
    int PlaySound(char* name, bool loop = false, int loops = 10);
    // volání funkce
    int vysl;
    vysl = PlaySound("chime.wav");
    vysl = PlaySound("ding.wav", true);
    vysl = PlaySound("bell.wave", true, 5);
    Implicitní parametry jsou z mnoha důvodů užitečné. Pokud při volání některé funkce používáme často stejné parametry, pak použití implicitních parametrů nám usnadní práci.
    Implicitní parametry musíme uvádět na konci seznamu parametrů. Následující deklarace funkce je chybná:
    int mojeFunkce(int x, int y = 10, int t = 5, int z);
    Implicitní parametry musíme přesunout na konec seznamu parametrů:
    int mojeFunkce(int x, int z, int y = 10, int t = 5);
  11. Metody (členské funkce třídy) se používají stejně jako normální funkce. Mohou být překryté, mohou mít implicitní parametry atd. Metody mohou být volány pouze prostřednictvím objektů třídy, do které metoda patří. K volání metody používáme přímý nebo nepřímý selektor složky. Např. máme třídu nazvanou Letadlo, kterou budeme používat v simulačním programu letadla. Tato třída pravděpodobně bude mít možnost získat současnou rychlost daného letadla pomocí metody nazvané ziskejRychlost. Následující příklad ukazuje, jak voláme tuto metodu objektu Letadlo:

  12. Letadlo sokol;    // vytvoření instance třídy
    int rychlost = sokol.ziskejRychlost();
    cout << "Současná rychlost letadla je " << rychlost << endl;
    Tento kód používá k volání funkce ziskejRychlost operátor přímého selektoru složky. Metody definujeme podobně jako normální funkce, je zde zapotřebí navíc uvést jméno třídy a operátor ::. Např. definice naší metody může vypadat takto:
    int Letadlo::získejRychlost()
    {
      return rychlost;    // rychlost je datová složka třídy
    }
    Operátor :: říká překladači, že funkce získejRychlost je metodou třídy Letadlo. Více o třídách a metodách se dozvíme později.
  13. Normálně strojový kód funkce se v přeloženém proveditelném kódu vyskytuje pouze jednou. Při volání funkce je proveden skok na vstupní bod funkce a při ukončení funkce se opět vrátíme na místo volání funkce. Existují také vložené (inline) funkce, kde kód funkce je vložen do proveditelného kódu programu na místo volání funkce. Vložené funkce se deklarují podobně jako normální funkce, ale jejich definice začíná klíčovým slovem inline. Pokaždé, když překladač ve zdrojovém kódu nalezne volání vložené funkce, pak umístí separátní kopii kódu funkce na toto místo do přeloženého programu. Vložené funkce pracují rychle, protože není zapotřebí provádět volání funkce.

  14. Je vhodné, aby vložené funkce byly velmi malé. Delší funkce není vhodné používat jako vložené, neboť výsledkem je zvětšení přeloženého spustitelného programu.
    Vložené funkce jsou obvykle metody. Často definici vložené funkce umisťujeme do hlavičkového souboru deklarace třídy. Metoda ziskejRychlost z předchozího zadání je krátká a je tedy možno z ní udělat vloženou funkci. To provedeme takto:
    inline int Letadlo::získejRychlost()
    {
      return rychlost;    // rychlost je datová složka třídy
    }

Nové pojmy:

Kontrolní otázky:

  1. Musíme vždy zrušit objekt, který byl vytvořen dynamicky pomocí operátoru new?
  2. Kdy vytváříme objekty v zásobníku a kdy v hromadě?
  3. Co to jsou překryté funkce?
  4. Kdy používáme vložené funkce?
  5. Co je to ukazatel?
  6. Co dělá dereference ukazatele?
  7. Co je vrácená hodnota operátoru new?
  8. Lze instance tříd a struktur předávat funkcím odkazem nebo hodnotou?
  9. Co dělá klíčové slovo const?
  10. Vytvářejí následující deklarace překryté funkce?

  11. void mojeFunkce(int x);
    int mojeFunkce(int x);
  12. Je lepší používat odkazy nebo ukazatele?
  13. Co je metoda?
  14. Jak se liší zacházení překladače s vloženou funkcí od normální funkce?
  15. Co je chybné na následující ukázce?

  16. char* buff = new char[200];
    // a později
    delete buff;
Řešení


19. Dynamické proměnné