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:
Z czasem pojawią się również inne pytania:
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:
bool failbit = false;
bool badbit = false;
bool eofbit = false;
Znaczenie tych flag opisuje poniższa tabela:
Metody good(), bad(), fail(), eof()
Więc co dokładnie zwracają te cztery metody?
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:
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.
#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;
}
while( true )
{
int iLiczba;
char cZnak;
plik >> iLiczba;
if( !plik.fail() )
std::cout << "Liczba = " << iLiczba << std::endl;
else
{
if( plik.bad() )
return false;
plik.clear();
plik >> cZnak;
if( !plik.fail() )
std::cout << "Znak = '" << cZnak << "'" << std::endl;
else
{
if( plik.eof() )
break;
return false;
}
}
}
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:
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:
bool czyNapotkanoZnakNowegoWiersza( std::ifstream & plik )
{
char cZnak;
for(;; )
{
plik.clear();
cZnak = plik.peek();
if( plik.fail() )
return false;
if( !isspace( cZnak ) )
return false;
plik.get( cZnak );
if( plik.fail() )
return false;
if( cZnak == '\n' )
return true;
}
}
Powyższej funkcji
nie wolno modyfikować.