Kurs C++ #12
Kurs C++ #12
|========== #12 ==========|
+-------------------------+
| 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ść. ;)
Bez obsługi I/O (In/Out, czyli wejścia/wyjścia) wszelkie progamy byłyby bezużyteczne. Nie byłoby można podać danych do przetworzenia i pokazać rezultatów ich przetwarzania. Jednak w C++ to temat trochę niewygodny. Bo przecież I/O to podstawa wszelkich programów, ale tutaj I/O funkcjonuje za pomocą klas i obiektów (oraz - nieznanych jeszcze - klas i metod szablonowych). Dlatego przed omówieniem ich trzeba było nauczyć się programowania obiektowego. Ale jak to zrobić, nie znając obsługi I/O?... To właśnie dlatego poznaliśmy obiekty cin i cout, badając ich możliwości "na ślepo". W dzisiejszej lekcji dowiemy się o nich nieco więcej, jak również powiemy sobie o plikach, przekierowaniu danych, strumieniu błędów i parametrach programu.
Standardowe wejście i wyjście
Mówiąc krótko: standardowe wejście to zazwyczaj klawiatura, a standardowe wyjście to ekran monitora. Oczywiście, można to zmienić, ale już z poziomu systemu operacyjnego. Przy wyjściu program kieruje swoje dane w określone miejsce, a system operacyjny odczytuje je stamtąd i wysyła na standardowe wyjście (które zazwyczaj jest monitorem, ale równie dobrze może być drukarką albo plikiem dyskowym). Odwrotnie jest przy standardowym wejściu - system operacyjny odczytuje dane (zazwyczaj z klawiatury, ale może również z pliku), a następnie zapisuje w określonym miejscu, z którego pobiera je program. To wszystko działa bardzo sprawnie i jest wykorzystywane w praktycznie wszystkich konsolowych systemach operacyjnych (DOS, UNIX, Linux).
Obiektami umożliwiającymi nam obsługę standardowego wejścia i wyjścia są znane już nieco cin i cout. Istnieje jeszcze jeden obiekt wyjściowy - cerr. Jest to wyjście standardowe błędów. Jest ono zawsze kierowane na ekran komputera (chociaż systemy UNIXowe potrafią go również przekierować za pomocą operatora 2>). Poza tym nie różni się on od cout - nazwa może sugerować związanie z obsługą błędów, ale ten obiekt nie ma żadnej szczególnej specjalizacji, może służyć również do zwykłego wyprowadzania danych.
Przekierowywanie standardowego wejścia/wyjścia
Wszystkie nasze programy uruchamia się za pomocą konsoli tekstowej. Ale pewnie większość z was nie męczyła się z przechodzieniem do folderu programu i uruchomiania go spod konsoli samemu. I bardzo dobrze - bo przecież kompilator ma nam ułatwiać pracę. Jednak tym razem napiszemy program, skompilujemy a potem dostaniemy się tam konsolą i pobawimy się przekierowaniami. Na początek - nasz prosty program:
prog.cpp
#include <iostream>
int main()
{
float x;
char str[64];
std::cerr << "Podaj liczbe: ";
std::cin >> x;
std::cerr << "Podaj tekst: ";
std::cin >> str;
std::cout << "*****************************************\n";
std::cout << "Podana liczba to " << x << std::endl;
std::cout << "Podany tekst: \"" << str << "\".\n";
return 0;
}
Nie ma problemu z "normalnym" uruchamianiem i używaniem programu, chociaż wyniki programu znikają z ekranu natychmiast po wyświetleniu (celowo nie użyłem tu funkcji system("pause")). Teraz przekierujemy dane wyjściowe. Aby tego dokonać, musimy uruchomić konsolę tekstową (start | uruchom | cmd lub command) i przejść do folderu, w którym znajduje się skompilowana wersja programu. Dla wygody możemy po prostu skopiować program do folderu aktywnego w konsoli (ścieżka wyświetlana przed znakiem zachęty).
Teraz możemy uruchomić program - wpisujemy po prostu jego nazwę w konsoli:
c:\sciezka_aktywna>program
Widzimy tu efekt działania programu. Wszystko działa normalnie. A teraz spróbujemy przekierowania standardowego wyjścia. służy do tego operator > lub >> (zarówno w DOSie, jak i w Linuksie czy UNIXie). Pierwszy z nich powoduje utworzenie pliku o nazwie podanej za operatorem, a gdyby taki plik już istniał, to zostanie on wyzerowany (skasowany). Drugi operator nakazuje dopisać przekierowywane dane na koniec istniejącego pliku. Spróbujmy więc:
c:\sciezka_aktywna>program >out.txt
Jak widać, na ekran trafiły dane z obiektu cerr, ale dane z cout znalazły się w pliku. Tutaj widzimy, w jaki sposób można zastosować cerr w praktyce. Gdyby wszystkie dane były wyświetlane za pomocą cout, to podczas przekierowania nie wiedzelibyśmy, jakich danych program od nas oczekuje.
W równie prosty sposób możemy przekierować standardowe wejście programu (obiekt cin). Stwórzmy w tym samym folderze, w którym znajduje się program, plik in.txt o następującej zawartości:
12.432
Jakistam sobie tekscik...
Przekierowane wejścia odbywa się za pomocą operatora <. Spróbujmy zatem:
c:\sciezka_aktywna>program <in.txt >out.txt
Przekierowywanie strumienia danych jest w niektórych sytuacjach przydatne, ale tylko w środowiskach tekstowych, takich jak DOS, UNIX, Linux.
Parametry wywołania programu
Jak wiemy, w C i C++ program składa się z głównej funkcji, wywoływanej przez system operacyjny (main()). Jeżeli jest to funkcja, to czy można jej przekazać parametry? Oczywiście. Można to zrobić podczas wywołania programu - są one tzw. parametrami wywołania. Przekazuje je system operacyjny. Tak działają tzw. demony w UNIXie i Linuksie. Tak naprawdę funkcja main() przyjmuje tylko dwa parametry: int argc (liczba argumentów) oraz char *argv[] (co jest równe deklaracji char **argv - uwaga: wskaźnik do tablicy wskaźników do łańcuchów tekstowych będących parametrami; to ma sens, jak się wczytacie. ;)). Za ich pomocą możemy przekazać dowolną liczbę argumentów do programu. System operacyjny każde wyrażenie następujące po wywołaniu programu traktuje jako jeden parametr. W zasadzie system robi wszystko za nas - nam pozostaje tylko sprawdzić wartość argc - jest to ilość przekazanych parametrów - oraz obsłużenie parametrów. Pierwszy parametr standardowo jest nazwą programu, więc argc jest zawsze większe lub równe 1, a argv[0] == "nazwa_programu". Uwaga dla systemów: spacja i tabulator są separatorami argumentów, więc wywołanie:
print Ala ma kota
przekaże do programu print 3 dodatkowe parametry. Jeżeli chcemy przekazać parametr zawierający białe znaki, musimy umieścić go w cudzysłowie:
print "Ala ma kota"
Pokażę, jak wygląda program print, nie zagłębiając się w szczegóły, bo to przecież prościzna:
print.cpp
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char *argv[])
{
cout << "Program: " << argv[0] << endl << endl;
for (int i = 1; i < argc; ++i)
cout << "Parametr " << i << ":\t" << argv[i];
system("PAUSE");
return 0;
}
Zastosowanie parametrów programu jest bardzo szerokie i z pewnością ta wiedza już niedługo się wam przyda. :)
PS. W Dev-C++ w menu uruchom macie polecenie 'parametry'. W otwartym oknie możecie wpisywać parametry waszego programu i zostaną one przekazane do programu tak, jakby były wpisane spod konsoli. Dzięki temu nie trzeba co chwilę kompilować programu, przełączać się na konsole i wpisywać parametry. Ponadto możemy wtedy debugować i obserwować zmienne z poziomu IDE.
Strumienie i bufory
Wejście i wyjście odbywa się poprzez strumienie i bufory. Zrozumienie różnicy pomiędzy tymi dwoma pojęciami znacząco pomaga w obsłudze I/O w C++.
Strumień jest ciągiem bajtów - np. znaków ASCII czy innych danych. Jest on odczytywany na bieżąco, w momencie, w którym nadchodzi.
Bufor jest swoistym pojemnikiem - jest on zapełniany, a odczyt jest możliwy dopiero po opróżnieniu go.
Po co taki podział? W zasadzie odczyt strumieniowy byłby wystarczający. Jednak istnienie bufora (np. wejściowego bufora klawiatury) umożliwia poprawę danych przed ich przekazaniem. W większości systemów opróżnienie bufora klawiatury następuje po wciśnięciu klawisza enter - przed jego wciśnięciem można wprowadzane dane dowolnie modyfikować. Podobnie jest z buforem wyjścia w C++ - znak nowej linii powoduje jego opróżnienie. Oczywiście za pomocą właściwego polecenia można to zrobić wcześniej - np. gdy program czeka na dane wejściowe.
Te wszystkie bufory i strumienie byłyby naprawdę skomplikowane, jednak STL oferuje nam genialny sposób ich obsługi - klasy istream i ostream. zdefiniowane są w pliku iostream. Mamy również cztery obiekty powiązane z wejściem i wyjściem: cin (bufuruwane standardowe wejście), cout (buforowane standardowe wyjście), cerr (strumieniowe standardowe wyjście błędów) oraz clog (strumieniowane standardowe wyjście logów). Obiekty cin i cout w każdym systemie możemy przekierowywać, obiekt cerr możemy przekierować tylko w systemach UNIXowych (np. Linux) za pomocą operatora 2>, natomiast clog jest na stałe "przywiązany" do ekranu.
Ten podział na strumienie i bufory ciągnie za sobą różne konsekwencje. Jedną z nich jest opróżnianie bufora wyjściowego. Rozmiar bufora wynosi 512 bajtów lub wielokrotność tej liczby. Przyspiesza to zapis w przypadku przekierowania standardowego wyjścia na dysk - niechcemy przecież, aby program żądał dostępu do dysku 512 razy. O wiele szybciej skumulować te 512 bajtów w buforze i wysłać je na dysk po zapełnieniu bufora w pojedynczej operacji dyskowej. Z pewnością przyspieszy to program oraz przedłuży żywotność dysku. ;)
Jednak w przypadku ekranu tak duży bufor jest niewygodny - nie będziemy przecież czekać, aż się on zapełni, a przekształcanie każdego komunikatu tak, aby zajmował 512 bajtów mija się z celem. Dlatego też podczas wyjścia na ekran bufor opróżniany jest dużo wcześniej. Opróżnianie zachodzi podczas przejścia do nowej linii oraz powinno zachodzić podczas oczekiwania na dane wejściowe. Np. w sytuacji:
cout << "Podaj liczbe ";
double x;
cin >> x;
Jeśli bufor nie zostanie opróżniony przed operacją wejścia, program będzie czekał na liczbę nie wyświetliwszy wcześniej komunikatu. W większości implementacji C++ przed operacją wejścia bufor jest opróżniany. Jeśli jednak tak się nie dzieje, należy opróżnić bufor jawnie, za pomocą instrukcji endl lub flush. Instrukcja flush powoduje opróżnienie bufora, a instrukcja endl opróżnia bufor i przechodzi do nowej linii. Sposób ich użycia jest znany: przekazujemy je tak, jak dane do wyświetlenia.
Klasa ostream
Klasa ostream definiuje nam wyjście programu. Obiekty cout, cerr i clog są właśnie obiektami klasy ostream. W tym podrozdziale omówię pokrótce metody tej klasy oraz sposoby formatowania tekstu.
Sposób realizacji wyjścia jest już znany - służy do tego operator <<. Jest on przeciążony tak, aby rozpoznawać wszystkie podstawowe typy C++: signed/unsigned char, int, short, long, long long, float, double i long double. W Dev-C++ niestety obsługa typu long double jest błędna, przez co nie można wyświetlać wyników, jeżeli ich zakres przekracza zakres typu double. Można samemu zaprojektować funkcję wyświetlającą typ long double prawidłowo, jednak to zadanie wykracza poza ten kurs. Zresztą - typ long double jest na prawdę rzadko wykorzystywany, dokładność i zakres typu double jest wystarczająca. Jeżeli stworzymy własny typ, możemy również dla niego przeciążyć operator <<. Zostało to przedstawione w #11 części kursu.
Wyświetlanie wartości w innym systemie liczbowym
Domyślnie wszelkie wartości są wyświetlane w systemie dziesiętnym (choć często adresy są domyślnie szesnastkowe). Jeżeli chcemy wyświetlić liczbę w innym systemie - a do wyboru mamy ósemkowy, dziesiętny i szesnastkowy - musimy poinformować o tym obiekt cout. Robimy to za pomocą manipulatorów:
dec powoduje wyświetlenie liczby w systemie dziesiętnym
oct przekształca liczbę do systemu ósemkowego
hex wyświetla wartość szesnastkową
Używanie manipulatorów jest identyczne jak w przypadku endl lub flush. Przykład:
Dostosowanie szerokości pól
Zazwyczaj wyświetlany "obiekt" (czyli liczba bądź łańcuch tekstowy) zajmuje całą swoją szerokość, np. liczba 12 zajmuje pole dwumiejscowe. I to świetnie zdaje egzamin, ale czasami chcemy zbudować np. jakąś ładną tabelkę. Wtedy wypdałoby, aby pola miały standardową, stałą szerokość. Do tego służy metoda składowa width(). Ma ona dwie formy:
int width();
int width(int n);
Pierwsza metoda zwraca aktualną szerokość pola (domyślna szerokość to 0, co oznacza, że cout sam dostosowuje szerokości pól). Druga ustawia szerokość n i zwraca szerokość dotychczasową (przed zmianą). Istotne jest, że ta metoda działa tylko dla pierwszego po sobie wyświetlanego elementu, a później powraca do standardowej szerokości:
cout << "|";
cout.width(10);
cout << 14 << "|" << 15 << "|" << 16 << "|\n";
Wynik wykonania tego fragmentu kodu to:
| 14|15|16|
Ważne jest też to, że w C++ treść jest ważniejsza od formy, co oznacza, że wartości nigdy nie są przycinane. Jeżeli liczba ma 5 cyfr, a pole ma szerokość 2, to pole zostanie rozszerzone tak, aby zmieścić liczbę.
Jest jeszcze jadna użyteczna metoda wiążąca się z szerokością pola - metoda fill(). Zmienia ona domyślne wypełnienie pola spacją na dowolny podany w parametrze znak (np. cout.fill('*')). Czasami bywa to przydatne. W przeciwieństwie do width(), wypełnienie obowiązuje do czasu jego zmiany.
Ustawianie precyzji wyświetlania liczb zmiennoprzecinkowych
Domyślna dkładność wyświetlania liczb zmiennoprzecinkowych to 6 cyfr. Jeżeli chcemy zmienić tę dokładność, możemy skorzystać z metody precision(). Przyjmuje ona w parametrze liczbę, która określa ilość wyświetlanych cyfr zmiennoprzecinkowych. Jednak często taka manipulacja to za mało - np. czasami lepiej wyglądają zachowane końcowe zera. Wtedy do pomocy możemy wykorzystać metodę setf() i klasę ios_base, zawartą w przestrzeni nazw std. W przestrzeli ios_base znajduje się kilka flag, które pozwalają na pełną manipulację wyświetlanymi wartościami.
Metoda setf() posiada dwa prototypy:
fmtflags setf(fmtflags);
fmtflags setf(fmtflags, fmtflags);
Nazwa fmtflags jest definicją typedef tzw. maski bitowej. Maska bitowa to po prostu liczba całkowita, w której poszczególne bity odpowiadają za ustawienie określonych flag (wartości boolowskich). Pierwszy prototyp odpowiada za parametry sterowane jednym bitem. W obu przypadkach wartość zwracana to dotychczasowe ustawienie flag. Jeśli chcemy ustawić bądź zmienić 7. bit, przekazujemy w parametrze liczbę, której 7. bit ma ustawianą wartość (1<<7). Wtedy funkcja zwróci liczbę, w której 7. bit ma wartość dotychczasową. Takie śledzenie stanu bitów jest trudne, nudne, żmudne i nieefektywne. Na szczęście w klasie ios_base mamy zdefiniowanych kilka wartości, które ustawiają właściwe flagi.
Parametry sterowane jednym bitem to np. ois_base::showpos, który nakazuje wyświetlaś znak plusa w wyświetlanych wartościach. Oto lista parametrów sterowanych metodą setf():
ios_base::showpos - wyświetlanie znaku plusa przed liczbami dodatnimi
ios_base::showpoint - wyświetlanie kropki dziesiętnej i końcowych zer (np. zamiast 2 - 2.00000)
ios_base::showbase - wyświetlanie przedrostków systemów liczbowych zgodnych z C++ (0, 0x)
ios_base::upperase - wyświetlanie wielkich liter w liczbach szesnastkowych
ios_base::boolalpha - wyświetlanie i pobieranie wartości boolowskich jako true i false
Parametry sterowane większą liczbą bitów są ustawiane za pomocą drugiego prototypu. Mają one zastosowanie wtedy, gdy zamiast zwykłego włączenia jednego pibu, trzeba zmienić stan innego - tak jest podczas zmiany systemów liczbowych, gdzie trzeba włączyć bit ustawianego systemu, a wyłączyć dotychczasowy. Pierwszym parametrem jest ustawiany bit, a drugim kasowane pole. Najpierw właściwe pole zostaje wykasowane, a następnie właściwy bit zostaje ustawiony.
Pierwszy parametr jest zależny od drugiego. Oto te zależności:
Notacja stałoprzecinkowa oznacza stosowanie zapisu 13.6 niezależnie od rozmiaru liczby, natomiast notacja naukowa oznacza stosowanie zapisu 1.36e01 niezależnie od rozmiaru liczby. Ponadto dla trybu domyślnego ustawienie precyzji wyświetlania oznacza ilość wszystkich cyfr w liczbie, natomiast przy notacjach fixed i scientific oznacza ona ilość miejsc po przecinku, a końcowe zera zawsze są wyświetlane.
Do manipulacji flagami służy jeszcze jedna metoda - unsetf(). powoduje ona skasowanie ustawionej wartości. Po ustawieniu ios_base::boolalpha, możemy tę flagę zmienić, wywołując cout.unsetf(ios_base::boolalpha). To samo dotyczy operacji dwuparametrowych - np. po ustawieniu wyświetlania liczb zmiennoprzecinkowych na fixed, możemy powrócić do domyślnego ustawienia zaq pomocą cout.unsetf(ios_base::floatfield). Spowoduje to wyzerowanie pól odpowiadających za wyświetlanie liczb zmiennoprzecinkowych, co powoduje powrót do ustawień domyślnych.
Teraz coś na osłodę: nie trzeba zapamiętywać tych wszystkich flag i przełączników. Większość implementacji C++ posiada zdefiniowane manipulatory strumieniowe odpowiadające za te operacje. Można ich używać tak samo, jak endl czy flush. Oto ich lista:
boolalpha - setf(ios_base::boolalpha)
noboolalpha - unsetf(ios_base::boolalpha)
showbase - setf(ios_base::showbase)
noshowbase - unsetf(ios_base::showbase)
showpoint - setf(ios_base::showpoint)
noshowpoint - unsetf(ios_base::showpoint)
showpos - setf(ios_base::showpos)
noshowpos - unsetf(ios_base::showpos)
uppercase - setf(ios_base::uppercase)
nouppercase - unsetf(ios_base::uppercase)
internal - setf(ios_base::internal, ios_base::adjustfield)
left - setf(ios_base::left, ios_base::adjustfield)
right - setf(ios_base::right, ios_base::adjustfield)
dec - setf(ios_base::dec, ios_base::basefield)
hex - setf(ios_base::hex, ios_base::basefield)
oct - setf(ios_base::oct, ios_base::basefield)
fixed - setf(ios_base::fixed, ios_base::floatfield)
scientific - setf(ios_base::scientific, ios_base::floatfield)
Klasa istream
...oraz
Pliki...
Wybaczcie, ale nie miałem czasu, aby dokończyć temat, dlatego omówienie wyjścia oraz obsługi plików przesuwam na następny miesiąc. Zresztą, już samo wyjście zajęło mi wystarczająco dużo tekstu, a wątpię, by ktoś chciał się jeszcze raz przebijać przez lekcję tak długą jak np. trzecia. ;) Na razie przyswójcie sobie ten temat, a w przyszłym miesiącu zajmę się równie dogłębnie wyjściem oraz plikami. Powodzenia!
autor("ArchiE","archie007@wp.pl")
PS. Na głośnikach: Kaiser Chiefs - Yours truly, angry mob
Wyszukiwarka
Podobne podstrony:
k cpl2k cpl?k cplk cpl1k cpl?k cpl?k cplk cplr08 cpl t (3)t p cplk cpl0k cpl0k cplk cplk cpl1k cplawięcej podobnych podstron