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:
- App
- main.cpp
- Kostka.h
- Kostka.cpp
#include "Kostka.h" #include <iostream> using namespace std; int main() { Kostka kostka(10); cout << kostka.pocet_sten << endl; cin.get(); return 0; }
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() { }
Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.
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:
#include <iostream> //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; } Kostka::~Kostka() { } using namespace std; int main() { //main.cpp Kostka sestisten; Kostka desetisten(10); cout << sestisten.pocet_sten << endl; cout << desetisten.pocet_sten << endl; cin.get(); return 0; }
Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.
Výstup:
Konzolová aplikace
6
10
Druhou variantou by bylo přetížit konstruktor a nadefinovat další, který nepřijímá žádný parametr.
- App
- Kostka.h
- Kostka.cpp
- main.cpp
#ifndef __KOSTKA_H__ #define __KOSTKA_H__ class Kostka { public: Kostka(); Kostka(int _pocet_sten); ~Kostka(); int pocet_sten; }; #endif
#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; }
Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.
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.

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í:
- App
- main.cpp
- Kostka.h
- Kostka.cpp
#include "Kostka.h" #include <iostream> using namespace std; int main() { Kostka sestisten; cin.get(); return 0; }
#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() { }
Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.
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é.
- App
- Arena.cpp
- Arena.h
- Hrac.h
- Hrac.cpp
- main.cpp
#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) { }
#ifndef __ARENA_H_ #define __ARENA_H_ #include "Hrac.h" class Arena { public: Hrac prvni; Hrac druhy; Arena(string jmeno_prvni, string jmeno_druhy); }; #endif
#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; }
Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.
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 125x (6.6 kB)
Aplikace je včetně zdrojových kódů v jazyce C++