Rozdział 9. Odczytywanie i zapisywanie plików

> Dodaj do ulubionych

Wszystkie przedstawione do tej pory programy są względnie proste, czyli w sam raz dla kogoś, kto dopiero uczy się programować. Dzięki zdobytym podstawom języka C++ przy odrobinie praktyki będziesz w stanie pisać także bardziej zaawansowane programy. Brakuje Ci jednak jeszcze wiedzy na jeden ważny temat: pracy z plikami.

Umiesz już wyświetlać dane na ekranie i pobierać informacje od użytkownika z konsoli. Na pewno się zgodzisz, że interakcja z samą konsolą to trochę za mało. Pomyśl o takich programach, jak Notatnik czy środowisko programistyczne, którego używasz albo arkusz kalkulacyjny. Wszystkie one potrafią zapisywać i odczytywać pliki. Także w programowaniu gier jest to bardzo potrzebne. Oprócz plików do zapisywania aktualnego stanu gry w grach wykorzystywane są też pliki graficzne, wideo, muzyczne i inne. Mówiąc krótko, program który nie współpracuje z plikami ma poważnie ograniczoną funkcjonalność.

Zatem bierzemy się do pracy. Jeśli wiesz jak posługiwać się instrukcjami cin i cout, to bez trudu nauczysz się też działać na plikach.

9.1. Zapisywanie danych w pliku

Pracę z plikiem rozpoczyna się od jego otwarcia. Gdy plik jest otwarty, dalsze czynności wykonuje się tak samo, jak przy użyciu instrukcji cin i cout, tzn. za pomocą operatorów << i >>. Uwierz mi, nauczysz się tego w mig.

Kiedy jest mowa o komunikacji programu z obiektami zewnętrznymi, używa się pojęcia strumienia. W tym rozdziale interesować nas będą strumienie plikowe, czyli strumienie umożliwiające zapis i odczyt plików.

9.1.1. Nagłówek fstream

Jak to zwykle bywa w języku C++, gdy potrzebujemy jakiejś funkcjonalności, musimy dołączyć do programu specjalny plik nagłówkowy. Narzędzia do pracy z plikami znajdują się w nagłówku fstream, który dołączamy na początku programu za pomocą dyrektywy #include <fstream>.

Najważniejsza różnica między strumieniem wejścia i wyjścia a strumieniem plikowym polega na tym, że w tym drugim przypadku każdy plik musi mieć utworzony osobny strumień. Najpierw nauczymy się tworzyć strumienie wyjściowe, czyli umożliwiające odczyt danych z plików.

9.1.2. Otwieranie pliku do zapisu

Faktycznie strumienie są obiektami. Przypomnę, że język C++ jest językiem obiektowym i strumienie są po prosty jednymi z jego wielu obiektów.

Nie martw się, jeśli nie wiesz co to jest obiekt. Wszystkiego dowiesz się później, a na razie obiekty wyobrażaj sobie jako duże zmienne. Zawierają one wiele informacji o otwartych plikach i udostępniają różne funkcje, takie jak np. do zamykania plików, przechodzenia na ich początek itp.

Co ciekawe, strumienie plikowe definiuje się dokładnie tak samo, jak zwykłe zmienne. Po prostu definiujemy zmienną typu ofstream o wartości będącej ścieżką do pliku, który chcemy otworzyć:

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    ofstream mojStrumien("C:/Nanoc/scores.txt");    // Deklaracja strumienia do zapisu w pliku
                                                // C:/Nanoc/scores.txt
    return 0;
}

Ścieżkę do pliku należy podać w cudzysłowie. Można stosować dwa rodzaje ścieżek:

  • Ścieżka bezwzględna: jest to ścieżka odnosząca się do katalogu głównego na dysku, np. C:/Nano/C++/Pliki/scores.txt
  • Ścieżka względna: jest to ścieżka określająca położenie pliku względem miejsca, w którym znajduje się program, np. Pliki/scores.txt, jeśli program znajduje się w folderze C:/Nanoc/C++.

Teraz można użyć utworzonego strumienia do zapisania danych w pliku.

Najczęściej nazwa pliku jest określona jako łańcuch znaków. Wówczas do jego otwarcia należy użyć funkcji c_str():

string const nazwaPliku("C:/Nanoc/scores.txt");

ofstream mojStrumien(nazwaPliku.c_str());    // Definicja strumienia do zapisu danych w pliku.

Podczas otwierania pliku mogą mieć miejsce niespodziewane zdarzenia, np. plik nie zostanie odnaleziony albo dysk twardy będzie zapełniony. Dlatego też zawsze przy otwieraniu pliku należy sprawdzić, czy operacja się powiodła. Do tego celu można użyć konstrukcji if(mojStrumien). Jeśli wynik tego testu jest negatywny, wiadomo, że wystąpił jakiś problem i nie można użyć żądanego pliku.

ofstream mojStrumien("C:/Nanoc/scores.txt"); // Próbujemy otworzyć plik

if(mojStrumien)    // Sprawdzamy czy plik został otwarty.
{
    // Wszystko jest w porządku, można pracować na pliku
}
else
{
    cout << "BŁĄD: nie można otworzyć pliku." << endl;
}

Możemy w końcu zapisać jakieś dane w pliku. Jak się przekonasz, nie jest to wcale trudne.

9.1.3. Zapisywanie danych w pliku

Napisałem, że z plikami pracuje się podobnie, jak ze strumieniami cout i cin. Tak jest rzeczywiście, ponieważ w przypadku zapisu do pliku używa się operatora <<:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
    string const nazwaPliku("E:/scores.txt");
    ofstream mojStrumien(nazwaPliku.c_str());

    if(mojStrumien)    
    {
        mojStrumien << "Witaj, jestem zdaniem zapisanym w pliku." << endl;
        mojStrumien << 42.1337 << endl;
        int age(23);
        mojStrumien << "Mam " << age << " lata." << endl;
    }
    else
    {
        cout << "BŁĄD: nie można otworzyć pliku." << endl;
    }
    return 0;
}

Po uruchomieniu tego programu na moim dysku znalazł się plik o nazwie scores.txt o następującej zawartości:

Zawartość pliku zapisana przez program
Zawartość pliku zapisana przez program

Wypróbuj go sam! Możesz na przykład napisać program pobierający od użytkownika imię i wiek i zapisujący te informacje w pliku tekstowym.

9.1.4. Tryby otwarcia pliku

Pozostała nam jeszcze jedna kwestia do wyjaśnienia. Co się dzieje, gdy plik o podanej nazwie już istnieje? Zostanie on wyczyszczony i zapisany od nowa, co nie zawsze jest tym, czego byśmy chcieli. Wyobraź sobie na przykład plik do przechowywania listy czynności wykonanych przez użytkownika. Nie chcemy za każdym razem kasować jego zawartości, tylko dodawać na jego końcu coraz to nowe wiersze danych.

Aby zapisać informacje na końcu pliku, należy wyrazić ten zamiar podczas jego otwierania. W tym celu dodaje się specjalny parametr do instrukcji tworzącej strumień:

ofstream mojStrumien("C:/Nanoc/scores.txt", ios::app);

Teraz nowe dane nie będą powodowały skasowania starych, tylko będą dodawane na końcu pliku.

9.2. Odczytywanie pliku

Wiesz już jak zapisać dane w pliku, a więc czas nauczyć się także je odczytywać. Czynność tę wykonuje się bardzo podobnie do poprzedniej.

9.2.1. Otwieranie pliku do odczytu

Zasada działania jest dokładnie taka sama, jak przy zapisywaniu plików, tylko zamiast strumienia ofstream będziemy używać strumienia ifstream. Oto ogólna struktura kodu do odczytywania zawartości pliku:

ifstream mojStrumien("C:/Nanoc/C++/data.txt");  // Otwarcie pliku do odczytu

if(mojStrumien)
{
    // Wszystko gotowe do odczytu.
}
else
{
    cout << "BŁĄD: nie można otworzyć pliku do odczytu." << endl;
}

Jak widać, nic nowego tu nie ma. Plik można odczytać na trzy sposoby:

  1. Linijka po linijce przy użyciu funkcji getLine().
  2. Słowo po słowie przy użyciu operatora >>.
  3. Znak po znaku przy użyciu funkcji get().

Poniżej znajduje się szczegółowy opis każdej z tych metod.

Odczytywanie pliku linijka po linijce

Ta metoda polega na odczytaniu jednej linijki tekstu i zapisaniu jej jako łańcucha znaków.

string linia;
getline(mojStrumien, linia); // Odczytanie jednej linii tekstu

Efekt zastosowania tej techniki jest identyczny, jak użycia instrukcji cin.

Odczytywanie pliku słowo po słowie

Ta metoda również jest łatwa do opanowania. Spójrz na poniższy przykład:

double liczba;
mojStrumien >> liczba; // Odczytanie liczby zmiennoprzecinkowej
string slowo;
mojStrumien >> slowo; // Odczytanie słowa z pliku

W tym przypadku odczytywane jest wszystko, co znajduje się między miejscem w pliku, w którym aktualnie się znajdujemy a najbliższą spacją. Odczytany tekst jest traktowany jako typ double, int lub string w zależności od typu użytej zmiennej.

Odczytywanie pliku znak po znaku

Ta metoda polega na odczytywaniu z pliku po jednym znaku i jako jedyna z wszystkich opisanych jest dla nas całkiem nowa. Ale oczywiście tak jak poprzednie, również łatwo ją opanować.

char a;
mojStrumien.get(a);

Powyższy kod odczyta jeden znak i zapisze go w zmiennej a.

9.2.2. Odczytywanie całego pliku

Często trzeba odczytać całą zawartość pliku. Na razie pokazałem Ci jak odczytywać dane z plików, ale jeszcze nie wyjaśniłem, jak zakończyć wczytywanie, gdy dojdzie się do końca pliku.

Aby dowiedzieć się czy można kontynuować odczytywanie danych, należy użyć wartości zwrotnej funkcji getline(). Funkcja ta nie tylko wczytuje linie zawartości plików, ale dodatkowo zwraca wartość logiczną informującą, czy można kontynuować odczyt. Zatem jeśli funkcja ta zwróci wartość true, to wiadomo, że można kontynuować odczytywanie. Jeśli natomiast zwróci false, jest to znak, że został osiągnięty koniec pliku albo wystąpił błąd. W obu tych przypadkach należy zakończyć operację odczytu.

Pamiętasz jeszcze pętle? Użyjemy jednej z nich, aby odczytać zawartość pliku do samego końca. Najlepiej nadaje się do tego pętla while:

#include <iostream>
#include <fstream>
#include <string>
using namespace std; 

int main()
{
    ifstream plik("C:/Nanoc/plik.txt");

    if(plik)
    {
         // Udało się otworzyć plik, a więc można rozpocząć odczytywanie

        string linia;     // Zmienna do przechowywania odczytanych wierszy tekstu

        while(getline(plik, linia))    // Jeśli jeszcze nie nastąpił koniec pliku, czytamy dalej
        {

            cout << linia << endl; // Wyświetlamy odczytany tekst w konsoli
                                   // Można też zrobić z nim coś innego
        }
    }
    else
    {
        cout << "BŁĄD: nie można otworzyć pliku do odczytu." << endl;
    }

    return 0;
}

Gdy wczytamy linię tekstu, możemy z nią zrobić, co tylko nam się podoba. Ten program tylko wyświetla po kolei każdy wiersz treści, ale w prawdziwym programie byłyby one używane w inny sposób. Jedynym ograniczeniem jest Twoja wyobraźnia.

Powyższa metoda odczytywania zawartości plików jest stosowana najczęściej ze wszystkich. Po wczytaniu linii tekstu do łańcucha znaków można na niej pracować przy użyciu funkcji operujących na łańcuchach.

9.3. Kilka sztuczek

Musisz poznać jeszcze tylko kilka sztuczek i będziesz wiedział już wszystko, co trzeba wiedzieć o technikach pracy z plikami.

9.3.1. Wczesne zamykanie pliku

Na początku tego rozdziału napisałem, jak otworzyć plik, ale jeszcze do tej pory nie wyjaśniłem, jak go potem zamknąć. Nie zapomniałem o tym, po prostu robienie tego nie jest konieczne. Otwarte pliki są automatycznie zamykane, gdy program kończy wykonywanie bloku, w którym zostały utworzone ich strumienie.

void f()
{
    ofstream strumien("C:/Nanoc/data.txt");   // Plik jest otwarty

    // Operacje na pliku

}   // Koniec bloku, a więc w tym miejscu plik zostanie automatycznie zamknięty

Nie trzeba nic robić, aby zamknąć plik. I całe szczęście, bo dzięki temu nigdy o tym nie zapomnimy.

Czasami jednak trzeba zamknąć plik wcześniej, niż powinno to nastąpić automatycznie. W takim przypadku należy użyć funkcji close().

void f()
{
    ofstream strumien("C:/Nanoc/data.txt");   // Plik jest otwarty

    // Operacje na pliku

    strumien.close(); // Zamknięcie pliku
                  // Od tego miejsca nie można dokonywać zapisu w pliku
}

Można też zadeklarować strumień, ale otwarcie pliku odłożyć na później. Służy do tego funkcja open().

void f()
{
    ofstream strumien;   // Strumień bez przypisanego pliku

    strumien.open("C:/Nanoc/data.txt");  // Otwarcie pliku C:/Nanoc/data.txt

    // Operacje na pliku

    flux.close(); // Zamknięcie pliku
                  // Od tego miejsca nie można dokonywać zapisu w pliku
}

Pragnę podkreślić, że wykonywanie powyższych czynności jest rzadko konieczne. Zazwyczaj wystarczy bezpośrednie otwarcie pliku i pozostawienie go do automatycznego zamknięcia.

9.3.2. Kursor w pliku

Na chwilę zagłębimy się trochę bardziej w szczegóły techniczne, aby dokładnie poznać mechanizm odczytywania zawartości plików. Gdy otworzymy plik w edytorze tekstu, np. Notatniku, znajdziemy w nim kursor wskazujący miejsce, w którym aktualnie się znajdujemy. Na poniższym rysunku kursor znajduje się za drugim „s” w słowie „Issac” w czwartym wierszu.

Kursor w Notatniku
Kursor w Notatniku

Gdy naciśniesz jakiś klawisz na klawiaturze, w miejscu, w którym znajduje się teraz kursor pojawi się nowa litera. Analogicznie w języku C++ istnieje odpowiednik kursora, za pomocą którego można wybrać miejsce do wpisywania danych. Spójrz na poniższy kod:

ifstream plik("C:/Nanoc/scores.txt")

Instrukcja ta spowoduje otwarcie pliku C:/Nanoc/scores.txt i umieszczenie kursora na samym jego początku. Gdy wczytamy pierwsze słowo z tego pliku, otrzymamy łańcuch znaków Nanoc:. Po tej operacji „kursor C++” przesunie się na początek następnego słowa, w miejsce pokazane na poniższym rysunku:

Przesunięcie kursora w Notatniku
Przesunięcie kursora w Notatniku

Następnym wczytanym słowem będzie 118218 itd. Oznacza to, że w ten sposób zawartość pliku można przeglądać tylko sekwencyjnie. To nie jest zbyt praktyczne.

Na szczęście istnieją sposoby na zmienianie pozycji kursora w pliku. Można na przykład przesunąć go do 20 miejsca od początku pliku albo o 32 znaki do przodu względem bieżącego położenia. Dzięki temu można odczytać dokładnie te fragmenty pliku, które nas interesują.

Aby to jednak zrobić, najpierw trzeba dowiedzieć się, w którym miejscu pliku aktualnie się znajdujemy. Dopiero potem możemy przesunąć kursor. Poniżej dowiesz się, jak to zrobić.

9.3.3. Sprawdzanie pozycji kursora w pliku

Istnieją specjalne funkcje do sprawdzania, który bajt w pliku wskazuje aktualnie kursor. Inaczej mówiąc funkcje te pozwalają sprawdzić, na którym znaku w danej chwili jest kursor. Piszę funkcje, nie funkcja, ponieważ są ich dwie: po jednej dla strumienia wejściowego i wyjściowego. Niestety ich nazwy brzmią dość dziwnie: tellg() (dla strumienia ifstream) i tellp() (dla strumienia ofstream). Na szczęście używa się ich tak samo.

ofstream plik("C:/Nanoc/data.txt");

int pozycja = plik.tellp(); // Sprawdzamy pozycję

cout << "Jesteśmy na znaku nr " << pozycja << " w pliku." << endl;

9.3.4. Przesuwanie kursora

Do przesuwania kursora również służą dwie funkcje: seekg() (dla strumienia ifstream) i seekp() (dla strumienia ofstream).

Tych funkcji również używa się tak samo, a więc poniżej przedstawiam przykład użycia tylko jednej z nich:

strumien.seekp(liczbaZnakow, pozycja);

Parametr pozycja może przyjmować jedną z trzech wartości:

  • ios::beg — początek pliku
  • ios::end — koniec pliku
  • ios::cur — bieżąca pozycja

Aby na przykład ustawić się dziesięć znaków za początkiem pliku, należy napisać instrukcję strumien.seekp(10, ios::beg);. A żeby przesunąć kursor o 20 znaków dalej od miejsca, w którym się on aktualnie znajduje, należy napisać strumien.seekp(20, ios::cur);. Chyba proste, prawda?

9.3.5. Sprawdzanie długości pliku

Aby sprawdzić długość pliku, należy wykorzystać wiedzę zdobytą w dwóch poprzednich podrozdziałach, tzn. najpierw trzeba przenieść kursor na koniec pliku, a następnie „spytać” strumień, gdzie jest kursor:

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    ifstream plik("C:/Nanoc/najlepszeWyniki.txt"); // Otwarcie pliku
    plik.seekg(0, ios::end); // Przejście na koniec pliku

    int dlugosc;
    dlugosc = plik.tellg();  // Sprawdzamy pozycję, która odpowiada długości pliku!

    cout << "Długość pliku w bajtach wynosi: " << dlugosc << "." << endl;

    return 0;
}

To wszystko, jeśli chodzi o podstawowe wiadomości. Możesz zacząć zgłębiać dalsze tajniki świata plików i języków programowania.

Po przyswojeniu materiału z tego rozdziału będziesz gotowy do rozpoczęcia poznawania bardziej zaawansowanych zagadnień i realizacji poważnych projektów. Dobrze się składa, bo w następnym rozdziale będziesz musiał wykonać praktyczny projekt, do którego realizacji będzie potrzebne wszystko to, czego nauczyłeś się do tej pory.

Zanim przejdziesz do tego projektu, przejrzyj jeszcze raz wszystkie zagadnienia, które do tej pory sprawiły Ci najwięcej problemów.

Autor: Mathieu Nebra i Matthieu Schaller

Źródło: http://openclassrooms.com/courses/programmez-avec-le-langage-c/lire-et-modifier-des-fichiers

Tłumaczenie: Łukasz Piwko

Treść tej strony jest dostępna na zasadach licencji CC BY-NC-SA 2.0

3 komentarze do “Rozdział 9. Odczytywanie i zapisywanie plików”

  1. Można przeskoczyć „kursorem” do następnej linii zapisanego pliku, a nie na następną literę? Jeśli tak to w jaki sposób?

    Pozdrawiam.

Możliwość komentowania została wyłączona.