Biblioteka odpowiedzialna za obsługę plików
Pisząc nasze programy, prędzej czy później zajdzie potrzeba zapisywania danych na dysku. Z pomocą przychodzi tu biblioteka
fstream, dzięki której uzyskujemy funkcje pozwalające nam zarówno zapisywać pliki jak i je odczytywać.
Typ zmiennej fstream
Zanim zaczniemy odczytywać, bądź zapisywać dane z/do pliku, musimy posiadać zmienną, dzięki której będziemy mogli wykonywać operacje na wybranym pliku. W tym celu utworzona została klasa
fstream. Klasa ta jest umieszczona w przestrzeni nazw
std::. Klasa ta udostępnia nam cały interfejs, dzięki któremu będziemy mogli obsłużyć dowolny plik znajdujący się na dysku lub innym nośniku danych.
Otwieranie pliku
Zmienna, którą utworzyliśmy aktualnie nie wskazuje na żaden plik. Aby przypisać konkretny plik do zmiennej wywołujemy funkcję
open(), której definicja wygląda następująco:
void open( const char * nazwa_pliku, ios_base::openmode tryb_otwarcia_pliku );
Pierwszy parametr funkcji (
nazwa_pliku) określa ścieżkę dostępu i nazwę pliku do jakiego chcemy uzyskać dostęp. Drugi parametr funkcji, czyli
tryb_otwarcia_pliku służy do poinformowania kompilatora w jakim trybie dany plik chcemy otworzyć. Lista dostępnych trybów wraz z opisami w poniższej tabeli.
Wszystkie wymienione tryby możemy łączyć ze sobą - oznacza to, że jeśli chcemy otrzymać plik do odczytu i zapisu wystarczy oddzielić je pojedynczym operatorem
|.
std::fstream plik;
plik.open( "nazwa_pliku.txt", std::ios::in | std::ios::out );
Czy udało otworzyć się plik?
Zdecydowana większość kursów pomija ten bardzo ważny krok, jaki należy implementować we własnych kodach źródłowych - sprawdzanie czy plik został otwarty prawidłowo. Operacja jest bardzo prosta, jednak prawie zawsze zaniedbywana nawet w książkach poświęconych programowaniu!
Po wykonaniu operacji otwarcia pliku, wewnątrz klasy ustawiane są odpowiednie flagi, które informują o tym, czy otrzymaliśmy dostęp do pliku czy też nie. Funkcje, jakie umożliwiają nam sprawdzenie tego stanu to
good() oraz
is_open(). Definicja tych funkcji wygląda następująco:
bool good();
bool is_open();
Obie funkcje zwrócą wartość
true, jeśli uzyskano dostęp do pliku, w przeciwnym wypadku otrzymamy wartość
false.
std::fstream plik;
plik.open( "nazwa_pliku.txt", std::ios::in | std::ios::out );
if( plik.good() == true )
{
std::cout << "Uzyskano dostep do pliku!" << std::endl;
} else std::cout << "Dostep do pliku zostal zabroniony!" << std::endl;
Kiedy nie uzyskamy dostępu do pliku
Próba odczytu:
Próba zapisu:
Zamykanie pliku
Każdy plik należy zamykać po zakończeniu pracy z nim. Jeśli plik ma być używany tylko przez jednego użytkownika szkodliwość jest stosunkowo mała - klasa
fstream sama zamknie plik przed usunięciem zmiennej z pamięci. Jeśli natomiast zapomnisz zamknąć plik, którego dane mają być współdzielone przez kilku użytkowników, automatycznie uniemożliwisz im dostęp do tego zasobu. Funkcja odpowiedzialna na zamykanie pliku nosi nazwę
close(). Deklaracja wygląda następująco:
Przykład
Poniższy przykład pokazuje jak należy prawidłowo posługiwać się otwartym plikiem.
#include <fstream>
int main()
{
std::fstream plik;
plik.open( "nazwa_pliku.txt", std::ios::in | std::ios::out );
if( plik.good() == true )
{
plik.close();
}
return( 0 );
}
Odczytywanie danych z pliku
Jeśli uzyskamy już dostęp do pliku w trybie do odczytu, możemy rozpocząć odczytywanie danych z pliku. Język C++ oferuje więcej niż jedną metodę odczytu danych z pliku.
Pobieranie danych za pomocą strumienia
Pierwszą, a zarazem bardzo wygodną metodą odczytywania danych z pliku jest strumień. Ponieważ zapis jest analogiczny do strumienia
std::cin>>, przedstawiam tylko formę zapisu.
nazwa_zmiennej_plikowej >> zmienna_do_ktorej_dane_maja_zostac_zapisane;
Co należy wiedzieć o strumieniu:
Pobieranie danych wierszami
Kolejną metodą na odczytanie danych, to użycie funkcji
getline(). Funkcja ta została omówiona w rozdziale
XVIII. Biblioteka <string>. Przykład:
std::fstream plik( "nazwa_pliku.txt", std::ios::in );
std::string dane;
getline( plik, dane );
Istnieje również druga funkcja służąca do wczytywania danych wierszami, jednak wydaje się ona mniej wygodna w użyciu. Jest nią funkcja
getline(), zaszyta wewnątrz klasy
fstream.
istream & getline( char * odczytane_dane, streamsize ilosc_danych, char znak_konca_linii );
Parametry oznaczają kolejno:
Przykład wykorzystania tej funkcji:
std::fstream plik( "nazwa_pliku.txt", std::ios::in );
char dane[ 255 ];
plik.getline( dane, 255 );
Co należy wiedzieć o obu funkcjach
getline():
Pobieranie danych blokami
Pobieranie danych blokami jest jedną z najszybszych metod na odczytywanie danych. Co więcej jest to bezpieczna metoda dla danych binarnych (pod warunkiem włączenia trybu
ios::binary). Minusem tej metody jest niezbyt poręczna forma w jakiej otrzymujemy dane. Budowa tej funkcji wygląda następująco:
istream & read( char * bufor, streamsize rozmiar_bufora );
Pierwszym parametrem przekazywanym do funkcji jest wskaźnik do którego mają zostać wczytane dane. Drugi parametr określa rozmiar bufora. Pamiętaj, że bufor może nie być wypełniony do końca danymi. Aby sprawdzić ile bajtów danych zostało faktycznie wczytanych do bufora, należy posłużyć się tu funkcją
gcount(). Przykład:
std::fstream plik( "nazwa_pliku.txt", std::ios::in );
char bufor[ 1024 ];
plik.read( bufor, 1024 );
std::cout << "Wczytano " << plik.gcount() << " bajtów do bufora" << std::endl;
Co należy wiedzieć o funkcji
read():
Inne metody na odczytywanie danych
C++ oferuje również inne metody odczytywania danych. Nie będą one jednak tu omówione ponieważ te, które zostały poruszone w tym rozdziale są wystarczające do pełnego wykorzystywania możliwości wczytywania danych z plików.
Zapisywanie danych do pliku
Zapisywanie danych do pliku jest równie proste jak ich odczytywanie. Po otwarciu pliku do zapisu możemy korzystać z kilku technik umożliwiających zapisywanie danych. Zanim jednak je poznasz musisz zdać sobie sprawę, że dane do pliku można albo dopisywać tylko i wyłącznie na końcu pliku albo
nadpisywać dane jeśli nie jesteśmy na jego końcu. Nie można dopisywać tekstu pomiędzy istniejące dane jak to często robimy w edytorach tekstowych. Pamiętaj więc, jeśli chcesz otworzyć plik do zapisu zastanów się conajmniej dwa razy, bo tutaj błąd może kosztować nawet utratę całej zawartości pliku. Jeśli chcesz testować działanie funkcji służących do zapisu danych polecam utworzyć najpierw pusty plik i wpisać ręcznie do niego jakieś nieistotne dane lub pracować na kopii pliku, zawierającego ważne dane. W razie wykonania jakiegoś rażącego błędu będziesz mógł przywrócić szybko dane.
16.6.1. Zapisywanie danych za pomocą strumienia
Zapisywanie danych za pomocą strumienia jest analogicznym działaniem do
std::cout<<. Jedyną istotną różnicą, jaka ma tu miejsce to fakt, że wyjściem jest teraz plik, a nie konsola.
nazwa_zmiennej_plikowej << zmienna_ktora_ma_zostac_zapisana_do_pliku;
Tak samo jak w przypadku odczytywania danych za pomocą strumienia, zapisywane dane tą techniką są zawsze traktowane jako tekst niezależnie od ustawienia trybu
ios::binary. Każdorazowe zapisanie danych powoduje przesunięcie wskaźnika o tyle znaków ile zostało zapisanych do pliku.
Zapisywanie danych blokami
Gdy zapisywanie danych w postaci tekstu jest dla nas niewystarczające (a przy profesjonalnym podejściu do większości projektów tak właśnie jest) z pomocą przychodzi nam kolejna funkcja klasy
fstream i jest to
write(). Definicja tej funkcji wygląda następująco:
ostream & write( const char * bufor, streamsize ilosc_danych_do_zapisu );
Pierwszy parametr (
bufor) to wskaźnik bufora, w którym znajdują się dane jakie chcemy zapisać do pliku. Drugim parametrem (
ilosc_danych_do_zapisu) informujemy kompilator ile danych ma zostać zapisanych do pliku z bufora. Wraz z wykonaniem tej operacji wskaźnik wewnętrzny pliku przesuwa się do przodu o ilość bajtów zapisanych do pliku.
std::fstream plik( "nazwa_pliku.txt", std::ios::out );
std::string napis;
getline( std::cin, napis );
plik.write( & napis[ 0 ], napis.length() );
Zapisywanie danych w szczegółach
Jeśli napiszesz sobie program, który będzie zapisywał do pliku wczytywane wiersze z klawiatury aż do napotkania pustego wiersza pewnie zauważysz, że rozmiar pliku się nie zmienia zaraz po dopisaniu danych. Dzieje się tak dlatego, że klasa
fstream ma wewnętrzny bufor, który ma na celu przyśpieszenie operacji dyskowych. Każdorazowy dostęp do wybranego obszaru dysku wymaga bardzo dużego czasu w porównaniu do szybkości pamięci podręcznej. Dane zanim trafią na dysk są umieszczane najpierw w buforze, a następnie gdy bufor się zapełni zostają zapisywane na dysk. Dzięki takiemu podejściu do zapisywania danych w pliku proces jest dużo szybszy. Przykładowo, jeśli jednorazowe ustawienie głowicy dysku na określonej pozycji zajmuje np. 2ms, to zapisanie długiego zdania znak po znaku zajęłoby: 2ms*ilość_znaków czasu. Wbudowany system buforowania danych zamiast zapisywać tak często dane, zapisze je najpierw do bufora, a później wyśle je na dysk oszczędzając jednocześnie mnóstwo zasobów sprzętowych komputera. System ten jest zawsze sprawny niezależnie od tego czy skaczesz po pliku w różne miejsca, czy dopisujesz stale dane na jego końcu.
Kontrola bufora zapisu
Klasa
fstream umożliwia nam 'kontrolowanie' wewnętrznego bufora zapisu. Cała ta kontrola sprowadza się do zmuszenia klasy
fstream, aby zapisała całą obecną zawartość bufora na dysk bez względu na to czy jest on zapełniony czy nie. W tym celu utworzono funkcję
flush(). Poniżej zamieszczam przykład demonstrujący użycie tej funkcji.
#include <fstream>
using namespace std;
int main()
{
fstream plik( "plik.txt", ios::out );
if( plik.good() )
{
for( int i = 1; i <= 100; i++ )
{
plik << i << ", ";
plik.flush();
}
plik.close();
}
return( 0 );
}
Pamiętaj jednak, że takie zapisywanie danych jak tu zostało zaprezentowane nie jest wydajne. Funkcja
flush() pomimo iż wydaje się w obecnym świetle dla Ciebie bezużyteczna znajduje ona swoje praktyczne zastosowanie chociażby w serwerach profesjonalnych baz danych.
Poruszanie się po pliku z danymi
Do tej pory odczytywaliśmy (zapisywaliśmy) dane z (do) pliku zawsze od tego miejsca na którym skończyliśmy operację odczytu (zapisu) ostatnim razem. Taka forma odczytu i zapisu danych jest bardzo wygodna, jednak czasem zachodzi potrzeba poruszania się po pliku w bardziej nietypowy sposób. Z pomocą przychodzą tu funkcje
seekg() i
seekp(). Obie funkcje służą do ustawiania nowej pozycji wewnętrznego wskaźnika pliku. Jest jednak między nimi jedna zasadnicza różnica:
Parametry obu tych funkcji są analogiczne:
istream & seekg( streamoff offset, ios_base::seekdir kierunek );
ostream & seekp( streamoff offset, ios_base::seekdir kierunek );
Pierwszy parametr (
offset) to przesunięcie, które informuje o ile bajtów ma zostać przesunięty wewnętrzny wskaźnik pliku. Drugi parametr (
kierunek) jest opcjonalny i informuje klasę
fstream względem czego ma zostać dokonane przesunięcie wskaźnika. Domyślną wartością, jaka jest przyjmowana za zmienną
kierunek, to
ios_base::beg. Kierunki jakie mamy do wyboru to:
Odczytywanie aktualnej pozycji wewnętrznego wskaźnika pliku
Jeśli będziemy mieli potrzebę odczytania aktualnej pozycji wewnętrznego wskaźnika pliku, możemy to zrobić za pomocą funkcji
tellg() i
tellp(). Definicja obu funkcji wygląda następująco:
streampos tellg();
streampos tellp();
Obie funkcje wyglądają tak samo, różnią się jednak działaniem.
Gdy wyjdziemy poza zasięg pliku
Aby sprawdzić, czy skok na nową pozycję zakończył się sukcesem możemy dokonać tego na dwa sposoby:
Pierwsza metoda jest logiczna, druga wymaga krótkiego omówienia. Definicja funkcji
fail() wygląda następująco:
Jeśli wystąpi błąd podczas wykonywania skoku (i nie tylko skoku) funkcja ta zwróci wartość
true informując nas, że ostatnia operacja na pliku nie powiodła się. Przykładowo:
std::fstream plik( "plik.txt", std::ios::in );
plik.seekg( + 2, std::ios_base::end );
if( plik.fail() ) std::cout << "Error! Nie udalo sie przesunac wewnetrznego wskaznika pliku" << std::endl;
Pozostałe funkcje, wykorzystywane podczas pracy z plikami
Ostatnią funkcją, jaką chciałbym omówić jest
eof(). Funkcja ta służy do sprawdzania, czy wskaźnik pliku znajduje się na końcu pliku. Definicja funkcji:
Funkcja zwróci wartość true wtedy, gdy nie będzie już w pliku więcej danych do odczytu. Dzięki tej funkcji możemy w bardzo łatwy sposób odczytać zawartość całego pliku. Poniższy przykład wyświetli zawartość całego pliku na ekran konsoli.
#include <iostream>
#include <fstream>
#include <conio.h>
using namespace std;
int main()
{
fstream plik;
plik.open( "dane.txt", ios::in );
if( plik.good() )
{
string napis;
cout << "Zawartosc pliku:" << endl;
while( !plik.eof() )
{
getline( plik, napis );
cout << napis << endl;
}
plik.close();
} else cout << "Error! Nie udalo otworzyc sie pliku!" << endl;
getch();
return( 0 );
}