Biblioteki wejścia/wyjścia
Wstęp
Biblioteka standardowa C++ w kwestii operacji wejścia/wyjścia składa się z dwóch rzeczy. Jedna to jest biblioteka iostream, której elementy już poznaliśmy. Druga to biblioteka odziedziczona z C, dostępna przez plik nagłówkowy <cstdio>.
Podstawowe operacje z biblioteki iostream już poznaliśmy. Były to predefiniowane strumienie cin i cout oraz operatory pobierania i wysyłania do strumienia (<< i >>). Zapoznamy się teraz z paroma szczegółami biblioteki.
Przede wszystkim mamy cztery predefiniowane strumienie: cin, cout, cerr i clog. Są one związane z trzema strumieniami systemowymi:
- 0
- strumień wejściowy - cin
- 1
- strumień wyjściowy - cout
- 2
- strumień diagnostyczny - cerr i clog
Standardowo takie strumienie posiada każdy program zaraz po uruchomieniu. Normalnie, strumień `cin' to "wejście konsoli" (inaczej standardowe wejście), czyli wejście z klawiatury (ogólnie: z terminala kontrolującego, ale to już szczegół implementacyjny), natomiast cout i cerr są kierowane na ekran. Uruchamiając program poleceniem powłoki można je jednak przekierować, o czym też wspominałem.
Strumienie są buforowane. Buforowanie strumienia cin jest zazwyczaj liniowe, ale sposób buforowania jest ustalany przez ustawienia terminala. Liniowe, tzn. dane do tego strumienia fizycznie przesyłane są dopiero po wprowadzeniu całej linii i naciśnięciu Enter (jeśli strumień został przekierowany to do znaku końca linii). No, ewentualnie jeśli nastąpił koniec pliku to już końca linii się nie wymaga (tzn. koniec pliku, jeśli wejście zostało przekierowane z konkretnego pliku, ale można wymusić z klawiatury zasygnalizowanie końca pliku, na unixach jest to Ctrl-D, na DOSie Ctrl-Z itd.). Sprawdzenie, czy dla danego strumienia nastąpił koniec pliku wykonuje się metodą istream::eof().
O buforowaniu strumieni wyjściowych decyduje już program. Strumień cout jest również domyślnie buforowany liniowo (ale można to zmienić, o czym za chwilę), co znaczy, że dopiero po wysłaniu do niego całej linii wraz z jej końcem (lub po zakończeniu programu) zawartość buforów jest faktycznie wysyłana. Tak samo jest buforowany strumień clog, zaś cerr w ogóle nie jest buforowany.
Strumienie zatem ogólnie mogą być wejściowe, wyjściowe i dwukierunkowe. Strumienie wyjściowe posiadają elementy obsługi potrzebne do strumieni wyjściowych itd.. Podstawowymi klasami obsługującymi strumienie są zatem: istream, ostream i iostream. Jak się można domyślić, iostream stanowi połączenie właściwości istream i ostream.
Klasa istream
Klasa ta zawiera m.in. poznany już operator >>. Powoduje on pobranie jednego ARGUMENTU (!), czyli ciągu znaków zakończonych białym znakiem (spacją, tabulatorem lub końcem linii - o ile nie skończą się znaki w strumieniu). Operator >> jest przeciążony dla następujących typów:
- char*
- char&
- int&, long&, short&
- unsigned (wszystkie poprzednie)
- bool&
- float&
- double&
- long double&
- streambuf* (będzie opisany później)
- <manipulatory> (również później)
Poza tym mamy do dyspozycji następujące metody (czyli odwołujemy się do nich np. cin.get() ). Zauważ, że większość metod zwraca istream&, zatem można wywoływać te metody w ciągu: cin.get(c).eof() itp.).
- int get(); - zwraca znak ze strumienia (lub EOF jeśli skończył się plik)
- istream& get(char& c); - wpisuje odczytany ze strumienia znak (koniec pliku należy sprawdzić przez eof() ).
- istream& get(char* buf,int len,char eot='\n'); - pobiera ze strumienia do `buf' maksymalnie `len' znaków i aż do napotkania znaku `eot' (tablica znaków jest terminowana zerem)
- istream& getline(char* buf,int len,char eot='\n'); - jak powyższa get, ale znak `eot' nie jest zapisywany w `buf'
- istream& read(char* ptr,long len);
- istream& read(void* ptr,long len); - odczytują ze strumienia maksymalnie `len' znaków do `ptr'; jest to jak widać funkcja do odczytu danych binarnych, zatem tablica NIE jest terminowana zerem!
Następujące metody są trochę niezwykłe, ale też niezwykle się przydają:
- int peek(); - podgląda następny znak w strumieniu (strumień nie ulega zmianie)
- size_t gcount(); - podaje, ile znaków ostatnio wczytano ze strumienia
- istream& ignore(int ile=1, int do=EOF); - ignoruje `ile' znaków ze strumienia lub aż do `do'.
- istream& putback(char); - wstawia znak z powrotem do strumienia
- istream& unget(); - to samo, tylko daje mniej okazji do popełnienia błędu :*)
Klasa ostream
Podobnie, ta klasa zawiera operator <<. Przeciążony jest dla następujących typów:
- char
- const char*
- const void*
- int, long, short, unsigned (wszystkie poprzednie), bool
- float, double, long double
- no i oczywiście manipulatory
Ponadto mamy do dyspozycji takie metody:
- ostream& flush(); - opróżnia bufor (przydatne, jeśli ostatnia porcja danych nie kończyła się znakiem końca linii, a nie chcemy rezygnować z buforowania)
- ostream& put(char); - wpisuje znak do strumienia
- ostream& write(const char* buf,long ile);
- ostream& write(const void* buf,long ile); - wypisuje `ile' znaków spod `buf'; podobnie jak przy podobnej do niej read(), jest to funkcja do zapisu danych binarnych, zatem jest niewrażliwa na ewentualny znak terminujący tablicę napisową
Modyfikatory i flagi formatowania
Każdy strumień posiada flagi, które oznaczają wiele różnych rzeczy, głównie opcje dotyczące liczb, ale nie tylko. Oto flagi modyfikujące format wyjścia. Wszystkie mogą mieć argument lub nie. Te, które go mają, ustawiają tak podany argument zwracając starą wartość. Te, które nie mają, zwracają aktualną wartość (będę przedstawiał tylko te; ewentualny argument jest tego samego typu, co zwracany):
- char fill();
- znak wypełniający (domyślnie spacja)
- int precision();
- ilość cyfr znaczących (domyślnie 6)
- int width();
- minimalna szerokość pola wyprowadzania (domyślnie 0)
- unsigned long flags();
- ustanawia nowe flagi formatowania
Następujące metody modyfikują flagi wybiórczo:
- unsigned long setf(unsigned long val);
- unsigned long setf(unsigned long val, unsigned long mask);
Flagi formatowania, które mogą być podane jako `val' i jako argument do flags() (wszystkie nazwy są poprzedzone `ios::') są następujące (w nawiasach [] podane są te, które są ustawione domyślnie):
- skipws - powoduje pominięcie (w istreamach) wiodących spacji
- [left], right, internal - wyrównanie w polu wyprowadzania (dla liczb - [right])
- [dec], oct, hex - system liczbowy, w jakim wypisuje się liczbę (dot. całkowitych)
- uppercase - litery w liczbach szesnastkowych (i E w notacji naukowej) są duże
- scientific, fixed - notacja naukowa lub zwykła - dla float/double [bez ustawienia określa się to na podstawie wielkości rzędu liczby]
- showbase - dodaje odpowiednie nagłówki dla hex i oct
- showpoint - nakazuje zawsze wypisać kropkę w liczbach float
- showpos - dodaje `+' przed liczbą dodatnią
- unitbuf - wyłącza buforowanie dla strumienia
Jak widać, niektóre flagi nie mogą współistnieć, tzn. nie wolno jednocześnie ustawiać np. dec i oct, czy left i right. Dlatego dla tych flag istnieje ta druga postać metody setf, gdzie pierwszy argument może być:
- basefield - dotyczy dec, oct i hex
- floatfield - dotyczy scientific i fixed
- adjustfield - dotyczy left i right
Nie wolno zatem wywołać cout.setf(ios::right), gdyż efekt takiego wywołania jest niezdefiniowany. Należy wywołać to jako:
cout.setf( ios::right, ios::adjustfield );
Proszę o tym bezwzględnie pamiętać! Niektóre implementacje próbują radzić sobie z tym za pomocą wprowadzania odpowiednich typów, ale niekoniecznie wszystkie. Z tego względu należy do tych wymienionych flag używać BEZWZGLĘDNIE tej drugiej postaci metody setf, wraz z odpowiadającym argumentem maskującym (tzn. efektem pomylenia maski flag i flag jest brak efektu :*).
Tak na marginesie wspomnę jeszcze o pewnej szczególności flag scientific/fixed, czyli inaczej floatfield. Otóż flaga ta jest trójstanowa, zatem mamy możliwość ustawienia scientific, fixed i również żadną z nich (ta ostatnia możliwość jest właśnie ustawieniem domyślnym). Żeby przywrócić owo ustawienie domyślne, należy napisać:
cout.setf( 0, ios::floatfield );
Diagnostyka strumieni
Stan strumienia można podejrzeć metodą rdstate() i zwrócona liczba całkowita zawiera następujące flagi:
- ios::badbit - strumień jest niepoprawny i nie nadaje się do użytku
- ios::failbit - ostatnia operacja nie powiodła się
- ios::eofbit - nie ma więcej danych w strumieniu
Do diagnozowania strumieni istnieją też następujące metody, które zwracają wartość zerową lub niezerową dotyczącą tych flag
- int eof(); - eofbit
- int fail(); - failbit lub badbit
- int bad(); - badbit
- int good(); - zwraca 1 jeśli żadna z tych flag nie jest ustawiona
Dostępne są też metody operator void* oraz operator !. Zwracają wartości good() i !good() (void* jest prawdopodobnie pozostałością historyczną z czasów, gdy w C++ nie było typu bool, ale istnieją podobno jakieś inne powody, dla których tak ustanowiono w regułach biblioteki standardowej - jakie, nie wiem).
Zgodnie ze standardem, metody w razie nagłego ustawienia którejś z tych flag, mogą rzucić wyjątkiem. Ustawia się to (i sprawdza) metodą exceptions. Wartością przyjmowaną lub zwracaną są właśnie te podane flagi, w wypadku ustawienia których powinno wystąpić rzucenie wyjątkiem ios::failure.
A przy okazji jedna ciekawostka. Swego czasu kilku moich znajomych programistów zarzekało się, że poniższy kawałek kodu jest prawidłowy:
char buf[N]; while ( !f.eof() ) { f.getline( buf, N ); cout << buf << endl; }
Z założenia ma to odczytywać kolejne linie z pliku 'f' i wyrzucać je na standardowe wyjście. Tu jest niestety błąd. Tak właśnie wygląda powszechna znajomość sposobu powstawania tzw. wartości EOF.
Zatem zaznaczam uczciwie: wartość EOF powstaje tylko i wyłącznie wtedy, kiedy nastąpi próba odczytania pliku, który już się skończył. To oznacza, że jeśli odczyt pliku się powiódł, to nawet jeśli ta operacja odczytała już z pliku wszystko, co tylko w nim zostało, nie będzie żadnego EOF. EOF będzie tylko wtedy, kiedy ostatnia operacja próbowała pobrać jakiś bajt z wejścia i się jej to nie udało! Powyższy fragment programu, jak się można domyślać, spowoduje wyrzucenie ostatniej linii dwukrotnie.
Oczywiście z tym czytaniem linia po linii jest drobny problem. Otóż EOF w przypadku czytania pliku tekstowego po linii powstanie zawsze dokładnie wtedy, gdy "następna" linia nie posiadała znaku końca. Może to - ale nie musi - oznaczać, że nie zawierała ona żadnego znaku. Funkcja czytająca linię bowiem czyta dotąd, aż dostanie znak końca linii. Jeśli go odczyta (i przypadkiem był to ostatni bajt pliku, tzn. ostatnia linia kończyła się znakiem końca linii), to żaden EOF nie powstanie. To powoduje, że powyższa sekwencja będzie działać dobrze, ale pod warunkiem, że czytany plik nie ma znaku końca linii po ostatniej linijce. Gdybyśmy jednak sprawdzali EOF zaraz po getline, to też będzie to oczywiście źle, bo w ten sposób nie stwierdzimy pobrania ostatniej linijki (bo dane zostaną odczytane, a także dostaniemy EOF). Żeby sobie z tym poradzić, trzeba by oczywiście zerować bufor przed każdym odczytem i sprawdzać, czy cokolwiek się wczytało i jednocześnie sprawdzać EOF. Prościej jednak jest po prostu sprawdzić stan strumienia, czy też nawet, bardziej po spartańsku, umieścić wywołanie getline wewnątrz while:
char buf[N]; while ( f.getline( buf, N ) ) cout << buf << endl;
Manipulatory
Manipulatory są to takie, jakby to powiedzieć, elementy, które mogą wystąpić w ciągu wywołań operatorów << i >>. Nie są to żadne dane wysyłane do strumienia ani miejsca do ich odbioru, lecz powodują odpowiednią zmianę ustawień w strumieniu. Te bezargumentowe manipulatory to po prostu funkcje, które z kolei wywołują odpowiednie metody; dla manipulatorów z argumentem zrobiono już trochę bardziej skomplikowane kombinacje (podobne do STL-owskich funkcjonałów), aczkolwiek dzięki temu zmiany ustawień strumienia dokonuje się w dość wygodny sposób.
Manipulatory są oczywiście osobne dla istreamów i ostreamów. Dla istreamów mamy następujące manipulatory:
- ws - powoduje wymuszenie zignorowania wiodących białych znaków przy wczytywaniu, np.:
string s; cin >> ws >> s;
Bez tego `ws', wszelkie białe znaki poprzedzające inne znaki będą umieszczone w `s' (oczywiście tylko, gdy wczśniej wymuszono, by ich nie omijać - szczegóły zaraz podam).
Manipulatorów dla ostream jest znacznie więcej. Mamy tutaj trzy bezargumentowe: flush, ends oraz poznany już endl. Powodują one odpowiednio opróżnienie bufora, wysłanie znaku zerowego (przydatne tylko dla strumieni napisowych, o których za chwilę) i wysłanie znaku '\n'. Mamy również manipulatory zmieniające podstawę systemu liczbowego: oct, hex i dec.
Dość przydatne są też manipulatory z argumentem:
- setbase - ustawia (liczbowo) podstawę systemu liczbowego. Szczerze powiedziawszy to bardzo dziwny manipulator; oczekuje liczby będącej podstawą systemu, ale tak naprawdę znaczenie mają jedynie liczby 8 i 16; inne wartości ustanawiają system dziesiętny. Istnienie tego manipulatora (ustanowionego pewnie żeby nie wprowadzać zamieszania dla wykluczających się flag) jest w dodatku o tyle dziwne, że nie istnieją podobne np. setadjust i setfloat; jest też w dodatku niepotrzebne, bo dużo prościej można to zrobić przez oct, hex i dec. Nie wiem, być może jest to pozostawione do przyszłych rozszerzeń.
- setfill - jak fill()
- setprecision - jak precision()
- setw - jak width()
- setiosflags, resetiosflags - ustawiają lub kasują podane flagi formatowania (zatem zaleca się ostrożność przy right, scientific itd.)
Obsługa plików
Można oczywiście stworzyć własny strumień skojarzony z plikiem. W odróżnieniu jednak od biblioteki stdio, może być on normalną zmienną lokalną. Można też stworzyć go nie kojarząc go z żadnym plikiem. Mamy tutaj do dyspozycji następujące typy strumieni plikowych: ifstream, ofstream i fstream, które są - jak się można domyślić - strumieniem wejściowym, wyjściowym i dwukierunkowym. Determinuje to oczywiście jedynie dostępność metod dla konkretnego kierunku przepływu danych (i oczywiście domyślny tryb otwarcia pliku, ale tylko domyślny; jeśli poda się własne flagi to trzeba o ustawieniu odpowiedniej flagi kierunkowej samemu pamiętać!). Używanie obsługi plików wymaga wczytania nagłówka <fstream>.
Parametry charakterystyczne pliku można podać albo w konstruktorze podczas deklaracji, albo później, wywołując metodę open. Ponieważ argumenty dla konstruktora są identyczne jak dla tych metod, skupię się więc na nich:
void open( const char* name, int mode, int prot=DEFAULT_PROT );
Metoda ta otwiera plik o nazwie `name' w trybie `mode'. Jeśli plik w efekcie będzie utworzony, jest tworzony z prawami `prot'. Tu drobna uwaga. Parametr `prot' ma wartość domyślną, ale jest ona różna na różnych systemach (system może mieć swój sposób określania uprawnień). Na uniksach np. ma wartość 0644 (oczywiście jest też uwzględniana umaska itd.).
Parametr `mode' (UWAGA: dla ifstream lub ofstream może mieć wartość domyślną) to zestaw następujących flag bitowych (oczywiście poprzedzone są `ios::'):
- in - do odczytu (domyślna dla ifstream)
- out - do zapisu (domyślna dla ofstream)
- ate - ustawia się na koniec pliku
- app - do dopisywania
- trunc - niszczy istniejący plik
- nocreate - nie tworzy nowego pliku (tzn. plik musi istnieć)
- noreplace - nie usuwa pliku (nie wiem czym się różni od nieustawionego trunc)
- bin (lub binary) - tryb binarny (zob. notatka o trybie binarnym w stdio).
Tu od razu uwaga! Tryb ios::app jest trybem dopisywania, co oznacza, że wszelka dotychczasowa zawartość pliku jest chroniona i NIE DA się jej zapisywać! Wszelkie próby ustawiania wskaźnika pliku są w takim wypadku ignorowane (tzn. ustawić się da, ale każda następna próba zapisu i tak go ustawi na koniec pliku). Aby nie mieć tego efektu należy użyć ios::out|ios::ate.
Poza tym strumienie plikowe posiadają jeszcze takie metody:
- bool is_open(); - zwraca true, jeśli strumień jest skojarzony z plikiem
- void setbuf(char* ptr, int len); - ustanawia bufor dla pliku o podanej długości (również można wyłączyć buforowanie i krócej robi to metoda raw(); pomijając już szczegół, że można też użyć flagi ios::unitbuf)
Jeszcze parę drobnych uwag. Strumień skojarzony z plikiem jest strumieniem błędnym (czyli jest !good()), dopóki się go nie otworzy, zatem w prosty sposób można operatorem ! sprawdzić, czy otwarcie się udało. Najważniejsze jednak, o czym trzeba pamiętać, to że jeśli strumień został użyty ponownie (tzn. wywołano mu metodę close(), a potem znów open()), to niestety (podobno to jest defekt w bibliotece standardowej) flagi strumienia nie są zerowane. Należy zatem przed "ponownym open()" użyć metody clear().
Jeżdżenie po pliku
Procedury przemieszczania się po pliku są nieco ulepszone w stosunku do tego, co jest w stdio (zdarza się niestety w niektórych kompilatorach, że działają wadliwie; mam nadzieje, że nie masz z takim do czynienia, aczkolwiek krótkie poeksperymentowanie z danym kompilatorem jest surowo wskazane!). Przede wszystkim, posiadają one osobne metody dla strumieni wejściowych i wyjściowych (dwukierunkowe posiadają je wszystkie), aczkolwiek podejrzewam, że stan taki został wymuszony przez problem z wielorakim dziedziczeniem (strumienie dwukierunkowe łączą w sobie wejściowe i wyjściowe; gdyby metody w obu tych strumieniach miały te same nazwy, stworzenie strumienia dwukierunkowego byłoby niemożliwe z powodu niejednoznaczności). Czy bowiem można w strumieniach dwukierunkowych osobno ustawiać wskaźnik na odczyt i wskaźnik na zapis, tego nie wiem (być może byłoby w tej rzeczy co nieco zależnego od systemu).
Jak się można spodziewać, nazwy metod zaczynają się na `tell' (odczytująca pozycję strumienia) i `seek' (ustawiająca tą pozycję). Dla istream mają dodatkowo literę `g', a dla ostream - `p'. Podsumowując, mamy następujące metody:
- tellp, tellg (bez argumentów) zwracają aktualną pozycję pliku
- seekg, seekp - ustawiają pozycję pliku:
seekg( int pos ); seekg( int off, seek_dir direction );
Metoda ta ustawia pozycję pliku `pos'. Drugi argument jest jednym z:
- ios::beg - względem początku
- ios::cur - względem bieżącej pozycji
- ios::end - względem końca
Wtedy argument `off' jest argumentem względnym i może być ujemny.
Strumienie napisowe
Są to dość niewielkie klasy, które implementują wszystko, co posiada ios i klasy *stream. Mamy tu zatem klasy istringstream, ostringstream i stringstream. Ich używanie wymaga włączenia nagłówka <sstream>.
Klasa istringstream jest pochodną istream i posiada tylko jeden konstruktor. Każe on sobie podać referencję do std::string. Jest to niejako nałożenie "nakładki" na bufor w celu "wydłubania" z niego danych. Ten konstruktor to właściwie wszystko, co posiada ta klasa ponad to, co ma istream.
Klasy ostringstream i stringstream są niemal identyczne (z tą tylko różnicą, że pierwsza jest pochodną ostream, a druga iostream). Posiadają konstruktor podobny do tego powyżej, ale również bezargumentowy. Ponadto:
- size_t pcount() - zwraca ilość wypisanych już znaków do bufora
- std::string str() - zwraca obiekt bufora