Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Piotr Szawdyński, pekfos
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. Ten sam zestaw metod dostępny jest również w innych strumieniach wejściowych, na przykład w std::cin, więc metody fail() i clear() powinny być Ci już znane. Informacje o błędzie w strumieniu zapisywane są w trzech flagach: eofbit, failbit, badbit.

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. Na Twoim poziomie wiedzy flagi mógłbyś wyobrazić sobie tak:
C/C++
bool failbit = false;
bool badbit = false;
bool eofbit = false;
Znaczenie tych flag opisuje poniższa tabela:
flagaopis
failbitOperacja zakończyła się błędem. Przykładowo: nie udało otworzyć się pliku, napotkano błędne dane wejściowe (oczekiwano liczby, napotkano litery)
badbitWystąpił poważny błąd. Na przykład napotkano bad sector na dysku, albo urządzenie wejściowe przestało być dostępne. Różni się od zwykłego błędu tym, że po wystąpieniu poważnego błędu strumień nie musi być zdatny do użycia.
eofbitNapotkano koniec pliku.

Metody good(), bad(), fail(), eof()

Więc co dokładnie zwracają te cztery metody?
  • good() zwraca informację, czy żadna flaga błędu nie jest ustawiona,
  • bad() zwraca informację, czy badbit jest ustawiony,
  • fail() zwraca informację, czy failbit, lub badbit jest ustawiony,
  • eof() zwraca informację, czy eofbit jest ustawiony.
Pamiętaj, że good() nie jest ani negacją, ani zamiennikiem fail(). Nigdy nie używaj good() do sprawdzania, czy nie wystąpił błąd odczytu. Jeśli napiszesz pętlę czytającą liczby z pliku i warunek pętli będzie oparty o metodę good(), lub eof(), to ta pętla nie zawsze będzie działać poprawnie. Osiągnięcie końca pliku w trakcie wczytywania liczby nie oznacza, że ta operacja zakończy się błędem!

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. Nie ma sensu rozważać programu do odczytywania danych z pliku, bez wcześniejszego zdefiniowania, jak plik ma wyglądać. Format danych, w jakimś stopniu, zawsze musi być zdefiniowany. Załóżmy plik tekstowy, w którym występują liczby całkowite, oraz inne znaki. Na przykład:
1 2 B 34c -2f
x86 1024.
Celem jest wypisanie każdej napotkanej liczby i każdego napotkanego znaku.
C/C++
#include <iostream>
#include <fstream>
#include <string>


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( true )
    {
        int iLiczba;
        char cZnak;
       
        plik >> iLiczba;
       
        if( !plik.fail() )
             std::cout << "Liczba = " << iLiczba << std::endl;
        else
        {
            if( plik.bad() )
                 return false; //wczytanie liczby nie powiodło się z powodu poważnego błędu
           
            plik.clear();
            plik >> cZnak;
           
            if( !plik.fail() )
                 std::cout << "Znak = '" << cZnak << "'" << std::endl;
            else
            {
                if( plik.eof() )
                     break;
               
                return false; // wczytanie znaku powinno zawsze się udać, chyba że skończyły się dane
            } //if
        } //if
    } //while
   
    std::cout << "Koniec pliku" << std::endl;
    return true;
}


int main()
{
    if( odczytajPlik( "cpp0x.txt" ) )
         std::cout << "Plik zostal poprawnie wczytany!" << std::endl;
   
    return 0;
}
Liczba = 1
Liczba = 2
Znak = 'B'
Liczba = 34
Znak = 'c'
Liczba = -2
Znak = 'f'
Znak = 'x'
Liczba = 86
Liczba = 1024
Znak = '.'
Koniec pliku
Plik zostal poprawnie wczytany!

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() )
             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() )
             return false; //wystąpił błąd odczytu danych
       
        if( cZnak == '\n' )
             return true;
       
    } //for
}
Powyższej funkcji nie wolno modyfikować.
Poprzedni dokument Następny dokument
Wczytywanie danych z pliku za pomocą operatora >> Poruszanie się po pliku w trybie do odczytu