16.5 Pliki

Klasy obsługujące operacje we/wy na plikach to:

Z dziedziczenia wynika, że można stosować te same metody (operatory '<<' i '>>', manipulatory, funkcje get, put, getline itd.) w odniesieniu do obiektów tych klas. Należy tylko utworzyć odpowiedni obiekt, odpowiednik predefiniowanych cin czy cout.

Aby posłużyć się strumieniem plikowym, trzeba stworzyć do niego „uchwyt”, związać go z konkretnym plikiem, otworzyć strumień, a po zakończeniu na nim operacji we/wy zamknąć.

Tworzenie uchwytu do pliku odbywa się poprzez utworzenie (zdefiniowanie) obiektu strumienia i wywołanie funkcji wiążącej strumień z konkretnym plikiem i otwierającej go. Na przykład następujący fragment

      1.      #include <fstream>
      2.      // ...
      3.      ofstream plik;
      4.      plik.open("plik.txt");
      5.      plik << "To bedzie w pliku \"plik.txt\"" << endl;
      6.      plik.close();
tworzy w linii 3 obiekt reprezentujący strumień związany z plikiem do zapisu (dlatego ofstream — output file stream). W następnej linii strumień ten jest wiązany z konkretnym plikiem i otwierany. W linii 5 widzimy, że nazwy plik możemy teraz używać tak samo, jak do tej pory używaliśmy cout; różnica jest tylko w ujściu strumienia — wtedy był to ekran komputera, teraz będzie plik. Po zakończeniu pracy z plikiem strumień należy zamknąć (linia 6). Zauważmy, że strumienie predefiniowane nie musiały być jawnie ani otwierane, ani zamykane.

Zamiast wywoływania funkcji open można posłać nazwę pliku bezpośrednio do konstruktora obiektu:

       #include <fstream>
       // ...
       ofstream plik("plik.txt");
       plik << "To bedzie w pliku \"plik.txt\"" << endl;
       plik.close();

Zarówno do funkcji open, jak i do konstruktora można posłać dodatkowy, prócz nazwy pliku, argument. Określa on tryb otwarcia (ang. opening mode)) pliku. Dla plików do czytania, dla których tworzymy obiekt klasy ifstream (input file stream), domyślny tryb to ios::in (jest to statyczna stała odziedziczona z klasy ios_base). Dla pliku z uchwytem klasy ofstream domyślnie przyjmowane jest ios::out —  plik do zapisywania. Stałe określające tryb pliku można „ORować” tak samo, jak to robiliśmy dla flag formatowania. Na przykład

       fstream strum("plik.txt", ios::in | ios::out);
tworzy obiekt strumienia (klasy fstream) w trybie do zapisu i odczytu.

Dostępne stałe, z których poprzez alternatywę bitową można budować wartości określające tryb otwarcia, to:

Ze strumieniami są związane lokalizatory zawierające numer, licząc od zera, bajtu w strumieniu (pliku), na który nastąpi następny zapis/odczyt. Ich typem jest streampos (zwykle tożsamy z  long). Po otwarciu pliku domyślnie lokalizatory ustawiane są na samym jego początku, a więc na bajcie numer zero, chyba że plik został otwarty w trybie ios::ate lub ios::app, gdy plik pozycjonowany jest na bajcie „pierwszym za ostatnim” (numeryczna wartość lokalizatora jest wtedy równa długości pliku w bajtach). Po każdej operacji czytania/pisania lokalizator przesuwany jest tak, aby wskazywał na pierwszy bajt jeszcze nie wczytany/zapisany. Nawet jeśli plik jest otwarty zarówno do zapisu jak i odczytu, to pamiętana jest tylko jedna pozycja w strumieniu: ta sama do operacji zapisu jak i odczytu.

Do „ręcznego” manipulowania lokalizatorami służą metody:

streampos tellg( ) —  zwraca aktualną pozycję lokalizatora do odczytu (litera 'g' na końcu nazwy pochodzi od get). Tryb otwarcia pliku musi zawierać ios::in.

streampos tellp( ) —  zwraca aktualną pozycję lokalizatora do zapisu (litera 'p' na końcu nazwy pochodzi od put). Tryb otwarcia pliku musi zawierać ios::out.

ostream& seekg(streampos poz) —  przesuwa lokalizator do odczytu na pozycję poz. Zwraca referencję do strumienia, na rzecz którego została wywołana. Tryb otwarcia pliku musi zawierać ios::in. Próba sięgnięcia przed początek bądź za koniec pliku powoduje jego przejście do stanu bad (patrz dalej), co można sprawdzić za pomocą warunku ' if (strum) ...'.

ostream& seekp(streampos poz) —  przesuwa lokalizator do zapisu; poza tym analogiczna do metody seekg(streampos).

ostream& seekg(streamoff offset, ios::seek_dir poz) —  przesuwa lokalizator do odczytu na pozycję offset bajtów licząc od pozycji poz. Argument offset może być ujemny. Typy streamoffios::seek_dir są aliasami pewnych typów całościowych. Argument poz musi być równy jednej ze stałych statycznych z klasy ios:
ios::beg — licz od początku pliku;
ios::cur — licz od aktualnej pozycji w pliku;
ios::end — licz od końca pliku;
Zwraca referencję do strumienia, na rzecz którego została wywołana. Próba sięgnięcia przed początek bądź za koniec pliku powoduje przejście strumienia do stanu bad.

ostream& seekp(streamoff offset, ios::seek_dir poz) —  przesuwa lokalizator do zapisu; poza tym analogiczna do metody seekg(streamoff offset, ios::seek_dir poz).

Jako przykład rozpatrzmy program


P126: plikrw.cpp     Zapis i odczyt z pliku

      1.  #include <iostream>
      2.  #include <fstream>
      3.  using namespace std;
      4.  
      5.  int main() {
      6.      int tab[] = { 97, 105, 115, 255, 111 },k;
      7.  
      8.      int size = sizeof(tab)/sizeof(tab[0]);
      9.  
     10.      cout << "Tablica o  wymiarze: " << size << endl;
     11.      for (int i = 0; i < size; ++i)
     12.          cout << tab[i] << " ";
     13.      cout << endl;
     14.  
     15.      ofstream file_out("file.dat",ios::out|ios::binary);
     16.      if (! file_out ) {
     17.          cout << "Nie mozna otworzyc file_out" << endl;
     18.          return -1;
     19.      }
     20.  
     21.      file_out.write((char*)tab, sizeof(tab));
     22.      file_out.close();
     23.  
     24.      fstream file("file.dat",ios::in|ios::out|ios::binary);
     25.      if (! file ) {
     26.          cout << "Nie mozna otworzyc file" << endl;
     27.          return -1;
     28.      }
     29.  
     30.      file.seekg(0,ios::end);
     31.      streamsize len = file.tellg();
     32.      cout << "Plik ma dlugosc " << len << " bajtow\n";
     33.      file.seekg(0);
     34.  
     35.      cout << "Kolejne bajty zawieraja:" << endl;
     36.      while ( (k = file.get()) != EOF )
     37.          cout << k << " ";
     38.      cout << endl;
     39.  
     40.      file.clear(); // <-- KONIECZNE !!!
     41.  
     42.      file.seekg(4);
     43.      file.read((char*)&k,4);
     44.      cout << "Integer od pozycji 4: " << k << endl;
     45.  
     46.      file.seekp(12);
     47.      file.write((char*)&k,4);
     48.  
     49.      file.seekg(0);
     50.      cout << "Kolejne bajty pliku teraz zawieraja:" << endl;
     51.      while ( (k = file.get()) != EOF )
     52.          cout << k << " ";
     53.      cout << endl;
     54.  
     55.      file.close();
     56.  }

W linii 15 tworzymy i otwieramy strumień zapisujący związany z plikiem file.dat. Aby uniknąć ewentualnych kłopotów w systemie Windows, plik jest otwierany w trybie binarnym.

Następnie, w linii 21, zapisujemy do tego pliku całą zawartość pięcioelementowej tablicy liczb typu int. Zapis jest binarny, więc zapisanych powinno zostać dokładnie 20 bajtów. Po zapisaniu tablicy plik zamykamy, a następnie otwieramy znowu, tym razem do czytania i pisania (linia 24). Pozycjonujemy strumień na końcu pliku i odczytujemy pozycję: powinna ona wskazywać na bajt „pierwszy za ostatnim”, a ostatni ma numer 19. Zatem powinna to być pozycja numer 20, co jest równe ilości bajtów w pliku (linie 30-32).

Przewijamy plik do początku (linia 33) i czytamy zawartość pliku bajt po bajcie, wypisując wynik na ekranie (linie 36-38).

Po tej operacji stan strumienia jest bad, ponieważ w pętli próbowaliśmy odczytać bajt po osiągnięciu końca pliku. Każda następna operacja wejścia/wyjścia na tym strumieniu byłaby zignorowana (choć nie zostałby zgłoszony żaden błąd — musimy to zawsze sami sprawdzać!). Dlatego przed dalszym użyciem strumienia musimy go „naprawić”. Robi się to za pomocą metody clear (linia 40), o której jeszcze powiemy za chwilę.

Pozycjonujemy teraz plik na bajcie numer 4 (czyli piątym, a pierwszym należącym do drugiej liczby z zapisanej tablicy). Poczynając od tej pozycji czytamy 4 bajty i kopiujemy je do zmiennej k typu int (linie 42-43). Po tej operacji wartość  k powinna być zatem równa 105. Teraz dokonujemy operacji odwrotnej: pozycjonujemy lokalizator do pisania na bajcie numer 12 (początek czwartej liczby) i zapisujemy tam 4 bajty zmiennej k (linie 46-47). Tak więc czwarta liczba zapisana w pliku powinna zmienić się na 105. Tak jest rzeczywiście, o czym przekonuje nas wydruk:

    Tablica o  wymiarze: 5
    97 105 115 255 111
    Plik ma dlugosc 20 bajtow
    Kolejne bajty zawieraja:
    97 0 0 0 105 0 0 0 115 0 0 0 255 0 0 0 111 0 0 0
    Integer od pozycji 4: 105
    Kolejne bajty pliku teraz zawieraja:
    97 0 0 0 105 0 0 0 115 0 0 0 105 0 0 0 111 0 0 0
Wydruk zależy tu od architektury komputera; w tym przypadku jest to little-endian. Na komputerze big-endian pierwsza liczba wydrukowana zostałaby jako '0 0 0 97', a nie tak jak w przykładzie '97 0 0 0'.

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