Programujeme v C++ (5) - Štruktúrované dátové typy

Programujeme v C++ (5) - Štruktúrované dátové typy
26.07.2009 16:00 | Články | Miroslav Bendík
Štruktúrovaný dátový typ je zoskupením niekoľkých jednoduchších dátových typov. Môže byť homogénny (všetky prvky sú rovnakého typu, napr. pole), alebo heterogénny (zložený z rôznych dátových typov). V tomto dieli sa budeme venovať heterogénnym dátovým typom - štruktúre, unionu a triede.

Dáta v štruktúrovanom dátovom type sú reprezentované jediným identifikátorom (premennou). Aby sa dalo pristupovať k členom musia štruktúrované dátové typy poskytovať možnosť prístupu k členom.

Zoskupenie dát do logických celkov sprehľadňuje výsledný kód. U programovacích jazykov s podporou objektovo orientovaného programovania môžeme používať dátový typ trieda obsahujúci okrem dátových členov aj funkcie na prácu s nimi (metódy).

Štruktúra (struct)

Dátový typ štruktúra môže uchovávať niekoľko na sebe nezávislých dátových členov. Typ aj počet dátových členov je ľubovoľný.

Deklarácia a definícia štruktúry

Deklarácia aj definícia štruktúry začínajú kľúčovým slovom struct. Ďalej nasleduje názov štruktúry, ktorým deklarácia končí. Definícia pokračuje telom štruktúry, ktoré je uzatvorené v zložených zátvorkách a skladá sa z deklarácie členských premenných. Za telom štruktúry je možné deklarovať jej inštancie. Za deklaráciou aj definíciou nasleduje bodkočiarka. Syntax definície štruktúry vyzerá nasledovne:

struct [Názov_štruktúry] {
    // deklarácie členských premenných
} [Deklarácia inštancií];

Hranaté zátvorky označujú nepovinné časti definície. Názov štruktúry budeme potrebovať v prípade, že chceme vytvárať jej inštancie kdekoľvek v kóde (nie len bezprostredne za telom štruktúry). V prípade, že nám stačí vytvorenie inštancií v mieste definície nemusíme uvádzať meno štruktúry – vznikne anonymná štruktúra.

// Štruktúra s názvom osoba
struct Osoba
{
    // Anonymná štruktúra
    struct
    {
        string krstneMeno;
        string priezvisko;
    } meno; // Členská premenná meno

    // Členská premenná vek
    int vek;
};

// Deklarácia a definícia štruktúry
struct Osoba novaOsoba;

V tomto príklade je definovaná komplexná (skladajúca sa z ďalších štruktúrovaných dátových typov) štruktúra Osoba. Skladá sa z anonymnej štruktúry, ktorá je členskou premennou s názvom meno a členskej premennej vek.

Inicializácia

Štruktúru je možné inicializovať buď zároveň s definíciou, alebo priradením hodnôt dátovým členom za jej definíciou.

Pri definícii je možné inicializovať štruktúru priradením inicializačného zoznamu. Inicializačný zoznam je zoznam hodnôt oddelených čiarkami uzavretý do zložených zátvoriek. V prípade komplexných štruktúr je možné zložené zátvorky vnoriť do seba. Poradie položiek v inicializačnom zozname musí byť rovnaké ako poradie členských premenných v štruktúre. Rovnakým spôsobom sa inicializujú polia (v tom prípade má samozrejme každá položka rovnaký typ). Inštanciu štruktúry Osoba je možné inicializovať nasledujúcim spôsobom:

struct Osoba novaOsoba = {{"Meno", "Priezvisko"}, 99};

Štruktúru môžeme inicializovať aj postupným priraďovaním hodnôt jednotlivým zložkám. Takáto inicializácia býva pomalšia než inicializácia pri definícii. Nasledujúci kód inicializuje štruktúru na rovnakú hodnotu, ako kód s inicializačným zoznamom.

struct Osoba novaOsoba;
novaOsoba.meno.krstneMeno = "Meno";
novaOsoba.meno.priezvisko = "Priezvisko";
novaOsoba.vek = 99;

Použitie smerníkov so štruktúrami

Štruktúry môžu obsahovať veľké množstvo dát. Objemné dáta nie je vhodné staticky alokovať na zásobníku. Dynamickou alokáciou na halde sa môžeme vyhnúť problémom s obmedzenou veľkosťou zásobníka. Smerníky sa používajú aj na zvýšenie efektivity programu. Pri volaní funkcie sa tak prenáša len smerník namiesto kopírovania celej štruktúry.

K prvkom štruktúry môžeme pristupovať po dereferencii rovnako ako v prípade staticky alokovanej štruktúry. Dôležité je aby sa dereferencia vykonala pred prístupom k prvku. Operátor prístupu k prvku (".") má vyššiu prioritu než operátor dereferencie ("*"). Pri zápise *smerník_na_štruktúru.prvok sa kompilátor najskôr pokúsi pristúpiť k prvku bez dereferencie. Prioritu operátorov musíme zmeniť pomocou zátvoriek: (*smerník_na_štruktúru).prvok. Pre zjednodušenie tohto zápisu bol zavedený nový operátor ->. Nasledujúce zápisy sú ekvivalentné:

(*smerník_na_štruktúru).prvok
smerník_na_štruktúru->prvok

Dynamická alokácia novej štruktúry, práca s ňou a jej zrušenie vyzerá takto:

struct Osoba *novaOsoba = new Osoba;
novaOsoba->meno.krstneMeno = "Meno";
novaOsoba->meno.priezvisko = "Priezvisko";
novaOsoba->vek = 99;
delete novaOsoba;

V tele štruktúry už kompilátor pozná jej deklaráciu. Prvkom štruktúry preto môže byť aj smerník na rovnakú štruktúru. Typickým príkladom takejto štruktúry je prvok zreťazeného zoznamu.

struct Prvok
{
    struct Prvok *dalsi;
}

Skrátenie zápisu pomocou typedef

Doteraz sme pri deklarácii každej inštancie štruktúry používali kľúčové slovo struct. Pomocou typedef je možné urobiť alias pre štruktúru. Pred deklaráciou nám v C kóde vypadne struct. V C++ kóde pre skrátený zápis nie je nutné použiť typedef.

typedef struct
{
    // deklarácie členských premenných
} Struktura;

Struktura novaStruktura;

Cvičenie

Implementujte pomocou štruktúry zreťazený zoznam. Prvok zoznamu sa bude skladať z dát, ktoré prvok obsahuje a smerníku na ďalší prvok. Napíšte funkcie na výpis zoznamu, pridanie prvku a uvoľnenie alokovanej pamäte. Na nasledujúcom obrázku je diagram jednoduchého zreťazeného zoznamu:

Zreťazený zoznam

Ak neviete ako začať postupujte podľa nasledujúcich bodov:

  • Vytvorte štruktúru Prvok obsahujúcu smerník na ďalší prvok a dáta (string).
  • Deklarujte premennú typu struct Prvkok * odkazujúcu na prvý prvok zoznamu.
    • Pre zrýchlenie pridávania prvku je dobré udržiavať aj smerník na posledný prvok.
    • Smerníky na prvý a posledný prvok je možné zoskupiť do štruktúry Zoznam.
  • Nakoniec napíšte samotný kód pre vkladanie a výpis.
  • Ak všetko prebehne v poriadku pokúste sa napísať kód pre uvoľnenie pamäte.

Pre inšpiráciu môžete využiť tento program s chýbajúcou implementáciou zreťazeného zoznamu.

Union

Zatiaľ čo štruktúra mala vyhradené miesto pre každú položku zvlášť u unionu sa položky navzájom prekrývajú – každá položka začína na tom istom pamäťovom mieste. Po priradení hodnoty niektorej z položiek už nie je možné ostatné položky používať pretože sa ich obsah prepíše. Ak by sme sa pokúšali prečítať inú položku dostali by sme zdanlivo nezmyselné hodnoty. V niektorých prípadoch môže byť táto vlastnosť užitočná.

Typ union v pamäti zaberá rovnaký priestor ako najväčšia položka unionu. Vďaka tomu je union vhodným typom ak chceme ušetriť pamäť, ktorú by zaberali všetky položky štruktúry. Syntax je podobná štruktúre. Namiesto kľúčového slova struct sa používa union.

Jazyk C++ nerieši identifikáciu typu uložené v unione. Riešenie tohto problému je ponechané programátorovi. Jedným z možných riešení je obalenie unionu do štruktúry, ktorá obsahuje naviac informáciu o type uložených dát v unione. V nasledujúcom príklade je union použitý v štruktúre spolu s typom údaju.

struct Udaje
{
    int typ;
    union
    {
        char ico[8];
        char r_cislo[10];
    } udaj;
};

Anonymný union

V špecifických prípadoch môže byť vhodné, ak niekoľko lokálnych premenných začína na tom istom pamäťovom mieste. Toto správanie je možné dosiahnuť použitím anonymného union-u.

union {
    int cislo;
    double desatinne;
};

cislo     = 3;
desatinne = 3.14;

Triedy

Jedným z hlavných rozdielov medzi C a C++ je podpora objektovo orientovaného programovania. Jazyk C neposkytuje prakticky žiadnu podporu pre objekty, čo ale neznamená, že by sa v ňom nedalo programovať „objektovo“.

Objektovo orientované programovanie

Pri objektovo orientovanom programovaní (OOP) vytvára programátor systém objektov a tried, ktorý má podobné vlastnosti ako systém v reálnom svete. Programy sa tak neskladajú zo samostatných funkcií a dát ako pri procedurálnom programovaní, ale zo sústavy objektov, ktoré majú svoje atribúty a metódy a môžu medzi sebou komunikovať.

Definíciu triedy môžeme považovať za šablónu, podľa ktorej kompilátor vytvára objekty. Trieda môže mať ľubovoľný počet inštancií. Triedami v reálnom svete sú napr. auto, mačka, zviera, strom … Ich inštancie budú mať atribúty a metódy definované v triede. Ak definujeme triedu Auto s atribútmi rýchlosť, najazdené kilometre a metódami ako zmeň rýchlosť, potom každá inštancia triedy Auto bude mať tieto atribúty a metódy.

Deklarácia, definícia a použitie tried

Deklarácia triedy začína kľúčovým slovom class. Syntax je identická ako u štruktúry. Trieda ekvivalentná štruktúre Osoba má nasledujúci kód:

class Osoba
{
public:
    class
    {
    public:
        string krstneMeno;
        string priezvisko;
    } meno;
    int vek;
};

Osoba novaOsoba;

Viditeľnosť

Atribúty a metódy triedy sú štandardne privátne (private) – prístup k nim je možný len z metód triedy. Aby boli atribúty prístupné aj mimo triedy je nutné explicitne nastaviť viditeľnosť na public.

V nasledujúcej tabuľke sú druhy viditeľnosti, ktoré podporuje C++.

Druhy viditeľnosti
Typ viditeľnosti Popis
public (verejné) členy prístupné všade, kde je prístupná samotná trieda
protected (chránené) členy prístupné z metód triedy a metód odvodených tried
private (privátne) členy prístupné výhradne z metód triedy

V časti o štruktúrach som popisoval štruktúru ako ju poznáme z jazyka C. V C++ sú tieto typy rovnocenné a jediný rozdiel je v implicitnej viditeľnosti členov. V nasledujúcej tabuľke je prehľad implicitnej viditeľnosti a možnosti jej predefinovania.

Viditeľnosť členov
Typ Štandardná viditeľnosť Možnosť predefinovania
struct public Áno
union public Nie
class private Áno

Metódy

Metódy sú podobné funkciám ako ich poznáme z procedurálnych jazykov. Majú prístup k všetkým členom triedy (aj privátnym). Platia pre ne rovnaké pravidlá viditeľnosti ako pre dáta objektu.

Syntax

Syntax metódy je rovnaká ako syntax funkcie. V triede sa môže vyskytovať buď celá definícia funkcie, alebo len jej deklarácia. Definícia funkcie nemusí byť v tom istom súbore ako definícia triedy. V nasledujúcom príklade je zápis metódy priamo v tele triedy:

class Trieda
{
public:
    void metoda()
    {
    };
};

Metódu je možné definovať aj mimo triedy tak, že v triede sa nachádza len deklarácia. Niekoľko tried môže mať metódu s rovnakým názvom, preto sa u metód definovaných mimo triedy určuje trieda, ktorej metóda patrí. Pred názov metódy sa pridáva názov triedy, ktorej metóda patrí nasledovaný operátorom rozsahu (::).

class Trieda
{
public:
    void metoda();
};

void Trieda::metoda()
{
}

Zapuzdrenie (Encapsulation)

Princípom zapuzdrenia je zamedzenie priameho prístupu k interným dátam a funkciám objektu. Komunikovať s objektom je možné len cez presne definované rozhranie nezávislé od internej reprezentácie dát v objekte. Medzi výhody zapuzdrenia patria:

Vyššia flexibilita
V budúcnosti bude možné zmeniť implementáciu objektu bez zmeny jeho rozhrania.
Nižšia komplexnosť
Pracuje sa len s presne definovaným rozhraním objektu. Znižujú sa tak závislosti medzi komponentmi systému.
Zamedzenie vzniku neprístupných stavov
Nie je možné nastaviť hodnoty interných premenných priamo, ale len cez metódy na to určené, ktoré môžu kontrolou zamedziť nastaveniu neprístupných hodnôt.
Zapuzdrenie

Inline funkcie

Medzi definíciou metódy v tele triedy a mimo triedy je jeden podstatný rozdiel, ktorý som nespomenul – metódy definované v tele triedy sú automaticky inline.

Pri volaní bežnej funkcie dochádza k skoku na pamäťové miesto, kde sa funkcia nachádza. Telo funkcie je v programe jeden krát. U inline funkcií sa na miesto volania funkcie vkladá celé telo funkcie. Funkcia sa tak vyskytuje na každom mieste, kde je volaná. Použitie inline funkcie síce väčšinou zväčší program (pri malých funkciách môže dôjsť k zmenšeniu programu), ale zníži sa tým počet skokov. Inštrukcia skoku patrí medzi najpomalšie inštrukcie a jej vynechanie na správnych miestach môže značne zvýšiť výkon aplikácie.

Štandardnú funkciu je možné zmeniť na inline pridaním kľúčového slova inline pred jej definíciu.

inline void funkcia()
{
// …
}

Inline metódy sú metódy definované v tele triedy a metódy s kľúčovým slovom inline pred jej definíciou.

Kompilátor potrebuje pri kompilácii každej časti, v ktorej sa vyskytuje inline funkcia jej definíciu. Obvykle sa preto definícia inline funkcie vkladá priamo do hlavičkového súboru s deklaráciou.

Tvorba a zánik objektov

Životný cyklus objektu sa skladá z niekoľkých fáz:

Vytvorenie objektu
  • Alokácia pamäte
  • Inicializácia

Ak bol objekt vytvorený na zásobníku pamäťové miesto sa mu pridelí automaticky. Pri dynamickej alokácii sa prideľuje pamäťové miesto použitím operátora new. Pre inicializáciu objektov sa používajú špeciálne metódy – konštruktory.

Používanie objektu
  • Volanie metód
  • Používanie a zmena premenných
Zánik objektu
  • Uvoľnenie prostriedkov
  • Uvoľnenie alokovanej pamäte

Miesto alokované pre objekt sa uvoľňuje automaticky na konci bloku, kde bol definovaný u staticky alokovaných objektov a pri použití operátoru delete u dynamicky alokovaných objektov. Pred uvoľnením pamäte sa vykoná špeciálna metóda – deštruktor.

Životný cyklus objektu

Konštruktor

Úlohou konštruktora je inicializovať počiatočný stav objektu – nastavenie hodnôt dátových členov, pridelenie prostriedkov (napr. otvorenie súboru) a ďalšie operácie potrebné pre inicializáciu objektu.

Implicitný konštruktor

Každá trieda má minimálne jeden konštruktor. Ak nedefinujeme žiaden konštruktor kompilátor automaticky vytvorí štandardný implicitný konštruktor, ktorý inicializuje objekt. Implicitný konštruktor však neinicializuje žiadne užívateľské premenné, ich hodnoty sú po vytvorení objektu náhodné.

Explicitný konštruktor

Pre inicializáciu užívateľských premenných musíme definovať explicitný konštruktor. Syntax konštruktora sa podobá na syntax metódy s rovnakým názvom ako má trieda, ale bez návratovej hodnoty (konštruktor nevracia ani void). Napriek podobnosti s metódou nie je možné konštruktor volať ako štandardnú metódu (ani vo vnútri iného konštruktora). S deklaráciou explicitného konštruktora sa zruší štandardný implicitný konštruktor.

Trieda môže mať niekoľko konštruktorov. Správny konštruktor vyberá kompilátor podľa parametrov použitých pri vytváraní objektu. Pre výber konštruktora platia rovnaké pravidlá ako pre výber preťaženej funkcie.

Nasledujúci príklad demonštruje použitie konštruktorov v triede:

class Kruh
{
public:
    Kruh(double x, double y, double r);
    Kruh(double x1, double y1,
         double x2, double y2,
         double x3, double y3);
private:
    double m_x;
    double m_y;
    double m_r;
};

// vytvorenie kruhu so stredom v x, y
// s polomerom r
Kruh::Kruh(double x, double y, double r)
{
    m_x = x;
    m_y = y;
    m_r = r;
}

// vytvorenie kruhu zo súradníc 3 bodov,
// cez ktoré prechádza kružnica
Kruh::Kruh(double x1, double y1,
           double x2, double y2,
           double x3, double y3)
{
    // výpočet x, y, r podľa zadaných 3 bodov

    m_x = x;
    m_y = y;
    m_r = r;
}

int main(int argc, char *argv[])
{
    Kruh k1(0, 0, 1);           // zavolá sa prvý konštruktor
    Kruh k2(0, 1, 1, 0, 0, -1); // zavolá sa druhý konštruktor
    Kruh k3;                    // chyba, štandardný konštruktor bol
                                // zrušený explicitným konštruktorom
    Kruh k4(0, 1, 2, 3);        // chyba, neexistuje vyhovujúci
                                // konštruktor
}

Trieda kruh má dva konštruktory. Prvý konštruktor vytvorí inštanciu zo súradníc stredu a polomeru. Druhý konštruktor prijíma ako argumenty 3 body, cez ktoré prechádza kružnica a podľa nich vypočíta súradnice stredu a polomer.

Pri vytváraní objektu k1 kompilátor vyberie prvý konštruktor. Typ atribútov sa automaticky konvertuje na double. Týmto argumentom vyhovuje jedine prvý konštruktor. Objekt k2 sa vytvára z pozície troch bodov. Jediný konštruktor, ktorý zodpovedá týmto atribútom je druhý konštruktor. Objekt k3 sa nedá vytvoriť, pretože trieda nemá žiaden štandardný konštruktor(konštruktor bez parametrov). Štandardný implicitný konštruktor sa automaticky zruší v prípade, že bol definovaný akýkoľvek explicitný konštruktor. Pri objekte k4 kompilátor nenájde žiaden konštruktor zodpovedajúci použitým argumentom.

Deštruktor

Uvoľnenie prostriedkov, ktoré používal objekt má na starosti deštruktor. Podobne ako konštruktor nemá žiadnu návratovú hodnotu. Deštruktor nemá žiadne argumenty (nie je teda možné definovať niekoľko deštruktorov). Syntakticky je zhodný so štandardným konštruktorom. Názov deštruktora sa skladá zo znaku tilda (~) a názvu triedy. V nasledujúcej ukážke je deštruktor použitý na uvoľnenie alokovanej pamäte.

class Trieda
{
public:
        Trieda()  { m_text = new char[200]; }
        ~Trieda() { delete []m_text; }
private:
        char *m_text;
}

Cvičenie

Implementujte pomocou tried zreťazený zoznam z predchádzajúceho cvičenia. Program sa bude skladať z tried Zoznam a Prvok. Trieda Zoznam bude podporovať operácie pridania prvku a výpisu. Po zrušení zoznamu deštruktor zruší všetky jeho prvky. Do zoznamu môžete pridať aj ďalšie operácie ako napr. získanie prvku podľa jeho indexu, alebo vymazanie zvoleného prvku.

UML diagram zreťazeného zoznamu

Záver

Tento diel seriálu bol ľahkým úvodom do problematiky objektovo orientovaného programovania. Budúcu časť začneme popisom statických členov tried. Podrobnejšie sa budeme zaoberať inicializáciou objektov. Nakoniec si preberieme dedičnosť, včasnú a neskorú väzbu a virtuálne funkcie.

    • dik 30.07.2009 | 22:04
      Avatar gabrielit Fedora 11 & Gentoo  Používateľ
      Vďaka za výborný článok, len tak ďalej, teším sa na ďalšie ! :)
    • gutwork 11.08.2009 | 14:25
      Avatar Baro Rai Linux Mint  Používateľ
      pekne rozpisany a prepracovany clanok :-)
      "from freedom came elegance." ; icq: 442-088-844
    • Obrazky/grafy 22.08.2009 | 20:49
      just me   Návštevník
      v com ich robis??
      • Re: Obrazky/grafy 24.08.2009 | 20:05
        Avatar Miroslav Bendík Gentoo  Administrátor
        Nepoteším. Teda môžno áno. Takže len odkážem na IBM - Rational Software Architect. Je to obrovské, pomalé, komerčné a kreslia sa v tom podobné diagramy ako v tomto článku. Žiaľ (alebo našťastie) som nikdy nemal rád komerčné monštrá v Jave, takže tieto diagramy, ktoré sú v článku sú kreslené v GIMP-e.
        • Re: Obrazky/grafy 26.08.2009 | 00:43
          just me   Návštevník
          fuuf v gimpe je to dobry overkill :)