C++ bez cholesterolu

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:

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

Następujące metody są trochę niezwykłe, ale też niezwykle się przydają:

Klasa ostream

Podobnie, ta klasa zawiera operator <<. Przeciążony jest dla następujących typów:

Ponadto mamy do dyspozycji takie metody:

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:

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

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

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:

Do diagnozowania strumieni istnieją też następujące metody, które zwracają wartość zerową lub niezerową dotyczącą tych flag

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:

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:

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::'):

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:

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:

seekg( int pos );
seekg( int off, seek_dir direction );

Metoda ta ustawia pozycję pliku `pos'. Drugi argument jest jednym z:

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: