Kurz C++ (9.)


Úvod do OOP

Vítám Vás na dalším pokračování kurzu o C++. Vlastně až ode dneška si kurz zaslouží název "o C++", protože do teď jsme se věnovali jazyku C a rozšířením, které přinesl jazyk C++, ale objektům (což je největší přínos jazyka C++ oproti jazyka C) jsme se nevěnovali, ale to dnes napravíme – začneme pronikat do tajů OOP, tedy objektově orientovaného programování.

Proč OOP

Programování, kterému jsme se věnovali do dnes, se říká imperativní. Program je prostě sled instrukcí (volání funkcí a příkazů jazyka), který běhá v nějakém pevně daném pořadí. Ale to není vše, navíc jsme předělali problémy, který jsme řešili, na jiné, jejichž struktura by více odpovídala struktuře počítače. Příkladem nám může být opět adresář osob. Adresář jsme naprogramovali jako strukturu obsahující data (pole jmen a telefonních čísel), ale operace s touto strukturou (které jsou s ní neoddělitelně spjaty - vyhledávání jména, přidání atd.) jsme oddělili a implementovali jako vnější funkce (proto, že počítač neumí spojit data a operace - data jsou v paměti a můžeme napsat funkce, která tyto data mění, ale funkce a data nemůžeme příliš dobře vázat). Nemáme tedy žádné prostředky pro přesnější reprezentaci problému.

Objekty a třídy

Objektově orientované programování nám umožňuje programově reprezentovat elementy problému. Výsledek této reprezentace je objekt – model nějaké entity z reálného světa. S objekty můžeme provádět různé operace, tak jak je tomu i v reálném světě. Objekt tedy obsahuje data, ale i operace, které je možné s ním provést. Navíc uživatel nemusí vědět, jak objekt provádí žádané operace, může k němu přistupovat jako k černé skřínce.

Každý objekt je nějakého typu (tak jako proměnná je nějakého typu) – v terminologii OOP se říká, že objekt je instancí nějaké třídy. Třída definuje objekt, např. určuje, jaké operace umí provádět. Každá třída může mít více instancí (tedy je možné vytvořit více objektů na základě jedné třídy).

Vztahy mezi třídami

Program napsaný pomocí OOP je vždy tvořen nějakými objekty, které spolupracují, mezi nimi jsou tedy vždy nějaké vztahy. Protože objekty jsou instancemi nějakých tříd, vztahy jsou vlastně mezi třídami. Pro větší přehlednost budu tyto vztahy kreslit, a to za pomocí diagramů jazyka UML (Unified Modeling Language). Pokud tento jazyk neznáte, nevadí, uvidíte, že diagramy jsou velmi jednoduché. Třídy se zjednodušeně kreslí obdélníkem s názvem třídy uvnitř, a každý vztah vysvětlím.

Asociace (association)

Je to vztah, který vyjadřuje, že třídy "vědí o sobě" – jsou určitým způsobem propojeny a mohou komunikovat. V diagramu se to značí čarou, která spojuje třídy účastnící se asociace. K této čáře připojíme text, vysvětlující asociaci. Například vztah zaměstnance a firmy, pro kterou pracuje, je asociace:

Agregace (aggregation)

Mluvíme o agregaci pokud nějaká třída obsahuje jiné třídy (také se jím říká komponenty). Pomocí agregace můžeme tvořit složitější třídy pomocí jednodušších. Vztah se značí šipkou, která začíná u komponenty a končí nevyplněným kosočtvercem u třídy, která komponentu obsahuje. Například třída Auto obsahuje třídu Motor:

Dědičnost

To je pravděpodobně nejznámější vztah mezi třídami. Potřebujeme-li vytvořit dvě třídy, které mají mnoho společného (například grafický objekty čtverec a kružnice), byla by škoda je vytvořit odděleně. Mnohem lepší je říct, že to jsou grafické objekty, tedy "dědí" od třídy grafický objekt (a obsahují tedy i operace, které je možné provést s grafickým objektem), a dále se liší jen v detailech (obdélník má šířku a délku, kružnice má poloměr). V podstatě mluvíme o dědičnosti pokud jedna třída je podmnožina druhé (druhá třída dědí od první). První (od které se dědí) se nazývá nadtřída (také předek), a druhá třída (která dědí) se nazývá podtřída (potomek). V diagramu se dědičnost značí šipkou od podtřídy k nadtřídě:

Deklarace a definice tříd

Po tomto, pro někoho poněkud nezáživném úvodu, se můžeme pustit do nějakého programováni. Abychom mohli zavést do programu nějaký nový objekt, je potřeba definovat pro tento objekt novou třídu. To se provádí klíčovým slovem class. Zkusíme například třídu Osoba:

// deklarace třídy
class Osoba {
public:
    char jmeno[30];
    char adresa[50];
};
Deklarovali jsme třídu Osoba, která obsahuje dvě složky (budeme jim říkat členské proměnné). Jak vidíte, deklarace se liší od struktury pouze klíčovým slovem class, ale přibyl nám řádek "public:". Značí úroveň přístupu k členům (od tohoto řádku dále budou mít všechny členy úroveň přístupu public). Každá členská proměnná (a funkce, jak uvidíme později) má nějakou úroveň přístupu (member access control), která určuje, kdo může k ní přistupovat. Existují tři úrovně přístupu:
Úroveň
přístupu
Popis
private přístup je pouze z třídy, ve které je člen deklarovaný
protected přístup je pouze z třídy, ve které je člen deklarovaný, a z tříd z ní odvozených dědičností
public přístup je z jakékoliv třídy nebo funkce

Není-li explicitně uvedena úroveň přístupu, platí private.

Použití v programu se neliší od použití struktury:

Osoba osoba;  // deklare objektu osoba (instance třídy Osoba)
strcpy(osoba.jmeno, "Jan Novak");
Ale tímto příkladem jsme si neukázali skoro nic nového, v podstatě jsme pouze přepsali klíčové slovo struct na class. Řekli jsme si výše, že objekt může vykonat nějaké operace. K implementaci operací slouží v C++ funkce, které jsou součástí třídy (jsou v ní obsažené podobně jako členské proměnné jmeno a adresa). Tyto funkce se nazývají členské funkce nebo také metody. Na rozdíl od obyčejných funkcí je možné je zavolat pouze v kontextu nějakého objektu:
class Osoba {
public:
    char jmeno[30];
    char adresa[50];
    
    // členská funkce
    char *vratJmeno() {
        return jmeno;
    }
};
Jak vidíte, není tu žádný rozdíl oproti deklaraci obyčejných funkcí. Volání se ovšem provádí tečkovou notací, podobně jako se odkazujeme k členským proměnným:
#include <iostream.h>
#include <string.h>

void main() {
    Osoba osoba;
    strcpy(osoba.jmeno, "Jan Novak");
    
    cout << osoba.vratJmeno() << endl;
}
Protože definice členských funkcí by mohly být dost velké, deklarace třídy by se mohla stát poněkud velkou a méně přehlednou. Proto je možné přesunout definici členských metod vně deklarace třídy:
class Osoba {
public:
    char jmeno[30];
    char adresa[50];
    
    char *vratJmeno();
};

// definice členské funkce - používá se tzv. ctyřtečková notace
char *Osoba::vratJmeno() {
    return jmeno;
}
Jak je vidět, definice vypadá jako definice obyčejné třídy, s tím rozdílem, že v hlavičce je uvedena i třída, ke které tato členská funkce patří (plné jméno této funkce tedy je Osoba::vratJmeno().

Možná si kladete otázku proč jsme napsali funkci, která nám vrací nějakou členskou proměnnou, copak nemůžeme k ní přistupovat rovnou? V popisu objektů jsme si řekli, že uživatel k nim může přistupovat jako k černé skřínce, dostane tedy seznam operaci, ale víc ne (tomu se říká zapouzdření). Když mu dáváme k dispozici přímo členskou proměnnou, je to porušení zapouzdření (je pravda, že někdy se v praxi na zapouzdření příliš nehledí, ale musíme se snažit, aby k takovým situacím nedocházelo). Napsáním členské funkce ještě nemáme vyhráno, uživatel ji může, ale nemusí použít. Musíme mu tedy zabránit v tom, aby se mohl přímo odkazovat na členskou proměnnou, a to provedeme tak, že ji dáme úroveň přístupu private. Protože tím přijdeme o možnost členskou proměnnou nastavit, přidáme metodu nastavJmeno():

class Osoba {
private:
    char jmeno[30];
    char adresa[50];
    
public:
    char *vratJmeno();
    void nastavJmeno(char *nove_jmeno);
};

char *Osoba::vratJmeno() {
    return jmeno;
}

void Osoba::nastavJmeno(char *nove_jmeno) {
    strncpy(jmeno, nove_jmeno, 30);
    jmeno[29] = 0;
}

void main() {
    Osoba osoba;
    
    osoba.nastavJmeno("Jan Novak");
    cout << osoba.vratJmeno() << endl;
}
A to je pro dnešní lekci vše. Příště budeme pokračovat v podobném duchu jako dnes a prohloubíme znalosti o třídách a objekt.
Andrei Badea