Wykład 2 - 3 godz.
Zakres tematyczny
1. Funkcje pomocnicze cd.
2. Operacje we/wy niskiego poziomu
3. Operacje we/wy na konsoli i portach
4. Operacje we/wy w ujęciu obiektowym
1. Funkcje pomocnicze cd.
long ftell(fp);
Podaje bieżącą pozycję pliku, liczoną jako przesunięcie względem początku zbioru.
Zwraca -1L jeśli błąd
void rewind(fp);
Ustawia fp, na początek zbioru. Kasuje wskaźnik końca zbioru i błędów. Może być zastąpiona odpowiednio ustawioną funkcja fseek która jednak nie kasuje wskazań błędów i EOF
int fgetpos(fp, pos)
Zapamiętuje w pos bieżącą pozycję file pointera.
Zwraca 0 lub != 0 gdy błąd.
int fsetpos(fp, pos)
Ustawia w pos bieżącą pozycję pliku.
Zwraca 0 lub != 0 gdy błąd.
Obie ostatnie funkcje ustawiają stałą errno na:
EINVAL - nieprawidłowe FILE * (nie wskazuje na strukturę)
EBADF - wskazania na błędną strukturę lub zbiór niedostępny
ZAMYKANIE ZBIORU
PO ZAKOŃCZENIU PRACY ZAMYKAMY STRUMIENIE!!! Z wyjątkiem predefiniowanych które automatycznie zamykane są po zakończeniu programu. Tak tez się dzieje z innymi, ale należy pamiętać, iż ilość otwartych w tym samym czasie strumieni jest ograniczona.
int fclose(fp);
int fcloseall();
Przykład - konwersja zbiorów mazow na latin
#include
#include
#define BYTE unsigned char
FILE *fi_inp, *fp_out;
BYTE mazow[18] = { 141,134,...,}; // 18 liczb >128
latin[18] = {157, 146,...,);
BYTE buf;
main()
{
int i;
clrscr();
if((fp_inp=fopen("inp_text.txt","rb")) == NULL)
{
printf("Bład otwarcia zbioru wejściowego");
exit(1);
}
if((fp_out=fopen("out_text.txt","rb")) == NULL)
{
printf("Bład otwarcia zbioru wyjściowego");
exit(1);
}
while(!feof(fp_inp))
{
fread(&buf,sizeof(BYTE),1,fp_inp);
for (i=0;i<18;i++)
if (buf==mazow[i])
{
buf = latin[i];
break;
}
fwrite(&buf,sizeof(BYTE),1,fp_out);
}
fseek(fp_out,0,SEEK_SET); //na początek zbioru
for( ; !feof(fp_out); )
{
fread(&buf, sizeof(BYTE), 1, fp_out);
putchar(buf);
}
fcloseall();
exit(0);
}
OBSŁUGA BŁĘDÓW
W początkowej fazie używania operacji we/wy pojawia się wiele błędów, dających dość nieoczekiwane efekty. Dla tego, pożytecznym jest nawyk sprawdzania poprawności wykonywanych operacji, w celu łatwiejszego wykrywania błędnych działań programu. Gdy jakaś operacja nie powiodła się, pojawia się błąd i dla danego strumienia ustawiany jest wskaźnik błędu.
Do testowania wskaźnika błędu i sprawdzania, czy w danej operacji zapisu/odczytu powstał błąd służy funkcja:
int ferror(fp);
zwraca 0 jeśli nie stwierdzono błędu, !0 w przeciwnym przypadku
Jeśli powstał błąd, to wskaźnik błędu pozostaje ustawiony aż do momentu zamknięcia strumienia, lub użycia funkcji:
void clearerr(fp);
ustawia wskaźnik błędu lub EOF na 0;
lub omówionej wcześniej funkcji :
rewind(fp);
Przykład - otwarcie do odczytu i próba zapisu
#include
FILE *fp;
char *string = "This should never be written";
main()
{
fp = fopen("data","r");
fprintf(fp, "%s
",string);
if (ferror(fp))
{
fprintf(stderr, "Write error
");
clearerr(fp);
}
2. Operacje we/wy niskiego poziomu
Operacje tego typu - "low-level" manipulują na danych nieformatowanych i niebuforowanych. Zbiory otwarte poprzez wywołanie funkcji niskiego poziomu kojarzone są ze zmienną "file handle", całkowita zmienną poprzez która system operacyjny dociera do zbioru.
Chociaż operacje niskiego poziomu nie wymagają bibliotek
OTWARCIE ZBIORU
Podobnie jak to było dla funkcji strumieniowych, przed przystąpieniem do pracy ze zbiorem, zbiór ten należy otworzyć jedną z funkcji: open, sopen, create. Wszystkie te funkcje zwracaja "file handle"
int open("nazwa",mode,pmode);
Otwiera zbiór o podanej nazwie w jednym z trybów mode:
O_APPEND - ustawia wskaźnik fp na koniec zbioru przed zapisem
O_BINARY - otwiera w trybie binarnym
O_CREATE - tworzy i otwiera nowy zbiór. Nie działa gdy zbiór istnieje.
O_RDONLY - tylko do odczytu
O_WRONLY 0 tylko do zapisu
i inne
połączenie trybów:
O_WRONLY | O_BINARY
parametr pmode działa tylko wtedy gdy ustawiony jest O_CREATE:
S_IWRITE - dozwolony zapis
S_IREAD - dozwolony odczyt
S_IWRITE | S_IREAD - dozwolony odczyt/zapis
int create("nazwa",tryb);
Funkcja zarówno tworzy nowy zbiór, lub obcina istniejący zbiór. Jeśli zbiór nie istnieje, jest tworzony i otwierany do zapisu. Jeśli istnieje, kasuje jego poprzednia zawartośc i otwiera do zapisu.
pmode jak wyżej
Predefiniowane "file handle"
W momencie rozpoczynania programu do standardowych we/wy przypisywanych jest 5 "file handle":
stdin 0 stderr 2 stdprn 4
stdout 1 stdoux 3
Standardowe zbiory otwierane są automatycznie w momencie rozpoczęcia programu.
ODCZYT I ZAPIS DO ZBIORU
W przeciwieństwie do funkcji strumieniowych mamy dwie funkcje write, read. Operacje we/wy wykonywane są od bieżącego fp, który aktualizowany jest po każdej operacji zapisu i odczytu. Do testowania warunku końca zbioru może być użyta procedura eof. W razie wystąpienia błędu, operacje te ustawiają errno.
int read(fh,buf,count);
Czyta do bufora count bajtów ze zbioru skojarzonego z fh.
int write(fh,buf,count);
zapisuje z bufora do zbioru skojarzonego zfh count bajtów
Obie funkcje zwracają ilość zapisanych/odczytanych bajtów. Jeśli jest < niż count to oznacza błąd w realizacji funkcji.
FUNKCJE POMOCNICZE
long seek(fh,offset,origin)
przesuwa fp do nowej pozycji oddalonej od origin o offset bajtów.
Zwraca offset (w bajtach)
long tell(fh);
podaje bieżącą pozycję fp w zbiorze skojarzonym przez fh. Pozycja to liczba bajtów od początku zbioru.
ZAMYKANIE ZBIORU
int close(fh);
zwraca 0 gdy pomyślnie zamknie zbiór, lub -1 gdy powstał błąd.
Przykład - zapis do zbioru
#include
#include
#include
char buffer[100] = "To jest zapis do zbioru";
main()
{
int fh;
unsigned int nbytes = 100, byteswritten;
if((fh=open(a:dane.dat",O_WRONLY)) == -1)
{
perror("błąd otwarcia");
exit(1);
}
if((byteswritten =write(fh,buffer,nbytes) ) == -1)
perror(" ");
else
printf("Wrote %u bytes to file
",byteswritten);
}
UWAGA!!!
Urządzenia zewnętrzne (np.konsola) nie maja wskaźników do pliku, więc rezultat funkcji seek lub tell jest niezdefiniowany.
3. Operacje we/wy na konsoli i portach
Funkcje wykonujące operacje we/wy na konsoli lub wyspecyfikowanym porcie są deklarowane w zbiorze
Konsola lub port nie moga buc otwierane i zamykane przed lub po wykonaniu operacji we/wy, dlatego w grupie tych funkcji nie ma funkcji open i close.
Do zapisu i odczytu pojedynczych bajtów służy funkcja inp oraz outp.
Do odczytu lub zapisu słów służy funkcja inpw i outpw.
Funkcje inp, inpw zwracają słowo lub bajt odczytany ze zbioru.
Funkcje outp, outpw zwracają słowo lub bajt zapisany do zbioru.
Przykład - zapis i odczyt z portu
#include
#include
/* odczyt z portu 0 */
unsigned int port = 0;
char result,res;
main()
{
result = inp(port);
printf("Wartość odczytana z portu %d = %d
",port,result);
port = 1;
res = outp(port,result);
printf("Wartość zapisana do %d = %d
",port,result);
}
4. Operacje we/wy w ujęciu obiektowym
Aby móc używać poprzednio omawianych funkcji realizujących operacje we/wy w ujęciu standardowym, musieliśmy dołączać biblioteki funkcji realizujących określone operacje. Inaczej się ma w przypadku C++. Poznaliśmy już zgrubsza, co to są klasy, i jak tworzy się obiekty danej klasy (czyli jej realizację).
Idąc dalej, język C++ rozszerzył biblioteki funkcji z języka C i stworzył biblioteki klas. Biblioteka klas, powstaje zwykle przez utworzenie plików nagłówkowych zawierających deklaracje klas i definicje odpowiednich funkcji składowych. Plik nagłówkowy, może być później dołączony do programu w standardowy sposób udostępniając zawarte w nim klasy.
W tym punkcie, po poznaniu pewnej minimalnej części biblioteki funkcji we/wy w języku C, przystąpimy do omawiania biblioteki klas we/wy języka C++, która zawiera klasy tworzące obiekty ułatwiające obsługę we/wy urządzenia i pliki). Poznaliśmy już wcześniej niektóre elementy systemu we/wy ( tj. << i >>), ale do poznania zostało nam jeszcze trochę.
4.1 Biblioteka iostream
Przy omawianiu tej biblioteki spotkamy się z trzema problemami:
1. wprowadzanie i wyprowadzanie danych ze standardowych urządzeń we/wy jak ekran i klawiatura
2. operacje na plikach danych znajdujących się na nośnikach zewnętrznych
3. formatowanie wewnętrzne
Aby móc skorzystać z tej biblioteki, musimy znaną dyrektywą #include
Jeśli dodatkowo w programie będziemy wykonywali operacje we/wy na plikach musimy dołączyć dodatkowo plik #include
Niektóre realizacje biblioteki iostream takie jak Borland C++ są tak skonstruowane, że przy używaniu operacji we/wy na plikach, wystarczy dołączyć jedynie fstream.h, ponieważ on sam dołącza niezbędny mu plik iostream.h. Jesli jednak sami go dołączymy nie spowoduje to błędu.
4.2 Pliki obiektów
Ponieważ mamy możliwość wyświetlania na ekranie obiektów oraz pobieranie ich danych z klawiatury, sensowne jest rozszerzenie obiektów we/wy C++ również na pliki. Jak mówiliśmy, klasy obsługujące pliki zadeklarowane są w pliku fsream.h. Są to klasy:
ofstream - output file stream - zapis do pliku
ifstream - input file stream - odczyt z dysku
fstream - file stream - oba powyższe
Idee leżące u podstaw operacji we/wy na plikach w CC (użycie klas strumieni) nie różnią się za bardzo od analogicznych operacji w standardowym języku C. Jednak w typ przypadku mamy do czynienia z klasą strumienia i utworzeniem obiektu tej klasy (tzn. rzeczywistego, fizycznego strumienia) , a następnie dla otwarcia pliku posługujemy się funkcjami składowymi tego obiektu (tj strumienia). Gdy otwieramy plik przy pomocy funkcji składowej strumienia open(), następuje automatyczne powiązanie pliku z tym strumieniem. Zobaczmy jak to się dzieje na przykładzie:
4.2.1 Zapis pliku w postaci ASCII
#include
#include
/* przykład operacji otwarcia , zapisu zamknięcia pliku */
void main(void)
{
ofstream my_file; (1)
my_file.open("Hello.dat",ios::out,0); (2)
my_file << "Hello guy
"; (3)
my_file.close(); (4)
}
Gdy otwieramy plik do zapisu posługujemy się klasą ofstream.
1. Zaczynamy od utworzenia małego pliku o nazwie "hello.dat", ktory będzie zawierał tylko napis "hello guy". Aby to wykonać, potrzebny będzie obsługujący zapis strumień klasy ofstream (1). Strumień ten nazwaliśmy my_file. Potrzebny będzie wtedy, gdy zaczniemy wykonywać na nim operacje.
2. Tworzymy rzeczywisty plik dyskowy o nazwie "hello.dat". Do tego celu w klasie ofstrem przewidziano funkcję składową open() (2). W ten sposób połączyliśmy strumień my_file z plikiem dyskowym (strumien ten jest jak gdyby fp lub fh w klasycznym C).
void open(char * nazwa, int tryb,int atrybut_pliku);
tryb:
io::app - dopis danych do pliku
io::ate - przejście na koniec pliku
io::in - otwarcie pliku do odczytu
io::nocreate - jeśli plik nie istnieje nie otwieraj go
io::out - otwarcie pliku do zapisu
io::trunc - obcięcie długości pliku do zera, następnie jego otwarcie
atrybut pliku:
0 - czysty tekst
1 - tylko odczyt
2 - plik ukryty
4 - plik systemowy
8 - ustawienie bitu archiwum
W powyższym przykładzie, otworzony został plik tekstowy (atrybut pliku = 0) do zapisu (ios::out). Ponieważ parametry te są domyślne, można tą linie zapisać jako:
my_file.open("Hello.dat");
Możemy zrobić znacznie więcej. Ponieważ połączenie pliku ze strumieniem odbywa się w momencie otwarcia pliku, dwie pierwsze linie możemy połączyć w jedna :
ofstream my_file("Hello.dat");
przesyłając do konstruktora strumienie te same parametry co do funkcji open(). Uprościmy więc program do postaci:
ofstream my_file("Hello.dat");
my_file << "Hello guy
";
my_file.close();
Inaczej mówiąc możemy jednocześnie zadeklarować obiekt i powiązać go z plikiem dyskowym.
3. Teraz możemy wysłać do niego dane (3). Robimy to w znany sposób. Operator << może być przeciążony.
4. Teraz nie pozostaje nic innego jak zamknąć plik (4). Możemy to zrobić za pomocą funkcji składowej klasy ofstream close():
4.2.2 Odczyt pliku w formacie ASCII
#include
#include
#include
/* Odczyt pliku ASCII */
void main(void)
{
char napis1[20], napis2[20]; (1)
ifstream my_file("Hello.dat"); (2)
my_file>>napis1; (3)
my_file>>napis2; (3)
cout<
my_file.close();
getche();
}
Mamy już na dysku plik. Możemy więc go np. odczytać. Zapisaliśmy go jako łańcuch, posługując się przeciążonym operatorem <<.
Przy odczycie tego łańcucha przy pomocy operatora >> odstsęp między słowami będzie traktowany jako znak spacji dlatego do pobrania dwóch wyrazów potrzebny jest dwukrotny odczyt z pliku (1) i (3).
1. Deklarujemy więc dwa buforowe łańcuchy (1).
2. Otwieramy plik przy pomocy funkcji składowej klasy tym razem ifstream open(). (2)
3. Odczytujemy zawartość strumienia (3).
Należ zwrócić uwagę na to iż w przypadku operacji na plikach tekstowych, kompilator C++ dokonuje różnych przekształceń i interpretacji. Z jedną mieliśmy już do czynienia. Teraz omówimy jeszcze jeden przypadek. Znak '
' przy zapisie zostaje zamieniany na dwa znaki: '
' oraz '
'. Przy odczycie zachodzi przekształcenie odwrotne. Nie stało by się to w przypadku, gdy potraktowalibyśmy nasze dane jako binarne. W typ przypadku nie zachodzą żadne przekształcenia.
4.2.3 Zapis i odczyt pliku w postaci binarnej
#include
#include
#include
void main(void)
{
char znak, napis[]="Hello guy";
int i = 0;
/* Zapis pliku binarnego */
ofstream my_file_out("Hello.dat");
while (napis[i])
my_file_out.put(napis[i++]);
my_file_out.close();
/* Odczyt pliku binarnego */
ifstream my_file_inp("Hello.dat");
while (my_file_inp)
{
my_file_inp.get(znak);
cout<
}
my_file_inp.close();
getch();
}
Dwoma najważniejszymi funkcjami składowymi strumienia pliku dla operacji we/wy na plikach binarnych są get() i put():
int stream.get(char c);
Odczytuje ze strumienia znak i zapisuje do zmiennej c. Odczytuje nawet spacje (gdy odczytuje z cin) podczas gdy cin>>c tego nie odczyta. Zwraca int, aby oddać EOF przy wyjęciu tego znaku ze strumienia. Inną formą tej funkcji jest:
void stream.get(char c);
void stream.put(char &c);
wstawia do strumienia jeden znak.
Obie te funkcje operują na pojedynczych bitach nie interpretując jednak ich znaczenia. Przykładowy program w pierwszej kolejności otwiera plik do zapisu. Po czym przy pomocy funkcji put() zapisuje łańcuch "hello guy" znak po znaku. Po zamknięciu tego pliku jest on ponownie otwarty tym razem do odczytu. Utworzony strumień my_file_inp połączony jest w tej chwili z rzeczywistymi danymi zawartymi w pliku Hello.dat. Funkcja get odczytuje znak po znaku zawartość pliku aż do wyczerpania jego zawartości. Po osiągnięciu końca pliku strumień zwraca sam 0 więc testowanie końca pliku można przeprowadzić tak jak w przykładzie, lub np.:
while(my_file_inp.get(znak))
ponieważ get zwraca strumień na którym pracuje.
Innym sposobem śledzenia osiągnięcia końca pliku jest użycie funkcji int eof(), zwracającej tru gdy znajdujemy siena końcu pliku, false w przeciwnym przypadku.
Chociaż powyższe funkcje są dobre, to jednak nie umożliwiają one zapisu i odczytu obiektu.
Omówimy teraz funkcje write, read pozwalające odczytać i zapisać bloki danych, a nie pojedyncze bajty.
4.2.4 Zapis i odczyt pliku z użyciem funkcji read - write
stream.read (unsigned char *bufor, int ilość_znaków);
stream.write(const char *bufor, int ilość_znaków);
#include
#include
#include
#include
void main(void)
{
char bufor[]="Hello guy"; (1)
/* Zapis pliku binarnego */
ofstream my_file_out ("Hello.dat"); (2)
my_file_out.write(bufor,sizeof(bufor));
my_file.close();
/* Odczyt pliku binarnego */
ifstream my_file_inp("Hello.dat");
my_file_inp.read(bufor,sizeof(bufor));
my_file_inp.close();
getche();
}
Posługując się funkcjami put i get musieliśmy operować poszczególnymi bajtami. Używając funkcji write-read operacje te wykonujemy niejako za jednym zamachem przekazując wskaźnik do łańcucha (czyli nazwę tablicy), oraz ilość bajtów które należy przesłać. Funkcji tych możemy używać także do odczytu i zapisu obiektów.
4.2.5 Zapis i odczyt obiektów
Niech dana będzie klasa:
class bank{
public:
char nazwisko[20];
float suma;
};
Poniższy program zapisze do pliku max.15 obiektów tej klasy:
#include
#include
#include
#include
#define INDEKS 15
class bank{
public:
char nazwisko[20];
float suma;
}dane[INDEKS];
/* Zapis tablicy obiektów */
void main(void)
{
ofstream my_file("DANE.dat");
strcpy(dane[0].nazwisko,"Kowalski");
dane[0].suma = 100.8;
strcpy(dane[1].nazwisko,"Kowal");
dane[1].suma = 1990.8;
.....................
my_file.write((char *)dane, INDEKS * sizeof(bamk));
my_file.close;
}
#include
#include
#define INDEKS 15
class bank{
public:
char nazwisko[20];
float suma;
}dane[INDEKS];
/* Odczyt z pliku tablicy obiektów */
void main(void)
{
ifstream my_file("DANE.dat");
my_file.read((char *)dane, INDEKS * sizeof(bamk));
cout<< dane[0].nazwisko <
cout<< dane[1].nazwisko <
.....................
my_file.close;
}
4.2.6 Informacje o pozycji wskaźników do pliku
Często jest tak, że chcemy z pliku odczytać jeden konkretny obiekt. Np chcemy odczytać 9-ty obiekt z poprzedniego przykładu. Nie musimy w tym celu czytać całego pliku i dopieroi po tym wyselekcjonować interesującej nas informacji.
Zdefiniowano bowiem dwa wskaźniki get i put. Położenie wskaźnika get określa dane które zostaną odczytane z pliku, natomiast położenie wskaźnika put informuje nas gdzie zostaną zapisane dane przy najbliższej operacji zapisu. Do ustawiania tych wskaźników służą funkcje:
stream.seekg( pozycja w pliku,seek_dir)
pozycjonuje wskaźnik get
stream.seekp(pozycja_w_pliku, seek_dir)
pozycjonuje wskaźnik put
pos: pokazuje na który bajt ma wskazać wskaźnik względem Seek_dir
seek_dir:
ios::beg - początek pliku
ios::end - koniec pliku
ios::cur - bieżąca pozycja pliku.
#include
#include
#define INDEKS 15
class bank{
public:
char nazwisko[20];
float suma;
}dane[INDEKS],record;
/* Odczyt z pliku 9-tego obiektu */
void main(void)
{
ifstream my_file("DANE.dat");
my_file.seekg(9*sizeof(bank),ios:beg);
my_file.read((char *)&rekord, INDEKS * sizeof(bamk));
cout<< rekord.nazwisko <
.....................
my_file.close;
}
4.2.7 Inne pomocnicze funkcje
strempos pos = tellg(void)
pokazuje pozycje wskaźnika do czytania get
streampos pos = tellp(void)
pokazuje pozycje wskaźnika do zapisu put