PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C++

Tworzenie i edycja pliku rekordów Program tworzenia pliku rekordów Moduł czytania znaku (wersja 2, CP852) Moduł edycji łańcucha i listy pól Moduł edycji daty Program edycji pliku rekordów Poprzedni przykład Następny przykład Kontakt

Tworzenie i edycja pliku rekordów

Chyba nie ma użytkownika komputera, który by nie próbował zobaczyć, posłu­gując się prostym edytorem tekstu, jak wygląda plik binarny. Wyświe­tlona na ekranie zawartość okazuje się nieczy­telna – oprócz łańcuchów, chociaż i one wymagają domysłów, np. gdzie się zaczynają i gdzie kończą. Przyczyna tkwi w trakto­waniu bajtów pliku jako kodów znaków drukarskich i symboli końca wierszy tekstu, pomimo że stanowią one wewnętrzną reprezen­tację danych – taką jak w pamięci komputera. Na przykład liczba całkowita 384400 wyraża­jąca w kilo­metrach średnią odle­głość Księżyca od Ziemi zajmuje w pliku binarnym 4 bajty zakodowane szesna­stkowo w kolej­ności od najmłod­szego do najstar­szego (konwencja Intel) jako 90, DD, 05 i 00. Ich obrazem w edytorze tekstu jest ciąg kilku nieinterpre­towalnych znaczków. Ta sama liczba w pliku tekstowym zajmuje 6 bajtów reprezen­tujących kolejne cyfry dziesiętne o kodach szesna­stkowych ASCII 33, 38, 34, 34, 30 i 30. Przesy­łanie danych pomiędzy pamięcią wewnętrzną a plikiem binarnym nie wymaga żadnej konwersji, przez co jest szybsze niż w przypadku plików tekstowych.

Plik binarny o określonej strukturze elementów składowych nazywanych zwyczajowo rekordami (rekordy w Pascalu, struktury w C i C++) można aktua­lizować, wymie­niając rekordy zawieraj­ące zdezaktua­lizowane dane na nowe. W pliku tekstowym nie można bez przepisy­wania całej jego zawartości zastąpić dotychcza­sowego wiersza nowym, gdyż długości obu wierszy mogą nie być takie same, można co najwyżej dopisać nowe wiersze na końcu pliku. Struktura plików zawiera­jących zwykły tekst (ang. plain text) jest uniwer­salna, toteż istnieje cała gama edytorów umożliwia­jących ich redago­wanie, jak np. Notatnik w Windows czy edytor wbudowany w środowisku programi­stycznym (dokument Worda nie jest plikiem tekstowym). W przypadku plików binarnych sytuacja jest zupełnie inna – odczyt, zapis i wyświe­tlanie zawartości tych plików wymaga opraco­wania specjal­nych programów uwzględnia­jących ich szczegółową strukturę.

Naszym zamierzeniem jest opracowanie programu konso­lowego w języku C++ umożliwia­jącego redago­wanie (tworzenie i edycja) przykła­dowego pliku binarnego, którego rekordy zawierają dane fikcyjnych osób: imię, nazwisko, płeć, data urodzenia, numer telefonu i adres e-mail. Naturalnym oczeki­waniem jest uwzglę­dnienie polskich liter kodowa­nych w stan­dardzie CP852. Strukturę daty i rekordów definiujemy w pliku nagłów­kowym osoby.h postaci:

#ifndef H_DATA_STR
#define H_DATA_STR

const int DT = 10;          // Długość łańcucha DD.MM.RRRR

struct Date
{
    short r;                // Numer roku
    char  m;                // Numer miesiąca
    char  d;                // Numer dnia
};

#endif // H_DATA_STR

#ifndef H_OSOBA_STR
#define H_OSOBA_STR

const int IM = 12;          // Maksymalna długość imienia
const int NA = 21;          // Maksymalna długość nazwiska
const int TE = 13;          // Maksymalna długość nru tel.
const int EM = 21;          // Maksymalna długość e-mail

struct Osoba
{
    char  imie[IM + 1];     // Imię
    char  nazwisko[NA + 1]; // Nazwisko
    char  plec;             // K - kobieta, M - mężczyzna
    Date  dtur;             // Data urodzenia (DD.MM.RRRR)
    char  tel[TE + 1];      // Numer telefonu
    char  email[EM + 1];    // Adres e-mail
};

#endif // H_OSOBA_STR

Rozmiary pól tablicowych struktury Osoba są o 1 większe od maksy­malnych długości łańcuchów dlatego, że w przypadku osiągnięcia takiej długości w polu powinien zmieścić się dodatkowy znak '\0' kończący łańcuch. Należy podkreślić, że zamieszczone na niniejszej witrynie pliki binarne osób zawierają fikcyjne dane i ich wszelkie podobień­stwo do danych prawdzi­wych postaci jest przypad­kowe i nieza­mierzone.

Program tworzenia pliku rekordów

W celu utworzenia pliku binarnego złożonego z rekordów typu struktu­ralnego Osoba budujemy prosty program, który dołącza sekwen­cyjnie kolejne rekordy na końcu pliku. Wartości pól rekordu przechowy­wanego w pamięci i pełniącego rolę bufora można wczytywać z klawia­tury. Zapis rekordu do pliku powinien nastę­pować dopiero po skomple­towaniu wszystkich pól. Rzecz jasna należy przewi­dzieć skrajny przypadek, gdy na początku plik nie istnieje, a także zadbać o wygodne zakoń­czenie procesu dopisy­wania nowych rekordów. Wersja tego programu z mini­malną kontrolą wczyty­wanych danych (płeć, data urodzenia, długości pól) może wyglądać następu­jąco:

#include <fstream>
#include <iostream>
#include <string.h>
#include <conio.h>
#include "osoby.h"

using namespace std;

bool stop(string txt)
{
    cout << endl << txt << ": T - tak, N - nie";
    int c;
    while ((c = toupper(_getch())) != 'T' && c != 'N')
        ;
    return c == 'N';
}

bool dataOK(int r, int m, int d)
{
    static int DM[] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if (r < 1582 || r > 9999 || m < 1 || m > 12 || d < 1 ||
        (r == 1582 && (m < 10 || (m == 10 && d < 15)))) return false;
    DM[2] = ((r % 4 == 0 && r % 100 != 0) || r % 400 == 0) ? 29 : 28;
    return d <= DM[m];
}

void czytaj(const char *info, int n, char *wynik)
{
    cout << info << ": ";
    char s[80];
    cin >> s;
    switch(n)
    {
        case 0:                       // Data
            int d, m, r;
            if (sscanf(s, "%d.%d.%d", &d, &m, &r) != 3 || !dataOK(r, m, d))
                d = m = r = 0;
            ((Date *)wynik)->r = r;
            ((Date *)wynik)->m = m;
            ((Date *)wynik)->d = d;
            break;
        case 1:                       // Płeć
            int c;
            *wynik = ((c = toupper(*s)) == 'K' || c == 'M') ? c : ' ';
            break;
        default:                      // Inne
            strncpy(wynik, s, n);
            wynik[n] = '\0';
    }
}

int main()
{
    ofstream plik("Osoby.dta", ios::app | ios::binary);
    if (!plik) return 1;
    Osoba rec;
    for (;;)
    {
        czytaj("Imię        ", IM, rec.imie);
        czytaj("Nazwisko    ", NA, rec.nazwisko);
        czytaj("Płeć (K,M)  ", 1, &rec.plec;
        czytaj("Data urodz. ", 0, (char *)&rec.dtur);
        czytaj("Nr telefonu ", TE, rec.tel);
        czytaj("Adres e-mail", EM, rec.email);
        if (stop("Zapis")) break;
        if (!plik.write((char *)&rec, sizeof(rec))) return 2;
        if (stop("Dalej")) break;
        cout << endl << endl;
    }
    plik.close();
    return 0;
}

Uwaga. Kompilator Visual C++ traktuje funkcje sscanfstrncpy wywoływane w funkcji czytaj jako poten­cjalnie niebez­pieczne i sugeruje użycie ich odpowie­dników sscanf_sstrncpy_s lub wyłą­czenie zabezpie­czeń dotyczą­cych zanie­chania przesta­rzałych funkcji. W programie uwzglę­dniono pierwszą sugestię.

Program używa standardowych strumieni wejścia-wyjścia języka C++. Plik jest w funkcji main kojarzony ze strumie­niem wyjściowym typu ofstream, a ponieważ jest binarny, należało wpłynąć na sposób otwarcia go za pomocą znacznika ios::binary. Jest to konieczne, gdyż domyślnie byłby traktowany jako tekstowy. Z kolei znacznik ios::app określa, że ma być otwarty w trybie dopisy­wania danych na końcu (ang. append). Gdy plik nie istnieje, zostanie utworzony. Można zablokować tworzenie nowego pliku, dodając znacznik ios::nocreate. Znaczniki są stałymi bitowymi, toteż można je łączyć, używając opera­torów bitowych. Zazwyczaj używa się alterna­tywy bitowej |.

Pola zmiennej strukturalnej rec w funkcji main są wypełniane przez funkcję czytaj, która pobiera z klawia­tury łańcuch i interpre­tuje go jako datę, płeć bądź jako imię, nazwisko, numer telefonu lub adres e-mail. Trzy liczby całko­wite reprezen­tujące datę (numer dnia, miesiąca i roku) są odczy­tywane przez funkcję sscanf, która zamiast standar­dowego wejścia w C przeszu­kuje łańcuch s zgodnie z formatem "%d.%d.%d". Kropka po symbolu formato­wania oznacza, że jest ona oczekiwana na wejściu, łańcuch powinien więc zawierać trzy liczby całko­wite oddzielone od siebie kropkami, czyli stanowić zapis daty w konwencji polskiej. Wynikiem wywołania funkcji sscanf jest, podobnie jak w przy­padku scanffscanf, liczba wczytanych pól. Jeśli zatem łańcuch reprezen­tuje datę, funkcja zwraca wartość 3. Funkcja dataOK sprawdza, czy wczytane trzy liczby są poprawnymi składnikami daty kalen­darza gregoriań­skiego.

Skompletowany w pamięci rekord jest zapisywany do pliku za pomocą metody write, której pierwszym argu­mentem jest wskaźnik typu char* na bufor pamięci zawiera­jący dane, drugim liczba przesy­łanych bajtów. Funkcja zwraca referencję do stru­mienia, gdy operacja przebiegła pomyślnie, lub zero, gdy w trakcie zapisu rekordu do strumienia wystąpił błąd. Operację zamknięcia pliku można pominąć, gdyż otwarty plik jest automa­tycznie zamykany przez destru­ktor obiektu, gdy kończy się wykonanie bloku, w którym strumień został utworzony (zob. mechanizm wywoły­wania konstru­ktorów i destru­ktorów). Zwracany przez funkcję main kod powrotu 0 oznacza, że program został wykonany poprawnie, kod 1, że otwarcie lub utwo­rzenie pliku nie powiodło się, kod 2, że wystąpił błąd zapisu do pliku.

Warto przy okazji dopowiedzieć, że binarny strumień wejściowy typu ifstream zawiera metodę read, która przesyła dane w odwrotnym do write kierunku, wczytując rekord (blok danych) z pliku do bufora pamięci. Domyślnym dla tego strumienia jest znacznik ios::in (ang. input, wejście/odczyt), a dla strumienia ofstream znacznik ios::out (ang. output, wyjście/zapis). Uniwersalny strumień binarny typu fstream umożliwia zarówno czytanie rekordów z pliku, jak i zapisy­wanie rekordów do pliku za pomocą funkcji readwrite, oczywiście pod warunkiem, że przy określaniu trybu otwarcia go użyto obydwu znaczników:

ios::in | ios::out

Moduł czytania znaku (wersja 2, CP852)

Podstawową wadą powyższego programu tworzenia pliku binarnego jest brak możliwości korekty zapisanych danych, a nawet ich podglądu. Zanim podej­miemy się budowy nowego programu o lepszych możliwo­ściach edycyjnych, zmodyfi­kujemy funkcję czytajZnak zdefinio­waną w module czytania znaku tak, by udostę­pniała kody polskich liter w systemie kodowania CP852 używanym w aplika­cjach konso­lowych polskiej wersji Windows. Kod źródłowy zawarty w pliku nagłów­kowym ulepszo­nego modułu klaw2 zawiera w porównaniu z poprze­dnim definicję dodatkowej stałej K_DEL określa­jącej kod klawisza Delete użyte­cznego w edyto­rach tekstu (usuwanie znaku pod kursorem):

// klaw2.h - Moduł czytania znaku z uwzględnieniem polskich liter
// --------------------------------------------------------------
// czytajZnak - pobiera znak z klawiatury i zwraca jego kod CP852
// --------------------------------------------------------------

#ifndef H_KLAW2
#define H_KLAW2

#include <conio.h>

#define K_UP    (256 + 72)   // Strzałka w górę
#define K_DN    (256 + 80)   // Strzałka w dół
#define K_PGUP  (256 + 73)   // PgUp
#define K_PGDN  (256 + 81)   // PgDn
#define K_LEFT  (256 + 75)   // Strzałka w lewo
#define K_RIGHT (256 + 77)   // Strzałka w prawo
#define K_HOME  (256 + 71)   // Home
#define K_END   (256 + 79)   // End
#define K_TAB   9            // Tab
#define K_DEL   (256 + 83)   // Delete
#define K_BACKS 8            // Backspace
#define K_ENTER 13           // Enter
#define K_ESC  27            // Esc

int czytajZnak();            // Czytanie znaku

#endif // H_KLAW2

Funkcja czytajZnak wymaga niewielkich zmian, jednak ze względu na nieco inne działanie funkcji bibliote­cznej getch w Borland C++ niż _getch w MinGW C++ i Visual C++ jej implemen­tację umieścimy w dwóch plikach klaw2.cpp, pierwszym dla kompila­tora Borland C++, drugim dla pozostałych. Przypo­mnijmy, że klawisze nawiga­cyjne (strzałki, Home, PgUp itp.), funkcyjne (F1, F2 itd.) i niektóre kombi­nacje klawiszy steru­jących (Ctrl i Alt) z innymi generują w buforze klawia­tury dwa znaki zamiast jednego – pierwszy o kodzie 0 lub 224, drugi o różnym od zera. W przypadku Borland C++ pierwszy z pary ma zawsze kod 0, a polskie znaki diakry­tyczne mają kod ujemny, który po zwiększeniu o 256 jest zgodny z tabelą CP852. Druga część modułu klaw2 dla tego kompila­tora może więc mieć postać:

#include "klaw2.h"

int czytajZnak()
{
    int c = getch();
    if (c < 0) c += 256;
    return (c != 0) ? c : 256 + getch();
}

Zwracane przez funkcję _getch dwóch nowszych kompila­torów kody polskich znaków są zgodne z systemem kodowania CP852, nie wymagają więc żadnej modyfi­kacji. Drobny kłopot sprawia litera Ó, która ma kod 224, czyli taki sam jak pierwszy z pary podwój­nych znaków. Problem rozwią­zujemy w ten sposób, że jeśli bufor nie jest pusty, kod ten nie repre­zentuje litery Ó, lecz pierwszy z pary podwójnych znaków:

#include "klaw2.h"

int czytajZnak()
{
    int c = _getch();
    if ((c == 224) & _kbhit())          // Jeśli nie litera Ó
        c = 0;                          // to znak specjalny
    return (c != 0) ? c : 256 + _getch();
}

Moduł edycji łańcucha i listy pól

Zajmiemy się obecnie zdefiniowaniem uniwersalnej klasy Edit służącej do edycji łańcucha przechowy­wanego w tablicy znaków i zakoń­czonego znakiem '\0'. Klasa ma umożliwiać typowe operacje edycyjne, takie jak przesu­wanie kursora w obrębie tekstu (klawisze strzałka w lewo i prawo), przeskok kursora na początek tekstu i jego koniec (Home i End), usuwanie znaku przed kursorem i pod nim (Backspace i Delete), zatwier­dzenie wprowa­dzonych zmian (Enter) i rezygnację z edycji (Esc). Ponadto powinna umożliwiać określenie maksy­malnej długości łańcucha i nie dopuszczać do jej przekro­czenia.

Zaproponowane poniżej rozwiązanie edycji łańcucha polega na wykorzy­staniu specjalnej funkcji editStr wywołuj­ącej edycję łańcucha reprezento­wanego przez obiekt klasy Edit. Dzięki mechani­zmowi dziedzi­czenia klas można ją stosować do edycji pól zawiera­jących inne rodzaje danych, jak np. data, płeć, liczba, kod pocztowy. Druga zaprezen­towana funkcja o nazwie editRec umożliwia edycję szeregu pól połączo­nych w listę jednokie­runkową. Funkcję można stosować do edycji rekordu wyświe­tlanego w układzie formu­larza.

Definiowanie klasy Edit rozpoczynamy od sprecyzo­wania jej pól składowych. Jest oczywiste, że powinny one określać bufor edycyjny przecho­wujący edytowany łańcuch, jego maksymalną długość, pozycję pola edycji na ekranie i bieżące położenie kursora. Potrzebny jest też dostęp do pamięci, w której znajduje się oryginalny łańcuch. Przed rozpo­częciem edycji łańcuch ten jest kopiowany do bufora jako roboczy, a po jej zakoń­czeniu łańcuch z bufora jest kopiowany w miejsce orygi­nalnego jako wynikowy. Rzecz jasna drugie kopiowanie jest wykonywane tylko wtedy, gdy edycję kończy naciśnięcie klawisza Enter. Rozwa­żania te prowadzą do dekla­racji następu­jących pól klasy:

char *pText;            // Łańcuch roboczy (bufor)
void *pDane;            // Edytowane pole (np. łańcuch)
int n;                  // Maksymalna długość łańcucha
int x, y;               // Współrzędne ekranowe pola edycji
int k;                  // Pozycja kursora w polu edycji

Wartości zmiennej pDane są wskaźnikami typu nieokre­ślonego, ponieważ w klasach pochodnych mogą wskazywać na pola zawiera­jące dane innego typu niż zwykły łańcuch. Maksymalna długość łańcucha roboczego zależy od typu edytowanej danej, pamięć dla bufora edycji jest więc przydzie­lana dynami­cznie przez konstru­ktor klasy i zwalniana przez jej destru­ktor:

Edit::Edit(void *s, int sLen, int xPoz, int yPoz) : pDane(s), n(sLen), x(xPoz), y(yPoz)
{
    pText = new char[n + 1];
}

Edit::~Edit()
{
    delete[] pText;
}

Przejdźmy teraz do sformułowania dwóch metod przesyłania łańcucha pomiędzy określonym przez wskaźnik nieznanego typu polem a buforem edycyjnym. Pierwsza kopiuje orygi­nalny łańcuch do bufora i dla uproszczenia operacji edycyjnych uzupełnia go spacjami do pełnej długości, zaś druga kopiuje łańcuch roboczy z bufora do pola i obcina końcowe spacje łańcucha wynikowego:

void Edit::setText(void *s)
{
    strcpy(pText, (char *)s);
    pText[n] = '\0';
    for (int i = strlen(pText); i < n; i++)
        pText[i] = ' ';
}

void Edit::getWynik(void *s)
{
    strcpy((char *)s, pText);
    for (int i = n - 1; i >= 0 && pText[i] == ' '; i--)
        ((char *)s)[i] = '\0';
}

Ze względu na możliwość użycia klasy Edit do edycji danych innego rodzaju niż prosty łańcuch obie metody powinny być wirtualne (virtual) i chronione (protected, dostępne w klasach pochodnych). Cztery pozostałe metody klasy określimy jako prywatne (private), gdyż w klasach pochodnych nie będą wymagać żadnych zmian. Pierwsza z nich wypisuje na ekranie edytowany łańcuch:

void Edit::piszText()
{
    gotoxy(x, y);
    std::cout << pText;
}

Kolejne dwie metody realizują podstawowe operacje edycyjne – wstawianie i usuwanie znaku w miejscu kursora, gdy znajduje się on wewnątrz pola, tj. na pozycji niższej od n. Wstawienie nowego znaku do łańcucha wymaga przesu­nięcia fragmentu łańcucha o jedną pozycję w kierunku końca, poczynając od pozycji k. Po wsta­wieniu znaku wartość k zostaje zwiększona o 1, co odpowiada przeskokowi kursora do nastę­pnego znaku. Z kolei usunięcie znaku polega na przesu­nięciu podobnego fragmentu tekstu w odwro­tnym kierunku i umieszczeniu znaku spacji na ostatniej, zwolnionej pozycji znakowej. Obie operacje kończy wypisanie zmodyfiko­wanego łańcucha:

void Edit::wstawZnak(char c)
{
    for (int i = n - 1; i > k; i--)
        pText[i] = pText[i - 1];
    pText[k++] = c;
    piszText();
}

void Edit::usunZnak()
{
    for (int i = k + 1; i < n; i++)
        pText[i - 1] = pText[i];
    pText[n - 1] = ' ';
    piszText();
}

Bardziej rozbudowana jest czwarta metoda klasy z grupy metod prywatnych, która pobiera kolejne znaki z klawia­tury, interpre­tuje je i wykonuje stosowne operacje edycyjne, posługując się omówionymi powyżej metodami. Jej nieco uproszczona wersja przedstawia się następu­jąco:

int Edit::edytuj()
{
    int c;
    setText(pDane);
    piszText();
    k = 0;
    for (bool stop = false; !stop; )
    {
        gotoxy(x + k, y);
        switch (c = czytajZnak())
        {
            case K_BACKS:
                if (k > 0)
                {
                    k--; usunZnak();
                }
                break;
            case K_DEL:
                if (k < n) usunZnak();
                break;
            case K_ENTER:
                getWynik(pDane); stop = true;
                break;
            case K_LEFT:
                if (k > 0) k--;
                break;
            case K_RIGHT:
                if (k < n) k++;
                break;
            case K_HOME:
                k = 0;
                break;
            case K_END:
                for (k = n; k > 0 && pText[k - 1] == ' '; k--)
                    ;
                break;
            case K_ESC:
                setText(pDane); stop = true;
                break;
            default:
                if (c >= ' ' && c < 256 && k < n) wstawZnak(c);
        }
    }
    piszText();
    return c;
}

Działanie metody rozpoczyna się od przesłania orygi­nalnego łańcucha do bufora i wypisania go. Wstępna wartość false zmiennej stop wymusza konty­nuację pętli edycyjnej, w której każdo­razowo przed pobraniem kolejnego znaku kursor jest umieszczany na pozycji znakowej określonej przez wartość zmiennej k (na początku 0). Interpre­tację pobranego znaku ułatwia instrukcja wyboru case, która pozwala zapisać w czytelnej postacji wielowa­riantowe decyzje:

Dwie operacje wyszczególnione jako ostatnie przypisują zmiennej stop wartość true, co skutkuje zakoń­czeniem pętli. Pierwsza przed przypi­saniem umieszcza wynik edycji w pamięci zajmowanej przez edytowane pole, druga przywraca pierwotny stan bufora, uniewa­żniając wprowa­dzone zmiany. Na koniec funkcja edytuj wypisuje zawartość bufora i zwraca kod znaku kończą­cego edycję (K_ENTER lub K_ESC).

Edycja pojedynczego pola zawierającego łańcuch lub daną innego typu będzie polegać na wywołaniu funkcji editStr, której jedynym argumentem jest tworzony za pomocą operatora new obiekt klasy Edit lub wywodzącej się od niej obiekt klasy pochodnej. Na przykład wczytanie z klawia­tury łańcucha reprezen­tującego imię i nazwisko osoby będzie można zaprogra­mować następu­jąco:

char im_naz[41];
*im_naz = '\0';
gotoxy(1, 4);
cout << "Imię i nazwisko: ";
editStr(new Edit(im_naz, 40, 18, 4));

Wydaje się, że kod funkcji editStr jest tak prosty, że nie powinien sprawiać żadnego problemu. Funkcja ma edytować łańcuch reprezen­towany przez utworzony "w locie" (bezpo­średnio w argu­mencie wywołania) obiekt, zwolnić go po zakończniu edycji i zwrócić kod znaku kończą­cego edycję:

int editStr(Edit *p)
{
    int c = p->edytuj();
    delete p;
    return c;
}

W trakcie kompilacji pojawia się jednak niespodzianka: funkcja odwołuje się do metody prywatnej klasy Edit, co jest niedozwolone. Wprawdzie można by przenieść składniki prywatne klasy do części publicznej, ale byłoby to nieele­ganckie, gdyż przeczy­łoby podsta­wowym zasadom kontrolo­wania dostępności składników klasy dla pozostałych części programu. Na szczęście w C++ istnieje możliwość udostę­pniania prywatnych i chro­nionych składników klasy wybranym funkcjom spoza niej. Deklarację takiej funkcji, zwanej funkcją zaprzyja­źnioną (ang. friend function), poprzedza się specyfika­torem friend i umieszcza wewnątrz klasy podobnie jak zwykłe pola i metody.

Zapowiedziana na wstępie druga funkcja o nazwie editRec zaprzyja­źniona z klasą Edit ma umożliwiać edycję wielu pól zawiera­jących łańcuchy i/lub dane innego typu ustawio­nych w listę jednokie­runkową. Nie ulega kwestii, że będzie ona równie wygodna jak editStr, gdy lista edyto­wanych pól będzie tworzona "w locie". Elementami listy będą struktury postaci:

struct Pole
{
    Edit *edit;             // Obiekt edycji pola
    Pole *nast;             // Następny element
};

Edycja pól listy wymaga uwzględnienia w metodzie edytuj klawisza Tab używanego zwyczajowo do sekwen­cyjnego przecho­dzenia od pola do pola, a być może także innych klawiszy. Przyjmiemy, że będą nimi Up i Dn (strzałka w górę i dół) oraz PgUp i PgDn – użyjemy ich do manipu­lacji rekordami pliku. To, czy klawisze dodatkowe mają funkcjo­nować, będzie określało pole logiczne TabOK klasy Edit. Jego wartość false ustalana domyślnie w konstru­ktorze klasy będzie implikować edycję pojedyn­czego pola i ignoro­wanie klawiszy dodatko­wych, a wartość true edycję pola listy i utożsa­mianie ich z klawiszem Enter:

case K_TAB:
case K_UP:
case K_DN:
case K_PGUP:
case K_PGDN:
    if (!TabOK) break;
case K_ENTER:
    getWynik(pDane); stop = true;
    break;

Listę pól budujemy za pomocą pomocniczej funkcji nowePole, która wstawia na początek listy nowy element wskazu­jący na obiekt klasy Edit lub klasy pochodnej i zwraca nowy początek listy:

Pole *nowePole(Edit *p, Pole *lista)
{
    p->TabOK = true;
    Pole *q = new Pole;
    q->edit = p;
    q->nast = lista;
    return q;
}

Funkcja modyfikuje pole TabOK obiektu, przypi­sując mu wartość true. Dzięki temu edycję łańcucha reprezen­towanego przez obiekt można kończyć za pomocą klawisza Enter, Esc i klawiszy dodatko­wych. Wykorzy­stanie jej w wywołaniu funkcji editRec do wczytania z klawiatury dwóch łańcuchów reprezen­tujących imię i nazwisko osoby ilustruje następu­jący przykład:

char im[13], naz[29];
*im = *naz = '\0';
gotoxy(1, 4); cout << "Imię    : ";
gotoxy(1, 5); cout << "Nazwisko: ";
editRec(nowePole(new Edit(im, 12, 10, 4),
        nowePole(new Edit(naz, 28, 10, 5),
        NULL)));

Argumentem wywołania funkcji editRec jest lista elementów typu Pole wygene­rowana "w locie" poprzez dwukrotne dodawanie nowego elementu na początek listy. Najpierw do listy pustej dodawany jest element z nowym obiektem klasy Edit reprezen­tującym nazwisko, a potem do listy jednoele­mentowej element z nowym obiektem reprezen­tującym imię. Wynikowy porządek pól jest odwrotny do porządku ich wstawiania. A oto kod funkcji editRec:

int editRec(Pole *lista)
{
    int c = 0;
    Pole *p = lista;
    while (p != NULL)
        switch (c = p->edit->edytuj())
        {
            case K_TAB:
                p = (p->nast != NULL) ? p->nast : lista;
                break;
            case K_ENTER:
                p = p->nast;
                break;
            default:
                p = NULL;
        }
    while (lista != NULL)
    {
        lista = (p = lista)->nast;
        delete p->edit;
        delete p;
    }
    return c;
}

Funkcja edytuje w pętli, poczynając od pierwszego elementu listy określonej w argumencie, łańcuch reprezen­towany przez wskazany w elemencie obiekt i w zależności od znaku kończącego edycję przechodzi do kolejnego elementu listy lub przerywa pętlę. Klawisze Tab i Enter powodują przejście do nastę­pnego elementu listy, gdy bieżący element nie jest ostatnim. W przeciwnym razie klawisz Tab przechodzi na początek listy, a Enter kończy edycję wszystkich pól. Pętla może zostać również przerwana w dowolnym momencie za pomocą klawiszy Esc, Up, Dn, PgUp i PgDn. Na koniec funkcja editRec zwalnia pamięć zajmo­waną przez elementy listy i wskazy­wane przez nie obiekty edycyjne oraz zwraca kod znaku kończącego edycję.

Po tych przydługich rozważaniach dotyczących budowy narzędzi edycy­jnych możemy wreszcie przystąpić do prezen­tacji kodu źródłowego modułu edycji łańcucha i listy pól reprezen­tujących łańcuchy. Jest oczywiste, że plik nagłów­kowy nowego modułu, który nazwiemy edycja, powinien zawierać definicję klasy Edit z proto­typami funkcji zaprzyja­źnionych editStr, editRecnowePole oraz definicję struktury Pole. Niespo­dzianka pojawia się przy określaniu kolej­ności tych definicji, bowiem dwie z funkcji zaprzyja­źnionych odwołują się do definicji struktury. Zmiana tej kolejności nie likwiduje problemu, gdyż struktura odwołuje się do klasy. Tę patową sytuację rozwiązuje dekla­racja struktury z wyprze­dzeniem. Warto przy okazji rozbudować klasę Edit, by metoda edytuj wyróżniała w trakcie edycji pole innym kolorem tła i tekstu, a po jej zakoń­czeniu przywra­cała kolory normalne. Argumentom konstru­ktora określa­jącym kolory warto również przypisać wartości domyślne, które zostaną przyjęte, gdy w wywołaniu go nie zostanie podany komplet argumentów. Rzecz jasna argumentom domyślnym powinny być przypi­sane wartości najczęściej występu­jące, gdyż ich zadaniem jest uproszczenie wywołania funkcji. Kod źródłowy zawarty w pliku nagłów­kowym modułu edycja, w którym dla kompila­tora Borland C++ należy pominąć dyrektywę włącza­jącą moduł bconio, ma następu­jącą postać:

// edycja.h - Moduł edycji łańcucha (pola) i rekordu (listy pól)
// -------------------------------------------------------------
// editStr - edycja łańcucha            editRec - edycja rekordu
// -------------------------------------------------------------

#ifndef H_EDYCJA
#define H_EDYCJA

#include "bconio.h"
#include "klaw2.h"

struct Pole;                // Deklaracja z wyprzedzeniem

class Edit
{
public:
    Edit(void *s, int sLen, int xPoz, int yPoz,
         int nTlo = WHITE, int nTxt = BLACK, int eTlo = BLUE, int eTxt = WHITE);
    virtual ~Edit();
    friend int editStr(Edit *p);
    friend int editRec(Pole *lista);
    friend Pole *nowePole(Edit *p, Pole *lista);
protected:
    virtual void setText(void *s);
    virtual void getWynik(void *s);
private:
    char *pText;            // Łańcuch roboczy (bufor)
    void *pDane;            // Edytowane pole (np. łańcuch)
    int n;                  // Maksymalna długość łańcucha
    int x, y;               // Współrzędne ekranowe pola edycji
    int k;                  // Pozycja kursora w polu edycji
    int nB, nT;             // Normalne kolory tła i tekstu
    int eB, eT;             // Edycyjne kolory tła i tekstu
    bool TabOK;             // Dozwolony Tab, Up, Dn, PgUp, PgDn
    void piszText();
    void wstawZnak(char c);
    void usunZnak();
    int edytuj();           // Edycja łańcucha
};

struct Pole
{
    Edit *edit;             // Obiekt edycji pola
    Pole *nast;             // Następny element
};

#endif // H_EDYCJA

Pełny kod źródłowy metod klasy Edit i funkcji z nią zaprzyja­źnionych zawarty w drugim pliku modułu jest przedsta­wiony na poniższym listingu. Gwoli ścisłości jest to wersja dla kompila­tora MinGW C++, dwie pozostałe dla kompila­torów Borland C++ i Visual C++ różnią się niezna­cznie (uwagi poniżej).

#include <iostream>
#include <string>
#include "edycja.h"

Edit::Edit(void *s, int sLen, int xPoz, int yPoz, int nTlo, int nTxt, int eTlo, int eTxt)
    : pDane(s), n(sLen), x(xPoz), y(yPoz), nB(nTlo), nT(nTxt), eB(eTlo), eT(eTxt), TabOK(false)
{
    pText = new char[n + 1];
}

Edit::~Edit()
{
    delete[] pText;
}

void Edit::setText(void *s)
{
    strcpy(pText, (char *)s);
    pText[n] = '\0';
    for (int i = strlen(pText); i < n; i++)
        pText[i] = ' ';
}

void Edit::getWynik(void *s)
{
    strcpy((char *)s, pText);
    for (int i = n - 1; i >= 0 && pText[i] == ' '; i--)
        ((char *)s)[i] = '\0';
}

void Edit::piszText()
{
    gotoxy(x, y);
    std::cout << pText;
}

void Edit::wstawZnak(char c)
{
    for (int i = n - 1; i > k; i--)
        pText[i] = pText[i - 1];
    pText[k++] = c;
    piszText();
}

void Edit::usunZnak()
{
    for (int i = k + 1; i < n; i++)
        pText[i - 1] = pText[i];
    pText[n - 1] = ' ';
    piszText();
}

int Edit::edytuj()
{
    int c;
    setText(pDane);
    textbackground(eB); textcolor(eT); piszText();
    k = 0;
    for (bool stop = false; !stop; )
    {
        gotoxy(x + k, y);
        switch (c = czytajZnak())
        {
            case K_BACKS:
                if (k > 0)
                {
                    k--; usunZnak();
                }
                break;
            case K_DEL:
                if (k < n) usunZnak();
                break;
            case K_TAB:
            case K_UP:
            case K_DN:
            case K_PGUP:
            case K_PGDN:
                if (!TabOK) break;
            case K_ENTER:
                getWynik(pDane); stop = true;
                break;
            case K_LEFT:
                if (k > 0) k--;
                break;
            case K_RIGHT:
                if (k < n) k++;
                break;
            case K_HOME:
                k = 0;
                break;
            case K_END:
                for (k = n; k > 0 && pText[k - 1] == ' '; k--)
                    ;
                break;
            case K_ESC:
                setText(pDane); stop = true;
                break;
            default:
                if (c >= ' ' && c < 256 && k < n) wstawZnak(c);
        }
    }
    textbackground(nB); textcolor(nT); piszText();
    return c;
}

int editStr(Edit *p)
{
    int c = p->edytuj();
    delete p;
    return c;
}

int editRec(Pole *lista)
{
    int c = 0;
    Pole *p = lista;
    while (p != NULL)
        switch (c = p->edit->edytuj())
        {
            case K_TAB:
                p = (p->nast != NULL) ? p->nast : lista;
                break;
            case K_ENTER:
                p = p->nast;
                break;
            default:
                p = NULL;
        }
    while (lista != NULL)
    {
        lista = (p = lista)->nast;
        delete p->edit;
        delete p;
    }
    return c;
}

Pole *nowePole(Edit *p, Pole *lista)
{
    p->TabOK = true;
    Pole *q = new Pole;
    q->edit = p;
    q->nast = lista;
    return q;
}

Uwagi. W wersji dla kompilatora Borland C++ ze względu oryginalny sposób wyświe­tlania kolorów tekstu i tła w aplika­cjach konso­lowych (zob. program koloro­wania kalen­darza) nie korzysta się z biblio­teki iostream, a zamiast stru­mienia cout w metodzie piszText używa się funkcji cprintf z biblio­teki conio. Z kolei w wersji dla kompila­tora Visual C++ zamiast funkcji strcpy w metodach setTextgetWynik używa się specyfi­cznej dla Microsoft funkcji strcpy_s, która wymaga dodatko­wego argumentu określa­jącego maksy­malną liczbę znaków kopiowanych łańcuchów (łącznie z '\0').

Moduł edycji daty

Jednym z ważniejszych i często występującym rodzajem danych jest data, dlatego warto jej edycję zaprogra­mować w postaci modułu, który nadawałby się do wielokro­tnego wykorzy­stania. Zakładamy, że data jest wyświe­tlana według polskiego wzorca DD.MM.RRRR, a jej wewnętrzną reprezen­tacją jest zdefinio­wana w pliku osoby.h struktura Date. Jej definicja zostanie w nowym module powtórzona w zakresie działania dyrektyw warun­kowej kompilacji, aby można było z niego korzystać również bez włączania do programu pliku osoby.h.

Jest oczywiste, że moduł edycji daty powinien zawierać definicję klasy pochodnej wywo­dzącej się od klasy Edit. Nową klasę nazwiemy DataEdit. Redefi­nicji wymagają w niej metody wirtualne setTextgetWynik. Pierwsza ma pobrać wartość pola typu Date, zamienić ją na łańcuch i przesłać do bufora edycyj­nego, zaś druga odwrotnie – pobrać z bufora łańcuch, zamienić go na datę i zapisać w polu typu Date. Plik nagłów­kowy modułu, który na dodatek zawiera proto­typy dwóch funkcji konwersji daty na łańcuch i łańcucha na datę przyda­tnych nie tylko w precyzo­waniu metod setTextgetWynik klasy DataEdit, może wyglądać następu­jąco:

// dataed.h - Moduł edycji daty (konwencja polska - DD.MM.RRRR)
// ------------------------------------------------------------
// dataToStr - konwersja daty na łańcuch
// strToData - konwersja łańcucha na datę
// ------------------------------------------------------------

#ifndef H_EDYCJA_DATY
#define H_EDYCJA_DATY

#include "edycja.h"

#ifndef H_DATA_STR
#define H_DATA_STR

const int DT = 10;          // Długość łańcucha DD.MM.RRRR

struct Date
{
    short r;                // Numer roku
    char  m;                // Numer miesiąca
    char  d;                // Numer dnia
};

#endif // H_DATA_STR

class DataEdit : public Edit
{
public:
    DataEdit(void *dt, int xPoz, int yPoz,
        int nTlo = WHITE, int nTxt = BLACK, int eTlo = BLUE, int eTxt = WHITE);
protected:
    virtual void setText(void *dt);
    virtual void getWynik(void *dt);
};

void dataToStr(char *s, Date *dt);
void strToData(Date *dt, char *s);

#endif // H_EDYCJA_DATY

Tworząc funkcje dataToStr i strToData, najprościej skorzystać z funkcji sprintfsscanf biblioteki stdio języka C, które realizują takie same operacje formato­wania tekstu jak printfscanf, ale używają tablicy znaków zamiast standar­dowego wejścia-wyjścia. Jeśli np. s jest tablicą 11-znakową i data strukturą typu Date zawiera­jącą datę, to instrukcja

sprintf(s, "%02d.%02d.%04d", data.d, data.m, data.r);

formatuje tę datę i umieszcza wynik w łańcuchu s. Zero występu­jące po znaku % w symbolach formato­wania oznacza, że wyprowa­dzany ciąg znaków ma być poprze­dzony znakami zero do pełnej szerokości pola, gdy jest za krótki. Podobnie instrukcja

sscanf(s, "%d.%d.%d", &data.d, &data.m, &data.r);

wczytuje do pól zmiennej data trzy liczby całkowite oddzielone od siebie kropkami, ale przeszu­kuje łańcuch s zamiast standar­dowego wejścia. Uwzglę­dniając kontrolę poprawności składników daty za pomocą funkcji dataOK zdefinio­wanej w zaprezen­towanym powyżej programie tworzenia pliku binarnego, funkcje konwersji daty na łańcuch i łańcucha na datę możemy zaprogra­mować następu­jąco:

void dataToStr(char *s, Date *dt)
{
    if (dataOK(dt->r, dt->m, dt->d))
        sprintf(s, "%02d.%02d.%d", dt->d, dt->m, dt->r);
    else
        *s = '\0';
}

void strToData(Date *dt, char *s)
{
    int d, m, r;
    if (sscanf(s, "%d.%d.%d", &d, &m, &r) != 3 || !dataOK(r, m, d))
        r = m = d = 0;
    dt->d = d;
    dt->m = m;
    dt->r = r;
}

Inne sformułowanie obydwu funkcji konwersji bazujące na biblio­tece iostream języka C++ polega na wykorzy­staniu strumieni ostring­streamistring­stream umożliwia­jących wyprowa­dzanie danych do łańcucha i wczyty­wanie danych z łańcucha za pomocą opera­torów <<>> podobnie jak w przypadku strumieni konsolowych coutcin. Takie rozwią­zanie zostało przyjęte w drugiej części modułu edycji daty:

#include <sstream>
#include <iomanip>
#include "dataed.h"

using namespace std;

DataEdit::DataEdit(void *dt, int xPoz, int yPoz, int nTlo, int nTxt, int eTlo, int eTxt)
    : Edit(dt, DT, xPoz, yPoz, nTlo, nTxt, eTlo, eTxt)
{}

void DataEdit::setText(void *dt)
{
    char s[DT + 1];
    dateToStr(s, (Date *)dt);
    Edit::setText(s);
}

void DataEdit::getWynik(void *dt)
{
    char s[DT + 1];
    Edit::getWynik(s);
    strToData((Date *)dt, s);
}

bool dataOK(int r, int m, int d)
{
    static int DM[] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if (r < 1582 || r > 9999 || m < 1 || m > 12 || d < 1 ||
        (r == 1582 && (m < 10 || (m == 10 && d < 15)))) return false;
    DM[2] = ((r % 4 == 0 && r % 100 != 0) || r % 400 == 0) ? 29 : 28;
    return d <= DM[m];
}

void dataToStr(char *s, Date *dt)
{
    int d = dt->d, m = dt->m, r = dt->r;
    if (dataOK(r, m, d))
    {
        ostringstream ostr;
        ostr << setfill('0') << setw(2) << d << '.' << setw(2) << m << '.' << r;
        strcpy(s, ostr.str().c_str());
    }
    else
        *s = '\0';
}

void strToData(Date *dt, char *s)
{
    int d, m, r;
    char c1, c2;
    istringstream istr(s);
    istr >> d >> c1 >> m >> c2 >> r;
    if (!istr || c1 != '.' || c2 != '.' || !dataOK(r, m, d))
        r = m = d = 0;
    dt->d = d;
    dt->m = m;
    dt->r = r;
}

Uwaga. Kompilator Visual C++ traktuje funkcję strcpy jako przesta­rzałą i poten­cjalnie niebez­pieczną, dlatego zgodnie z jego sugestią w funkcji dataToStr użyta została zamiast niej funkcja strcpy_s.

Jak widać, konstruktor klasy DataEdit wywołuje w liście inicjali­zującej konstru­ktora klasy Edit, któremu przekazuje swoje argumenty i długość łańcucha reprezen­tującego datę. To wywołanie jest nieodzowne, gdyż w trakcie tworzenia obiektu klasy pochodnej wszystkie jego odziedzi­czone pola powinny zostać zainicja­lizowane przez konstru­ktor klasy bazowej. Do metod klasy Edit odwołują się również metody setTextgetWynik klasy DataEdit. Obie nie mają dostępu do bufora edycyj­nego, muszą więc wykorzystać odpowia­dające im metody klasy bazowej, by przesłać do niego łańcuch będący zapisem daty do edycji lub pobrać z niego łańcuch stano­wiący wynik tej edycji.

Program edycji pliku rekordów

Rekordy strumienia kojarzonego z plikiem binarnym wygodnie jest numerować kolejnymi liczbami całko­witymi od zera wzwyż. Podobnie jak w poprze­dnim programie do przechowy­wania edyto­wanego rekordu użyjemy bufora rec typu struktu­ralnego Osoba, a także zmiennej n typu int do określania numeru tego rekordu. Znając numer rekordu, możemy łatwo wyznaczyć jego pozycję w pliku (numer bajtu od początku pliku):

n * sizeof(rec)

Jak wiadomo, operacje odczytu rekordu z pliku i zapisu rekordu do pliku są reali­zowane przez metody readwrite. Dostęp swobodny oznacza jednak, że to przesyłanie danych może dotyczyć dowolnego miejsca w pliku. Przed operacją niesekwen­cyjnego odczytu lub zapisu należy ustalić pozycję precyzującą miejsce w pliku, z którego dane mają być pobrane lub do którego mają być wstawione. Do sterowania pozycją służą metody seekg dla strumieni wejścio­wych i seekp dla strumieni wyjścio­wych. Przyrostek g wskazuje, że jest to pozycja używana do pobierania (ang. get, pobierz), zaś p, że do wstawiania (ang. put, wstaw). Pierwszym argu­mentem obu metod jest wielkość przesu­nięcia względem punktu odnie­sienia, a drugim punkt odnie­sienia, który można wyrazić za pomocą znacznika:

Metody seekg i seekp można także wywoływać z jednym argu­mentem. Wartością domyślną drugiego argumentu jest wtedy ios::beg.

Strumienie wejściowy i wyjściowy mają również bezargu­mentowe metody tellgtellp, które udostę­pniają wielkość przesu­nięcia bieżącej pozycji w strumieniu względem jego początku. Rzecz jasna po przesta­wieniu strumienia na koniec metody te zwracają rozmiar (liczbę bajtów) strumienia. Oto przykład ilustru­jący, jak można otworzyć utworzony w poprze­dnim programie plik Osoby.dta, przestawić go na koniec, obliczyć liczbę zawartych w nim rekordów i wrócić na jego początek:

ifstream plik("Osoby.dta", ios::binary);
plik.seekg(0, ios::end);
int n = int(plik.tellg() / sizeof(Osoba));
plik.seekg(0);

Na poniższym listingu zaprezentowanyny jest program konsolowy używający klas Edit (edycja łańcucha) i DataEdit (edycja daty) zdefinio­wanych w modułach edycjadataed oraz własnej klasy PlecEdit (edycja jedno­znakowego pola reprezen­tującego płeć) do obsługi pliku binarnego złożonego z rekordów typu Osoba. Program umożliwia przeglą­danie pliku, aktuali­zację zawartych w nim danych i dopisy­wanie nowych pozycji. W programie nie przewi­dziano operacji usuwania rekordu z pliku, która wymaga podjęcia decyzji, co powinno się zrobić z powstałym po usuniętym rekordzie pustym miejscem.

#include <fstream>
#include <iostream>
#include <iomanip>
#include <string>
#include "bconio.h"
#include "klaw2.h"
#include "dataed.h"
#include "oceny.h"

using namespace std;

class PlecEdit : public Edit
{
public:
    PlecEdit(void *sex, int xPoz, int yPoz,
        int nTlo = WHITE, int nTxt = BLACK, int eTlo = BLUE, int eTxt = WHITE);
protected:
    virtual void setText(void *sex);
    virtual void getWynik(void *sex);
};

PlecEdit::PlecEdit(void *sex, int xPoz, int yPoz, int nTlo, int nTxt, int eTlo, int eTxt)
        : Edit(sex, 1, xPoz, yPoz, nTlo, nTxt, eTlo, eTxt)
{}

void PlecEdit::setText(void *sex)
{
    char s[2];
    s[0] = *(char *)sex;
    s[1] = '\0';
    Edit::setText(s);
}

void PlecEdit::getWynik(void *sex)
{
    char s[2];
    Edit::getWynik(s);
    switch (*s)
    {
        case 'k':
        case 'K':
            *(char *)sex = 'K';
            break;
        case 'm':
        case 'M':
            *(char *)sex = 'M';
            break;
        default:
            *(char *)sex = ' ';
    }
}

const string FileName = "Osoby.dta";

void plansza(Osoba &rec, int nRec)
{
    clrscr();
    cout << " Plik: " << FileName;
    gotoxy(36, 1);
    cout << "Record:" << setw(3) << nRec;
    cout << "\n --------------------------------------------";
    cout << "\n\n Imię    : " << rec.imie;
    cout << "\n Nazwisko: " << rec.nazwisko;
    gotoxy(2, 7);
    cout << "\nPłeć (K,M): " << rec.plec;
    char s[DT + 1];
    dataToStr(s, &rec.dtur);
    gotoxy(23, 7);
    cout << "Data urodz.: " << s;
    cout << "\n\n Nr telefonu : " << rec.tel;
    cout << "\n Adres e-mail: " << rec.email;
    cout << "\n\n --------------------------------------------";
    cout << "\n Potwierdzenie: Enter         Rezygnacja: Esc";
    cout << "\n Następne pole: Tab";
    cout << "\n Poprzedni rekord: Up     Następny rekord: Dn";
    cout << "\n Początek pliku: PgUp      Koniec pliku: PgDn";
}

void czysc(Osoba &rec)
{
    *rec.imie = *rec.nazwisko = *rec.tel = *rec.email = '\0';
    rec.plec = ' ';
    rec.dtur.r = rec.dtur.m = rec.dtur.d = 0;
}

int main()
{
    fstream plik(FileName.c_str(), ios::in | ios::out | ios::binary);
    if (!plik) return 1;
    textbackground(WHITE);
    textcolor(BLACK);
    Osoba rec;
    int n = 0;
    for (int c = 0; c != K_ESC; )
    {
        plik.seekg(n * sizeof(rec));
        if (!plik.read((char *)&rec, sizeof(rec)))
        {
            plik.clear();
            czysc(rec);
        }
        plansza(rec, n + 1);
        switch (c = editRec(
            nowePole(new Edit(rec.imie, IM, 14, 4),
            nowePole(new Edit(rec.nazwisko, NA, 14, 5),
            nowePole(new PlecEdit(&rec.plec, 14, 7),
            nowePole(new DataEdit(&rec.dtur, 36, 7),
            nowePole(new Edit(rec.tel, TE, 16, 9),
            nowePole(new Edit(rec.email, EM, 16, 10),
            NULL))))))))
        {
            case K_ENTER:
                plik.seekp(n * sizeof(rec));
                plik.write((char *)&rec, sizeof(rec));
                break;
            case K_UP:
                if (n > 0) n--;
                break;
            case K_DN:
                plik.seekg(0, ios::end);
                if (n < int(plik.tellg() / sizeof(rec))) n++;
                break;
            case K_PGUP:
                n = 0;
                break;
            case K_PGDN:
                plik.seekg(0, ios::end);
                if ((n = int(plik.tellg() / sizeof(rec))) > 0) n--;
        }
    }
    plik.close();
    gotoxy(1, 17);
    return 0;
}

Uwaga. W wersji dla kompilatora Borland C++ nie korzysta się z bibliotek iostreamiomanip oraz modułu bconio, a zamiast stru­mienia cout w funkcji plansza używa się funkcji cprintf z biblio­teki conio.

Program rozpoczyna działanie od otwarcia strumienia binarnego do odczytu i zapisu skojarzonego z plikiem o nazwie Osoby.dta. W przypadku nieuda­nego otwarcia program w wersji Borland C++ podejmuje próbę utworzenia nowego, pustego pliku, a gdy ta operacja nie powiedzie się, kończy działanie. Pozostałe wersje traktują brak pliku jako błąd. Aby wtedy utworzyć nowy plik, można uruchomić omówiony na początku niniejszej strony prosty program tworzenia pliku rekordów i zakończyć go bez wprowa­dzania danych, można też za pomocą menu podrę­cznego przywo­łanego prawym przyciskiem myszki utworzyć nowy plik tekstowy bądź innego typu i zmienić jego nazwę.

Zasadnicze operacje edycyjne pliku są wykonywane w pętli for. W każdym jej kroku do bufora rec wczytywany jest rekord o numerze określonym przez wartość zmiennej n (wstępnie równą zero), a następnie jest wyświe­tlany na ekranie za pomocą funkcji plansza. Jeśli operacja czytania się nie powiedzie, strumień przechodzi w stan błędu. Najczęstszą przyczyną zaistnienia takiej sytuacji jest próba czytania poza końcem strumienia, co ma miejsce, gdy plik jest pusty albo wyświetlony jest jego ostatni rekord i użytkownik naciśnie klawisz strzałki w dół, by przejść do nastę­pnego rekordu. Wtedy każda następna operacja dotycząca pliku się nie powiedzie, dopóki nie zostanie on przywró­cony do normalnego stanu. Służy do tego metoda clear. Po jej wywołaniu bufor jest czyszczony, co wskazuje na zamiar dopisania nowego rekordu do pliku. Wyświe­tlenie planszy przedsta­wiającej pola rekordu zawartego w buforze, ich opisy i menu informu­jącego o dostępnych klawiszach steru­jących poprzedza edycję, w trakcie której użytkownik może wprowadzać dane do pól rekordu i modyfi­kować je. Zakończenie edycji klawiszem Enter powoduje przesłanie edytowa­nego rekordu do pliku. Przejście do poprze­dniego i nastę­pnego rekordu umożliwiają klawisze Up i Dn (strzałka w dół i górę), a przeskok do pierwszego i osta­tniego rekordu klawisze PgUp i PgDn. Klawisz Esc kończy pętlę edycyjną pliku.

Oto przykładowe okna wygenerowane przez dwie wersje programu edycji pliku binarnego:

a) wersja Borland C++,

b) wersja MinGW C++.


Opracowanie przykładu: luty 2020