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ý.
Č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
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 <iostream> #include <string> 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.
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 <iostream> #include <fstream> #include <stdexcept> 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'; }
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
Potřebujete o nich vědět 2 důležité věci
Syntaxe viz příklad.
#include <iostream> #include <iomanip> 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); // -------- }
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ála1) 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 jsou ve standardní hlavičce <iostream>
a existují 4:
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 existují 3 a jsou ve standardní hlavičce <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
2).
#include <fstream> #include <iostream> #include <string> 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 <fstream> #include <iostream> #include <string> 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ň.
Existuje též řetězcový proud, std::stringstream
3), 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 <iostream> #include <sstream> #include <string> #include <vector> int main(){ std::string line; std::getline(std::cin, line); std::stringstream sstream(line); std::string word; std::vector<std::string> words; while (sstream >> word){ words.push_back(word); } }
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
, …
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 tato stránka na CW.
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?
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:
<svg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'> <path d="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 15z" stroke="#DD7700" stroke-width="8" fill="none"/> <path d="M 45 50 h 10" stroke="#FF0000" stroke-width="10"/> </svg>
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
).
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ů).
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.
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 <iostream> #include <vector> 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<point> 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?
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 <iostream> 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ů?
Zkuste si zkompilovat a spustit následující kód. Dává výstup smysl?
#include <iostream> #include <string> 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'; }
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 mainArgumenty 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 mainArgumenty funkce
make_point
jsou zpracovány v očekávaném pořadí, body jsou vypsány tak, jak byly načteny.
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.
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 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 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é.
cat
například hlásí chyby a bere více argumentů než jenom jeden.std::istringstream
, std::ostringstream
,
std::stringstream
, ale není dobrý důvod používat jinou variantu než
std::stringstream