{{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é.