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ć.