Image
12.6.2016 0 Comments

C++ / Od členských funkcií k triedam / 17. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Priznám sa vám, že tentoraz nemám chuť vyjadrovať sa na úvod ani k prebiehajúcemu ročnému obdobiu (aj tak je ešte stále), ani k súčasnej politickej situácii (ak berieme ohľad na vlastné duševné zdravie, čítanie dennej tlače je vzhľadom na určitú - našťastie menšinovú - skupinu „občanov“, platených z našich daní, v poslednom čase výrazne rizikovou činnosťou). Čo sa týka kultúry, v krátkom čase za sebou som videl dva veľmi dobré filmy – You‘ve Got Mail a La vita e bella. Ja viem, mohli by ste namietať, že ten prvý je len taký romantický „blábol“ pre slabšie povahy, ale mne sa páčil práve preto, že bol taký jednoduchý. Okrem toho som si pri ňom vynikajúco oddýchol. A druhý z filmov? Azda budete so mnou súhlasiť (za predpokladu, že ste ho videli), že to bol Film s veľkým F, jeden z najlepších za posledný rok, dva a možno aj viac. Ďalej som si konečne našiel čas na prečítanie vynikajúceho románu 1984 od Georgea Orwella. Ak ste niekedy koketovali s myšlienkou, že za socializmu nebolo až tak zle, po prečítaní tejto knihy asi veľmi rýchlo zmeníte názor. Šokujúco pôsobí fakt, ako brilantne dokázal Orwell už v roku 1948 predpovedať vývoj sveta ovládaného komunistickou ideológiou!

Aby sa však tento seriál nezmenil na kultúrnu revue, vrátime sa radšej k C++ a jeho tajomstvám. V predošlej časti sme prebrali štruktúry a uniony. Povedali sme si o nich, že sú to agregované typy, združujúce pod jednou strechou a jedným názvom množinu (obyčajne nehomogénnu) iných typov. Vieme, že členmi štruktúry môžu byť klasické premenné, ktoré sa takto stávajú členskými premennými. Pristupujeme k nim za pomoci binárneho operátora . resp. ->. O čom sme však bližšie nehovorili, len sme to naznačili, je skutočnosť, že členmi štruktúr môžu byť aj funkcie. V tomto pokračovaní sa preto budeme venovať členským funkciám, ďalej sesterským typom štruktúr – triedam a riadeniu prístupu k členom tried.

Členské funkcie

Po preštudovaní predchádzajúcej časti už vieme, ako sa štruktúra deklaruje. Po kľúčovom slove struct a názve štruktúry uvedieme v krútených zátvorkách postupnosť deklarácií jednotlivých členov. Deklarácie sú, samozrejme, vždy ukončené bodkočiarkou. Bodkočiarku musíme uviesť aj za pravú krútenú zátvorku, ukončujúcu deklaráciu štruktúry. Pozabudnutie na túto maličkosť vedie k veľmi častej chybe pri preklade, ktorá bude obyčajne ohlásená na úplne inom mieste, a my sa nestačíme diviť, čo to ten náš prekladač zase vyvádza.

Členmi štruktúry však nemusia byť iba premenné, môžu nimi byť aj funkcie, tak ako ich poznáme doteraz. Takéto funkcie nazývame členskými funkciami, prípadne aj metódami. V ďalšom texte budeme používať termín členská funkcia (member function) a metódy si ponecháme pre Javu.

Členské funkcie deklarujeme jednoducho, stačí ich definíciu vložiť do deklarácie príslušnej štruktúry:

struct Account
{
  double balance;
 
  void deposit(double amount)
  {
    if (amount > 0)
      balance += amount;
  }
};

V tomto jednoduchom príklade máme štruktúru Account, reprezentujúcu (veľmi zjednodušene) účet v banke. Jej člen balance, ktorý je typu double, predstavuje aktuálny zostatok na účte. Na uľahčenie realizácie operácie vkladania na účet sme do štruktúry zabudovali funkciu deposit(). Jej argumentom je hodnota, ktorú chceme vložiť na účet. Funkcia deposit() skontroluje, či naozaj vkladáme zmysluplnú sumu (hodnota musí byť kladná), a potom o túto sumu zvýši aktuálny stav účtu.

Musíme si ešte ukázať, akým spôsobom členskú funkciu zavoláme. Spôsob je v princípe zhodný s prístupom k údajovým členom štruktúry, teda pomocou operátorov . a ->:

Account* myAcc;
myAcc = new Account();
myAcc->balance = 50000.0;
myAcc->deposit(11000.0);
myAcc->deposit(45000.0);
myAcc->deposit(20000.0);
...
delete myAcc;

Nový objekt typu Account sme vytvorili dynamicky pomocou operátora new. Získaný ukazovateľ sme uložili do premennej myAcc. „Počiatočný vklad“ sme zatiaľ pre nedostatok iných možností museli realizovali jednoduchým priradením hodnoty 50 000 do členskej premennej balance. No a potom sme už jednotlivé vklady realizovali výhradne volaním funkcie deposit(). Z nášho príkladu nie je zatiaľ jasné, načo zavádzať členské funkcie, keď môžeme operovať priamo s členom balance. Tento člen, reprezentujúci aktuálny zostatok na účte, však môžeme považovať za súčasť vnútorného stavu objektu typu Account a z predošlej časti vieme, že v zmysle zapuzdrenia tento vnútorný stav objektu nesmie byť prístupný zvonka. O chvíľu si ukážeme, ako prostriedkami jazyka C++ dosiahnuť, aby člen balance zvonka naozaj nebol prístupný a aby jediným spôsobom zmeny stavu objektu bolo volanie jeho členských funkcií (ekvivalentné posielaniu správ objektu, o ktorom sme hovorili minule).

Členské funkcie štruktúr sú niečím špecifické. Predstavme si situáciu, keď máme dva rôzne účty, a teda dva rôzne objekty typu Account a na každý z nich chceme vložiť inú sumu:

Account a1, a2;
a1.deposit(1000.0);
a2.deposit(2200.0);

Keď sa pozrieme na deklaráciu funkcie deposit(), vidíme, že v jej tele odkazujeme jednak na formálny argument amount (ktorý sa inicializuje jedinečným spôsobom pri volaní funkcie), a jednak na údajový člen balance našej štruktúry Account. Ale my vyvolávame funkciu deposit() nad dvoma rôznymi objektmi. Ako teda táto funkcia vie, s ktorým objektom pracuje (a na ktorý účet má vlastne vložiť peniaze)? Logicky môžeme predpokladať, že funkcia bude pracovať vždy s tým objektom, pomocou ktorého bola zavolaná. To však znamená, že pri jej volaní musíme nejakým spôsobom odovzdať odkaz na tento objekt. A presne toto sa deje aj v skutočnosti – každá členská funkcia dostáva ako jeden z argumentov ukazovateľ na ten objekt, nad ktorým bola zavolaná. Tento argument je na pohľad skrytý a nijako sa nedeklaruje, v tele funkcie je však vždy dostupný pomocou kľúčového slova this. Každý odkaz na ľubovoľný údajový člen štruktúry sa v tele členskej funkcie realizuje prostredníctvom ukazovateľa this. Vďaka tomu, že členská funkcia je členom príslušnej štruktúry a teda má prístup k údajovým členom štruktúry priamo, je jeho písanie nepovinné. Zvyčajne ukazovateľ this používame len na sprehľadnenie zápisu kódu, prípadne na odlíšenie rovnako pomenovanej členskej premennej a formálneho argumentu funkcie (nič nezvyčajné). Našu funkciu deposit() by sme teda mohli rovnako dobre zapísať aj takto:

void deposit(double amount)
{
  if (amount > 0)
    this->balance += amount;
}

Je vhodné uvedomiť si, že ukazovateľ this je konštantný, pre členské funkcie triedy X je jeho typ X* const, preto akákoľvek jeho zmena je zakázaná.

Dovolím si teraz malú vložku, v ktorej sa pokúsim opraviť prípadné nesprávne chápanie konceptu štruktúr (ergo tried), objektov, členských premenných a funkcií. Deklaráciou štruktúry (triedy, unionu) zavádzame do programu nový typ. Nedefinujeme tým však nijaký nový objekt a prekladač neprideľuje nijakú pamäť. Ak ďalej v programe deklarujeme objekt typu tejto štruktúry, prípadne ak ho vytvoríme dynamicky pomocou operátora new, hovoríme, že vytvárame novú inštanciu štruktúry, resp. triedy (v angličtine je na to výraz „to instantiate“, doslova niečo ako „inštancionalizovať“). Novému objektu sa v pamäti vyhradí miesto, v ktorom budú obsiahnuté všetky údajové členy danej štruktúry, nie však jej členské funkcie (!). Z tohto hľadiska sa členské funkcie správajú ako bežné funkcie C++, iba s tým rozdielom, že pri volaní dostávajú ako argument skrytý ukazovateľ na príslušný objekt a z hľadiska linkera sú prísne typovo označkované, čiže ich naozaj nemožno volať iným spôsobom, iba prostredníctvom objektov danej štruktúry. Všetky inštancie jedného typu teda zdieľajú jeden kód pre každú členskú funkciu, ale každá si uchováva vlastný privátny stav. To nám presne harmonizuje s predstavou, ktorú sme si opísali minule, že každý objekt má svoj vlastný stav a okrem toho exportuje zoznam správ, na ktoré dokáže reagovať. Je zrejmé, že správanie objektov rovnakého typu bude rovnaké, preto je kód členských funkcií zdieľaný. (A mimochodom, je to úplne logické – predstavte si desaťtisíc objektov, z ktorých každý by mal vlastnú kópiu jedného a toho istého kódu – to je, samozrejme, nezmysel).

Už sme spomenuli, že členské funkcie deklarujeme uvedením ich definície v zozname deklarácií členov štruktúry. Nie je to celkom presné, pretože existuje ešte jedna alternatíva. Namiesto definície funkcie vložíme do štruktúry iba jej deklaráciu, t. j. prototyp tejto členskej funkcie a samotné telo uvedieme niekde mimo (samozrejme, až za deklaráciu štruktúry). V takom prípade však musíme meno členskej funkcie, modifikovať, aby bolo prekladaču jasné, ku ktorej štruktúre ktorá funkcia patrí. Úplné, tzv. kvalifikované meno členskej funkcie získame predradením mena jej rodičovskej štruktúry spolu so známym operátorom štvorbodky ::. Deklarácia štruktúry Account bude potom vyzerať takto:

struct Account
{
  double balance;
  void deposit(double);
};
 
void Account::deposit(double amount)
{
  if (amount > 0)
    balance += amount;
}

Členská funkcia deposit() štruktúry Account má teda kvalifikované meno Account::deposit(). Oba spôsoby deklarácie sú v zásade ekvivalentné okrem jedného významného detailu. Funkcie, definované prvým spôsobom, t. j. priamo v deklarácii štruktúry, sú implicitne inline. Čo to znamená, to sme si už hovorili, pre tých, ktorí zabudli – ide o funkčný ekvivalent makra, keď sa volanie funkcie nahradí priamo jej telom. Druhý spôsob nám inline-ovosť funkcie nijako nezabezpečuje, a teda pokiaľ ju požadujeme, musíme kľúčové slovo inline uviesť explicitne:

inline void Amount::deposit(double amount)
{
  if (amount > 0)
    balance += amount;
}

(V princípe je uvedenie definície členskej funkcie priamo v deklarácii štruktúry úplne ekvivalentné jej zápisu za touto deklaráciou so špecifikátorom inline.)

Je dobrým zvykom členské funkcie s veľmi jednoduchým telom definovať priamo v deklarácii triedy (resp. štruktúry, ale prakticky môžeme bez ujmy na všeobecnosti hovoriť o triedach z dôvodov, ktoré uvidíme o chvíľu). Tým automaticky zabezpečíme, že takéto funkcie budú linkované ako inline. Zložitejšie funkcie definujeme samostatne, aby zbytočne nezneprehľadňovali samotnú deklaráciu triedy. V praxi bývajú deklarácie tried uložené v samostatných hlavičkových súboroch (samozrejme, pre každú triedu nemusí nevyhnutne existovať jeden súbor i keď neraz sa to robí práve takto). Každý hlavičkový súbor môže byť pomocou direktívy #include vložený do tých zdrojových súborov, v ktorých s príslušnou triedou potrebujeme pracovať. Samotná implementácia členských funkcií tej - ktorej triedy sa však nachádza v samostatných súboroch. Takto môžeme dať iným programátorom na prípadné znovupoužitie našich tried k dispozícii sériu objektových a hlavičkových súborov bez toho, aby sme zverejňovali detaily implementácie týchto tried. A programátor, ktorému nevyhovuje správanie určitej triedy, si jednoducho pomocou mechanizmu dedičnosti odvodí novú triedu s upraveným správaním. Ale o tom až v kapitole venovanej dedičnosti.

Riadenie prístupu k členom

Pomyselným mostíkom medzi štruktúrami a triedami je kontrola prístupu k jednotlivým členom agregovaných typov. Každý jeden člen štruktúry, triedy či unionu, či je to premenná, alebo funkcia, má pridelený zvláštny atribút, ktorý určuje, kedy a za akých okolností je tento člen viditeľný a prístupný.

V C++ existujú tri úrovne oprávnení k prístupu, charakterizované kľúčovými slovami private, protected a public. Tieto prístupové špecifikátory sa uvádzajú v deklarácii štruktúry v rovnakom tvare ako návestia case v príkaze switch. Každý špecifikátor určuje prístupovú úroveň pre všetky nasledujúce členy až do výskytu ďalšieho špecifikátora, resp. až po koniec deklarácie štruktúry. Špecifikátory sa môžu v jednej deklarácii použiť aj viackrát. Demonštrujeme si ich použitie na príklade:

struct X
{
public:
  int a;
  double b;
 
private:
  char c;
  void out();
 
protected:
  long d;
 
public:
  char* str;
  void print();
};

V príklade deklarujeme štruktúru X, ktorá obsahuje premennú c a funkciu out() typu private, ďalej premennú d typu protected a nakoniec premenné a, b, str a funkciu print() typu public . Aký je však význam jednotlivých špecifikátorov? To nám čiastočne napovie ich preklad. Členy deklarované ako private sa považujú za súkromné pre danú štruktúru a sú prístupné iba členským funkciám tejto štruktúry. Iný spôsob prístupu je zakázaný. Majme nasledujúcu štruktúru:

struct A
{
private:
  int x;
 
public:
  void set(int i)
  {
    x = i;
  }
};

Zmena hodnoty privátneho člena x je prostredníctvom členskej funkcie set() povolená – tento člen je v rámci štruktúry prístupný hocijakej členskej funkcii. Ak by sme však chceli zmeniť hodnotu x priamo, prekladač ohlási chybu:

A a;
a.set(5); // OK
a.x = 5;  // chyba!

Privátne môžu byť aj členské funkcie, ktoré v takom prípade nemôžeme volať priamo, ale len z iných členských funkcií.

Členy deklarované ako public sú, presne naopak, verejnými členmi danej triedy. Aj keď to odporuje princípu zapuzdrenia, je možné takto niektoré členské premenné štruktúry sprístupniť okoliu priamo. Táto voľnosť však takmer vždy vedie k zamedzeniu dôslednej kontroly a udržiavania konzistencie vnútorného stavu objektov. Ako public sa preto prevažne deklarujú členské funkcie. Zoznam všetkých verejných funkcií tej - ktorej štruktúry/triedy predstavuje jej externé programátorské rozhranie, a teda de facto zoznam správ, na ktoré je každá inštancia tejto štruktúry schopná reagovať. Ideálny stav predstavuje taká štruktúra, ktorej všetky údajové členy sú privátne, pretože spolu vytvárajú vnútorný stav objektu, a ktorý exportuje určitú pevne definovanú množinu verejných funkcií predstavujúcich API objektu.

Tretí typ prístupu k členom, reprezentovaný kľúčovým slovom protected, nachádza uplatnenie až po zavedení vzťahov dedičnosti medzi triedami. Protected údajové členy či funkcie takisto nie sú prístupné zvonka. Prístup k nim však majú povolený okrem členských funkcií danej triedy aj členské funkcie všetkých tried z nej odvodených. Bližšie si o tomto špecifikátore povieme neskôr.

Triedy verzus štruktúry

Konečne máme dostatok informácií na to, aby sme si objasnili zásadný rozdiel medzi štruktúrami a triedami. Okrem čisto kozmetického faktu, že triedy deklarujeme s kľúčovým slovom class namiesto struct  pri štruktúrach používaného, rozdiel spočíva v implicitnom prístupe k jednotlivým členom oboch typov. Zatiaľ čo štruktúry (a takisto uniony) majú všetky svoje členy implicitne verejné (public), čo nám umožnilo v našich príkladoch týkajúcich sa štruktúr špecifikátory prístupu vynechať, členy tried sa implicitne považujú za privátne (private). To znamená, že deklarácia typu:

class A { int x, y; };

je nanič, pretože členy x a y z tejto triedy nijakým spôsobom „nevydolujeme“.

Teraz, keď vieme, čo sú to triedy a ako sa líšia od dosiaľ preberaných štruktúr, dohodneme sa, že na štruktúry zabudneme. Ich použitie v OO programoch je zbytočné, pretože sa dajú rovnako dobre realizovať pomocou tried. (V skutočnosti sú štruktúry dedičstvom po jazyku C, v ktorom však ich použitie bolo výrazne obmedzené oproti C++ – nebolo napríklad možné ako členy štruktúry uviesť funkcie). Odteraz budeme preto pracovať výhradne s pojmom trieda.

Pre lepšie podchytenie konceptu tried si ukážeme príklad takého malého ideálneho objektu. Bude to trieda Word, zapuzdrujúca 16-bitové celé číslo bez znamienka. O užitočnosti tejto triedy by sme mohli polemizovať, ale na výučbové účely nám celkom postačí. Tu je jej deklarácia:

typedef unsigned short WORD;
 
class Word
{
  WORD w;
 
public:
  void set(WORD val)
  { w = val; }
  WORD get()
  { return w; }
};

Vidíme, že trieda Word obsahuje jediný dátový člen w, ktorý bude predstavovať uchovávanú hodnotu. Tento člen je implicitne privátny, preto sme špecifikátor private vynechali. Okrem toho máme k dispozícii dve funkcie - Word::set() a Word::get(). Ich názvy napovedajú, na čo ich budeme používať. Prvá z nich slúži na nastavenie či zmenu uloženého čísla, keď sa jednoducho priradí členu w hodnota argumentu tejto funkcie. Druhá z funkcií, naopak, slúži na zistenie uloženej hodnoty, ktorú vráti ako návratovú hodnotu. Poviete si, načo sme vôbec skrývali člen w pred svetom, keď sme vyexportovali dve funkcie, pomocou ktorých si môžeme s obsahom triedy Word robiť, čo sa nám zapáči. Ale v tom je práve skryté celé tajomstvo. Môžeme síce pracovať s obsahom triedy Word ľubovoľne, ale len pomocou funkcií set() a get(). Ak sa v budúcnosti rozhodneme, že zmeníme vnútornú reprezentáciu triedy Word, okolie sa to nedozvie, pretože nezmeníme programátorské rozhranie tejto triedy. Nuž a v tom je vlastne podstata celého OOP. Každý objekt sa stará sám o seba a služby iných objektov využíva len prostredníctvom ich rozhrania. Pokiaľ sa toto rozhranie nezmení, môžeme vnútro objektov meniť podľa ľubovôle, a predsa nebudeme musieť meniť všetky funkcie, ktoré s daným objektom pracujú. V tejto na pohľad triviálnej vete je skrytá obrovská, predovšetkým časová úspora pri vývoji programov.

Statické členy tried

Rovnako ako nečlenské premenné alebo funkcie aj jednotlivé členy triedy môžu byť deklarované s kľúčovým slovom static. Tieto tzv. statické členské premenné, resp. statické členské funkcie, však majú odlišný význam od svojich nečlenských ekvivalentov. Zopakujme si pre osvieženie pamäti, čo spôsobí doplnenie špecifikátora static k deklarácii (nečlenského) objektu. Globálna statická premenná či funkcia (so súborovým rozsahom platnosti) je neviditeľná mimo zdrojového súboru, v ktorom je deklarovaná. V podstate ide o taký nie príliš dokonalý a neobjektový ekvivalent špecifikátora private, ibaže nie v rámci triedy, ale v rámci súboru (t. j. modulu). Lokálna statická premenná zase existuje aj vtedy, keď sa program práve nenachádza v bloku, v ktorom je deklarovaná.

Deklarácia statického člena triedy naproti tomu „odoberá“ vlastníctvo tohto člena inštancii triedy a prenáša ho na samotnú triedu. To znamená, že statická členská premenná nebude súčasťou vnútorného stavu každej inštancie, nebude existovať jej samostatná kópia pre každú vytvorenú inštanciu, ale bude existovať jediná kópia, ktorá bude de facto súčasťou samotnej triedy. Okrem iného to znamená, že na prístup k statickej premennej nepotrebujeme existujúcu inštanciu triedy. Ukážeme si príklad. Predstavme si, že chceme implementovať zariadenie známe z rôznych bánk, ktoré každému návštevníkovi vytlačí lístok s jeho poradovým číslom. Od zariadenia požadujeme, aby bolo poradové číslo pre každý lístok jedinečné a o jednotku väčšie ako číslo na predchádzajúcom lístku. Nech je každý lístok inštanciou triedy Ticket. Keďže nám zatiaľ chýbajú potrebné znalosti o špeciálnej členskej funkcii zodpovednej za inicializáciu objektu po jeho vytvorení (tzv. konštruktor), implementujeme len jednu členskú funkciu nextNumber(), ktorá bude vracať číslo nového lístka. Aby sme splnili uvedené požiadavky, musíme si pamätať číslo, ktoré bolo vygenerované naposledy. Na tento účel do triedy zavedieme statickú členskú premennú lastNumber. Funkcia nextNumber() pri každom volaní túto premennú inkrementuje a jej obsah vráti. Tu je deklarácia triedy:

class Ticket
{
  static int lastNumber;
 
  // ...
  // iné premenné
  // ...
 
public:
  int nextNumber()
  {
    return ++lastNumber;
  }
 
  // ...
  // iné funkcie
  // ...
};

Oproti nestatickým údajovým členom sa statické premenné vyznačujú ešte jednou zvláštnosťou. Nestačí ich totiž len deklarovať v rámci triedy, musíme ich navyše povinne definovať mimo triedy, akoby to boli bežné globálne premenné. Pri tejto definícii, rovnako ako pri definícii členských funkcií mimo triedy treba použiť úplné, kvalifikované meno danej premennej. Našu deklaráciu teda musíme doplniť ešte riadkom:

int Ticket::lastNumber;

Tento riadok nesmie byť súčasťou deklarácie triedy, musí sa nachádzať mimo nej, na úrovni súboru.

Zostáva už len otázka, ako premennú lastNumber inicializovať. Na tomto mieste si musíme uviesť fakt, ktorý som dosiaľ nijako nezdôrazňoval – deklarácie členských premenných tried nesmú (na rozdiel napríklad od Javy) za žiadnych okolností obsahovať inicializátory! Nasledujúca deklarácia je preto chybná:

class C
{ int a = 123; };

Členské premenné sa inicializujú výhradne pomocou špeciálnej členskej funkcie (už spomínaného konštruktora). Čo však so statickými premennými? Tie nie sú súčasťou vytváraných objektov, existujú dávno pred vytvorením prvej inštancie. Našťastie ich je možné (dokonca nutné) inicializovať jednoducho doplnením inicializátora do ich samostatne uvedenej definície. Premenná lastNumber z našej triedy Ticket by mohla byť inicializovaná napríklad takto:

int Ticket::lastNumber = 0;

Ako sme už uviedli, statické údajové členy existujú nezávisle od inštancií danej triedy, a teda je možné k nim pristupovať priamo, bez použitia operátorov . či ->. Napríklad majme triedu:

class A
{ public: static int a; };
 
int A::a;

Člen a je verejný, a teda prístupný aj mimo triedy. Pracovať s ním môžeme jednoducho - použitím jeho plne kvalifikovaného mena A::a:

A::a = 10;

K statickým členom, samozrejme, môžeme pristupovať aj prostredníctvom jednotlivých inštancií triedy A, no v takom prípade sa ľavý operand operátora . či -> nevyhodnocuje:

A a1, *pa = &a1;
 
a1.a = 111;
pa->a = 222;
(++pa)->a = 333;

Vo všetkých troch prípadoch sa pracuje s jedným a tým istým členom a, navyše sa v treťom prípade hodnota ukazovateľa pa nijako nemení.

Statické môžu byť aj členské funkcie. Od nestatických sa líšia veľmi podstatne v tom, že nedostávajú pri volaní ako jeden z argumentov ukazovateľ this. Každé použitie tohto kľúčového slova v rámci statickej členskej funkcie je chybou. Keď sa nad tým zamyslíme, je to nanajvýš logické, pretože volanie statickej funkcie sa nevzťahuje na žiadnu inštanciu. Podobne ako k statickej premennej možno k statickej funkcii pristupovať (teda ju volať) priamo, s použitím jej kvalifikovaného mena a takisto ju možno volať aj v okamihu, keď žiadna inštancia príslušnej triedy neexistuje. Je, samozrejme, dovolené volať statickú funkciu aj klasicky - prostredníctvom operátorov . alebo ->, ale ani v tomto prípade sa ľavý operand oboch operátorov nevyhodnocuje. Statické funkcie smú používať len statické premenné danej triedy (resp. všetko, čo netreba sprístupňovať pomocou ukazovateľa this).

Ako príklad statickej členskej funkcie si paradoxne uvedieme našu funkciu nextNumber(). Keď ste sa nad triedou Ticket poriadne zamysleli, isto ste dospeli k názoru, že funkcia ako nextNumber() sa nevzťahuje na nijakú konkrétnu inštanciu. Jej služby potrebujeme len v okamihu vytvorenia nového lístka. Len čo je lístok vytlačený, už by sme jeho číslo nemali meniť, preto každé ďalšie volanie nextNumber() stráca zmysel. Z tohto dôvodu je vcelku logické urobiť funkciu nextNumber() statickou a nezávislou od jednotlivých inštancií. Zmeníme preto triedu Ticket takto:

class Ticket
{
  static int lastNumber;
 
  // ...
  // iné premenné
  // ...
 
public:
  static int nextNumber()
  {
    return ++lastNumber;
  }
 
  // ...
  // iné funkcie
  // ...
};

Odteraz môžeme funkciu nextNumber() volať priamo (a triedu Ticket zneužiť na generovanie postupnosti celých čísel):

int n = Ticket::nextNumber();

Statická funkcia nemusí byť nevyhnutne definovaná mimo triedy, jej telo sa môže nachádzať aj priamo v deklarácii triedy.

Vnorené a lokálne triedy

Triedu môžeme deklarovať aj vnútri inej triedy. Takáto trieda sa nazýva vnorená. Rozsah platnosti vnorenej triedy je trieda, ktorá jej deklaráciu obsahuje. Vnorená trieda môže zo svojej „nadradenej“ triedy (nie nadradenej v rámci dedičnosti!) priamo používať len prípadné statické členy, typy a enumerátory, všetky ostatné členy musí sprístupňovať bežným spôsobom cez existujúce inštancie. Pozrime sa na príklad:

class outer
{
public:
  int x;
  static int s;
 
  class inner
  {
    static int t;
    void f(int i, outer* p);
  };
};

V rámci funkcie f triedy inner nemáme prístup k členu x triedy outer, priradenie

x = i;

je preto chybou. Naproti tomu k členu s prístup máme, preto priradenie

s = i;

je povolené. Ak však máme k dispozícii platnú inštanciu triedy outer, napríklad prostredníctvom ukazovateľa, môžeme bez problémov pristupovať aj k jej členu x:

p->x = i;

Názov triedy inner je sám osebe súčasťou triedneho rozsahu platnosti triedy outer, preto deklarácia

inner *ptr = new inner();

je chybná, musíme použiť plne kvalifikované meno:

outer::inner *ptr = new outer::inner();

Statické členy a členské funkcie triedy inner môžeme definovať na globálnej úrovni, ale takisto len s použitím kvalifikovaných mien:

int outer::inner::t = 0;
void outer::inner::f(int i, outer* p)
{ ... }

Medzi triedami outer a inner neexistuje nijaký iný vzájomný vzťah, čo sa týka možnosti prístupu k členom – trieda outer nemá prístup k privátnym členom triedy inner a trieda inner takisto nemá prístup k privátnym členom triedy outer.

Typy definované pomocou mechanizmu typedef vo vnorených triedach sú takisto prístupné len pomocou kvalifikovaných mien:

class A
{
  class B
  {
  public:
    typedef int I;
    // ...
  };
  // ...
};
 
I i1;      // chyba
B::I i2;   // chyba
A::B::I i; // OK

Trieda deklarovaná v rámci definície nejakej funkcie sa nazýva lokálna. Jej názov je lokálny pre danú funkciu a táto trieda sama nesmie používať lokálne premenné danej funkcie. Všetky členské funkcie lokálnej triedy musia byť definované priamo v jej deklarácii. Lokálna trieda nesmie obsahovať statické údajové členy. Funkcia obsahujúca deklaráciu lokálnej triedy nemá špeciálny prístup k členom tejto triedy (t. j. nemá prístup k neverejným členom triedy).

„Priateľské“ deklarácie

Povedali sme si, že v C++ existujú tri úrovne prístupu k členom tried: private, protected a public. Často sa však dostaneme do situácie, keď by sme potrebovali riadiť povolenie prístupu k neverejným členom v závislosti od funkcie, ktorá ho požaduje. Na tieto účely máme k dispozícii kľúčové slovo friend. Ide o funkčný špecifikátor, ktorý povoľuje nečlenskej funkcii prístup k private a protected členom nejakej triedy. Funkcia sa však nestáva členom tejto triedy ani nie je volaná prostredníctvom jej inštancií. Takisto sa na ňu nevzťahujú špecifikátory prístupu, uvedené v deklarácii triedy.

Nasledujúci príklad názorne ukazuje rozdiel medzi prístupom k privátnemu členu prostredníctvom členskej funkcie a prostredníctvom spriatelenej funkcie:

class A
{
  int x;
 
  friend void friendSet(A*, int);
 
public:
  void memberSet(int)
  {
    x = i;
  }
};
 
void friend_set(A* pa, int i)
{
  pa->x = i;
}

V príklade máme dve funkcie slúžiace na nastavenie hodnoty člena x triedy A. Jedna z nich, memberSet(), je členská a má, samozrejme, prístup k privátnemu členu x. Druhá z nich, friendSet(), síce nie je členská, ale zato je deklarovaná ako spriatelená (friend), a preto tiež môže pristupovať k členu x. Obe nasledujúce volania sú preto správne:

A a;
friend_set(&a, 10);
a.member_set(10);

Ako spriatelenú funkciu môžeme uviesť aj členskú funkciu inej triedy (použitím kvalifikovaného mena) a takisto môžeme určiť za spriatelenú celú cudziu triedu. V takom prípade budú mať všetky členské funkcie tejto triedy prístup k neverejným členom triedy, ktorá priateľstvo udelila:

class X
{
  void f();
  // ...
};
 
class Y
{
  friend void X::f();
  // ...
};
 
class Z
{
  friend class X;
};

Spriatelené funkcie sú obyčajne definované mimo triedy, ktorá im priateľstvo udelila, ale v princípe je možné ich definíciu uviesť aj priamo v deklarácii tejto triedy. V takom prípade sú tieto funkcie implicitne inline. Priateľstvo nie je ani dedičné, ani tranzitívne, t. j. ak je trieda B priateľom triedy A a trieda C priateľom triedy B, neznamená to, že trieda C musí byť automaticky priateľom triedy A.

Priateľstvo môže byť deklarované aj symetricky, v takom prípade však musíme použiť tzv. predbežnú deklaráciu triedy:

class Y;
 
class X
{
  void f();
  friend void Y::g();
  // ...
};
 
class Y
{
  void g();
  friend void X::f();
  // ...
};

Na to, aby sme mohli funkciu Y::g() deklarovať ako spriatelenú pre triedu X, ktorej deklarácia sa nachádza pred deklaráciou triedy Y, musíme prekladaču oznámiť, že bude niekedy v budúcnosti deklarovaná nejaká trieda Y, inak dostaneme len chybové hlásenie.

Konštantné členské funkcie

Na záver tejto časti si ešte povieme o aplikácii špecifikátorov const a volatile na členské funkcie. Predstavme si, že máme inštanciu nejakej triedy, deklarovanú ako konštantnú (t. j. so špecifikátorom const). Ak prostredníctvom tejto inštancie zavoláme niektorú členskú funkciu, odovzdá sa tejto funkcii ukazovateľ this na danú inštanciu. Prostredníctvom ukazovateľa by však funkcia mohla meniť obsah inštancie bez ohľadu na to, či bola inštancia deklarovaná ako konštantná, alebo nie! Nekonštantná členská funkcia triedy X očakáva ukazovateľ this typu X* const (čo je, ešte raz zdôrazňujem, konštantný ukazovateľ, nie ukazovateľ na konštantu). Ukazovateľ na našu inštanciu je však typu const X* const, a preto nám prekladač takéto volanie nepovolí. Z tohto dôvodu máme možnosť špecifikovať ľubovoľnú členskú funkciu ako konštantnú pridaním kľúčového slova const za jej deklarátor, ale ešte pred jej telo. Takáto členská funkcia, samozrejme, nemá povolené meniť údajové členy inštancie, nad ktorou bola zavolaná, môžeme ju však volať aj pre konštantné objekty. Najlepšie bude vysvetliť celú situáciu na príklade:

class A
{
  int x;
 
public:
  void set(int i)
  {
    x = i;
  }
  int get() const
  {
    return x;
  }
};

Trieda A obsahuje jedinú celočíselnú premennú x. Na zmenu hodnoty tejto premennej slúži funkcia set(), na jej čítanie funkcia get(). Keďže get() nemení obsah x a teoreticky by mohla byť volaná aj pre konštantné objekty, deklarujeme ju ako konštantnú. Naproti tomu set() mení obsah x, preto ju pre konštantné objekty nemôžeme volať:

A a1;
const A a2;
a1.set(5);               // OK
int i1 = a1.get(); // OK
a2.set(8);         // chyba
int i2 = a2.get(); // OK

Konštantnú funkciu môžeme volať pre konštantné i nekonštantné objekty, nekonštantnú iba pre nekonštantné. Konštantnosť je súčasťou typu funkcie, preto je možné mať dve členské funkcie s rovnakým názvom, počtom a typmi argumentov, z ktorých jedna bude konštantná a jedna nie. Typický príklad (aj keď silne oklieštený):

class String
{
  char* str;
 
public:
  char* get()
  {
    return str;
  }
  const char* get() const
  {
    return str;
  }
};

Obe funkcie get() sa inak líšia ešte aj v návratovej hodnote, to však za normálnych okolností nestačí na rozlíšenie dvoch funkčných synoným. Ako vidno z príkladu, telá oboch funkcií sú zhodné, líšia sa len tým, že jedna vracia nekonštantný a druhá konštantný ukazovateľ na zapuzdrený reťazec.

Rovnaké pravidlá ako pre špecifikátor const platia aj pre špecifikátor volatile. Typ ukazovateľa this sa, samozrejme, mení na volatile X* const. Okrem toho je možné deklarovať členskú funkciu ako konštantnú a volatile zároveň uvedením oboch špecifikátorov, typ ukazovateľa this bude potom const volatile X* const.

Nabudúce

Dnes sme to ho prebrali, až-až. Pohodlne si to prejdite aj viackrát, kým nebudete mať istotu, že všetkému rozumiete. V nasledujúcej časti budeme hovoriť o konštruktoroch, deštruktoroch a iných cudzích slovách.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá