{{page>courses:b6b36pjc:styles#common&noheader&nofooter}}
{{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}}
======= Cvičení 3: Předávání parametrů =======
{{ :courses:b6b36pcc:cviceni:pruvodce.png?90|}}
Při programování v C%%++%% se s předáváním parametrů setkáváme neustále. Zároveň
je mnoho způsobů, jak parametr předat. Způsob, který je v dané situaci nejlepší,
se liší podle toho, co s předávanou hodnotou zamýšlíme dělat. Proto parametry
funkcí rozlišujeme na:
* vstupní -- parametry s hodnotami, které funkce potřebuje //pouze získat//,
* výstupní -- parametry s hodnotami, které funkce potřebuje //pouze poskytnout//,
* vstupně-výstupní -- parametry s hodnotami, které funkce potřebuje //získat, upravit a poskytnout//.
Dejme tomu, že se snažíme funkci předat proměnnou typu ''T''.
* Pokud se jedná o vstupní parametr, použijeme předání hodnotou (''T''), konstantní referencí (''const T&''), nebo ukazatelem na konstatní hodnotu (''const T*'').
* Pokud se jedná o výstupní parametr, vrátíme jej příkazem ''return''.
* Pokud se jedná o vstupně-výstupní parametr, použijeme předání referencí (''T&'') nebo ukazatelem (''T*'').
Tím, jak proměnnou předáme, také dokumentujeme, co s proměnnou děláme uvnitř funkce.
===== Příklad: Oslovení =====
Začněme tímto jednoduchým programem:
#include
#include
#include
int main() {
std::vector jmena = { "Petr", "Jan", "Jana", "Karel", "Katka" };
for (const auto& s : jmena) {
std::cout << s << '\n';
}
}
Tento program vypíše:
Petr
Jan
Jana
Karel
Katka
Naším cílem bude upravit tento program tak, aby přidal oslovení ke každému
jménu, a následně je vypsal:
pan Petr
pan Jan
slecna Jana
pan Karel
slecna Katka
Doporučujeme postupovat tak, že
* Vytvoříte enum ''Pohlavi'' a funkci, která pro ''std::string'' vždy vrátí ''Pohlavi::muz'' (nebo zena).
* Vytvoříte funkci, která vezme vektor jmen a vrátí vektor jmen rozšířených o oslovení.
* Implementujete rozhodovací pravidlo, které správně ohodnotí jména v příkladu.
* Vypíšete jméno rozšířené o oslovení.
Mohou se hodit některé z těchto metod ''vector''u a ''string''u:
* ''.size()'' poskytne počet prvků ve vektoru, příp. počet znaků v řetězci.
* ''.empty()'' rozhodne, zda počet prvků ve vektoru je 0, příp. počet znaků v řetězci je 0.
* ''.front()'' poskytne referenci na první prvek, příp. znak.
* ''.back()'' poskytne referenci na poslední prvek, příp. znak.
Navíc ''string'' má k dispozici operátor ''+'', který zřetězí (spojí) dva řetězce.
{{:courses:b6b36pcc:cviceni:cviceni_3_osloveni.zip|Řešení}}
===== Příklad: Datum =====
Zkopírujte si následující kód:
#include
enum class Mesic {
leden,
unor,
brezen,
duben,
kveten,
cerven,
cervenec,
srpen,
zari,
rijen,
listopad,
prosinec
};
void vypis(std::ostream& out, Mesic m) {
switch (m) {
case Mesic::leden: out << "1."; break;
case Mesic::unor: out << "2."; break;
case Mesic::brezen: out << "3."; break;
case Mesic::duben: out << "4."; break;
case Mesic::kveten: out << "5."; break;
case Mesic::cerven: out << "6."; break;
case Mesic::cervenec: out << "7."; break;
case Mesic::srpen: out << "8."; break;
case Mesic::zari: out << "9."; break;
case Mesic::rijen: out << "10."; break;
case Mesic::listopad: out << "11."; break;
case Mesic::prosinec: out << "12."; break;
}
}
Mesic dalsiMesic(Mesic m) {
switch (m) {
case Mesic::leden: return Mesic::unor;
case Mesic::unor: return Mesic::brezen;
case Mesic::brezen: return Mesic::duben;
case Mesic::duben: return Mesic::kveten;
case Mesic::kveten: return Mesic::cerven;
case Mesic::cerven: return Mesic::cervenec;
case Mesic::cervenec: return Mesic::srpen;
case Mesic::srpen: return Mesic::rijen;
case Mesic::zari: return Mesic::zari;
case Mesic::rijen: return Mesic::listopad;
case Mesic::listopad: return Mesic::prosinec;
case Mesic::prosinec: return Mesic::leden;
}
}
int main() {
Mesic m1 = Mesic::leden;
Mesic m2 = Mesic::prosinec;
vypis(std::cout, m1);
std::cout << '\n';
vypis(std::cout, m2);
std::cout << '\n';
vypis(std::cout, dalsiMesic(m1));
std::cout << '\n';
vypis(std::cout, dalsiMesic(m2));
std::cout << '\n';
}
{{ :courses:b6b36pcc:cviceni:ukoly.png?90| Úkoly k procvičení}}Úkoly:
* Najděte chybu v programu.
* Změňte implementaci funkce ''vypis'' tak, aby neobsahovala příkaz ''switch'' se dvanácti návěštími ''case''. K tomu je možné využít ''static_cast(m)'', kde ''m'' je proměnná typu měsíc -- takto získáme číslo, pomocí kterého je položka enumerace reprezentována.
* Podobně zkraťte funkci ''dalsiMesic''. Použítím ''static_cast(cislo)'' naopak získáme položku enumerace z čísla.
* Implementujte funkci ''predchoziMesic''.
* Vytvořte strukturu ''Datum'', která obsahuje den, měsíc a rok.
* Vytvořte přetížení funkce ''vypis'' pro typ ''Datum''. Datum se vypíše do daného proudu ve formátu ''den. mesic. rok''.
* Vytvořte funkci ''pocetDni(mesic, rok)'', která vrátí, kolik dní obsahuje daný měsíc v daném roce.
* Vytvořte funkci ''pocetDni(rok)'', která vrátí, kolik dní obsahuje daný rok.
* Vytvořte funkci ''pridejRok(datum, pocet)'', která posune datum o daný počet let.
* Vytvořte funkci ''pridejMesic(datum, pocet)'', která posune datum o daný počet měsíců.
* Vytvořte funkci ''pridejDen(datum, pocet)'', která posune datum o daný počet dní.
* Upravte ''pridejRok'', ''pridejMesic'' a ''pridejDen'' tak, aby mohl být počet přičtených let/měsíců/dní záporný.
//Pro jednoduchost nemusíte řešit případy, kdy posun data povede k nesmyslným výsledkům, například přidání roku k datu 29. 2. 2016//
{{:courses:b6b36pcc:cviceni:cviceni_3_datum.zip|Řešení}}
{{ :courses:b6b36pcc:cviceni:ukoly.png?90| Úkol k procvičení}}
===== Úkoly k procvičení =====
==== Úkol 1 ====
Vytváříme hru Piškvorky. Máme typ
enum class e_player { wheel, cross };
Vytvořte funkci ''change_player'', která změní hráče (z ''wheel'' na ''cross'' a naopak). Definujte dvě varianty funkce
* výsledek je vrácen přes návratovou hodnotu
* výsledek je vrácen přes parametr funkce.
Mohou se obě funkce jmenovat stejně?
Typ upravíme následovně
enum class e_player { wheel, cross, nothing };
Upravte funkci ''change_player'' tak, aby vše opět fungovalo.
===== Koutek nedefinovaného chování =====
Na prvním cvičení jsme se zmínili o tom, že přístup mimo alokované pole je
nedefinované chování, ale neukázali si, k čemu to může vést. Tentokrát si to
ukážeme.
==== Příklad 1 ====
Následující program obsahuje drobnou chybu v přístupu do pole čísel, která může
vést k přístupu mimo pole (a tudíž k nedefinovanému chování). Zkuste si ho
zkompilovat a spustit, nejdříve bez optimalizací, poté s nimi:
#include
#include
int elements[] = {1, 2, 3, 4};
bool contains(int elem) {
for (int i = 0; i <= 4; ++i) {
if (elements[i] == elem) {
return true;
}
}
return false;
}
int main() {
int num;
while (std::cin >> num){
std::cout << std::boolalpha << contains(num) << '\n';
}
}
==== Příklad 2 ====
Následující program, přeložený s optimalizacemi, se vám na Linuxu pokusí
smazat všechna data. Již znáte vše co potřebujete, abyste mohli vymyslet,
proč je to validní interpretace programu...
Proč se tedy výsledný program pokusí smazat všechna data?
Poznámka: Příklad slouží jen pro ilustraci nedefinovaného chování.
#include
using Function = int(*)();
static Function Do;
static int EraseAll() {
return system("rm -rf / --no-preserve-root"); // system("cat main.cpp");
}
void NeverCalled() {
Do = EraseAll;
}
int main() {
return Do();
}
{{ :courses:b6b36pcc:cviceni:reseni.png?90| Řešení a odpovědi}}
==== Řešení a odpovědi k nedefinovanému chování ====
=== Příklad 1 - index mimo hranice pole ===
Standardní překlad z příkazové řádky:
g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main
clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main
Program správně vypíše informace o tom, že čísla **1 až 4 jsou prvky pole** a ostatní čísla se v poli nevyskytují.
Kompilace s optimalizací:
g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main
Program odpovídá na všechny zadávané číselné hodnoty true, tj. jakoby se **všechna celá čísla nacházela v poli**.
clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main
Zde dochází pouze k jedné chybě, a to pro **číslo 0**, které podle výsledku programu do definovaného pole patří. V ostatních případech program vypisuje očekávanou odpověď.
\\
=== Příklad 2 - neinicializovaný nebo nulový ukazatel ===
Před experimentováním s programem nezapomeňte změnit příkaz na smazání všech dat z disku!
Standardní překlad z příkazové řádky:
g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main
clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main
Program skončí s chybou ''Segmentation fault (core dumped)''. Funkce ''Do'' je pouze deklarována, není inicializována a hlavní program nedokáže zjistit návratovou hodnotu.
Kompilace s optimalizací:
g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main
Při použití překladače g%%++%% je výsledek s využitím optimalizace stejný, jako bez optimalizace. Program skončí s chybou ''Segmentation fault''.
clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main
Kompilace s optimalizací zjistí, že funkce ''Do()'' obsahuje nulový ukazatel a přiřadí ji automaticky jedinou možnou funkci – ''EraseAll()''. Důvodem výběru této funkce je to, že se jedná o statickou funkci se stejnou hlavičkou, jako funkce ''Do()''.