Podręcznik

3. Pliki tekstowe

Wczytywanie danych z pliku odbywa się w sposób analogiczny jak w przypadku strumieniowej obsługi strumienia wejścia oraz wyjścia konsoli. W przypadku obsługi konsoli wykorzystywaliśmy bibliotekę iostream, natomiast w przypadku obsługi plików tekstowych wykorzystywana jest biblioteka fstream

W tej bibliotece znajdziemy dwie główne klasy do obsługi plików:

  1. std::ifstream – służy do strumieniowego odczytu z plików.
  2. std::ofstream – służy do strumieniowego zapisu do plików.

Do otwarcia pliku używamy obiektu jednej z klas strumieniowych i metody open(), podając jako argument nazwę pliku oraz (opcjonalnie) tryb otwarcia. 

ifstream plk_we; // zmienna plk_we określa plik do odczytu
plk_we.open("dane.txt");
Jako parametr metody open podawana jest ścieżka dostępu do pliku. Jeżeli nie podamy pełnej ścieżki dostępu do pliku, a jedynie jego nazwę z rozszerzeniem to musi ona być umieszczona w katalogu roboczym. 

Domyślnie katalog roboczy to:
  • Katalog, w którym znajduje się plik wykonywalny, jeśli uruchamiasz program bezpośrednio.
  • Katalog, z którego uruchamiasz program z terminala lub konsoli.
Aby upewnić się, w jakim katalogu program szuka plików, można sprawdzić katalog roboczy w kodzie:


#include <iostream>
#include <filesystem> // C++17

int main() {
    std::cout << std::filesystem::current_path() << std::endl;
    return 0;
}
    
Funkcja std::filesystem::current_path() (dostępna od C++17) zwróci aktualny katalog roboczy, co pozwoli zobaczyć, skąd program próbuje otworzyć plik.
Przy wpisywaniu ścieżek dostępu należy być bardzo ostrożnym. W stałych napisowych w C++ znak odwróconego ukośnika (backslash) jest znakiem specjalnym, więc jeśli chcecie uzyskać go jako znak, musi wystąpić dwa razy. Tak więc ścieżkę C:\katalog\plik w kodzie C++ zapisujemy "C:\\katalog\\plik". Możemy także zastosować zapis "C:/katalog/plik".
Aby funkcja działała w sposób prawidłowy nalęzy upewnić się że plik istnieje i jest możliwy jego odczyt. W tym celu możemy wykorzystać dwie metody good() lub nowszej i bardziej uniwersalnej is_open().

int main() {
…
    dane.open (... );
    if ( !dane.is_open() ) { // jeśli nie ma podanego pliku
        cout << " Blad otwarcia pliku" << endl ;
        return 1; // błędne zakończenie pracy programu
    }
    // dalej działania na plikach
    // i cala reszta programu

    return 0; // poprawne zakończenie pracy programu
}
Metoda is_open() służy do sprawdzenia, czy plik został poprawnie otwarty, zwracając true, jeśli plik jest otwarty, a false, jeśli otwarcie się nie powiodło. Nie monitoruje ona stanu operacji odczytu czy zapisu. Z kolei metoda good() ocenia ogólny stan strumienia, sprawdzając, czy wszystkie operacje przebiegały bez błędów, w tym odczyt i zapis, oraz czy strumień nadal działa poprawnie. Metoda good() wykrywa błędy takie jak koniec pliku lub problemy z odczytem, a is_open() dotyczy wyłącznie poprawności otwarcia pliku. Nazwa pliku otwieranego do odczytu lub zapisu może być wczytywana, czyli może byc zmienną, ale w tym szczególnym przypadku zmienna ta nie może być typu string. W przypadku straszych wersji C++ (np. C++98 lub C++03), funkcje takie jak open() w klasie std::ifstream czy std::ofstream wymagały podania ścieżki pliku w postaci wskaźnika do tablicy znaków typu const char*. W tych starszych wersjach C++, obiekty typu std::string nie były automatycznie konwertowane na tablice znaków, dlatego konwersja przez c_str() była potrzebna. Przykład w starszych wersjach C++:
std::string nazpl = "dane.txt";
std::ifstream dane;
dane.open(nazpl.c_str()); // Konwersja std::string na const char* jest konieczna
Od C++11, standard został zaktualizowany, aby funkcje open() przyjmowały również argumenty typu std::string, dzięki czemu wywołanie c_str() nie jest już konieczne. Można bezpośrednio przekazywać obiekt typu std::string do metody open().
std::string nazpl = "dane.txt";
std::ifstream dane;
dane.open(nazpl); // Nie trzeba używać c_str() w C++11 i nowszych
Warto wiedzieć, że możemy wybrać różne tryby otwarcia pliku, wykorzystując flagi std::ios. Najczęściej używane tryby to:
  • std::ios::in – otwarcie pliku do odczytu,
  • std::ios::out – otwarcie pliku do zapisu (usuwa poprzednią zawartość pliku),
  • std::ios::app – otwarcie pliku w trybie dopisywania (dodawanie danych na końcu pliku),
  • std::ios::binary – otwarcie pliku w trybie binarnym,
  • std::ios::ate – otwarcie pliku i ustawienie wskaźnika na koniec.
Przykład otwarcia pliku w trybie binarnym i dopisywania:
dane.open("nazwa_pliku.bin", std::ios::binary | std::ios::app);  
Po otwarciu pliku możemy zacząć z niego korzystać. Po zakończeniu zaś pracy z plikiem, musicie go zamknąć, czyli wywołać metodę close().
dane.close();
Dane z pliku odczytywane są w sposób strumieniowy, analogicznie jak w przypadku odczytu danych z klawiatury.zmienna_plikowa >> zmienna_1 >> zmienna_2 >> ... >> zmienna_n;
Ze strumienia można wczytywać bezproblemowo zmienne typów prostych, przy czym zawsze wykonana zostanie odpowiednia konwersja i rzutowanie. 

Często istnieje konieczność sprawdzenia czy wczytany został znak końca pliku. W tym celu możemy wykorzystać metodę eof(), która sprawdza, czy osiągnięto koniec pliku (ang. end of file). Metodę możemy wykorzystać w połączeniu z pętlą while w celu wczytywania danych z pliku do momentu, gdy zostaną wczytane wszystkie dane.
while (!plk.eof())
    // dopóki nie napotkano końca pliku, wykonuj
    
W programie tym wczytywaliśmy plik znak po znaku. Można także korzystać z innych metod odczytu - bezpośredniego pobierania całego wiersza do bufora. Można nawet (w zadaniu tak zdefiniowanym jak powyższy przykład) w ogóle nie odczytywać jawnie danych, tylko w odpowiedni sposób powiązać ze sobą bufory pliku i ekranu - a wyświetlanie zawartości wykona się "samo ". Jednakże to już jest wyższa szkoła jazdy, i wymaga programowania obiektowego. Tu wspomnimy jedynie o możliwości odczytu całej linii naraz za pomocą instrukcji getline. Pamiętamy z początku tego rozdziału, że funkcja getline przyjmuje jako argument zmienną typu string. Wywołując tę funkcję wczytujemy całą linię (aż do napotkania znaku końca linii) do zmiennej przekazanej jako drugi parametr. Jeśli wiec wczytujemy całą linię z pliku, to pierwszym parametrem jest plik, z którego dane odczytujemy. Tę funkcję pokażemy w ostatnim przykładzie z tej lekcji, w następnym podrozdziale. Sposób jej wywołania jest następujący:
// zmienna, do której wczytujemy linię
string linia;
...
// Nasza zmienna plikowa nazywa się plk
getline(plk, linia);
Zapisywanie danych do pliku w C++ odbywa się przy użyciu klasy std::ofstream, która jest częścią biblioteki <fstream>. Aby zapisać dane, należy utworzyć obiekt std::ofstream i otworzyć plik, podając jego nazwę. Jeśli plik nie istnieje, zostanie utworzony, a jeśli istnieje, jego zawartość zostanie nadpisana (chyba że użyjemy trybu dopisywania std::ios::app).
std::ofstream plk;
plk.open("plik.txt");
Po otwarciu pliku, dane można zapisywać za pomocą operatora <<, podobnie jak przy wypisywaniu na konsolę.
plk << "Wartosc zmiennej: " << zmienna << endl;
Na koniec, po zakończeniu operacji zapisu, należy zamknąć plik, wywołując metodę close(), aby upewnić się, że wszystkie dane zostały poprawnie zapisane i zasoby zostały zwolnione.