Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 3 - Hrací kostka v C++ a konstruktory

V předešlém cvičení, Řešené úlohy k 1. a 2. lekci OOP v C++, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Dnes začneme pracovat na slíbené aréně, ve které budou proti sobě bojovat bojovníci. Boj bude tahový (napřeskáčku) a bojovník vždy druhému ubere život na základě síly jeho útoku a obrany druhého bojovníka. Simulujeme v podstatě stolní hru, budeme tedy simulovat i hrací kostku, která dodá hře prvek náhodnosti. Začněme zvolna a vytvořme si dnes právě tuto hrací kostku. Zároveň se naučíme jak definovat vlastní konstruktor.

Vytvoření projektu

Vytvořme si novou prázdnou aplikaci a pojmenujme ji Arena. Jako obvykle vytvoříme soubor main.cpp se základním kódem:

#include <iostream>

using namespace std;

int main()
{
    cin.get();
    return 0;
}

K projektu si přidejme novou třídu s názvem Kostka. Zamysleme se nad atributy, které kostce dáme. Jistě by se hodilo, kdybychom si mohli zvolit počet stěn kostky (klasicky 6 nebo 10 stěn, jak je zvykem u tohoto typu her). Naše třída bude mít nyní 1 atribut: pocet_sten typu int. Naše třída nyní vypadá asi takto:

Pozn.: Vygenerované #pragma once jsme odstranili, ale předgenerované metody jsme ponechali.

Kostka.h

#ifndef __KOSTKA_H__
#define __KOSTKA_H__

class Kostka
{
public:
    Kostka();
    ~Kostka();
    int pocet_sten;
};

#endif

Konstruktory

Až doposud jsme při vytváření nové instance neuměli nastavit atributy třídy. Museli jsme psát něco v tomto smyslu:

Kostka k;
k.pocet_sten = 6;

My bychom ale chtěli nastavit počet stěn již během vytváření instance. Minule jsme si letmo zmínili, že existuje konstruktor objektu. Je to metoda, která se zavolá ve chvíli vytvoření instance. Slouží samozřejmě k nastavení vnitřního stavu objektu a k provedení případné inicializace. Jednoduchý konstruktor nám již vygenerovalo Visual Studio. Je zapsaný takto: Kostka(). V souboru Kostka.cpp má prázdné tělo({}). My si však nyní do jeho těla něco doplníme. V konstruktoru nastavíme počet stěn na pevnou hodnotu. Konstruktor bude vypadat následovně:

Kostka.cpp

Kostka::Kostka()
{
    pocet_sten = 6;
}

Konstruktor se deklaruje jako metoda, ale nemá návratový typ a musí mít stejné jméno jako je jméno třídy, v našem případě tedy Kostka.

Pokud kostku nyní vytvoříme, bude mít v atributu pocet_sten hodnotu 6.

main.cpp

Vypišme si počet stěn do konzole, ať vidíme, že tam hodnota opravdu je. V main.cpp si kostku vytvoříme a hodnotu si vypíšeme.

#include <iostream>
#include "Kostka.h"

using namespace std;

int main()
{
    Kostka kostka;
    cout << kostka.pocet_sten << endl;
    cin.get();
    return 0;
}

Výsledek:

Konzolová aplikace
6

Vidíme, že se konstruktor opravdu zavolal. My bychom ale chtěli, abychom mohli u každé kostky specifikovat při vytvoření, kolik stěn budeme potřebovat.

Kostka.cpp

Dáme tedy konstruktoru parametr (musíme upravit i deklaraci v Kostka.h, aby přijímala parametr typu int):

Kostka::Kostka(int _pocet_sten)
{
    pocet_sten = _pocet_sten;
}

Všimněte si, že jsme před název parametru metody přidali znak "_", protože jinak by měl stejný název jako atribut a C++ by to zmátlo. Vraťme se k main.cpp a zadejme tento parametr do konstruktoru:

    Kostka kostka(10);
    cout << kostka.pocet_sten << endl;
using namespace std;
class Kostka
{
public:
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
};
#include <iostream>
#include "Kostka.h"

using namespace std;
Kostka::Kostka(int _pocet_sten)
{
    pocet_sten = _pocet_sten;
}

Kostka::~Kostka()
{
}

Výsledek:

Konzolová aplikace
10

Všimněte si použití závorek. Pokud závorky vynecháme (jako jsme to dělali doteď), potom se volá bezparametrický konstruktor (ten, který byl původně vygenerován). Pokud má třída pouze konstruktory, které přijímají parametry, potom instanci musíme vytvořit tímto způsobem. Pokud by třída měla bezparametrický konstruktor a nějaké další, poté se při vytváření instance bez závorek zavolá právě ten bez parametrů. Jediný rozdíl je při dynamickém vytváření instance, kde závorky být mohou, ale nemusí.

Kostka sestisten;
Kostka sestisten2();  // nebude fungovat
Kostka desetisten(10);
Kostka* dynamicky_sestisten = new Kostka;
Kostka* dynamicky_sestisten2 = new Kostka(); // bude fungovat
Kostka* dynamicky_desetisten = new Kostka(10);

Pozn.: Pokud není nadefinovaný žádný konstruktor (jak tomu bylo v minulé lekci), poté kompilátor sám vygeneruje bezparametrický prázdný konstruktor. Protože jsme konstruktor nadefinovali, ten bezparametrický není k dispozici. Ukázka výše tedy funguje až s následujícím kódem.

V konstruktoru bychom měli požadovat všechny informace, které třída během svého působení nutně potřebuje. Donutíme tím programátory, kteří naši třídu používají, aby parametry zadali (jinak instanci nevytvoří).

My ale předpokládáme 6-ti stěnnou kostku jako výchozí, proto bychom chtěli automaticky vytvořit šestistěnnou kostku, pokud nezadáme parametr. To lze provést dvěma způsoby - prvním z nich je použít výchozí hodnoty parametru:

//Kostka.h
class Kostka
{
public:
    Kostka(int _pocet_sten = 6);
    ~Kostka();
    int pocet_sten;
};

//Kostka.cpp
Kostka::Kostka(int _pocet_sten)
{
    pocet_sten = _pocet_sten;
}

//main.cpp
Kostka sestisten;
Kostka desetisten(10);
cout << sestisten.pocet_sten << endl;
cout << desetisten.pocet_sten << endl;

Výstup:

Konzolová aplikace
6
10

Druhou variantou by bylo přetížit konstruktor a nadefinovat další, který nepřijímá žádný parametr.

class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
};
#include <iostream>
#include "Kostka.h"

using namespace std;
Kostka::Kostka()
{
    pocet_sten = 6;
}

Kostka::Kostka(int _pocet_sten)
{
    pocet_sten = _pocet_sten;
}
Kostka::~Kostka()
{
}
#include "Kostka.h"
#include <iostream>
using namespace std;
int main()
{
//...main
Kostka sestisten;
Kostka desetisten(10);
cout << sestisten.pocet_sten << endl;
cout << desetisten.pocet_sten << endl;
    cin.get();
    return 0;
}

Taková implementace není úplně šťastná, ale o tom si povíme za chvíli.

C++ nevadí, že máme 2 metody se stejným názvem, protože jejich parametry jsou různé. Hovoříme o tom, že metoda Kostka() (tedy zde konstruktor) má přetížení (overload). Toho můžeme využívat i u všech dalších metod, nejen u konstruktorů. Mnoho metod v C++ má hned několik přetížení, zkuste se podívat např. na metodu getline() na objektu cin (Visual Studio parametry napovídá). Je dobré si u metod projít jejich přetížení, abyste neprogramovali něco, co již někdo udělal před vámi.

cin.getline přetížení v C++ - Objektově orientované programování v C++

Provolávání konstruktorů

V předchozím případě jsme přímo v bezparametrickém konstruktoru přiřadili atributu pocet_sten hodnotu 6. Představme si, že máme například 5 atributů a k tomu ještě 3 konstruktory. Nastavovat ve všech konstruktorech všechny atributy není moc šťastné, protože opakovaně děláme to stejné a můžeme udělat chybu. Bývá pravidlem, aby specifičtější konstruktor (s méně parametry) volal ten obecnější (ten s více parametrů). Jak ale zavoláme konstruktor, když se volá automaticky při vytvoření instance? Pro tento případ zavádí C++ tzv. delegující konstruktor (delegating constructor). Syntaxe je taková, že za hlavičku definice konstruktoru přidáme dvojtečku a následně název konstruktoru s jeho parametry. Nejlépe poslouží ukázka:

Kostka.cpp

#include <iostream>
#include "Kostka.h"

using namespace std;

Kostka::Kostka() : Kostka(6)
{
    cout << "Volani bezparametrickeho konstruktoru" << endl;
}

Kostka::Kostka(int _pocet_sten)
{
    cout << "Volani konstruktoru s parametrem" << endl;
    pocet_sten = _pocet_sten;
}

Kostka::~Kostka()
{
}

A použití:

Kostka sestisten;
#ifndef __KOSTKA_H__
#define __KOSTKA_H__
#include <iostream>
using namespace std;

class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
};
#endif
#include <iostream>
#include "Kostka.h"
Kostka::Kostka() : Kostka(6)
{
    cout << "Volani bezparametrickeho konstruktoru" << endl;
}

Kostka::Kostka(int _pocet_sten)
{
    cout << "Volani konstruktoru s parametrem" << endl;
    pocet_sten = _pocet_sten;
}

Kostka::~Kostka()
{
}

V ukázce vidíme, že se provolaly oba konstruktory.

Konzolová aplikace
Volani konstruktoru s parametrem
Volani bezparametrickeho konstruktoru

Pozn.: Delegating constructor funguje až od standardu C++11, proto nemusí fungovat pro všechny kompilery.

Toto delegování konstruktoru má ještě jedno uplatnění. Představme si, že chceme vytvořit instanci uživatele, od kterého potřebujeme jméno. To ještě není problém. My ho ale budeme potřebovat deklarovat v jiném objektu, v aréně, a to tehdy, kdy jméno ještě nebudeme znát. Následuje ukázka třídy pro hráče a arénu:

Hrac.h

#ifndef __HRAC__H_
#define __HRAC__H_
#include <string>
using namespace std;

class Hrac
{
public:
    string jmeno;
    Hrac(string _jmeno);
};
#endif

Hrac.cpp

#include "Hrac.h"

Hrac::Hrac(string _jmeno)
{
    jmeno = _jmeno;
}

Arena.h

#ifndef __ARENA_H_
#define __ARENA_H_
#include "Hrac.h"

class Arena
{
public:
    Hrac prvni;
    Hrac druhy;
    Arena();
};
#endif

Arena.cpp

#include "Arena.h"

Arena::Arena()
{
}

Projekt by neměl jít zkompilovat a bude hlásit následující hlášku (pro Visual Studio):

error C2512: 'Hrac': no appropriate default constructor available

Důvod je jednoduchý. Konstruktor se stará o vytvoření instance. To znamená, že ve chvíli, kdy je konstruktor ukončen, musí být třída připravena k použití. Toho ale nemáme jak docílit, protože hráč potřebuje jméno. Mohli bychom zkusit následující kód:

Arena::Arena()
{
    prvni = Hrac("Karel");
    druhy = Hrac("Pavel");
}

Dostaneme ale stále stejnou chybu. Než se na instanci zavolá libovolná metoda (včetně konstruktoru), musí být již instance správně vytvořena. Musíme tedy C++ nějak říci, jak má hráče vytvořit ještě před tím, než je vytvořena samotná Arena. Syntaxe je stejná jako u delegujícího konstruktoru, pouze místo názvu třídy použijeme název proměnné.

#include "Arena.h"

// Zde se volají konstruktor hráče ještě před tím, než vstoupíme do samotné implementace konstruktoru
Arena::Arena(string jmeno_prvni, string jmeno_druhy): prvni(jmeno_prvni), druhy(jmeno_druhy)
{
}
class Arena
{
public:
    Hrac prvni;
    Hrac druhy;
    Arena(string jmeno_prvni, string jmeno_druhy);
};
#ifndef __HRAC__H_
#define __HRAC__H_
#include <string>
using namespace std;
class Hrac
{
public:
    string jmeno;
    Hrac(string _jmeno);
};
#endif
#include "Hrac.h"
Hrac::Hrac(string _jmeno)
{
    jmeno = _jmeno;
}
#include <iostream>
#include <string>
#include "Arena.h"
#include "Hrac.h"
using namespace std;
int main()
{
    Arena arena("Karel", "Pavel");
    cout << "Souboj mezi " << arena.prvni.jmeno << " a " << arena.druhy.jmeno << endl;
    cin.get();
    return 0;
}
Konzolová aplikace
Souboj mezi Karel a Pavel

To by bylo pro dnešní lekci vše.

V příštím lekci, Destruktory a aplikace konstruktorů v C++, si řekneme o hlavním důvodu existence konstruktorů, popíšeme si destruktory a dokončíme naši hrací kostku.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 119x (6.6 kB)
Aplikace je včetně zdrojových kódů v jazyce C++

 

Předchozí článek
Řešené úlohy k 1. a 2. lekci OOP v C++
Všechny články v sekci
Objektově orientované programování v C++
Přeskočit článek
(nedoporučujeme)
Destruktory a aplikace konstruktorů v C++
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
40 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity