« Wczytywanie zawartości pliku, a kontrola błędów, lekcja »
Rozdział 34. Omówienie dostępnych mechanizmów do kontroli błędów podczas wczytywania zawartości pliku oraz przedstawienie kilku sposobów na radzenie sobie z nimi. (lekcja)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
Autor: Piotr Szawdyński
Kurs C++

Wczytywanie zawartości pliku, a kontrola błędów

[lekcja] Rozdział 34. Omówienie dostępnych mechanizmów do kontroli błędów podczas wczytywania zawartości pliku oraz przedstawienie kilku sposobów na radzenie sobie z nimi.

Wprowadzenie

Najtrudniejszym elementem w dziedzinie wytwarzania oprogramowania jest zapewnienie właściwej obsługi błędów w powstającej aplikacji. Programista tworzący kod musi bowiem z pełną świadomością pisać każdą linijkę kodu, zadając sobie przy tym różne pytania związane z jego bezpieczeństwem. Oto kilka przykładowych pytań pojawiających się każdego dnia w pracy programisty:
  • "Czy tworzony fragment kodu może zadziałać źle dla skrajnie niepoprawnych danych wejściowych?";
  • "Jak powinien zachować się fragment kodu w przypadku niepoprawnych danych?";
  • "Czy analizowany fragment kodu prawidłowo obsłuży wszystkie nieoczekiwane błędy?".
Z czasem pojawią się również inne pytania:
  • "Jak pisać bezpieczny kod?";
  • "Jak zabezpieczyć się przed występowaniem błędów w kodzie?";
  • "Czy da się automatycznie testować poprawność działania kodu?".
Choć powyższe pytania pozostaną na chwilę obecną bez odpowiedzi, to mimo wszystko warto zacząć zwracać szczególną uwagę na wszelkie problemy związane z bezpieczeństwem i obsługą błędów. Pomimo, iż aspekty związane z wytwarzaniem bezpiecznego kodu są miejscami bardzo ciekawe, to teraz musimy wrócić do właściwej części rozdziału. W niniejszej lekcji skoncentrujemy się na zagadnieniach związanych z obsługiwaniem błędów, jakie mogą się pojawić podczas pracy z plikami.

Pliki, a narzędzia do kontroli błędów

Pracując z plikami w trybie do odczytu kontroluje się przede wszystkim dane, które się z nich odczytuje. W idealnym przypadku dane w plikach są zawsze poprawne, ponieważ pliki używane przez własne programy zazwyczaj są stworzone przez nas samych. Problem zaczyna się pojawiać wtedy, gdy użytkownik aplikacji będzie mógł samodzielnie przygotować plik z danymi, a te z kolei mogą okazać się niekoniecznie poprawne dla Twojego programu. Z pomocą przychodzą nam wówczas takie metody jak eof, good, bad, fail oraz clear, należące do znanej już Ci klasy ifstream.

Metoda ifstream::eof

Metoda eof, należąca do klasy std::ifstream zwraca prawdę, jeżeli ostatnio wykonana operacja odczytu danych została zakończona z powodu osiągnięcia końca pliku. Informację tą bardzo często wykorzystuje się wtedy, gdy chcemy odczytać zawartość całego pliku nie wiedząc ile danych się w nim znajduje. Przykład:
C/C++
while( !plik.eof() )
{
    std::string sWiersz;
    std::getline( plik, sWiersz ); //odczytujemy wiersz z pliku
}

Metoda ifstream::bad

Metoda bad należy również do klasy std::ifstream. Zwraca ona prawdę, jeżeli ostatnio wykonana operacja odczytu danych zakończy się niepowodzeniem z powodu wystąpienia błędu sprzętowego. Przez błąd sprzętowy należy rozumieć pojawienie się badsectorów na dysku, niedostępność urządzenia na którym znajdował się otwarty plik (np. pendrive lub dysk sieciowy) lub zabranie dostępu do pliku przez inny proces. Przykład:
C/C++
bool odczytajPlik( std::string sNazwaPliku )
{
    std::ifstream plik;
    plik.open( sNazwaPliku.c_str() );
    if( !plik.good() )
         return false; //Nie udało się otworzyć pliku
   
    while( !plik.eof() )
    {
        std::string sWiersz;
        std::getline( plik, sWiersz );
        if( plik.bad() ) //podczas próby odczytania danych wystąpił błąd sprzętowy
        {
            plik.close();
            return false; //wychodzimy z funkcji i informujemy, że odczytanie pliku zakończyło się niepowodzeniem
        }
        std::cout << sWiersz << std::endl;
    }
    plik.close();
    return true;
}

Metoda ifstream::fail

Metoda fail zwróci prawdę, gdy odczytanie danych zakończy się niepowodzeniem z powodu wystąpienia błędu sprzętowego lub z powodu błędu logicznego jaki miał miejsce podczas odczytu danych. Przez błąd logiczny odczytu danych należy rozumieć sytuację, w której aplikacja została zaprogramowana tak, aby odczytywała liczby, natomiast w pliku znalazły się również inne znaki np. nieoczekiwane litery alfabetu bądź znaki specjalne.
C/C++
int iLiczba;
plik >> iLiczba;
if( plik.fail() )
     std::cout << "Nie udalo sie wczytac liczby!" << std::endl;

Metoda ifstream::good

Metoda good zwraca prawdę wtedy, gdy strumień danych jest w prawidłowym stanie tj. żaden błąd nie wystąpił podczas pracy z plikiem. Wspomnianą metodę warto wykorzystywać przede wszystkim do sprawdzania, czy otwarcie pliku zakończyło się powodzeniem. Przykład:
C/C++
std::ifstream plik;
plik.open( "plik.txt" );
if( plik.good() )
     std::cout << "Plik zostal otwarty" << std::endl;
else
     std::cout << "Nie udalo sie otworzyc pliku" << std::endl;

Metoda ifstream::clear

Funkcje jak również metody do odczytu danych z pliku działają tylko wtedy, gdy strumień danych jest w poprawnym stanie. Jeżeli w trakcie pracy z plikiem wystąpił jakikolwiek błąd (tj. napotkano koniec pliku, wystąpił błąd logiczny lub sprzętowy, albo nie udało się otworzyć pliku do odczytu), to wówczas kontynuowanie pracy z obiektem std::ifstream nie będzie możliwe. Aby móc wznowić pracę ze strumieniem, należy skorzystać z metody clear, która czyści tzw. flagi błędów. Przykład:
C/C++
while( !plik.eof() )
{
    int iLiczba;
    plik >> iLiczba; //Wczytujemy liczbę
    if( plik.fail() )
    {
        std::cout << "Nie udalo sie wczytac liczby!" << std::endl;
        plik.clear(); //Czyścimy flagi błędów
        char cZnak;
        plik >> cZnak; //Wczytujemy znak
        if( plik.fail() )
             break; //Nie udało się wczytać znaku - wychodzimy z pętli (jeden znak zawsze powinno dać się odczytać jeżeli plik działa prawidłowo i nie napotkaliśmy końca pliku)
        else
             std::cout << "Napotkano znak '" << cZnak << "'" << std::endl;
       
    } else
         std::cout << "Liczba = " << iLiczba << std::endl;
   
}

Flagi w programowaniu

Czym są flagi błędów?

Flagi są to w dużym uproszczeniu zmienne, które przechowują informacje o stanie obiektu. Jedna flaga opisuje jeden stan i może przyjmować tylko dwie wartości tj. zero lub jeden. Wartość 'jeden' można utożsamiać z wartością true, choć znacznie częściej mówi się, że flaga jest ustawiona. Analogicznie jest z wartością 'zero', którą utożsamia się z wartością false i wówczas mówi się, że dana flaga nie jest ustawiona. Flagi błędów można więc wyobrazić sobie jako zmienne, które przechowują informacje o błędach jakie wystąpiły podczas używania pliku. Flagi błędów już poznałeś, jednak nie zostały one do tej pory tak nazwane. Jedna flaga błędów przechowuje jedną informację - np. czy napotkano koniec pliku podczas odczytywania danych czy też nie. Kolejna flaga błędów zawiera informację o tym, czy wystąpił błąd sprzętowy podczas odczytu pliku. Trzecia flaga i czwarta flaga również jest Ci już znana i jak zapewne się domyślasz dotyczy ona metod fail oraz good. Na Twoim poziomie wiedzy flagi mógłbyś wyobrazić sobie tak:
C/C++
bool bFlagaFail = false;
bool bFlagaBad = false;
bool bFlagaEOF = false;
bool bFlagaGood = true;
Tym samym, w przypadku napotkania końca pliku podczas odczytywania danych, ustawiana jest flaga bFlagaEOF = true, dzięki czemu możemy później poznać przyczynę zakończenia odczytywania danych z pliku.

Czyszczenie flag błędów

Przez czyszczenie flag błędów należy rozumieć przywracanie ich wartości do stanu domyślnego. Stanem domyślnym nie musi być wcale wartość false, czego najlepszym przykładem jest flaga informująca o tym, czy strumień ifstream znajduje się w poprawnym stanie (czyli omówiona wcześniej metoda good).

Flagi, a ich reprezentacja w pamięci

Bardzo ważną cechą flag jest to, że można ich stan przechowywać na jednym bicie. Wspomnianą własność wykorzystuje się do zapisywania w jednej zmiennej kilku różnych informacji, dzięki czemu aplikacja zajmuje mniej miejsca w pamięci. Widząc pojęcie flag w kontekście programowania powinieneś wiedzieć, że chodzi o jedną zmienną, która przechowuje wiele flag, gdzie każda flaga oznacza jedną, konkretną informację. Przykładowo zmienna typu char zajmuje jeden bajt, a bajt składa się z 8 bitów, więc na jednym bajcie można przechować aż 8 flag. Do tego celu wykorzystuje się operacje bitowe, które w odpowiednim momencie zostaną omówione, jednak na chwilę obecną na tym się kończy teoria związana z flagami. 

Stosowane techniki obsługi błędów

Poznałeś już narzędzia umożliwiające kontrolowanie poprawności wczytywanych danych. Warto również w tym rozdziale dowiedzieć się czegoś więcej, tj. jak wykorzystuje się poznane mechanizmy do kontroli błędów. Generalnie rzecz biorąc można wyróżnić trzy podejścia obsługi plików:
  • Jeżeli podczas wczytywania pliku napotkamy jakikolwiek błąd to kończymy wczytywanie plików i informujemy użytkownika o niepowodzeniu (np. "błędny format danych pliku - wczytywanie danych zakończyło się niepowodzeniem");
  • Jeżeli wczytanie pliku zakończyło się niepowodzeniem, to informujemy użytkownika o miejscu wystąpienia błędu w pliku (np. "wystąpił błąd w wierszu 13, kolumna 15 - oczekiwano znaku '(', a napotkano znak '{'");
  • Trzecie podejście polega na ignorowaniu błędnych wierszy z danymi i odczytywaniu wszystkiego co się da w sposób poprawny (takie podejście się przydaje w przypadku, gdy plik uległ częściowemu nadpisaniu i próbujemy odzyskać maksymalną ilość utraconych danych).
Każde z powyższych podejść ma swoje zalety i wady. Najczęściej spotykanym rozwiązaniem jest podejście pierwsze tj. albo się uda wczytać wszystkie dane albo użytkownik jest informowany o wystąpieniu błędu i wówczas żadne dane nie są wyświetlane na ekranie. Takie rozwiązanie jest praktyczne i najmniej czasochłonne, więc do większości pisanych aplikacji warto stosować właśnie to podejście.

Przykład

Czas podsumować wiedzę zdobytą w niniejszym rozdziale w postaci trochę większego programu. Oto on:
C/C++
#include <string>
#include <fstream>
#include <iostream>

bool wczytajLiczbe( std::ifstream & plik, int & iLiczba )
{
    plik.clear(); //Wyczyszczenie ewentualnych flag błędów
    plik >> iLiczba;
    if( plik.bad() )
    {
        std::cout << "Wystapil blad sprzetowy!" << std::endl;
        plik.close();
        return false;
    } else
    if( plik.fail() )
    {
        std::cout << "Nie udalo sie wczytac liczby!" << std::endl;
        return false;
    } else
         std::cout << "Liczba = " << iLiczba << std::endl;
   
    return true;
}

bool wczytajZnak( std::ifstream & plik, char & cZnak )
{
    plik.clear(); //Wyczyszczenie ewentualnych flag błędów
    plik >> cZnak;
    if( plik.bad() )
    {
        std::cout << "Wystapil blad sprzetowy!" << std::endl;
        plik.close();
        return false;
    } else
    if( plik.fail() )
    {
        std::cout << "Nie udalo sie wczytac znaku!" << std::endl;
        return false;
    } //if
    return true;
}

bool odczytajPlik( std::string sNazwaPliku )
{
    std::ifstream plik;
    plik.open( sNazwaPliku.c_str() );
    if( !plik.good() )
    {
        std::cout << "Nie udalo sie otworzyc pliku." << std::endl;
        return false;
    } //if
    while( !plik.eof() )
    {
        int iLiczba;
        char cZnak;
       
        if( !wczytajLiczbe( plik, iLiczba ) && plik.bad() )
             return false; //wczytanie liczby nie powiodło się z powodu błędu sprzętowego
        else
        if( !wczytajZnak( plik, cZnak ) )
        {
            if( plik.bad() )
                 return false; //wczytanie znaku nie powiodło się z powodu błędu sprzętowego
            else
                 break; //nie ma więcej danych w strumieniu (bo jeden znak zawsze powinno się dać odczytać)
           
        } //if
        std::cout << "Napotkany znak = '" << cZnak << "'" << std::endl;
    } //while
    plik.close();
    return true;
}

int main()
{
    if( odczytajPlik( "cpp0x.txt" ) )
         std::cout << "Plik zostal wczytany!" << std::endl;
   
    return 0;
}

Podsumowanie

Zagadnieniom związanym z wykrywaniem i obsługą błędów poświęcono wbrew pozorom dużo książek oraz publikacji, które stanowią całkiem spory obszar w dziedzinie informatyki. U podstaw tworzenia bezpiecznego kodu leży jednak przede wszystkim świadomość, doświadczenie i umiejętności analityczne programisty. Wiedz, że każdy programista doskonali swoje umiejętności latami poprzez okazjonalne czytanie książek i przede wszystkim poprzez pisanie setek tysięcy linii kodu, podczas realizacji wymyślonych przez siebie (bądź zleconych) projektów. Pamiętaj, że zdobyte przez Ciebie doświadczenie, jak również zaangażowanie w realizowane projekty będą kluczowymi elementami, wpływającymi pozytywnie na jakość kodu, a tym samym na jego odporność na błędy.

Zadanie domowe

1. Napisz program, który wczyta z pliku liczby całkowite i wypisze je na ekranie. Wszelkie nieprawidłowe znaki mają zostać pominięte. Program ma wypisać również sumę wszystkich wczytanych liczb. Przykładowa zawartość pliku z danymi:
a 1 2 321b9 ac.de ef#@g 5 #3
Oczekiwane standardowe wyjście programu dla przykładowego zestawu danych:
1 2 321 9 5 3
Suma liczb wynosi: 341
2. [trudne zadanie] Napisz program, który dla każdego wiersza w pliku:
  • wczyta liczby i wypisze ich sumę w przypadku, gdy wszystkie liczby uda się wczytać;
  • wypisze komunikat o błędnych danych, jeżeli wystąpi błąd podczas wczytywania liczb (komunikat ma wyświetlać numer wiersza, w którym wystąpił błąd).
Przykładowe dane:
1 2 3
3 4 a 5
3 2
5 2 2 1 3
# 3 4
Oczekiwane standardowe wyjście programu dla przykładowego zestawu danych:
6
Bledne dane w wierszu nr 2!
5
13
Bledne dane w wierszu nr 5!
Wykorzystaj poniższą funkcję do wykrywania znaku przejścia do nowej linii:
C/C++
bool czyNapotkanoZnakNowegoWiersza( std::ifstream & plik )
{
    char cZnak;
    for(;; ) //nieskończona pętla
    {
        plik.clear();
        cZnak = plik.peek(); //sprawdzamy jaki kolejny znak zostanie zwrócony przez operację odczytu
        if( plik.fail() || plik.bad() )
             return false; //wystąpił błąd odczytu danych
       
        if( !isspace( cZnak ) )
             return false; //pobrany znak nie jest białym znakiem
       
        plik.get( cZnak ); //odczytujemy biały znak z pliku
        if( plik.fail() || plik.bad() )
             return false; //wystąpił błąd odczytu danych
       
        if( cZnak == '\n' )
             return true;
       
    } //for
}
Powyższej funkcji nie wolno modyfikować.
Poprzedni dokumentNastępny dokument
Wczytywanie danych z pliku za pomocą operatora >>Poruszanie się po pliku w trybie do odczytu