Podrozdziały


16.4 Zapis i odczyt nieformatowany

Do tej pory mówiliśmy o zapisie/odczycie formatowanym —  informacja czytana lub pisana jest w jakiś sposób interpretowana: opuszczane są białe znaki, dokonuje się przekształceń liczb do napisów w różnych formatach itd. Istnieją też operacje we/wy nieformatowane, które czytają lub piszą „surowe” bajty — bez ich żadnej interpretacji.


16.4.1 Odczyt nieformatowany

Na rzecz obiektu strumieniowego mogą być też wywołane metody powodujące wczytanie danych ze strumienia bez ich interpretacji i formatowania. Należą do nich następujące metody:

istream& get(char& c) — czyta jeden bajt; zwraca referencję do strumienia, na rzecz którego została wywołana, więc może być używana kaskadowo. Argument typu char jest przesyłany przez referencję; po powrocie jego wartością będzie wczytany znak. Może to być dowolny znak, również znak kontrolny, biały lub zerowy (czyli ' \0'). Jeśli czytanie nie powiodło się, bo napotkany został koniec pliku, znak c będzie równy EOF, czyli znak, który w danym systemie operacyjnym oznacza koniec danych (Ctrl-Z w Windows, Ctrl-D pod Uniksem/Linuksem). Sam strumień będzie wtedy w stanie błędu. Poznać to można przez wymuszenie konwersji zmiennej strumieniowej do typu void* — jeśli strumień jest „dobry” to otrzymamy wartość niezerową, jeśli zły, to otrzymamay NULL. Taka konwersja zachodzi automatycznie w kontekście, w którym wymagana jest wartość logiczna, a zatem można jej użyć bezpośrednio do części testującej instrukcji if, for, while itd. Na przykład, jeśli strin jest strumieniem wejściowym związanym z plikiem, to można zawartość tego pliku przekopiować na standardowe wyjście prostą pętlą

           char c;
           while (strin.get(c)) cout << c;

Kaskadowość funkcji get można wykorzystać w konstrukcjach typu

           char a, b, c;
           strm.get(a).get(b).get(c);

przy czym w ten sposób możemy wczytać dowolne znaki; bez żadnego opuszczania białych znaków, interpretacji znaku końca linii itp.

int get() — zwraca wczytany znak w formie liczby typu int; w razie napotkania końca pliku zwrócone zostanie EOF. Ta forma funkcji get nie zwraca referencji do strumienia, więc nie może być używana kaskadowo.

istream& get(char* buf, streamsize length, char termin = '\n') —  czyta do łańcucha (napisu) buf — czyli do pamięci poczynając od adresu będącego wartością wskaźnika buf — maksymalnie length-1 bajtów. Znak NUL (zerowy) jest automatycznie dodawany na koniec łańcucha buf. Bajt numer length ze strumienia nie jest już wczytywany. Wczytywanie kończy się również, jeśli napotkany zostanie znak termin (domyślnie, jak widać z nagłówka, jest to znak końca linii '\n'). Znak NUL też zostanie wtedy dopisany do łańcucha buf, ale sam znak termin pozostaje w strumieniu jako „niewyczytany” i nie jest umieszczany w wyjściowym buforze buf. Zatem przed wczytaniem kolejnych danych należy go zwykle „wyczytać” — można to zrobić za pomocą funkcji ignore opisanej dalej. Oczywiście przekazany do funkcji bufor musi istnieć, to znaczy pamięć na ten bufor musiała zostać zaalokowana w funkcji wołającej. Drugi parametr funkcji jest typu streamsize, co jest aliasem pewnego typu całkowitego. Funkcja zwraca referencję do strumienia, na rzecz którego została wywołana (*this).

istream& getline(char* buf, streamsize length, char termin = '\n') —  działa dokładnie tak jak poprzednia funkcja, ale znak termin jest wyjmowany ze strumienia, choć nie jest umieszczany w buforze buf. Jest to zatem metoda znacznie praktyczniejsza od poprzedniej przy wczytywania kolejnych linii tekstu.

istream& read(char* buf, streamsize length) —  wczytuje length bajtów do bufora buf. Czytanie może się zakończyć przedwcześnie tylko, jeśli napotkany został koniec danych. O tym, ile rzeczywiście bajtów zostało wczytanych, można się przekonać wywołując bezpośrednio po czytaniu funkcję gcount (patrz dalej). Funkcja czyta surowe bajty i nie dostawia znaku końca napisu ani żadnego innego. Stosuje się ją zwykle do czytania danych nietekstowych. Funkcja zwraca referencję do strumienia na rzecz którego została wywołana (*this).

istream& ignore(streamsize length = 1, int termin = EOF) —  wczytuje length znaków (domyślnie jeden znak), nigdzie ich nie wpisuje. Jeśli napotkany został znak termin (domyślnie koniec pliku), to jest on usuwany ze strumienia i czytanie kończy się. Funkcja zwraca referencję do strumienia, na rzecz którego została wywołana.

int gcount( ) —  zwraca liczbę wczytanych ze strumienia bajtów podczas wykonywania ostatniej funkcji nieformatowanego wejścia.

int peek( ) —  zwraca jeden znak ze strumienia w postaci zmiennej typu int — może to być EOF jeśli napotkany został koniec pliku. Znak pozostaje w strumieniu i będzie pierwszym znakiem wczytanym przy następnej operacji czytania.

istream& putback(char znak) —  wstawia znak znak do strumienia; będzie on pierwszym znakiem wczytanym przez następną operację czytania. Zwraca odnośnik do strumienia, na rzecz którego została wywołana. Nie do każdego strumienia i nie zawsze można wstawić znak.

istream& unget() —  wstawia ostatnio przeczytany znak z powrotem do strumienia; będzie on pierwszym znakiem wczytanym przez następną operację czytania. Zwraca referencję do strumienia, na rzecz którego została wywołana.

Kilka z tych funkcji użytych zostało w poniższym programie, który czyta kolejne linie ze standardowego wejścia i wyświetla je bez zmian, ale pomijając komentarze, czyli fragmenty od dwuznaku '//' do końca linii:


P125: czytnf.cpp     Czytanie nieformatowane

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      cout << "Wpisuj linie tekstu. Zakoncz znakiem konca "
      6.              "pliku\n(Ctrl-Z w Windows, Ctrl-D w Linuksie) "
      7.              "Komentarze\nod \'//\' do konca linii beda "
      8.              "pomijane.\n";
      9.      char c;
     10.      while ( cin.get(c) ) {
     11.          if ( c == '/' )
     12.              if ( cin.peek() == '/') {
     13.                  cin.ignore(1024,'\n');
     14.                  cout << endl;
     15.                  continue;
     16.              }
     17.          cout << c;
     18.      }
     19.  }

Po wczytaniu każdego znaku prgram sprawdza, czy jest to znak '/' (linia 11). Jeśli tak, to za pomocą peek „podgądany” jest następny znak. Jeśli to też jest '/', to linia jest „wyczytywana” do końca (za pomocą funkcji ignore; linia 13), wypisywany jest znak nowej linii, po czym funkcja kontunuuje czytanie kolejnych znaków. Program kończy się, gdy wczytanym znakiem będzie znak końca pliku EOF, czyli gdy użytkownik wpisze Ctrl-Z (Windows) lub Ctrl-D (Linux). Strumień sprawdzany w linii 10 będzie wtedy przekonwertowany do NULL, co zakończy pętlę. Przykładowe uruchomienie programy dało
    Wpisuj linie tekstu. Zakoncz znakiem konca pliku
    (Ctrl-Z w Windows, Ctrl-D w Linuksie) Komentarze
    od '//' do konca linii beda pomijane.
    int main(void) {
    int main(void) {
        const int DIM = 15; // wymiar tablicy
        const int DIM = 15;
        int tab[DIM];       // tablica znakow
        int tab[DIM];
        double x=1, y=2, z=x/y;  // trzy liczby
        double x=1, y=2, z=x/y;
        // ...

    }
    }
    ^D
Zwróćmy też uwagę na linie 5-8 programu. Zilustrowana tu jest bardzo przydatna cecha kompilatora C/C++: literały napisowe, a więc napisy ograniczone znakami cudzysłowu, są łączone (konkatenowane) w jeden napis, jeśli oddzielone są od siebie tylko białymi znakami (którym jest, w szczególności, znak końca linii).


16.4.2 Zapis nieformatowany

Bardzo ważna jest umiejętność nieformatowanego zapisu. Oczywiście przydaje się ona głównie przy zapisie do pliku lub gniazda sieciowego, nie na ekran komputera. Pozwala zapisywać dane z pełną dokładnością w formie oszczędzającej miejsce na dysku (zauważmy, że zapis liczby -1.234567E+19 zajmuje 13 znaków, choć ta sama liczba typu float z pełną dokładnością zajmuje w pamięci tylko 4 bajty, a więc tylko 4 bajty muszą być wyprowadzone do binarnego pliku).

Pliki takie nie mogą być, co prawda, oglądane czy modyfikowane w zwykłych edytorach tekstu, ale mogą być czytane przez ten sam lub inne programy, również za pomocą operacji nieformatowanych. Nieformatowane operacje we/wy są też konieczne do pracy z plikami nietekstowymi, na przykład graficznymi, dźwiękowymi itd.

Opisane poniżej dwie metody są metodami z klasy ostream: mogą zatem być wywoływane na rzecz dowolnego strumienia wyjściowego, nie tylko strumienia cout.

ostream& put(char c) —  wstawia znak (bajt) c do strumienia. Zwraca referencję do strumienia, na rzecz którego metoda została wywołana.

ostream& write(const char* buf, streamsize length) —  zapisuje length znaków (bajtów) z bufora buf do strumienia. Zwraca referencję do strumienia, na rzecz którego została wywołana.

Przykłady na zastosowanie tych operacji podamy po zapoznaniu się z obsługą strumieni związanych z plikami.

T.R. Werner, 25 lutego 2017; 22:31