Warning
This page is located in archive.

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?

Poznámka: Příklad slouží jen pro ilustraci nedefinovaného chování.

#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: 2021/10/19 23:07 by richta