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í 2: Vstup a výstup

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

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 <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';
}

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

Potřebujete o nich vědět 2 důležité věci

  1. Některé se dají použít nejen pro zápis, ale i pro čtení
  2. Některé modifikátory mění stav proudu trvale, některé ne

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);                                    // --------
}

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á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

Konzolové proudy jsou ve standardní hlavičce <iostream> 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 <fstream>.

  • 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ší cat2).

#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ň.

Řetězcové proudy

Existuje též řetězcový proud, std::stringstream3), 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);
    }
}

 Čá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 tato stránka na CW.

 Ú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:

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

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 <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?

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 <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ů?

Příklad 3

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';
}

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

1)
Nevíme o dalším, ale též nenosíme v hlavě celou standardní knihovnu
2)
Klasický cat například hlásí chyby a bere více argumentů než jenom jeden.
3)
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
courses/b6b36pcc/cviceni/cviceni-02.txt · Last modified: 2022/09/27 18:24 by nagyoing