Kurs C++ #12a
Kurs C++ #12a
|========== #12a =========|
+-------------------------+
| K U R S C + + |
+-------------------------+
o b s ł u g a
w e j ś c i a
/ w y j ś c i a
...czyli autor pokazuje, jak kulturalne wejść i wyjść. :P
Tutaj już bez zbędnych wstępów. :)
Klasa istream
Klasa istream definiuje wejście programu. Obiektem tej klasy jest cin. W tym podrozdziale znajduje się omówienie podstawowych metod tej klasy pozwalających na lepsze manipulowanie wprowadzanymi danymi.
Jak wiemy, obiekt cin pozwala na wydobywanie z wejścia danych (które zawsze są tekstem - w końcu wprowadzamy je z klawiatury) oraz w razie potrzeby dokonuje ukrytej konwersji. Zdefiniowane konwersje w obiekcie cin są następujące:
char &
signed char &
unsigned char &
short &
unsigned short &
int &
unsigned int &
long &
unsigned long &
long long &
unsigned long long &
float &
double &
long double &
char *
signed char *
unsigned char *
Jak widzimy, cin potrafi przeprowadzać konwersję na typy wbudowane - najbardziej podstawowe. W metodach wejścia parametrem jest referencja - pozwala to na przeprowadzanie operacji na parametrze, a nie na jego kopii (jak byłoby w przypadku przekazania przez wartość).
W przypadku pobierania danych, cin pobiera jedno słowo z wejścia (z wyjątkiem trybu jednoznakowego - wtedy pobiera jeden znak drukowalny). Mówiąc słowo, mam na myśli ciąg znaków nierozdzielony białym znakiem (spacją, tabulatorem, końcem linii czy końcem pliku). Jeżeli cin napotka białe znaki, to (oprócz napotkania EOF) pomija je dopóki nie trafi na inny znak (alfanumeryczny). Obiekt cin odczytuje wszystkie znaki począwszy od pierwszego drukowalnego aż do pierwszego znaku, który nie jest zgodny z typem docelowym. Np. w przypadku pobierania liczby:
int liczba;
cin >> liczba;
Załóżmy, że wprowadzimy następujące dane:
-246uj
Obiekt odczyta znaki -, 2, 4 i 6, bo są one elementami liczby całkowitej. Natomiast znaki "uj" nie są zgodne, więc w tym miejscu cin zakończy wczytywanie liczby, a znaki "uj" pozostaną w strumieniu wejściowym i od nich rozpocznie się pobieranie kolejnych danych.
A co, jeśli już pierwszy znak będzie błędny? Wtedy cin pozostawi zmienną, do której przypisujemy wartość, niezmienioną, a sam zwróci false. Wartość zwracana pozwala sprawdzić, czy dane wejściowe są prawidłowe:
suma.cpp
#include <iostream>
#include <cstdlib>
using std::cin;
using std::cout;
using std::endl;
using std::system;
int main()
{
cout << "Podaj liczby:\n";
int suma = 0, in;
while (cin >> in)
suma += in;
cout << "Ostatnia wartosc: " << in << "\nSuma: " << suma << endl;
system("PAUSE");
return 0;
}
Stany strumienia
Obiekty klas istream i ostream oraz pochodnych mają zdefiniowaną zmienną opisującą stan strumienia. Ta zmienna jest typem maski bitowej, na którą składają się włąsciwie trzy bity:
eofbit - jeżeli jest ustawiony oznacza, że cin napotkał koniec pliku (używane przy przekierowaniach)
badbit - ustawiony mówi, że wystąpił nieznany błąd strumienia lub strumień został uszkodzony
failbit - wskazuje na niepowodzenie dostępu do pliku (np. pisanie do read-only) lub, jak w poprzednim przykładzie, na brak oczekiwanych znaków na wejściu
Jeśli wszystkie trzy bity są wyzerowane, wszystko jest OK. ;)
Program może kontrolować stan strumienia za pomocą kilku metod:
good() - zwraca true, jeśli strumień jest nienaruszony
eof() - zwraca true, jeśli jest ustawiony eofbit (wykorzystywany przy przekierowaniu wejścia z pliku, ale może również zostać zasymulowany z klawiatury - w DOS jest to skrót ^Z (Ctrl+Z) a w UNIXie ^D (Control+D) na początku wiersza)
bad() - zwraca true w przypadku ustawienia badbit
fail() - zwraca true, jeśli failbit jest ustawiony
rdstate() - zwraca stan strumienia
exceptions() - zwraca maskę bitową, która określa bity zgłaszające wyjątek (o wyjątkach więcej już w kolejnej części kursu)
exceptions(iostate ex) - ustawia, które stany strumienia będą powodowały zgłoszenie wyjątku przez metodę clear(); np. jeśli ex odpowiada bitowi eofbit, to metoda clear() zgłosi wyjątek, jeśli eofbit jest ustawiony
clear(iostate s) - ustawia stan strumienia na s; domyślnie s == 0
setstate(iostate s) - wywołuje metodę clear(rdstate() | s); powoduje to ustawienie tych bitów strumienia, którym odpowiada maska s; pozostałe bity pozostaną niezmienione
Po co nam te wzystkie stany i bity? W zasadzie potrzebne są do wyjątków, czyli obsługi błędów w programie. Jednak nie tylko - jeżeli którykolwiek z bitów stanu zostaje ustawiony, to operacje wejścia (obiekt cin) są zablokowane. Aby je odblokować, należy wyzerować stan strumienia, czyli wyzerować wszystkie bity.
Spróbujmy więc usprawnić nasz program sumujący:
suma2.cpp
#include <iostream>
#include <cstdlib>
#include <cctype>
using std::cin;
using std::cout;
using std::endl;
using std::system;
using std::isspace;
int main()
{
cout << "Podaj liczby:\n";
int suma = 0, in;
while (cin >> in)
{
suma += in;
cout << "Ostatnia wartosc: " << in << "\nSuma: " << suma << endl;
if (cin.good()) continue;
else if (cin.bad())
{
cout << "Blad danych wejsciowych!\nSumowanie moze byc kontynuowane.\n";
cin.clear();
while (!isspace(cin.get())) continue; // pominięcie błędnych danych wejściowych
continue;
}
cout << "Nieznany błąd!\nDotychczasowa suma: " << suma << "\nKonczenie dzialania programu...\n\n";
system("PAUSE");
exit(1);
}
cout << "Sumowanie zakonczono pomyslnie!\nSuma: " << suma << endl << endl;
system("PAUSE");
return 0;
}
O tym, jak obsługiwać i wywoływać wyjątki podczas operacji I/O, powiemy sobie w części kursu poświęconej wyjątkom właśnie.
Pliki
Obsługa plików - hmm... To chyba znowu jakieś dziwne funkcje, klasy... Głupi ten C++ w ogóle. Tak z pewnością uważa wielu laików i tych, którzy znają język "po łebkach". Ale tutaj muszę ich zdziwić. Obsługa plików w C++ jest wręcz obrzydliwie łatwa. :) Mechanizm ten wykorzystuje dziedziczenie z klas istream i ostream, tworząc pochodne klasy ifstream i ofstream. Easy? Bardzo easy, jak zwykł mawiać mój nauczyciel angielskiego. ;) Jeżeli chcemy coś zapisać do pliku, to deklarujemy obiekt, otwieramy plik i piszemy - identyczne jak do cout. Pobranie z pliku? Nic prostszego - obiekt ifstream, otwieramy plik metodą open(nazwa_pliku) i czytamy jak ze strumienia cin. I już!
Oczywiście, to są "podstawy podstaw", bo dochodzi jeszcze obsługa plików binarnych i tryby otwarcia pliku, ale z tym również damy sobie radę. Ale na początek właśnie podstawy. Do podstawowej obsługi plików tekstowych potrzebujemy odpowiednich obiektów oraz trzech metod:
open(nazwa) - otwiera plik; jeżeli wywołano spod obiektu ifstream, t jeśli plik nie istnieje metoda is_open() zwróci false; jeżeli wywołana spod obiektu ofstream, stworzony zostanie pusty plik, a istniejący zostanie skasowany
close() - zamyka plik; instrukcja bardzo ważna, bo niezamknięcie pliku może spowodować, że nie wszystkie dane zostaną zapisane (przy zamykaniu opróżniany jest bufor wyjścia)
is_open() - zwraca true, jeśli operacja otwierania się powiodła; przyczyną niepowodzenia może być próba odczytania pliku, do którego nie ma dostępu, otwarcia pliku nieistniejącego czy pisania do pliku read-only
Nie będę pisał przykładów do tak prostych zagadnień. Przykłady pojawią się później. :)
Tryby otwarcia pliku
Tryb określa, w jaki sposób plik może być wykorzystywany. Tryby otwarcia są zdefiniowane w klasie ios_base jako maski bitowe, co oznacza, że możan tryby łączyć. Oto tabela stałych trybu otwarcia plików:
Stała Znaczenie
===============================================================================
ios_base::in otwarcie do odczytu
ios_base::out otwarcie do zapisu
ios_base::ate po otwarciu ustaw wskaźnik odczytu lub zapisu na końcu pliku
ios_base::app otwórz do dopisywania
ios_base::trunc jeśli plik istnieje, zredukuj jego rozmiar do zera
ios_base::binary plik binarny
Jak użyć trybów otwarcia pliku? W metodzie open w drugim parametrze. Jak łatwo zauważyć, klasy ifstream i ofstream mają zdefiniowane tryby domyślne. Dla ifstream jest to ios_base::in, natomiast dla ofstream ios_base::out|ios_base::trunc.
Istnieje ważna różnica między trybami ios_base::ate a ios_base::app. Ale na początek - wskaźnik pliku. Jest to miejsce, z którego pobierane są dane w pliku lub do niego zapisywane. Klasy istream oraz ostream (bazowe dla ifstream oraz ofstream) mają odpowiednie wskaźniki - jeden do zapisu, a drugi do odczytu. Po obu klasach dziedziczy klasa iostream, a po niej fstream, która może służyć jednocześnie do odczytu i zapisu. W obiekcie klasy fstream wskaźniki odczytu i zapisu (bo istnieją dwa - po klase istram i ostream) są przesuwane synchronicznie. Jeżeli piszemy przy użyciu operatora << lub pobieramy operatorem >> dane z pliku, program działa identycznie, jak podczas wypisywania na ekran. Po wypisaniu czy pobraniu jakiejś ilości danych, wskaźnik pliku jest przesuwany do przodu. Możemy jednak sami operować wskaźnikami pliku - o tym za chwilę.
A teraz różnica między ios_base::ate i ios_base::app. Jeżeli zastosujemy ios_base::ate, po otwarciu wskaźniki są ustawiane na koniec pliku, jednak możemy nimi dowolnie manipulować. Natomiast w trybie ios_base::app wskaźniki są ustawiane na koniec i nie możemy ich ruszyć.
W celu wizualizacji - prosty program z listą kontaktów.
kontakt.cpp
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
const char *file = "nazwiska.txt";
int main()
{
using namespace std;
ifstream fin;
char c;
fin.open(file);
if (fin.is_open())
{
cout << "Aktualna lista nazwisk:\n";
while (fin.get(c))
cout << c;
fin.close();
}
ofstream fout;
fout.open(file, ios_base::out|ios_base::app);
if (!fout.is_open())
{
cerr << "Nie można otworzyć pliku " << file << " do odczytu!\n";
system("pause");
exit(1);
}
cout << "\n############################\n\nPodaj nazwiska do dopisania:\n\n";
char name[50];
while((cin >> name) && strcmp(name, "end"))
{
fout << name << " ";
cin >> name;
fout << name << endl;
}
fout.close();
system("pause");
return 0;
}
Program bardzo prosty, ale chyba przejrzyście pokazuje sposób stosowania trybów otwarcia pliku. Podajemy nazwiska, a wpisywanie kończy słowo 'end'.
Pliki binarne
Umiemy już obsługiwać pliki tekstowe - czyli czytelne dla człowieka. Ale to, co czytelne dla nas, niekoniecznie jest czytelne dla komputera. Aby zapisać dane do pliku tekstowego, potrzeba mnóstwo jawnych i mniej jawnych konwersji. Podobnie jest z odczytem. Do tego dochodzi znacząca utrata precyzji przez liczby zmiennoprzecinkowe. Dlatego często lepiej jest zapisywać pliki binarne. Plik binarny to po prostu kopia danych z pamięci zrzucona bezpośrednio na dysk twardy. Taki zapis oraz odczyt są znacznie szybsze - program po prostu kopiuje dane z jednego miejsca do drugiego, bez żadnej uprzedniej obróbki.
Niestety, obsługa takich plików jest już dużo mniej wygodna, jak plików tekstowych. Nie możemy tu stosować operatorów << i >> oraz trzeba jasno określać, jak duży fragment danych zapisujemy (nieodzowny tutaj staje się operator sizeof).Do zapisu w trybie binarnym służy metoda write(char *data, int size), a do odczytu metoda read(char *data, int size). Ponieważ przekazywać należy dane będące wskaźnikiem na char, wynikają z tego dwie zasady:
wszelkie dane, które chcemy zapisać, musimy rzutować na typ char*
wielkość danych zawsze określamy w bajtach
W plikach binarnych są dwie rzeczy, których nie można bezproblemowo zrobić w plikach tekstowych: mamy dostęp swobodny do pliku oraz możemy zapisywać w pliku tak skomplikowane dane jak np. struktury czy tablice.
Dostęp swobodny
Na czym polega dostęp wobodny? Dzięki niemu możemy swobodnie poruszać się po pliku (czyli zmieniać pozycje wskaźników zapisu i odczytu tak, jak chcemy, a nie w sposób sekwencyjny). Dostęp swobodny jest niezwykle istotny np. we wszelkiego rodzaju bazach danych.
Ważna uwaga: dostęp swobodny najłatwiej i najefektywniej zrealizować, gdy dane do zapisu (rekordy) mają jednakową wielkość.
Do sterowania wskaźnikami pliku służą dwie przeciążone metody:
seekg(int streamoffset, ios_base::seekdir)
seekg(int streampos)
seekp(int streamoffset, ios_base::seekdir)
seekp(int streampos)
Metody seekg() są zdefiniowane w klasie istream, natomiast seekp() w klasie ostream. Ale ponieważ w klasie fstream wskaźniki pliku są zawsze identyczne, nie musimy wykorzystywać różnych metod - my weźmiemy seekg() - głównie z przyzwyczajenia, bo większej różnicy nie ma. :)
Pierwszy prototyp pobiera dwa argumenty. Pierwszy to ilość bajtów, o jaką chcemy przesunąc wskaźnik względem pozycji wskazywanej przez drugi parametr. Wartości, jakie może przyjąć drugi parametr, to ios_base::beg, ios_base::end oraz ios_base::cur. Odpowiednio oznaczają one początek pliku, koniec pliku oraz aktualną pozycję w pliku. Pierwszy parametr może przyjmować zarówno wartości dodatnie (przesunięcie w przód), jak i ujemne (cofanie wskaźnika). Prototyp drugi pobiera tylko jeden parametr, który oznacza przesunięcie względem początku pliku. Tutaj parametr może być tylko dodatni. Oto przykłady użycia metod seekg():
fin.seekg(12, ios_base::beg); // 12 bajtów od początku pliku
fin.seekg(-2, ios_base::cur); // cofnij wskaźnik o 2 bajty
fin.seekg(0, ios_base::end); // idź na koniec pliku
Jeśli chcemy sprawdzić pozycję wskaźnika w pliku, mamy do dyspozycji dwie metody (odpowiednie dla istream i ostream) tellg() oraz tellp(). Zwracają one odległość w bajtach od początku pliku.
A na zakończenie dyskusji o wejściu i wyjściu - program bazodanowy (oczywiście prostacki, a więc z ograniczeniem ilości rekordów, bez usuwania ich oraz sortowania; to możecie już zrobić sami - wierzę, że potraficie):
place.cpp
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std; // to z czystego lenistwa :P
struct DANE
{
char imie[16];
char nazw[26];
float placa;
};
DANE dane[100];
void pisz(int i)
{
cout.width(3);
cout << right << i+1 << ": " << dane[i].imie << " " << dane[i].nazw;
cout.precision(2);
cout << "\n Zarobki: " << fixed << dane[i].placa << endl << endl;
}
void pobierz(int i)
{
cout << "Podaj imie i nazwisko:\n";
cin >> dane[i].imie >> dane[i].nazw;
cout << "Podaj zarobki dla " << dane[i].imie << " " << dane[i].nazw << ":\n";
cin >> dane[i].placa;
}
int main()
{
fstream iof;
int i = 0;
iof.open("dane.dat", ios_base::in|ios_base::out|ios_base::binary);
if (iof.is_open())
{
while (!iof.eof())
iof.read((char*)(&dane[i++]), sizeof(DANE));
iof.clear();
}
else iof.open("dane.dat", ios_base::out|ios_base::app|ios_base::binary);
for (int j = 0; j < i-1; ++j)
pisz(j);
cout << "\n==========\n\n";
pobierz(i);
iof.write((char*)(&dane[i]), sizeof(DANE));
iof.close();
system("pause");
return 0;
}
To tyle na dziś. Potraficie już obsługiwać pliki zarówno tekstowe, jak i binarne. W kolejnej części poznamy obsługę wyjątków w C++ - chociaż kolejna lekcja może się obsunąć o miesiąc z powodu początku studiów i mnóstwa pracy z nim związanej. Mimo wszystko - do następnego razu!
autor("ArchiE","archie007@wp.pl")
PS. Na głośnikach: Strachy na Lachy - Piła Tango
Wyszukiwarka
Podobne podstrony:
k cplIn A?ze The GameaIn A?ze Welch ein LebenDing Dong?adLa respuesta planeada será un regalo a Bin LadenOchrona przed zagroĹĽeniem piorunowym w strefach zagroĹĽonych poĹĽaremwykAJakie rodzaje i gatunki literackie występowały w II poło~AF2więcej podobnych podstron