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

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.

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

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

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/b6b36pjc/cviceni/cviceni-02.txt · Last modified: 2018/10/10 23:13 by jerabma7