{{page>courses:b6b36pjc:styles#common&noheader&nofooter}} {{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}} ======= Cvičení 2: Vstup a výstup ======= {{ :courses:b6b36pcc:cviceni:pruvodce.png?90|}} Cílem dnešního cvičení je seznámit se s komplikovanějším použití standardních proudů v C%%++%%, protože program, který nekomunikuje s okolím, není zrovna užitečný. ===== Řádkové čtení ===== Často je užitečné číst ze vstupu po celých řádcích, namísto čtení po řetězcích/číslech. K tomuto účelu poslouží funkce [[http://en.cppreference.com/w/cpp/string/basic_string/getline|std::getline]], která, jak název napovídá, čte ze vstupu, dokud nenarazí na konec řádku, a následně přečtená data uloží do ''std::string''. Nyní bychom mohli upravit ukázku z minulého cvičení, kde chceme po uživateli, aby nám napsal své jméno, kterým ho pak pozdravíme. #include #include int main() { std::cout << "Napis sve jmeno:\n"; std::string jmeno; std::getline(std::cin, jmeno); std::cout << "Ahoj " << jmeno << '\n'; } Tentokrát načteme a uživateli vytiskneme všechen text, který napsal až do chvíle, kdy zmáčkl Enter. ===== Ošetření chyb ===== Doteď jsme možnost výskytu chyb při čtení vstupu prostě ignorovali a předpokládali, že nám uživatel vždy dá správné vstupy. To je samozřejmě z dlouhodobého hlediska neudržitelné. #include #include #include double readDouble() { double d; std::cin >> d; if (std::cin.good()) { return d; } else if (std::cin.bad() || std::cin.eof()) { throw std::runtime_error("readDouble() failed"); } else { std::cin.clear(); std::cin.ignore(1, '\n'); return readDouble(); } } int main() { double a = readDouble(); double b = readDouble(); double c = readDouble(); std::cout << (a + b + c) << '\n'; } ===== Formátování ===== Občas můžeme chtít zapsat výstup ve specifickém formátu. Pro formátování zápisu do C%%++%% proudů slouží tzv. I/O manipulátory, z hlaviček * [[http://en.cppreference.com/w/cpp/header/iomanip|iomanip]], a * [[http://en.cppreference.com/w/cpp/header/ios|ios]]. Potřebujete o nich vědět 2 důležité věci - Některé se dají použít nejen pro zápis, ale i pro čtení - Některé modifikátory mění stav proudu trvale, některé ne Syntaxe viz příklad. #include #include void hline(int W) { for (int i = 0; i < W; ++i) { std::cout << '-'; } std::cout << '\n'; } int main() { const int W = 8; hline(W); // -------- std::cout << std::setw(W) << 1234 << '\n'; // 1234 std::cout << std::setw(W) << 1.2 << '\n'; // 1.2 std::cout << std::setw(W) << 1.2e10 << '\n'; // 1.2e+10 std::cout << std::setw(W) << "abcd" << '\n'; // abcd hline(W); // -------- std::cout << std::hex; std::cout << std::setw(W) << 255 << '\n'; // ff std::cout << std::showbase; std::cout << std::setw(W) << 255 << '\n'; // 0xff std::cout << std::dec; std::cout << std::setw(W) << 255 << '\n'; // 255 hline(W); // -------- } ===== Druhy proudů ===== Ve standardní knihovně se dají najít 3 druhy proudů -- konzolové, souborové a řetězcové. S těmi prvními jsme se již potkali na minulém cvičení. Protože jsou proudy jedno z mála((Nevíme o dalším, ale též nenosíme v hlavě celou standardní knihovnu)) míst, kde standardní knihovna C%%++%% používá dědičnost a virtuální funkce, typ (objekt), který se umí vypsat na konzolový výstup, se umí též vypsat do souboru nebo do řetězce. To samé samozřejmě platí i o načítání. ==== Konzolové proudy ==== Konzolové proudy jsou ve standardní hlavičce '''' a existují 4: * std::cin * std::cout * std::cerr * std::clog S prvními dvěma jsme se již setkali na minulém cvičení, s posledními dvěma ne. Dle jména není těžké si domyslet, že ''std::cerr'' vypisuje na ''stderr''. Co ale dělá ''std::clog''? Též vypisuje na ''stderr'', ale výstup bufferuje, to jest, výstup zapsaný do ''std::clog'' se může dostat na ''stderr'' až „někdy časem“ (není přesně řečeno, kdy; nejpozději s ukončením programu). Proto zabere zápis na ''std::clog'' výrazně méně času než zápis na ''std::cerr''. ==== Souborové proudy ==== Souborové proudy existují 3 a jsou ve standardní hlavičce ''''. * std::ifstream * std::ofstream * std::fstream ''std::ifstream'' slouží ke čtení ze souboru (i - input, f - file, stream - proud). Po jeho konstrukci se s ním pracuje stejně jako se ''std::cin''. Tato ukázka otevře soubor, který dostala jako argument, a celý ho vypíše na standardní výstup. Jedná se vlastně o (hodně) horší ''cat''((Klasický ''cat'' například hlásí chyby a bere více argumentů než jenom jeden.)). #include #include #include int main(int argc, char** argv) { std::ifstream infile(argv[1]); std::string line; while (std::getline(infile, line)) { std::cout << line << '\n'; } } ''std::ofstream'' pak slouží k zápisu do souboru (o - output, f - file, stream - proud). Po jeho konstrukci se s ním pracuje stejně jako se ''std::cout''. Tato ukázka napodobuje utilitu ''tee'': zapíše vše ze standardního vstupu na standardní výstup a do souboru, který dostala jako argument. #include #include #include int main(int argc, char** argv) { std::ofstream outfile(argv[1]); std::string line; while (std::getline(std::cin, line)) { outfile << line << '\n'; std::cout << line << '\n'; } } ''std::fstream'' samotný pak umožňuje otevřít soubor pro zápis i pro čtení zároveň. ==== Řetězcové proudy ==== Existuje též řetězcový proud, ''std::stringstream''((Ve skutečnosti existují zase 3 varianty, ''std::istringstream'', ''std::ostringstream'', ''std::stringstream'', ale není dobrý důvod používat jinou variantu než ''std::stringstream'')), který umožňuje formátované čtení z/do řetězce. Řetězcové proudy se obvykle používají, pokud máme ''std::string'' a chceme z něj formátovaně číst, nebo do něj dále formátovaně psát. Ukázka načte řádek ze stdin a následně ten řádek rozdělí na slova, která uloží. #include #include #include #include int main(){ std::string line; std::getline(std::cin, line); std::stringstream sstream(line); std::string word; std::vector words; while (sstream >> word){ words.push_back(word); } } {{ :courses:b6b36pcc:cviceni:zajemce.png?90| Část pro zájemce}} === Možnosti dalšího procvičení === To co jsme vám zde ukázali stačí na napodobení několika dalších linuxích utilit: * ''wc'' -- spočítá a vytiskne množství řádků/slov v souborech, které byly předány jako argumenty * ''uniq'' -- Vyfiltruje za sebou se opakující řádky ze stdin na stdout * ''paste'' -- Vezme jako argument sadu souborů a pak vytiskne na stdout jejich 1. řádky spojené ''\t'', 2. řádky spojené ''\t'', ... ===== Seznámení se s Brute ===== Během tohoto cvičení byste se též měli seznámit s odevzdáváním úloh do Brute. Jsou v něm otevřeny dvě testovací úlohy, viz [[http://cw.fel.cvut.cz/b181/courses/b6b36pcc/ukoly/test_upload|tato stránka na CW]]. {{ :courses:b6b36pcc:cviceni:ukoly.png?90| Úkol k procvičení}} ===== Úkoly k procvičení ===== ==== Úkol 1 ==== Pokud definujeme řetězec v jazyce Python, nelze jej změnit. Následující sekvence příkazů vyvolá chybu: abc = 'Python' abc[0] = 'K' Jsou řetězce v jazyce C%%++%% **immutable** (neměnné, podobně jako v Pythonu) nebo **mutable** (dají se změnit)? Jak definujeme řetězec, pokud chceme immutable, resp. mutable? ==== Úkol 2 ==== V grafické studiu kreslí vektorové obrázky. Odčítají potřebné souřadnice z předem nakreslených obrázků a přidávají mezi ně speciální značky. Výsledek může vypadat takto: stroke="#DD7700" stroke-width="8" fill="none" M 65 20 a 15 15 0 1 1 15 15 h -60 a 15 15 0 1 1 15 -15 v 60 a 15 15 0 1 1 -15 -15 h 60 a 15 15 0 1 1 -15 15 stroke="#FF0000" stroke-width="10" M 45 50 h 10 Řádky začínající na ''M'' popisují souřadnice jednotlivých objektů. Ostatní řádky popisují formát pro vykreslení následujících řádků (jednoho nebo více). Vytvořte obrázek typu ''SVG'' ze zadaných popisů. Postup znázorňuje výsledek: Písmeno ''z'' na konci řetězce doplňte tehdy, když řetězec souřadnic obsahuje víc než jedno písmeno (mimo ''M'' – viz první řádek, který obsahuje písmena ''a, h, a, v, a, h a''). {{ :courses:b6b36pcc:cviceni:obr_svg.zip |Další testovací vstupy}} **Možnosti úpravy programu:** Pokuste se ze vstupu načíst jednotlivá čísla (typu ''int''), čísla vypište do sloupce pod sebe. Program upravte pro čtení čísel zadaných hexadecimálně (barvy objektů). ===== Nespecifikované chování ===== Krom nedefinovaného chování existuje i tzv. nespecifikované chování. Zatímco nedefinované chování „nesmí nastat“, k nespecifikovanému chování dojít může, pouze není přesně specifikováno, co se má dít. ==== Příklad 1 ==== Zkuste této ukázce dát nějaká data... očekává nejdříve jedno číslo, udávající množství bodů, a pak ''n'' bodů, [x, y], které dané body definují. #include #include struct point { int x, y; }; int get_int(std::istream& in) { int temp; in >> temp; return temp; } point make_point(int x, int y) { return{ x, y }; } int main() { std::vector points; int n = get_int(std::cin); for (int i = 0; i < n; ++i) { points.push_back(make_point(get_int(std::cin), get_int(std::cin))); } for (auto& p : points) { std::cout << p.x << ' ' << p.y << std::endl; } } Vypsaly se vám zpátky body stejně, jako jste je načítali? ==== Příklad 2 ==== Zkompilujte a spusťte následující kód. Jsou vypsané příklady při použití kompilátoru clang%%++%% a g%%++%% vyřešeny správně? Jsou stejné? #include int f() { std::cout << "In f\n"; return rand() % 100; // 4 } int g() { std::cout << "In g\n"; return rand() % 100; // 3 } int diff(int i, int j) { std::cout << j << " - " << i << " = "; return j - i; } int main() { std::cout << diff(f(), g()) << std::endl; return 0; } Nahraďte řádky hodnotami v poznámce. Změní se chování zkompilovaných programů? ==== Příklad 3 ==== Zkuste si zkompilovat a spustit následující kód. Dává výstup smysl? #include #include int main(){ std::string s = "but I have heard it works even if you don't believe in it"; s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don't"), 6, ""); std::cout << s << '\n'; } {{ :courses:b6b36pcc:cviceni:reseni.png?90| Řešení a odpovědi}} ==== Řešení a odpovědi k nespecifikovanému chování ==== === Příklad 1 - pořadí vyhodnocování argumentů funkce === Překlad pomocí překladače g%%++%% (bez ohledu na optimalizaci): g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main g++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main Argumenty funkce ''make_point'' jsou zpracovány **v obráceném pořadí**, než předpokládáme, nejprve pravý, pak levý argument. Souřadnice vypsaných bodů jsou prohozené. Překlad pomocí překladače clang%%++%% (bez ohledu na optimalizaci): clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 main.cpp -o main clang++ -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++17 –O3 main.cpp -o main Argumenty funkce ''make_point'' jsou zpracovány **v očekávaném pořadí**, body jsou vypsány tak, jak byly načteny. \\ === Příklad 2 - pořadí vyhodnocování argumentů funkce === Při překladu pomocí jednotlivých kompilátorů dostáváme následující výsledky: ^ clang%%++%% ^ g%%++%% ^ | In f In g 86 - 83 = 3 | In g In f 83 - 86 = -3 | Funkce ''f'' a ''g'' jsou volány v opačném pořadí, příklady jsou rozličné, přestože jsou vypsány a vyřešeny správně. Pokud je návratovou hodnotou funkcí ''f'' a ''g'' konstanta, výsledky jsou následující: ^ clang%%++%% ^ g%%++%% ^ | In f In g 3 - 4 = -1 | In g In f 3 - 4 = -1 | Vygenerované příklady jsou stejné i přesto, že funkce ''f'' a ''g'' jsou volány v rozličném pořadí. K prohození zpracovaných argumentů funkce ''diff'' nedošlo. \\ === Příklad 3 – pořadí vyhodnocování podvýrazů === Problém tohoto příkladu je dnes již celkem obtížné demonstrovat. V novějších verzích jazyka C%%++%% a odpovídajících překladačů je problém vyřešen - viz překlad pomocí clang%%++%% na [[http://coliru.stacked-crooked.com/a/7900a53372d91cfa | Coliru]]. Pokud chceme vidět, k jakým problémům může dojít, musíme použít starší překladače, například překladač g%%++%%-4.8 a verzi jazyka C%%++%%11: g++-4.8 -Wall -Wextra -Wunreachable-code -Wpedantic -std=c++11 main.cpp -o main Výstupem takto zkompilovaného programu (viz i příklad na [[http://coliru.stacked-crooked.com/a/c30504069b80cfa1 | Corilu]]) je neočekáváný řetězec **I have heard it works evenonlyyou donieve in it** Jakým způsobem došlo k zpracování vstupního řetězce do této podoby? Nejprve jsou vyhodnoceny výsledky funkcí ''find'', tj. původní výraz pro úpravu vstupního řetězce s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don't"), 6, ""); je upraven na s.replace(0, 4, "").replace(26, 4, "only").replace(37, 6, ""); Následně jsou provedena nahrazení (funkce ''replace'') v pořadí zleva doprava. Hned první ''replace'' vymaže část řetězce a absolutní hodnoty příkazů jsou pak dále neplatné.