Kurz C++ (7.)


Struktury

Základy

K vysvětlení struktur si zase vezmu na pomoc příklad adresáře osob. Ukázali jsme si, že pomocí pole můžeme jednoduše ukládat do jedné proměnné víc údajů stejného druhu. Například do pole řetězců bychom mohli ukládat jména. Ale my potřebujeme ukládat i adresy, data narození, prostě spoustu údajů. Můžeme si poradit dalšími poli, ale sami asi uznáte, že to není příliš elegantní. Mnohem lepší řešení je použití tzv. struktur. Jsou to "složené proměnné", tedy takové proměnné, které obsahují jiné proměnné (složky). Struktury se deklarují klíčovým slovem struct:

struct Clovek {
  char jmeno[30];
  char adresa[50];
  unsigned vek;
};

Deklarovali jsme nový datový typ: strukturu, která se nazývá Clovek, a má 3 složky: dva řetězce (jmeno a adresa) a jednu celočíselnou proměnnou (vek). Pozor na to, že deklarace struktury musí končit středníkem.


Poznámka: je zvykem deklarovat struktury mimo těla funkcí. Deklarujte tedy strukturu Clovek mimo funkci main().

Protože se jedná o datový typ, deklaraci proměnných provedeme tak, jak jsme zvyklí:

Clovek osoba1; // deklarace proměnné nového datového typu
Clovek osoba2;
Clovek osoba3;

strcpy(osoba1.jmeno, "Jan Novák"); // přistupujeme k složkám struktury
strcpy(osoba1.adresa, "Kocourkov");
osoba1.vek = 30;

strcpy(osoba2.jmeno, "Petr Nejedlý");
*osoba2.adresa = 0; // adresu neznáme, osoba2.adresa je prázdný řetězec
osoba2.vek = 35;

osoba3 = osoba2;

Deklarovali jsme tři proměnné datového typu Clovek, které jsme dále inicializovali. Všimněte si, že přistup k složkám struktur se provádí operátorem "." (tečka). Dále je na posledním řádku vidět, že struktury je možné vzájemně přiřazovat.


Poznámka: na řádku *osoba2.adresa = 0; používáme dva operátory: "*" a ".". Zvídavého čtenáře možná napadne, že na takovou konstrukci je možné se dívat dvěma způsoby:

*(osoba2.adresa) = 0;

tzn. vyhodnotí se osoba2.adresa a na výsledné adrese se uloží 0, nebo

(*osoba2).adresa = 0;

tzn. provede se dereference osoba2 (ačkoli to není ukazatel, ale co kdyby byl?) a pak se na první místo složky adresa výsledku dereference uloží 0. Jinak položena otázka by mohla znít: má větší prioritu operátor "." (což odpovídá první možnosti), nebo operátor "*"? Správná je ovšem první možnost, což ale znamená, že kdyby proměnná osoba2 skutečně byla ukazatelem, museli bychom k jejím složkám přistupovat poněkud těžkopádnou konstrukcí se závorkami: (*osoba2).adresa). Ovšem, jak uvidíme dále, existuje zvláštní operátor pro přístup k složkám ukazatele na strukturu.

Struktury je možné vnořovat (nějaká složka struktury je jiná struktura), ale není možné, aby složka struktury byla právě deklarovaná struktura. Jinými slovy, není možný takový zápis:

struct Clovek {
  char jmeno[30];
  char adresa[50];
  unsigned vek;
  Clovek otec;
};

Ukázali jsme si, že datový typ struktura deklarujeme klíčovým slovem struct. Existuje ještě jeden způsob deklarace, který je dokonce o něco mocnější (umožňuje deklarovat např. datový typ "ukazatel na strukturu"): je jím klíčové slovo typedef. Jeho prostřednictvím je možné například přejmenovat datový typ na jiný (pro větší přehlednost):

typedef unsigned int uint;

Tímto příkazem jsme deklarovali nový datový typ uint (v tomto okamžiku uint znamená to samé, co unsigned int). Také můžeme deklarovat například datový typ ukazatel na int:

typedef int *pint;

Použití takového datového typu by bylo například takové:

int i;
pint p; // to samé jako int *p

p = &i;

Úplně stejně lze deklarovat nový datový typ struktura:

typedef struct Clovek {
  char jmeno[30];
  char adresa[50];
  unsigned vek;
} NovyClovek;

Nyní lze deklarovat proměnnou jak datového typu Clovek, tak i NovyClovek (datový typ Clovek jsme deklarovali klíčovým slovem struct, datový typ NovyClovek zase klíčovým slovem typedef) – výše uvedený zápis je to samé jako:

struct Clovek {
  char jmeno[30];
  char adresa[50];
  unsigned vek;
};

typedef Clovek NovyClovek;

Uvedení nějakého názvu jak pro struct, tak i pro typedef je zbytečné, takže se většinou název za struct neuvádí. Ještě bych chtěl podotknout, že deklarace struktur klíčovým slovem typedef je spíše záležitost jazyka C, v C++ si až na jednu výjimku, kterou si ukážeme dále, vystačíme s klíčovým slovem struct.


Poznámka: deklarace proměnné datového typu struktura se provádí v C uvedením klíčového slova struct:

struct Clovek {
    ...
}

struct Clovek osoba;                    // uvedení struct je povinné


Protože to je nepohodlné, v C se používá právě typedef, který tuto nevýhodu odstraňuje.

Při deklaraci proměnných odvozených od struktur je možné je i inicializovat:

Clovek osoba = {"Jan Novák", "Kocourkov", 30};

Musíme dodržet pořadí složek ve struktuře, a také musíme dbát, abychom neinicializovali víc složek, než má struktura (to by skončilo chybou). Ale nemusíme inicializovat všechny složky a pak ty, které neinicializujeme, budou mít nulovou hodnotu:

Clovek osoba = {"Jan Novák", "Kocourkov"};       // vynecháváme složku vek
cout << osoba.vek;                               // vypíše se 0


Ukazatele na struktury

Jako i u základních datových typu, je možné se strukturami pracovat pomocí ukazatelů. Deklarace se provádí naprosto stejně:

Clovek osoba;
Clovek *p;

p = &osoba;

Jak jsme si řekli výše, přístup k složkám nebudeme provádět operátorem ".", ale operátorem šipka ("->"):

Clovek osoba1;

strcpy(osoba1->jmeno, "Jan Novák");       
strcpy(osoba1->adresa, "Kocourkov");
osoba1->vek = 30;


Poznámka: píšete-li českou klávesnicí, může být problém psaní znaku "většítko". Nemusíte kvůli tomu přepínat klávesnici, zkuste klávesovou zkratku Alt + > (jedná se o pravý Alt).

Výše jsme si řekli, že složkou struktury nemůže být právě deklarovaná struktura. Složkou struktury ale může být ukazatel na právě deklarovanou strukturu.

struct Uzel {
    int hodnota;
    Uzel *dalsi;
};

Takový datový typ se používá ve spojových seznamech, o kterých si budeme více povídat v jednom z dalších dílů, prozatím prosím berte uvedený příklad jako teoretický.

V případě, že strukturu deklarujeme klíčovým slovem typedef, potřebujeme ve struktuře složku ukazatel na právě deklarovanou strukturu, je třeba uvést nějaký název i za struct, je to totiž jediný způsob jak se na odkazovat na strukturu, kterou právě deklarujeme:

typedef struct Uzel {
    int hodnota;
    Uzel *dalsi;
} Uzel;

A konečně si můžeme ukázat i případ, na který samotné klíčové slovo struct nestačí: chceme-li deklarovat datový typ "ukazatel na strukturu" musíme použít typedef:

typedef struct {
    char jmeno[30];
    char adresa[50];
    unsigned vek;
} *PClovek;

Struktury jako parametry funkcí

V jednom z minulých dílů jsme si řekli, že nejčastěji se parametry funkcí předávají hodnotou, což představuje zkopírování parametrů do funkce. Struktury se také kopírují, ale zde může nastat problém: zkopírování větších struktur není příliš efektivní. Mnohem lepším řešením je předávání struktur operátorem reference:

void vypisOsobu(Clovek &osoba) {
    cout << "Jméno: " << osoba.jmeno << "\n";
    cout << "Příjmení: " << osoba.adresa << "\n";
    cout << "Věk: " << osoba.vek << "\n";
}


Řekli jsme si, že reference je ukazatel, který se syntakticky chová jako proměnná daného datového typu, takže při zavolání funkce vypisOsobu() dojde ke zkopírování pouze 4 bytů.


Poznámka: další způsob předávání struktur je ukazatelem, ale to je praktika používaná spíše v jazyce C. Setkáte se s tím například budete-li psát programy pro Windows.

Příklad

Dnes si ukážeme nějaký větší příklad, bude to adresář osob, do kterého budeme moci ukládat jméno a příjmení, ulici, město a telefonní číslo. Adresář má umožňovat přidávání osoby, její vymazání, a hledání podle jména. Ukládat do souboru adresář nebude umět, to ani my ještě neumíme, ale přidáme to příště.

Začneme náš program rozhodnutím, jak dlouhé budou řetězce do kterých budeme ukládat data, a deklarováním příslušné struktury:

const unsigned DELKA_JMENO = 30;
const unsigned DELKA_ULICE = 40;
const unsigned DELKA_MESTO = 30;
const unsigned DELKA_TELEFON = 20;

struct Osoba {
    char jmeno[DELKA_JMENO];
    char ulice[DELKA_ULICE];
    char mesto[DELKA_MESTO];
    char telefon[DELKA_TELEFON];
};

Dále bychom si měli rozmyslet jak budeme ukládat více osob. Pravděpodobně do pole, ale pole nám neřekne, kolik osob v něm máme uloženo, to si musíme pamatovat v další proměnné, nazvěme ji pocet. Tyto dva údaje blízce souvisí, takže je můžeme také dat do nějaké struktury, která se bude nazývat např. Osoby. V souvislosti s polem musíme ještě rozhodnout, jaká bude jeho velikost (počet prvků). Abychom mohli tento počet lehce změnit pokud to budeme někdy potřebovat, deklarujeme si k tomu konstantu:

const unsigned MAX_POCET_OSOB = 100;

struct Osoby {
    Osoba pole[MAX_POCET_OSOB];
    int pocet;
};

Datové typy již máme, pojďme si nyní napsat nějaké funkce pro práci s nimi. Předně budeme potřebovat nějakou funkci, která bude inicializovat proměnnou odvozenou od datového typu Osoby, a to nastavením složky pocet na nulu (na začátku programu je adresář prázdný, nejsou v něm žádné osoby). Tato funkce dostane jako parametr proměnnou, kterou chceme inicializovat (samozřejmě referencí):

void osobyInit(Osoby &osoby) {
    osoby.pocet = 0;
}

Dále budeme potřebovat nějakou funkci pro uložení nové osoby. Jako parametry dostane proměnnou typu Osoby a proměnnou typu Osoba, a bude vracet hodnotu bool podle toho, zda se uložení podařilo či ne:

bool osobyPridej(Osoby &osoby, Osoba &osoba) {
    if (osoby.pocet < MAX_POCET_OSOB) {
        osoby.pole[osoby.pocet] = osoba;
        osoby.pocet++;
        return true;
    }
    else
        return false;
}

Uložení proběhne zkopírováním nové osoby do pole na místo, které udává právě proměnná osoby.pocet (je-li například pocet = 3, jsou v poli obsazené polohy 0, 1 a 2, další volná poloha je tedy právě 3). Proměnnou pocet následně zvětšíme o jedna. Samozřejmě ještě před zkopírováním musíme zkontrolovat, zda pole není náhodnou plné, v tomto případě není kam ukládat a funkce vrací hodnotu false. V případě úspěchu vracíme true.

Dále budeme potřebovat funkci, která ve struktuře Osoby vyhledá osobu se zadaným jménem. Tato funkce dostane jako parametry proměnnou typu Osoby, ve které se vyhledá, a také řetězec obsahující jméno, které se má vyhledat. Procházíme pole osob a porovnáváme jména se zadaným jménem. K porovnávání použijeme funkci strnicmp(), která porovnává nějaký zadaný počet znaků v řetězcích bez ohledu na velká/malá písmena. Funkce vrací polohu v poli v případě nalezení, a -1 jinak (protože -1 není platný index do pole):

int osobyHledej(Osoby &osoby, char *jmeno) {
    int i;

    for (i = 0; i < osoby.pocet; i++) {
        if (!strnicmp(osoby.pole[i].jmeno, jmeno, DELKA_JMENO)) {
            return i;
        }
    }

    return -1;
}

Potřebujeme ještě funkci na vymazání osoby, což je možná nejsložitější akce. Není-li vymazaná osoba poslední v poli, bude potřeba veškeré osoby za ní přesunout o jednu polohu doleva, jak je vidět na obrázku (maže se osoba na poloze 1):

Parametry funkce budou proměnná typu Osoby a polohu v poli, udávající osobu, která se má vymazat. Samozřejmě je třeba zkontrolovat platnost polohy (musí to být vetší nebo rovna nule, a nesmí být větší nebo rovno existujícímu počtu osob. Nakonec je třeba snížit počet osob.

bool osobyVymaz(Osoby &osoby, int poloha) {
    int i;

    if (poloha < 0 || poloha >= osoby.pocet)
        return false;

    for (i = poloha + 1; i < osoby.pocet; i++)
        osoby.pole[i - 1] = osoby.pole[i];
    osoby.pocet--;
    return true;
}

Poslední funkci pro práci se strukturou Osoby je funkce, která vrací true je-li v poli místo pro novou osobu, a false jinak:

bool osobyJeMisto(Osoby &osoby) {
    return osoby.pocet < MAX_POCET_OSOB;
}

Nyní je potřeba si napsat funkce pro uživatelské rozhraní (funkci, která přečte údaje o osobě z klávesnice a z nich vytvoří novou proměnnou typu Osoba kterou přidá do seznamu osob, dále funkci pro vyhledání a vymazání osoby). K tomu je potřeba vstup z klávesnice a tak si vytvoříme nějakou obecnou funkci právě pro tento účel. V jednom z prvních dílů jsme si řekli, že vstup z klávesnice lze přečíst funkcí scanf(). Například chceme-li přečíst řetězec, použijeme:

scanf("%s", retezec);

Takový přístup má ale jednu velkou chybu a to je že funkce scanf() by mohla uložit do proměnné rezetec víc znaků, než by se tam mohlo vejít (velikost proměnné nezná a uloží tolik znaků, kolik zadá uživatel). Použijeme tedy trochu jiný formát, kterým lze udávat nejvyšší počet, který lze do výsledku (proměnné retezec) uložit:

scanf("%30s", retezec);

V tomto případě je tedy nejvyšší počet znaků 30. Ovšem zapíše-li se do výsledku maximální počet znaků, pak se nezapíše nulový ukončovací znak, který musíme doplnit sami.

Dalším problémem funkce scanf() je, že chápe mezeru jako oddělovač polí. To má za následek uložení do výsledu pouze znaků do první mezery. Ale to lze také vyřešit: místo formátu %s lze zadat seznam platných znaků, nebo naopak seznam neplatných znaků. To se provádí uvedením seznam platných znaků do hranatých závorek:

scanf("%[abc]", retezec);
scanf("%[a-z]", retezec);

V prvním případě jsou platné znaky a, b a c, v druhém znaky a až z. Seznam neplatných znaků zase zadáme uvedením stříšky hned po otevírací hranaté závorce:

scanf("%[^abc]", retezec);

To znamená, že platné znaky jsou všechny až na a, b a c. My použijeme formát "%30[^\n]", který udává, že platné znaky jsou všechny až na znak konec řádku, a že se má uložit nejvýše 30 znaků

Další zádrhel souvisí s nejvyšším počtem znaků výsledku. Znaky, které uživatel zadá navíc, se neztrácejí, nýbrž zůstanou uloženy ve vyrovnávací paměti, a funkce scanf() je bude zpracovávat při dalším zavolání. Před každým zavoláním musíme tedy tuto vyrovnávací paměť vyprázdnit, což provedeme funkcí fflush(), jejíž přesný význam si vysvětlíme příště:

fflush(stdin);

Úplně poslední věc na kterou musíme dát pozor je, že když uživatel nezadá nic, funkce scanf() neuloží do výsledku nic, tedy ani nulový ukončovací znak. Ten si musíme doplnit sami na základě návratové hodnoty scanf(). Návratová hodnota uvádí kolik polí bylo zpracováno (podle našeho formátovacího řetězce "%30[^\n]" jedno pole), nebo 0 pokud nebylo zpracováno nic. Nyní si můžeme napsat funkcí, která přečte z klávesnice řetězec za podmínek, které jsme uvedli. Funkce dostane jako parametr řetězec, kam se má výsledek uložit, a nejvyšší počet znaků, který se má do výsledku uložit. Z tohoto důvodu musíme vytvořit formátovací řetězec, pomocí funkce sprintf(), která funguje jako printf(), ale výsledek uloží do řetězce:

const char *strFmt = "%%%u[^\n]";              // %% znamená znak procento, dále %u se nahradí nějakým celým číslem                                                     // výsledek je např. "%30[^\n]"

const unsigned MAX_FORMAT = 32;                     // délka pro řetězec, do kterého vytváříme formátovací řetězec

void precti(char *vysledek, unsigned maxDelka) {
    char fmt[MAX_FORMAT];                           // format. řetězec
    sprintf(fmt, strFmt, maxDelka);                 // vytváříme formátovací řetězec
    fflush(stdin);                                  // vyprázdnění vyrovnávací paměti
    if (scanf(fmt, vysledek))                       // čtení
        vysledek[maxDelka - 1] = 0;                 // scanf vrátilo 1, takže pro jistotu doplňujeme nulový ukončovací znak
    else
        *vysledek = 0;                             // scanf vrátilo 0, taže doplňujeme na záčátek řetězce nulový ukončovací znak                                                     // vysledek je tedy přázdný řetězec
}

Nyní si můžeme napsat funkce pridej(), hledej() a vymaz(), jejich výpis najdete přiložený. Funkce hledej() a vymaz() používají funkci hledejPolohu(), která přečte z klávesnice jméno, které se má vyhledat, popř. vymazat, a vrací nalezené osoby v poli nebo -1.

Ve funkci main() je pouze kód, který vytváří jednoduché menu. Uživatel volí z menu pomocí jednoznakových zkratek. K přečtení znaku z klávesnice použijeme funkci getche() z hlavičkového souboru conio.h. Také je na začátku potřeba vypnout vyrovnávací paměť objektu cout, jinak by se vypsané řetězce nezobrazovaly na obrazovce hned, ale jen "jednou za čas" (po naplnění vyrovnávací paměti). Vyrovnávací paměť se vypíná voláním funkce objektu cout, ale tomu zatím nemusíte rozumět, všechno si to povíme až si vysvětlíme třídy a objekty.

void main() {
    Osoby osoby;
    char volba = -1;

    cout.setf(ios::unitbuf);                       // nastavujeme výpis na obrazovce po každé výpis do objektu cout

    osobyInit(osoby);

    while (volba != 'k' && volba != 'K') {
        cout << strZadejVolbu;
        volba = getche();
        cout << endl;
       
        switch (volba) {
        case 'p':
        case 'P':
            if (!pridej(osoby))
                cout << errAdrPlny;
            break;

        case 'h':
        case 'H':
            if (!hledej(osoby))
                cout << errNenalezeno;
            break;

        case 'v':
        case 'V':
            if (!vymaz(osoby))
                cout << errNenalezeno;
        }

        cout << endl;
    }
}

Příklad rozhodně není příliš jednoduchý. Způsobují to různé kontroly zda všechno proběhlo v pořádku, a také komplikace kolem funkce scanf(). Ale programování už je takové, musíte dávat pozor na všechno. Děkuji za pozornost a těším se příští měsíc nashledanou.
 

Andrei Badea