Wykład 3 - 2 godz
Zakres tematyczny
1. Obsługa plików - ujęcie obiektowe (powtórka i uzupełnienia).
2. Formatowanie wewnętrzne
3. Grafika w implementacji Borland C++.
1. Obsługa plików - powtórka i uzupełnienie
W ujęciu języka C++, jeśli chcemy zapisać coś do pliku, lub z niego czytać, mamy do dyspozycji klasy, które takie operacje umożliwiają:
ofstream - zapis do pliku
ifstream - odczyt z pliku
fstream - oba powyższe
Podczas pracy ze strumieniami niepredefiniowanymi, sami musimy te strumienie zdefiniować- to oczywiste, ponieważ musimy wyraźnie określić do lub od jakiego pliku dyskowego strumień ma płynąć. Zatem aby czytać lub pisać do pliku, należy:
1. zdefiniować strumień, czyli wykreować strumień odpowiedniej klasy.
2. określić strumieniowi z jakim konkretnie plikiem ma się komunikować i otworzyć ten plik. Otwarcie strumienia można wykonać albo funkcją open albo za pomocą konstruktora
3. wykonać odpowiednie operacje we/wy
4. zlikwidować strumień, gdy uznamy, że praca z plikiem jest zakończona.
Możliwe jest także wielokrotne otwierania strumienia. Strumień po zakończeniu pracy z jakimś plikiem może posłużyć do pracy z innym:
np.:
ofstream stream("dane.txt");
stream<<"teks do zapisu";
stream.close();
stream.open("dane1.txt");
stream<<"teks do zapisu do drugiego zbioru";
stream.close();
Oczywiście strumienia można użyć tylko do celów do jakich był wykreowany, jeśli jest to więc strumień wyjściowy to tylko takimi operacjami może się dalej zajmować. Innymi słowy strumień jest nadal tej samej klasy, ale zmienia się tylko plik z jakim się komunikuje.
Błędy w trakcie pracy strumienia. Nie zawsze otwarcie pliku się udaje. Nie zawsze wymagana operacja odczytu lub zapisu może zostać zrealizowana. W programach przykładowych można się bez kontroli poprawności obejść , jednak przy pisaniu większych aplikacji, należy pamiętać o kontroli poprawności operacji na strumieniach. W dalszej części wykładu pomówimy o narzędziach do tego celu przeznaczonych.
Zebrane zostału w klasie ios.
W każdym obiekcie klasy strumień znajduje się słowo odpowiadające za stan błędu strumienia. W przypadku wystąpienia błędu w pracy strumienia, ustawiany jest bit odpowiadający za daną kategorie błędu. Kategorie te określane są typem wyliczeniowym zdefinowanym w klasie ios. :
enum io_state{
goodbit = 0,
eofbit = 1,
failbit = 2,
badbit = 4 };
goodbit - nie jest to właściwie flaga błędu. Stan goodbit jest wtedy, gdy wszystkie bity stanu błędów są wyzerowane.
eofbit - flaga ustawiana jest wtedy, gdy przy czytaniu napotkany został koniec pliku EOF
failbit - ustawienie tej flagi oznacza, że jakaś operacja we\wy nie powiodła się. Strumień tkwi w stanie błędu, ale po wyzerowaniu tej flagi nadaje się do pracy.
badbit- ustawienie tej flagi oznacza, ze powstał jakiś poważniejszy błąd. Dalsza praca z tym strumieniem jest nie możliwa.
Flagami tymi nie zajmujemy się zwykle bezpośrednio. Do tego celu służą funkcję zdefiniowane w klasie ios.
int good()
zwraca wartość niezerową, jeśli wszystko jest w porządku, czyli żaden z bitów błędów nie jest ustawiony.
char c;
do {
cin>>c;
cout<
while( cin.good() );
pętla wykonuje się dopóki nie wystapi błąd strumienia cin.
int eof()
zwraca wartość niezerową, jesli jest ustawiona flafa eofbit, czyli przy operacji odczytu napotkany został koniec pliku.
Użycie tej funkcji mieliście państwo na poprzednim wykładzie przy okazji omawiania funkcji odczytu z pliku
int fail()
zwraca wartośc niezerowa gdy flagi failbit lub badbit są ustawione.
int num;
cin>>num;
if(cin.fail())
cout<<"Błędna dana";
zamiast funkcji fail możemy użyć zdefiniowanego w klasie ios operatora !. Robi to samo co funkcja fail:
cin >>num;
if(cin.fail()) cout <<"Błąd";
if(!cin ) cout <<"Błąd";
Dwa powyższe zapisy są równoważne.
flaga failbit ustawiona zostanie wówczas, gdy zamiast wartości liczbowej z klawiatury strumieniem przypłynie znak tekstowy.
int bad()
zwraca wartość niezerową gdy flaga badbit jest ustawiona. Dzieje się tak, gdy np. chcemy aby strumień czytał z pliku, który nie istnieje.
ifstream list("dane", ios::in);
if(list.bad())
cout<<"Błąd otwarcia pliku";
W klasie ios zdefiniowane są jeszcze dwie funkcje pracujące na flagach błędów. Ponieważ nie są one powszechnie używane nie będziemy się nimi zajmować szczegółowo. Powiem tylko, że są to:
rdstate - zwraca jako rezultat słowo int, w którym odpowiednie bity odpowiadają flagom błędów
clear - umożliwia ustawianie "ręczne" flag błędów.
Na zakończenie rozważań na temat obsługi plików dyskowych, należy wspomnieć o możliwości zmiany koryta strumienia. Czasami podejmujemy decyzję, że np. od tej chwili zamykamy plik a ujściem strumienia jest np. ekran. Do realizacji takiego zadania służy funkcja:
void attach(int)
argumentem tej funkcji jest liczba int oznaczająca tzw. deskryptor pliku, czyli numer oznaczający plik lub urządzenie wyjściowe (klawiatura ekran).
0 - st. wejście (klawiatura - używa go strumien cin)
1 - st. wyjście (ekran - używa go strumień cout)
2 - st.wyjście dla komunikatu o błędach(- używa go strumień cerr)
char c;
ofstream s("dane");
s<<"proba zapisu do pliku";
cout<<"zamknac plik i pisac dalej ? t/n";
cin.get(c);
if (c=='t') {
s.close();
s.attach(1);
}
s<<"proba zapisu na ekran";
2. Formatowanie wewnętrzne
Dotychczas jeśli mówiliśmy o operacjach we/wy, mieliśmy na myśli komunikowanie się programu z urządzeniami zewnętrznymi - klawiaturą i ekranem, bądź plikami dyskowymi. Tymczasem nie są to jedyne operacje we\wy. Strumienie o których teraz krótko porozmawiamy, płynąć będą nie do plików , ale do wybranych obszarów pamięci, tak jakby tam właśnie znajdował się plik. Zwykle tym miejscem w pamięci jest tablica znakowa.
Jak łatwo się domyślać inaczej będzie się realizować to zadanie w klasycznym języku C, a inaczej w C++. Zacznijmy od tego drugiego:
Załóżmy, że mamy instrukcje wypisującą na ekranie pewien komunikat:
cout << "Pierwsza dana" << dana1 <<"Druga dana" << dana2
<< "Trzecia dana" << dana3;
Z jakiś powodów chodzi nam o skierowanie strumienia nie na ekran, ale do tablicy np.:
char dane[50];
W bibliotece klas istnieje klasa która umożliwia posługiwanie na tego typu strumieniami. Musimy do programu włączyć plik nagłówkowy strstream.h, zawierający odpowiednie deklaracje.
Wpisanie stringu do tablicy obsługuje strumień klasy ostrstream (output string stream - wyjściowy strumień do tablicy znakowej). Można powiedzieć wpisywanie stringu do tablicy to nic takiego - można tego dokonać np. funkcją strcpy. Tak, ale nie będziemy mogli wykorzystywać formatowania oferowanego przez strumienie we/wy - możemy określić precyzję, typ notacji, szerokość. Podamy teraz przykład:
#include
#include
#include
main()
{
int dane1 = 2, dane2 = 7;
float dane3 = 123.123;
char dane[50];
/* tworzymy strumień klasy osttrstream */
ostrstream tablica(dane, sizeof(dane)); ( !!! )
tablica << "Pierwsza dana" << dana1 <<"Druga dana" << dana2
<< "Trzecia dana" << dana3 <
cout << "Odczyt zawartosci tablicy\n" <
/* zmieniamy zawartosc tablicy tak jak byłby to plik */
tablica.seekp(3, ios::beg); (2)
tablica<<"AAA";
cout<
}
(1) Wpisanie do tablicy nie powoduje automatycznego dostawiania znaku NULL. Jeśli chcielibyśmy jednak go tam umieścić musimy użyć manipulatora ends (end string). Jeśli w trakcie operacji zapełniania tablicy przekroczylibyśmy liczbę 49 znaków, wówczas wpisywanie zostaje przerwane, a na pozycji 50 dopisywany jest znak NULL. Ustawiana jest flaga badbit.
(2) praca strumienia płynącego do tablicy przypomina prace strumienia płynącego do pliku. Można pozycjonować wskaźnik pisania.
( !!! ) Przy tworzeniu strumienia wykorzystywany został konstruktor strumienia klasy ostrstream:
ostrstream::ostrstream(char *tab, int rozmiar,int tryb);
char *tab - adres miejsca ujścia strumienia (tablicy)
int rozmiar - ile bajtów w pamięci zajmuje ta tablica
int tryb - tryb pracy strumienia. Przez domniemani dla tej klasy przyjęty jest tryb out - do pisania w tablicy. Można użyć trybu ios::app - dopisanie.
Jeśli zastosujemy tryb app strumien uznaje, że w tablicy jest już coś zapisane, odnajduje znak NULL kończący poprzednią wartość, a od tego miejsca zapisuje nową treść:
char napis[30] = {"Pamiętaj!!!"};
ostrstream pisz(napis , sizeof(napis), ios::app);
pisz <<"To nie takie trudne "<
Takie podejście do zapisu nie jest wygodne, nie wiemy bowiem zawczasu jaka długa jest tablica, ktora chcemy zapisać. Klasa ostrstream zapewnia możliwość zrezygnowania z określania rozmiaru tablicy:
ostrstream pisz;
korzystamy z konstruktora, któremu przy tworzeniu strumienia nie podajemy tablicy z jaka chcemy pracować. Używając operatora new rezerwuje sobie pewien obszar pamięci. Jednak w czasie pisania obszar ten może okazać się za mały. Wtedy rezerwowany jest większy obszar zawartość starego przepisywana jest do nowego, a stary usuwany operatorem delete. Jeśli i on nie wystarcza, wtedy sytuacja powtarza się.
#include
#include
#include
main()
{
ostrstream pisz;
for(int i = 0; i<10; i++)
{
pisz<<"\npetla "<< i ;
if(!pisz)
{
cout<<"Błąd strumienia";
return (1);
}
pisz<
char *wsk;
wsk = pisz.str(); !!!
cout<<"Adrees tablicy do której pisalismy:" << wsk;
delete wsk; !!!
}
Po utworzeniu strumienia i wpisaniu so niego potrzebnych informacji, wywołujemy na rzecz tego strumienie funkcje składowa klasy ostrstream : str(). Rezultatem działania tej funkcji jest adres tablicy na której pracował strumien. Od tego momentu strumień nie wykona żadnej operacji zapisu do tablicy. Zwolnienie obszaru rezerwowanego przez strumień należy do nas.
Gdybyśmy nie wywołali tej funkcji strumien byłby w stałej gotowości do wykonywania na tej tablicy dalszych operacji. Przy zakończeniu zakresu ważności obiektu destruktor skasowałby ta tablicę.
Za pomocą strumieni można także odczytać informacje znajdujące się w jakiejś tablicy:
Do tegocelu służą strumienie klasy istrstream (input stream string - strumien odczytujący z tablicy.
#include
#include
#include
main()
{
char tablica[30] = {"3.14 to jest pi"};
istrstream napis(tablica, sizeof(tablica));
float lpi,modyf = 1.1;
char text[30];
napis>>lpi >>text;
pli +=modyf;
cout<<"Odczyt z tablicy" <<LPI<
}
Na strumieniu tym można wykonywać tez operacje typu seekg(wskaźnik odczytu) , get(c) itp.
Konstruktor użyty w przykładzie może mieć inną postać:
istrstream napis(tablica);
stosujemy go gdy strumien pracuje z tablica w której string zakończony jest znakiem NULL. Określenie rozmiaru stringu nie jest wówczas konieczne. Strumien sam jest w stanie odczytac znak NULL.
Zapis i odczyt realizowany jest za pomocą strumienia klasy strstream pochodnej klas ostrstream i istrstream.Jest to strumień który zarówno pisze jak i czyta z tablicy. Konstruktor ma takie same parametry jak konstruktor klasy ostrstream. Dostępna jest funkcja str(), pozwalające gromadzić znaki w tablicy której rozmiaru nie określamy.
#include
#include
main()
{
char tablica[30];
strstream zapis_odczyt(tablica, sizeof(tablica),ios::in|ios::out);
zapis_odczyt<<"123 10 11.22";
int a,b;
float c;
zapis_odczyt >>a;
zapis_odczyt>>b;
zapis_odczyt>>c;
cout << "razem" << (a+b+c);
zapis_odczyt.seekg(2,ios::beg);
char t[10];
zapis_odczyt.getline(t,4);
t[4] = NULL';
cout<<"znaki wyjęte"<<T<
}