Operacje We/Wy

Operacje we/wy nie są zdefiniowane w samym języku C++. Umożliwiają je biblioteki.
Takie biblioteki standardowo dołączane są przez producenta danego kompilatora.
Wyróżnia się trzy typowe biblioteki:

- biblioteka stdio (standard input/output) - wykorzystywana przez programistów C.
Używanie jej w C++ nie jest w dobrym stylu.
- biblioteka stream - stara wersja biblioteki iostream - wycofywana z kompilatorów.
- biblioteka iostream (input/output stream - strumień wejściowy/wyjściowy) -
Ta biblioteka jest zalecana do stosowania w programowaniu w C++.

W poniższym tekście będzie opisana biblioteka iostream. Aby przeprowadzić jakiekolwiek
operacje na plikach, trzeba do programu dołączyć pliki iostream.h, fstream.h. Najpierw jednak
wprowadzimy kilka niezbędnych pojęć:

Strumień

Wprowadzanie i wyprowadzanie informacji można potraktować jako strumień bajtów płynący od
źródła do ujścia. Są cztery predefiniowane strumienie:

cout    - jest powiązany ze standardowym wyjściem (ekranem)
cin     - jest powiązany ze standardowym wejściem (klawiatura)
cerr    - jest powiązany ze standardowym urządzeniem, na które chcemy wypisywać komunikaty
o błędach (zwykle ekran). Strumień jest niebuforowany.
clog    - jak wyżej, ale strumień jest buforowany.

Strumienie cout, cin, cerr, clog są obiektami klasy istream i klasy ostream, zdefiniowanych
w bibliotece iostream.h.

Operator << oraz operator >>

cin >> x;

Powyższa instrukcja pozwala na wczytanie z klawiatury liczby x (ze standardowego wejścia
wyjmowany jest strumień bajtów i wstawiany do zmiennej x). Operator >> jest nazywany operatorem
ekstrakcji. Instrukcja

cout << "hello world!";

pozwala na wyświetlenie na ekranie łańcucha znaków (łańcuch znaków wstawiany jest na standardowe
wyjście). Operator << nazywa się operatorem wstawiania.

Ćwiczenie: Przetestować poniższy program.

#include <stdio.h>
main()
{
int i;
float f;
char tekst[80];

cout << "podaj liczbe int: ";
cin >> i;
cout << "podales: " << i << endl;

cout << "podaj liczbe float: ";
cin >> f;
cout << "podales: " << f << endl;

cout << "napisz wyraz: ";
cin >> tekst;
cout << "napisales: " << tekst << "\n";
}

Sterowanie formatem i flagi stanu formatowania

Przy operacjach na strumieniu używane są pewne domniemania dotyczące formatu informacji
wstawianej, bądź wyjmowanej ze strumienia. Jeśli owe domniemania nam nie odpowiadają, to
możemy je zmienić i od tej pory dany strumień będzie wczytywał, lub wypisywał według nowych
zasad (według nowego formatu).

Bieżące zasady formatowania zapisane są w tak zwanych flagach stanu formatowania (format
state flags). Flagi te są poszczególnymi bitami w słowie long i znajdują się w klasie
nazwanej ios (input output state), która jest dziedziczona przez klasy istream, oraz ostream.

// flagi stanu formatowania sa nastepujace
public:
enum {
    skipws      = 0x0001,   // przeskakuje białe znaki
    left        = 0x0002,   // wyrównanie do lewej
    right       = 0x0004,   // wyrównanie do prawej
    internal    = 0x0008,   // justowanie
    dec     = 0x0010,   // konwersja decymalna
    oct     = 0x0020,   //      oktalna
    hex     = 0x0040,   //      hexadecymalna
    showbase    = 0x0080,   // pokaż podstawę konwersji
    showpoint   = 0x0100,   // pokaż kropkę dziesiętną
    uppercase   = 0x0200,   // wielkie litery
    showpos = 0x0400,   // znak "+" w liczbach dodatnich
    scientific  = 0x0800,   // notacja naukowa
    fixed       = 0x1000,   // notacja zwykła
    unitbuf     = 0x2000,   // buforowanie
    stdio       = 0x4000,   // współpraca z stdio
};

Klasa ios w przybliżeniu wygląda następująco:

class ios {
    long flagi_stanu_formatowania;
public:
    long setf(long, long);
    long setf(long);
    long unsetf(long);

    int width(int);
    int precision(int);
        // i wiele innych
}

Jak widać, w klasie ios znajdują się również funkcje składowe. Za ich pomocą możemy mniej,
lub bardziej wygodnie posługiwać się flagami.

Ponieważ standardowe strumienie cin, cout, clog, cerr są obiektami klasy istream lub ostream
i dziedziczą po klasie ios, więc funkcję składową klasy ios można wywołać następująco:

cout.funkcja_skladowa_klasy_ios();

czyli na przykład

cout.precision(x);

Przejdźmy do omówienia poszczególnych funkcji klasy ios. Jeśli chcemy zmienić jakąś flagę
formatowania, to możemy to zrobić posługując się na przykład takimi funkcjami składowymi
klasy ios jak:

long setf(long które_flagi);
long unsetf(long_ktore_flagi);

Funkcje jako rezultat zwracają dotychczasową wartość słowa stanu formatowania. Instrukcja

cin.setf(ios::skipws);

ustawi flagę skipws odpowiadającą za ignorowanie białych znaków przez strumień wejściowy cin.
Inne flagi nie zmienią się. Instrukcja

cin.unsetf(ios::skipws);

działa odwrotnie: flaga skipws zostanie skasowana.

Ćwiczenie: Przetestować działanie programu

#include <iostream.h>

main()
{
float x = 1175;
long stare_flagi;

    cout << x << endl;
    cout << "zapamietanie stanu formatowania\n";
    stare_flagi = cout.flags();

    cout.setf(ios::showpoint);
    cout << x << endl;

    cout.setf(ios::scientific, ios::floatfield);
    cout << x << endl;

    cout.setf(ios::uppercase);
    cout << x << endl;

    unsetf(ios::showpoint);
    cout << x << endl;

    cout << "powrot do starego sposobu formatowania\n";
    cout.flags(stare_flagi);
    cout << x << endl;
}

funkcja flags() nie wymaga komentarza. Natomiast należy skomentować linijkę

cout.setf(ios::scientific, ios::floatfield);

Otóż niektóre flagi należą do wspólnych grup zwanych polami. Na przykład flagi ios::dec,
ios::hex i ios::oct należą do wspólnej grupy zwanej polem ios::basefield. Jeśli chcemy, by na
przykład strumień wyjściowy wypisywał liczby w zapisie szesnastkowym, to musimy nie tylko ustawić
flagę ios::hex, ale też wyczyścić flagę ios::dec. Zapis

cout.setf(ios::hex);
cout.unsetf(ios::dec);

jest dość niewygodny. Dzięki temu, że flagi należą do wspólnego pola ios::basefield, możliwy
jest zapis

cout.setf(ios::hex, ios::basefield);

który ustawi flagę ios::hex i skasuje wszystkie pozostałe należące do pola. Podobnie ma się
sprawa z flagami ios::scientific i ios::fixed - należą do wspólnego pola ios::floatfield;

Stan formatowania można też zmieniać wygodniej - za pomocą dodatkowych funkcji, np.:

int width(int);
int width();

int precision(int);
int precision();

in fill(char);
int fill();

Ćwiczenie: Przetestować działanie poniższego fragmentu programu; na przykładzie programu
zapoznać się z działaniem funkcji precision() i fill()

int liczba = 107.51578;
cout << "Wypis:" << liczba << "\nWypis:";
cout.width(15);
cout << liczba << endl;

Operacje we/wy na plikach

Chcąc czytać, bądź zapisywać do plików, mamy do dyspozycji klasy:

ofstream - output file stream - zapis do plików,
ifstream - input file stream - odczyt z plików,
fstream - file stream - oba powyższe.

Aby móc posłużyć się tymi klasami należy do programu włączyć plik nagłówkowy tego
fragmentu biblioteki, czyli:

#include <fstream.h>

Klasa ofstream jest pochodną klasy ostream (klasa ofstream dziedziczy po klasie ostream),
a klasa ifstream jest pochodną klasy istream. Analogicznie klasa fstream jest pochodną
klasy iostream.

Tych faktów w zasadzie nie trzeba pamiętać, ale ważny jest wniosek z nich płynący. Skoro
te klasy są pochodnymi klas istream i ostream, to znaczy, że dziedziczą wszystkie cechy
i zachowania swych klas podstawowych. Oznacza to w praktyce, że wszystko, co powiedzieliśmy
do tej pory o strumieniach, obowiązuje także tutaj. Konkretnie: możemy także używać
operatorów << i >> i wcześniej omówionych funkcji. Różnica polega na tym, że tutaj strumienie
nie są predefiniowane - czyli zdefiniowane za nas przez kompilator. Musimy je więc sami
zdefiniować. Zatem, aby czytać z pliku (lub pisać do niego) należy:

- zdefiniować strumień, czyli stworzyć obiekt klasy ifstream, ofstream, bądź fstream,
- określić strumieniowi z jakim konkretnie plikiem ma się komunikować (przypisać strumień
do pliku) i otworzyć ten plik,
- przeprowadzić operacje we/wy
- zlikwidować strumień, gdy praca z plikiem jest zakończona.

Ćwiczenie: Przetestować działanie poniższego programu

#include <iostream.h>
#include <fstream.h>

main()
{
ofstream osrodek;

    osrodek.open("ksiezyc.tmp");
    osrodek << "to takie cialo niebieskie";
    osrodek.close();
}

Otwieranie i zamykanie strumienia

Za otwieranie strumienia odpowiedzialna jest funkcja open.

void open(char* nazwa, int tryb = ***, int prot = filebuf::openprot);

Jeśli chodzi o trzeci argument, to reguluje on takie sprawy, jak prawa dostępu do pliku
innych użytkowników - nie będziemy się tym zajmować.

Pierwszy argument to oczywiście nazwa pliku.

Drugi parametr określa tryb pracy z zadanym plikiem (czy będzie to odczyt danych, czy zapis).
W miejsce gwiazdek należy wstawić odpowiedni tryb pracy. A oto następujące tryby:

ios::in - (input) Otwórz plik do czytania
ios::out - (output) Otwórz plik do zapisu
ios::ate - (at end) Otwórz plik i ustaw się na końcu zawartości
ios::app - (append) Otwórz do dopisywania
ios::trunc - (truncate) Otwórz, a jeśli plik istnieje - skasuj starą zawartość
ios::oncreate - (o create) Otwórz jeśli plik już istnieje
ios:: noreplace - (no replace) Otwórz jeśli plik nie istnieje
ios::binary - Tryb ma być binarny

Zamykamy strumień wykorzystując funkcję close();

Ćwiczenie: Przetestować działanie poniższego programu

main()
{
int x,y,z;
fstream plik;
plik.open("tabela.tmp", ios::out|ios::trunc);
plik.width(10);
plik << 10 << " " << 20 << " " << 30;
plik.close();

plik.open("tabela.tmp", ios::in);
plik >> x >> y >> z;
plik.close();

cout << x << endl << y << endl << z << endl;
getch();
}

Wskaźniki

Z każdym plikiem skojarzone są dwa wskaźniki: wskaźnik get - wskazujący na miejsce,
z którego czytamy w pliku oraz wskaźnik put - wskazujący miejsce w pliku, gdzie zapisywane
są dane. Wskaźniki te można dowolnie pozycjonować funkcjami seekg(), seekp(). Pierwsza
z nich pozycjonuje wskaźnik get, druga z nich pozycjonuje wskaźnik put. Dzięki temu, w połączeniu
ze sposobami otwarcia pliku, możliwy jest jednoczesny odczyt i zapis do tego samego pliku
w dowolnych miejscach, wskazywanych przez wskaźniki get i put.

Ćwiczenie: Przetestować działanie programu

main()
{
    char tekst[] = {"To jest pierwsza linia tekstu\n"
            "To jest druga linia tekstu\n"
            "A to trzecia i ostatnia linia tekstu\n"};

    fstream strum("wiersz.tmp", ios::in|ios::out);
    strum << tekst;

    strum.seekp(25);  //pozycjonowanie kursora pisania na literze 25
    strum << "ABCDE";

    strum.seekp(-6, ios::end); //pozycjonowanie kursora pisania na 6 literze od konca
    strum << "X";
}
