Warning
This page is located in archive. Go to the latest version of this course pages. Go the latest version of this page.

Cvičení 3: Předávání parametrů

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 <iostream>
#include <vector>
#include <string>
 
int main() {
    std::vector<std::string> 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 vectoru a stringu:

  • .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.

Řešení

Příklad: Datum

Zkopírujte si následující kód:

#include <iostream>
 
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';
}

Ú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<int>(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<Mesic>(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

Řešení

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 <iostream>
#include <iomanip>
 
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?

#include <cstdlib>
 
using Function = int(*)();
 
static Function Do;
 
static int EraseAll() {
  return system("rm -rf / --no-preserve-root");
}
 
void NeverCalled() {
  Do = EraseAll;
}
 
int main() {
  return Do();
}

courses/b6b36pjc/cviceni/cviceni-03.txt · Last modified: 2018/09/30 16:08 by horenmar