C/C++ (39) - Objektově orientované programování

Za nejčastější důvod proč použít C++ místo C se uvádí objektově orientované programování (OOP). Dnes se zamyslíme nad tím, co to OOP vlastně je a jak se liší od klasického procedurálního programování ve stylu C.

4.10.2006 10:00 | Jan Němec | přečteno 41119×

Objektově orientované programování

Hned ze začátku bych rád čtenáře varoval. Objektově orientovaná programování (OOP) je stále ještě určitý fenomén. O všem, co je nebo kdy bylo moderní a populární v pozitivním i negativním smyslu a do určité míry kontroverzní (nejen ve světě počítačů), si můžeme přečíst spoustu článků silně zkreslených názorem autora. Diskuse, zda je OOP dobré nebo špatné a který objektově orientovaný jazyk je nejlepší tak silně připomínají diskuse na téma Linux kontra Windows a tahanice o nejlepší linuxovou distribuci.

Programy se obvykle snaží řešit konkrétní problém a jsou určitým modelem nějakého prostředí, ať už se jedná o implementaci algoritmu z abstraktního světa matematiky, simulaci z našeho fyzického světa nebo třeba GUI knihovnu. Z vlastní programátorské praxe vím, že OOP zjednodušuje a zpřehledňuje kód (především větších projektů) všude tam, kde objekty vyskytují i v problému samotném. Pokud píšeme Eukleidův algoritmus pro hledání největšího společného dělitele dvou čísel typu int, OOP nám nepomůže, neboť v algoritmu samotném se objekty nevyskytují. Naopak třeba při diskrétní simulaci dopravního problému se nám objekty mohou hodit pro dopravní prostředky a jejich datové struktury, semafory a podobně. Celá hierarchie typů objektů pro nejrůznější prvky GUI pak bude v každé objektově orientované knihovně grafického uživatelského rozhraní. Programovat objektově orientovaný problém pochopitelně lze i klasicky, procedurálně, ve stylu C, nicméně s OOP je to jednodušší.

Občas se setkáme s názorem, že OOP pomáhá odstraňovat nepříjemné paměťové chyby. Moje programátorská praxe to nepotvrzuje, správně procedurálně navržený kód považuji za zhruba stejně bezpečný jako kvalitní kód v duchu OOP. Rozdíly vyplývají spíše z toho, že se v C obvykle programuje poněkud nižším způsobem než v C++ a jazyky se používají na řešení odlišných problémů. I zde však platí, že klasické céčkovské problémy spojené například s chybným použitím ukazatelů a přetečením zásobníku vyvažují paměťové chyby specifické pro C++ a jeho standardní knihovnu. Odhalit programátorskou chybu, která se projevuje pádem programu v kódu C++ STL knihovny bývá často těžší než detekovat chybnou alokaci v klasickém programu v C. Celkově platí, že C klade větší nároky na obecnou schopnost programovat, C++ zase na konkrétní znalosti. Ochranu proti paměťovým chybám a automatickou správu paměti poskytují až pokročilejší jazyky jako je dnes například Java nebo C#, ale ani v jejich případě to nesouvisí s OOP (případná procedurální Java by byla bezpečná stejně) a i v jejich případě to s sebou zároveň přináší i určité nevýhody.

V OOP se vyskytují pojmy třída (anglicky class) a objekt (object). Třída znamená objektový typ, objekt pak instanci daného typu. Obojí známe již z klasického procedurálního programování. Třídám se nejvíce podobají strukturované typy (typedef struct ...) a objektům odpovídají proměnné těchto typů. Pojmy typ a proměnná se používají i v OOP.

Tři základní vlastnosti OOP

OOP není jen konkrétní syntaxe jazyka, ale především způsob programování. V C lze při troše snahy psát objektově a platí to i naopak, v objektově orientovaných jazycích jako například C++ nebo Java je možné psát klasicky procedurálně. Podpora na úrovni syntaxe jazyka OOP pouze výrazně zjednodušuje. Důležité jsou 3 ideje objektově orientovaného programování: zapouzdření, dědičnost a polymorfismus. Dnes se blíže podíváme na zapouzdření, zbylé dvě vlastnosti probereme příště.

Zapouzdření (encapsulation)

Algoritmy a datové struktury řešící jeden problém patří v kódu k sobě. Na data a metody, které netvoří rozhraní by se nemělo sahat z vnějšku. Výsledkem této snahy je třída. Ta se podobá céčkovské struktuře, ale na rozdíl od ní obsahuje i funkce. Funkcím třídy se říká metody. Třída navíc může definovat oprávnění, která omezí z vnějšku přístup k datům a volání metod. Abychom si ujasnili význam zapouzdření, ukážeme si kód, který se této zásadě příčí. Budeme jej postupně upravovat, tak aby ideji zapouzdření vyhovoval.

Představme si datovou strukturu, reprezentující množinu čísel typu int, jejíchž počet předem neznáme. Datovou strukturu opakovaně používáme třeba v nějakém matematickém algoritmu, který ladíme, takže operace potřebujeme logovat. Jedním z řešení by bylo dynamicky alokované pole, které podle potřeby zvětšujeme a případně i zmenšujeme. Abychom se vyhnuli realokaci po každém přidaném číslu, můžeme alokovat například vždy na dvojnásobek předchozí hodnoty tj. pokud máme naalokováno třeba 1024 intů a chceme přidat 1025. int, provedeme realokaci rovnou na 2048. Nejhorší implementace s použitím globálních proměnných by vypadala asi takhle:

Řešení 1

int *p;
int naalokovanoPrvku;
int pocetPrvku;

void init() {
  naalokovanoPrvku = 16;
  pocetPrvku = 0;
  p = (int *) malloc(naalokovanoPrvku * sizeof(int));
}

void loguj(int i, const char *akce) {
  printf("%s %i\n", akce, i);
}

void pridej(int cislo) {
  loguj(cislo, "pridej");
  if (naalokovanoPrvku == pocetPrvku) {
    // realokace
  }
  p[pocetPrvku++] = cislo;
}

void uber(int cislo) {
  loguj(cislo, "uber");
  // ...
}

bool obsahuje(int cislo) {
  loguj(cislo, "obsahuje");
  // ...
}

void done() {
  free(p);
  p = NULL;
}

// ...

Kód jsem navrhl schválně špatně, nevyhovuje nejen OOP, ale ani zásadám procedurálního programování. Funkce využívají globální proměnné, což je vždy potenciálním zdrojem problémů. Představme si třeba, že bychom chtěli mít dvě instance datové struktury najednou. Při takto navrženém kódu bychom potřebovali nejen dvě sady proměnných, ale i dvě sady funkcí.

Řešení 2

Chceme-li strukturu používat ve více instancích a neduplikovat kód, musíme se vzdát přímého přístupu ke globálním proměnným. Funkcím tak přibudou parametry.

// proměnné pro 1. instanci
int *struktura1_p;
int struktura1_naalokovanoPrvku;
int struktura1_pocetPrvku;

// a pro druhou instanci
int *struktura2_p;
int struktura2_naalokovanoPrvku;
int struktura2_pocetPrvku;

// definice funkcí (pro všechny instance dohromady)

void init(int **p, int *pocetPrvku, int *naalokovanoPrvku) {
  *naalokovanoPrvku = 16;
  *pocetPrvku = 0;
  *p = (int *) malloc(*naalokovanoPrvku * sizeof(int));
}

void pridej(int **p, int *naalokovanoPrvku, int *pocetPrvku, int cislo) {
  // ...
}

// ostatní funkce obdobně

// ...

// použití 1. instance

init(&struktura1_p, &struktura1_naalokovanoPrvku, &struktura1_pocetPrvku);
pridej(&struktura1_p, &struktura1_naalokovanoPrvku, &struktura1_pocetPrvku, 7);
// atd.

Řešení 3

Pokud se vám druhé řešení nelíbilo, nejste sami. Neustálé předávání tří parametrů - proměnných datové struktury je velmi otravné. Pomůžeme si první vlastností OOP, zapouzdřením. Nakročením na půl cesty k OOP je již klasická céčkovská struktura, zatím tedy stále nic nového.

struct DynamickePole {
  int *p;
  int naalokovanoPrvku;
  int pocetPrvku;
};

void init(/* v C bychom sem museli napsat "struct", v C++ ne */
  DynamickePole *d) {

  d->naalokovanoPrvku = 16;
  d->pocetPrvku = 0;
  d->p = (int *) malloc(d->naalokovanoPrvku * sizeof(int));
}

void pridej(DynamickePole *d, int cislo) {
  // ...
}

// další funkce obdobně

// použití

DynamickePole d;
init(&d);
pridej(&d, 7);
// ...
Řešení 4

Je vidět, že snaha o zapouzdření má smysl i v klasickém procedurálním kódu ve stylu C, kód z třetího řešení již je docela dobře použitelný. OOP v C++ ovšem jde dál, zapouzdřuje nejen proměnné, ale i funkce. Místo klíčového slova struct se potom většinou používá nové klíčové slovo class.

class DynamickePole {
private:
  int *p;
  int naalokovanoPrvku;
  int pocetPrvku;  
public:
  void init() {
    naalokovanoPrvku = 16;
    pocetPrvku = 0;
    p = (int *) malloc(naalokovanoPrvku * sizeof(int));
  }

  void pridej(int cislo) {
    // ...
  }

  // ...

};

// Použití

DynamickePole d;
d.init();
d.pridej(7);
// ...

Uvedený kód ještě zdaleka není ideální, ale zásadu zapouzdření už splňuje i z hlediska vysokých nároků OOP. Každý objekt třídy DynamickePole obsahuje 3 proměnné, které jsou soukromé, private. To znamená, že k nim máme přístup pouze z metod třídy DynamickePole a nikoli z vnějšku. To je důležité, jde o určitou ochranu vnitřních dat objektu, tedy dat, která netvoří rozhraní třídy. Naopak všechny metody v našem případě rozhraní tvoří, jsou tedy veřejné, public a lze je volat z vnějšku. C++ samozřejmě umožňuje i naopak definovat veřejnou proměnnou a soukromou metodu. V našem případě by mohla být soukromá třeba nějaké metoda provádějící realokaci, volali bychom ji z pridej a uber. Všimněte si, že metody se píší dovnitř třídy. Nejsou to tedy globální funkce, volat je můžeme pouze přes nějakou instanci třídy DynamickePole. Možnost vlastnit kromě proměnných i metody je hlavním rozdílem C++ třídy oproti C struktuře. Do definice třídy nemusíme psát celé tělo funkce, stačí hlavička ukončená středníkem. Metodu potom definujeme podobně jako globální funkci, překladač ovšem musí nějak poznat, ke které třídě patří. Proto v tomto případě předřadíme jméno třídy a čtyřtečku.

class DynamickePole {
// ...
public:
  void init();
// ...
}

// a někde později
void DynamickePole::init() {
  // ...
}

Domácí úkol

Zkuste si některé z řešení 1 až 4 naprogramovat, samozřejmě nejlépe řešení 4. Představte si, že se jedná o knihovnu, kterou bude používat jiný programátor a rozmyslete si, která řešení pro tento případ vyhovují.

Pokračování příště

Příště se zamyslíme nad dědičností a polymorfismem.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1296