Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Piotr Szawdyński
Poprawki: pekfos
Kurs C++

Obsługa strumienia wejściowego

[lekcja] Rozdział 9. Omówienie obsługi standardowego wejścia za pomocą strumienia std::cin oraz przedstawienie sposobu walidacji wczytywanych danych.
Idąc małymi krokami do przodu nadszedł czas na obsługę strumienia wejściowego, czyli mówiąc prościej na wczytywanie danych. Zagadnienie to o ile jest stosunkowo proste do zrozumienia to wymaga ono sporej otoczki związanej z kontrolowaniem błędnie wprowadzonych danych. Zanim jednak dojdziemy do tych zagadnień zajmijmy się najpierw podstawami.

Wczytywanie danych

Wczytywanie danych do zmiennych odbywa się za pomocą strumienia std::cin. Aby móc wczytać dane znajdujące się na standardowym wejściu, należy wykorzystać do tego celu operator >>. Wczytanie danych wygląda więc następująco:
C/C++
std::cin >> nazwa_zmiennej;
W powyższy sposób możemy wczytywać wszystkie podstawowe typy danych - poczynając od liczb całkowitych, przechodząc przez liczby rzeczywiste oraz znaki, a kończąc na tekście. W niniejszym rozdziale skupimy się tylko i wyłącznie na wczytywaniu liczb całkowitych i rzeczywistych. Tekstem zajmiemy się w dalszej części kursu.
Zwróć uwagę, że nawiasy ostre przy wczytywaniu danych wskazują w przeciwną stronę, niż przy wypisywaniu danych na wyjście. Ich ustawienie w pewnym sensie wskazuje kierunek przepływu danych.
Rozpatrzmy teraz następujący przykład:
C/C++
#include <iostream>
int main()
{
    int a;
    float b;
    std::cout << "Podaj liczbe calkowita: ";
    std::cin >> a;
    std::cout << "Podaj liczbe rzeczywista: ";
    std::cin >> b;
    std::cout << "Liczba a = " << a << std::endl;
    std::cout << "Liczba b = " << b << std::endl;
    return 0;
}
Powyższy program zapyta się użytkownika o podanie dwóch liczb. Pierwszą z nich ma być liczba całkowita, a drugą liczba rzeczywista. Jeśli użytkownik będzie posłuszny, program wyświetli przykładowo następującą treść:
Podaj liczbe calkowita: 13
Podaj liczbe rzeczywista: 16.6
Liczba a = 13
Liczba b = 16.6
Problem zaczyna się pojawiać jednak wtedy, gdy mamy do czynienia z Yeti, czyli kimś kto niekoniecznie chce używać programu zgodnie z wolą programisty.

Sprawdzanie poprawności wprowadzonych danych

Wczujmy się przez chwilę w złośliwego użytkownika i zacznijmy działać tak jak on - wprowadźmy więc zamiast liczby literę. Ja wprowadziłem literę a i otrzymałem następujący wynik na ekranie:
Podaj liczbe calkowita: a
Podaj liczbe rzeczywista: Liczba a = 4273126
Liczba b = 3.21401e-039
Jak widać już na pierwszy rzut oka program zrobił dwie złe rzeczy. Pierwsza - nie wczytał liczby całkowitej; druga - pominął wczytywanie liczby rzeczywistej. Jako programiści powinniśmy zadać jednak pytanie: czy wystąpił błąd? Dopiszmy więc nowy wiersz do programu po każdym wczytywaniu danych:
C/C++
std::cout << "Czy cos nawalilo? " << std::cin.fail() << std::endl;
Po dokonaniu powyższej zmiany program będzie wyglądał tak:
C/C++
#include <iostream>
int main()
{
    int a;
    float b;
    std::cout << "Podaj liczbe calkowita: ";
    std::cin >> a;
    std::cout << "Czy cos nawalilo? " << std::cin.fail() << std::endl;
   
    std::cout << "Podaj liczbe rzeczywista: ";
    std::cin >> b;
    std::cout << "Czy cos nawalilo? " << std::cin.fail() << std::endl;
   
    std::cout << "Liczba a = " << a << std::endl;
    std::cout << "Liczba b = " << b << std::endl;
    return 0;
}
W programie pojawił się nowy zapis: std::cin.fail(), który jest metodą należącą do strumienia std::cin. Za jego pomocą możemy odczytać informację, czy wystąpił bład w trakcie wykonywania którejś z poprzednich operacji. Jeżeli coś zakończy się niepowodzeniem, strumień przechodzi w stan błędu i dalsze operacje na strumieniu są ignorowane.
Posiadając wiedzę na temat tego czy udało się wczytać poprawnie dane czy też nie - będziemy mogli podjąć w przyszłości odpowiednie działania. Na chwilę obecną nie potrafisz jednak sterować przebiegiem programu, dlatego też powrócimy do tego zagadnienia ponownie, gdy będziesz miał odpowiednią wiedzę.

Opis działania strumienia wejściowego

Skoro nauczyliśmy się już korzystać ze strumienia wejściowego w podstawowym wymiarze, przyjrzyjmy się teraz jego działaniu. Wyobraźmy więc sobie, że początkowo strumień jest pusty. Wysyłamy następnie żądanie: "daj mi liczbę całkowitą" (czyli: std::cin>>liczba). Strumień jest pusty, więc nie można z niego pobrać danych, a więc użytkownik musi wprowadzić nowe dane do strumienia. Wprowadźmy teraz do strumienia następujące dane, wpisując je w okno konsoli:
12345, 321. Czy 2+2 wynosi 4?
Po wciśnięciu klawisza ENTER dane te trafiają do bufora strumienia wejściowego, z którego następnie odczytywane są dane. Po wczytaniu liczby w buforze strumienia wejściowego zostaną następujące dane:
, 321. Czy 2+2 wynosi 4?
Co się teraz stanie, gdy zechcemy wczytać kolejną liczbę? Strumień stwierdzi, że pierwszym znakiem w strumieniu jest znak
,
, który nie jest liczbą, a więc nie zostanie wczytana liczba. Operacja wczytywania się nie powiedzie, a flaga błędu zostanie ustawiona.

Strumień, a białe znaki

Białymi znakami nazywamy te, które nie mają swojej reprezentacji wizualnej, a istnieją w tekście. Białe znaki to: spacja, tabulacja i znak nowej linii ("enter").
Gdy używamy strumienia std::cin>>, ewentualne białe znaki poprzedzające dane są pomijane. Tak więc gdyby w buforze strumienia nie znajdował się przecinek, tylko biały znak, strumień by go po prostu pominął i przeszedł do kolejnego znaku. W konsekwencji druga operacja wczytywania liczby powiodłaby się, a w strumieniu pozostałyby dane:
. Czy 2+2 wynosi 4?

Czyszczenie zawartości strumienia wejściowego

W poprzednim przykładzie, w kodzie pojawiło się żądanie jednej liczby całkowitej, a jako użytkownik podaliśmy kilka liczb oraz błędnych znaków. Po wczytaniu liczby reszta danych nie przepada, tylko zostanie wykorzystana w następnym odczycie. Jeśli użytkownik poda z góry wszystkie dane o jakie poprosi program, program nigdy nie zatrzyma się na odczycie, oczekując czegoś od użytkownika. To jest naturalne zastosowanie strumieni. Wato to sobie uświadomić, jeśli planujesz szlifować swoje umiejętności na automatycznie sprawdzanych zadaniach, jak na przykład tych ze SPOJa.
Mogą być jednak powody, dla których chcemy odrzucić zawartość strumienia. Najprostszy przykład to błędne dane - szkodliwy użytkownik podał literę, która została w strumieniu po wczytywaniu liczby, które zakończyło się błędem. W tej sytuacji w pierwszej kolejności trzeba wyczyścić flagi błędów w strumieniu, ponieważ stan sprawi, że inne operacje po prostu nie zadziałają. Służy do tego metoda std::cin.clear(). Gdy już możemy wykonywać operacje na strumieniu, należy odrzucić dane które wywołały błąd. Można to zrobić dowolną operacją odczytu (która nie wyrzuci błędu dla tych danych), ale najprościej użyć do tego metody std::cin.ignore(), bo nie będziemy wtedy potrzebować żadnej zmiennej na przechowanie danych, które i tak nas nie interesują. Wywołana bez żadnych argumentów ignoruje jeden znak, ale możemy podać do dwóch argumentów, by lepiej opisać nasze potrzeby. Pierwszy argument to ilość znaków do zignorowania - samo w sobie niezbyt przydatne, bo nie wiemy ile znaków jest do usunięcia. Drugi argument to znak stopu, który odczytany z wejścia zostanie zignorowany jak inne, ale dodatkowo przerwie proces odrzucania danych przed osiągnięciem liczby znaków podanej pierwszym argumentem. Zatem kod, który wyczyści bufor może wyglądać następująco:
C/C++
std::cin.clear();
std::cin.ignore( 1000, '\n' );
Czyścimy flagi i usuwamy do tysiąca znaków, ale kończąc po napotkaniu znaku nowej linii - czyli zostanie usunięta cała reszta linii wejścia podanej przez użytkownika, o ile nie jest tam więcej niż 1000 znaków. Jako pierwszy argument możemy podać specjalną wartość, która oznacza brak limitu znaków, ale będziemy potrzebować do tego nagłówka <limits>.
C/C++
#include <limits>

int liczba;
std::cin >> liczba;
bool bCzyBlad = std::cin.fail();
std::cin.clear();
std::cin.ignore( std::numeric_limits < std::streamsize >::max(), '\n' );
//(...) tu dalsza część programu
//INFO: zmienna bCzyBlad zawiera informację czy wystąpił błąd podczas wczytywania danych
Nigdy nie zapominaj, że ignore() to taka sama operacja odczytu jak każda inna. Jeżeli niczego nie będzie w strumieniu, ignore() zatrzyma program w oczekiwaniu na dane. Operacja "zignoruj linię tekstu" każe użytkownikowi tę linię wpisać, żeby mogła zostać zignorowana.
Wczytywanie operatorem >> zostawia w strumieniu pierwszy niepoprawny znak, w tym znak nowej linii, więc ignorowanie linii tekstu po wczytywaniu nie zatrzyma programu.
Z powodów historycznych (jest cała masa kodu na forum który tego używa i na który możesz się natknąć) należy wspomnieć o std::cin.sync(). Nie używaj tej metody. Przez lata była używana w tym kursie, ponieważ na Windowsie działa(ła) jak "lepsze ignore()". Ale poza Windowsem nigdy tak nie działała, a nawet jeśli używasz Windowsa, na nowszych kompilatorach ta metoda nie ma już swojego starego zachowania.

Zadanie domowe

Napisz program, który wczyta trzy liczby rzeczywiste, a na końcu programu je wszystkie wypisze. Zadbaj o to, by bufor strumienia wejściowego był za każdym razem czyszczony. Wynik końcowy powinien również zawierać informacje czy wczytanie danej liczby się powiodło.

Przykładowe dane wejściowe

13.3
tak 123
33.22nie

Przykładowe dane wyjściowe

Liczba pierwsza to: 13.3. Blad? 0.
Liczba druga to: 0. Blad? 1.
Liczba trzecia to: 33.22. Blad? 0.

Wskazówka

Informacje o poprawnym (albo niepoprawnym) wczytaniu danych należy przechować w dodatkowych zmiennych typu bool.
Poprzedni dokument Następny dokument
Pojęcie zmiennej i podstawowe typy danych Operacje matematyczne