Zapis i odczyt danych


Wstęp

Powyższy dokument zawiera informacje o możliwościach zapisu i odczytu danych w aplikacjach pracujących w systemie Windows. Został w nim omówiony mechanizm serializacji charakterystyczny dla aplikacji opartych o bibliotekę MFC, jak również podstawowe funkcje pozwalające na tworzenie własnych procedur obsługi plików.

 

Czym jest serializacja ?

Serializacja jest procesem zapisu lub odczytu obiektu do lub z pamięci masowej, na przykład: pliku dyskowego. Biblioteka MFC zawiera wbudowaną obsługę serializacji w klasie CObject, co oznacza, że wszystkie klasy z niej wyprowadzone mogą wykorzystać zalety serializacji.
Twórcy biblioteki MFC dostarczyli mechanizm umożliwiający serializację obiektów typów podstawowych, a także zatroszczyli się o szczegóły związane z serializacją obiektów bibliotecznych (np. CString, CRect, CPtrList). Obiekt zachowany z użyciem serializacji może zostać odtworzony poprzez odczyt, a raczej - w wyniku deserializacji. Istotą serializacji jest odpowiedzialność obiektu za właściwe zachowanie a następnie odtworzenie swojego stanu, w konsekwencji czego każda serializowalna klasa musi posiadać zaimplementowane podstawowe operacje serializacji zawarte w metodzie Serialize(CArchive &ar).
Dzięki serializacji programista nie musi troszczyć się o format zapisu danych a jedynie o zachowanie odpowiedniej kolejności przy serializacji i deserializacji danych, co w znacznym stopniu przyspiesza i upraszcza tworzenie procedur zapisu i odczytu danych.

 

Jak korzystać z serializacji ?

Serializacji mogą zostać poddawane jedynie klasy wyprowadzone bezpośrednio lub pośrednio z CObject. Za serializację zawartości obiektu należącego do danej klasy jest odpowiedzialna metoda Serialize(CArchive& ar) będąca składową tej klasy. Mimo że plik dyskowy, w którym dane będą przechowywane jest reprezentowany poprzez obiekt klasy CFile, do serializacji wykorzystuje się obiekt archiwizacji należący do klasy CArchive, która stanowi "pomost" pomiędzy plikiem a funkcją serializującą (taki obiekt należący do aplikacji nazywa się "ar"). Funkcja składowa CArchive::IsStoring mówi, czy archiwum w danym momencie wykorzystuje się do zapisywania czy do ładowania pliku. Klasa CArchive ma przeciążone operatory wstawiania (<<) i pobierania (>>) dla typów danych: BYTE, WORD, DWORD, int, float, double i CObject* (typ zdefiniowany przez użytkownika). Klasy biblioteki MFC nie pochodzące od CObject (np. CString i CRect) mają własne, przeciążone operatory pobierania i wstawiania, dzięki czemu mogą współpracować z klasą CArchive. Po dodaniu do nowo utworzonej klasy metody serializującej należy poprzez makra DECLARE_SERIAL (w deklaracji klasy) i IMPLEMENT_SERIAL (w pliku implementacji klasy) zarejestrować tą metodę.
Poniżej przedstawiono przykład ilustrujący praktyczne wykorzystanie opisanych wiadomości.

////////////////////////////////////////////////////////

// CAge.h - plik nagłówkowy klasy CAge



class CAge : public CObject

{

 private:

    CString m_Name;

    int m_Age;



    // DECLARE_SERIAL(<nazwa klasy>)

    DECLARE_SERIAL (CAge);



 public:

    ...

    void Serialize (CArchive& ar);

    ...

};



////////////////////////////////////////////////////////

// CAge.cpp - ciało klasy CAge



...



void CAge::Serialize (CArchive& archive)

{

    CObject.Serialize(archive);

    if( archive.IsStoring() )

        archive << m_Name << m_Age;

    else

        archive >> m_Name >> m_Age;

};



// IMPLEMENT_SERIAL(<nazwa klasy>, <nazwa klasy bazowej>, <Ver>)

// Ver (zmienna typu UINT) - nr wersji procedury serializującej 

// (przydatne przy rozwoju aplikacji)



IMPLEMENT_SERIAL(CAge, CObject, 0)



...



////////////////////////////////////////////////////////

// CAgeDoc.h - plik nagłówkowy klasy dokumentu



#include "CAge.h"



...



class CAgeDoc : public CDocument

{

    ...



 public:

    CAge m_Age1, m_Age2;

    ...

};



////////////////////////////////////////////////////////

// CAgeDoc.cpp - ciało klasy dokumentu



...



void CAgeDoc::Serialize(CArchive& ar)

{

    m_Age1.Serialize(ar);

    m_Age2.Serialize(ar);

}

Z reguły w aplikacjach o architekturze dokument-widok obsługą danych zajmuje się obiekt dokumentu. Gdy w metodzie Serialize(CArchive &ar) należącej do tego obiektu zostanie umieszczony kod serializujący dane na dysk i z powrotem automatycznie będzie umożliwione korzystanie z odpowiednich poleceń menu File (zapis, odczyt dokumentu) i standardowego okna dialogowego obsługującego operacje na plikach (obiekt klasy CFileDialog).

W celu uzyskania pełnej funkcjonalności konieczne jest informowanie obiektu dokumentu o zmianie danych w nim zawartych, dzięki czemu, gdy użytkownik spróbuje opuścić program bez wcześniejszego zachowania danych, pojawi się standardowe okno dialogowe z zapytaniem "Czy zapisać zmiany w ... ?". O zmianie danych można poinformować obiekt widoku poprzez wywołania metody CDocument::SetModifiedFlag zapalającej (lub kasującej) znacznik modyfikacji dokumentu. Znacznik ten jest automatycznie zerowany po każdorazowym zapisie dokumentu do pliku.

 

Zalety i wady serializacji

Jak każda metoda zapisu danych serializacja posiada wiele zalet, ale również jest obarczona pewnymi wadami. Do argumentów przemawiających na niekorzyść serializacji można zaliczyć:

Niewątpliwymi zaletami serializacji są:

Wady wynikające z wykorzystywania serializacji można wyeliminować stosując własne funkcje do zapisu danych zawartych w obiektach, jest to jednak związane z koniecznością przeznaczenia dużej ilości czasu na opracowanie formatu zapisu danych i implementacje odpowiednich procedur. Jak widać stosowanie serializacji można zalecić we wszystkich przypadkach w których wymienione wady mają charakter drugorzędny.

 

Własne procedury I/O

Możliwość zapisu i odczytu danych w aplikacjach systemu Windows bazujących na bibliotece MFC nie jest ograniczona do serializacji, Visual C++ pozwala na tworzenie w pełni funkcjonalnych, własnych procedur obsługi plików. Procedury takie bazują na klasie CFile zawierającej metody umożliwiające współpracę aplikacji z plikami, deklaracja tej klasy znajduje się w pliku nagłówkowym afx.h. Z obiektem klasy CFile zostaje powiązany plik, na którym operują wszystkie metody tej klasy. Uchwyt do pliku jest przechowywany w zmiennej CFile::m_hFile typu UINT.
Poniżej przedstawiono tabelę z wybranymi metodami klasy CFile.

Konstruktor CFile();
CFile(int hFile);
CFile(LPCTSTR pFileName, UINT nOpFlags);
    throw(CFileException)
Konstruktor obiektu klasy CFile na podstawie ścieżki lub uchwytu do pliku. W przypadku wystąpienia błędu zgłasza wyjątek należący do klasy CFileException.
Argumenty hFile uchwyt do już otwartego pliku
pFileName ścieżka dostępu i nazwa pliku do otwarcia
nOpFlags tryby otwierania pliku (opisane w dalszej części)

Open virtual BOOL Open(LPCTSTR pFileName, UINT nOpFlags, CFileException* pError=NULL);
Otwiera plik w bezpieczny sposób korzystając z opcji wykrywania błędów. Zwraca True jeśli otwarcie nastąpiło prawidłowo, w przeciwnym przypadku False.
Argumenty pFileName ścieżka dostępu i nazwa pliku do otwarcia
nOpFlags tryby otwierania pliku (opisane w dalszej części)
pError wskaźnik do zaistniałego wyjątku, gdy otwarcie nastąpiło prawidłowo parametr ten nie ma znaczenia

Close virtual void Close();
    throw(CFileException);
Zamyka plik i likwiduje obiekt klasy CFile. Wołanie tej metody nie jest zazwyczaj konieczne gdyż destruktor klasy automatycznie zamyka plik kiedy tylko obiekt wyjdzie z zasięgu.

Abort virtual void Abort();
Zamyka plik ignorując wszystkie błędy i ostrzeżenia. Metoda różni się od Close pod dwoma względami: po pierwsze, jak już wspomniano nie zgłasza wyjątków gdyż ignoruje błędy; a po drugie, nie wystąpi asertacja nawet jeśli plik nie został otworzony lub został wcześniej zamknięty!

Read virtual UINT Read(void* lpBuf, UINT nCount);
    throw(CFileException);
Odczytuje dane z pozycji wskazanej przez bieżącą wartość wskaźnika pliku. Zwraca ilość bitów przetransmitowanych do bufora. W przypadku niepowodzenia zgłasza wyjątek należący do klasy CFileException.
Argumenty lpBuf wskaźnik do bufora, w którym mają być zapisane odczytywane dane
nCount ilość bajtów, która ma być odczytana z pliku

Write virtual void Write(const void* lpBuf, UINT nCount);
    throw(CFileException);
Zapisuje dane do pliku w miejsce wskazywane przez bieżącą wartość wskaźnika pliku.
Argumenty lpBuf wskaźnik do bufora zawierającego dane do zapisu
nCount ilość bajtów danych do przetransferowania z bufora do pliku

Flush virtual void Flush();
    throw(CFileException);
Przesyła do pliku wszystkie dane, które jeszcze znajdują się w buforze zapisu. W przypadku współpracy z klasą CArchive aby zagwarantować zapis danych należy wcześniej wywołać metodę CArchive::Flush!

GetLength virtual DWORD GetLength() const;
    throw(CFileException);
Zwraca logiczną długość pliku wyrażoną w bajtach. Ciekawostka: metoda ta działa w oparciu o powszechnie znany trick z wołaniem metody Seek.

GetPosition virtual DWORD GetPosition() const;
    throw(CFileException);
Zwraca bieżącą wartość wskaźnika pliku określoną jako 32-bitową wartość doubleword, która następnie może być wykorzystana jako argument dla metody Seek.

Seek virtual LONG Seek(LONG lOff, UINT nFrom);
    throw(CFileException);
Zmienia wartość wskaźnika pliku. Jeśli żądana pozycja jest prawidłowa zwraca wartość wskaźnika pliku liczoną względem początku pliku, w przeciwnym wypadku zgłaszany jest wyjątek a zwracana wartość jest niezdefiniowana.
Argumenty lOff wymagane przesunięcie wskaźnika względem pozycji określonej w parametrze nFrom
nFrom określa miejsce odniesienia względem, którego ma nastąpić przesunięcie wskaźnika pliku; możliwe są ustawienia CFile::begin, CFile::current, CFile::end określające odpowiednio początek pliku, bieżącą pozycje, koniec pliku

SeekToBegin void SeekToBegin();
DWORD SeekToEnd();
    throw(CFileException);
Przesuwa wskaźnik pliku odpowiednio na początek i koniec pliku. Metoda SeekToEnd zwraca długość pliku.

Zarówno konstruktor klasy CFile jak i jej metoda Open wymagają podania stałych określających tryb otwarcia pliku. Stałe te opisują takie parametry związane z plikiem jak: tryb pracy binarny lub tekstowy, plik tylko do zapisu bądź odczytu, itp. W celu uzyskania wymaganego trybu pracy można łączyć poszczególne stałe wykorzystując operator " | " języka C++.
Poniżej przedstawiono stałe określające tryb otwarcia pliku:

Stała Działanie
CFile::modeCreate Zapewnia utworzenie nowego pliku. W przypadku gdy plik o podanej nazwie już istnieje następuje jego wyzerowanie..
CFile::modeNoInherit Zabezpiecza plik przed przejęciem przez proces potomny.
CFile::modeNoTruncate Stała ta w połączeniu z CFile::modeCreate powoduje, że jeśli tworzony plik już istnieje jego długość nie jest obcinana do zera.
CFile::modeRead Plik jest otwierany tylko do odczytu.
CFile::modeReadWrite Plik jest otwierany do odczytu i zapisu.
CFile::modeWrite Plik jest otwierany tylko do zapisu.
CFile::shareCompat Stała użyta w metodzie CFile::Open odpowiada CFile::shareExclusive.
CFile::shareDenyNone Inne procesy mogą odczytywać i zapisywać dane do otwartego pliku.
CFile::shareDenyRead Inne procesy nie maja dostępu do odczytu danych.
CFile::shareDenyWrite Inne procesy nie mają dostępu do zapisu danych.
CFile::shareExclusive Otwarcie pliku w trybie wyłączności - inne procesy nie mogą zapisywać ani odczytywać danych zawartych w pliku.
CFile::typeBinary Binarny tryb korzystania z pliku.
CFile::typeText Tekstowy tryb zapisu do pliku.

Szczegółowy opis klasy CFile oraz metod do niej należących (wraz z przykładami wykorzystania) można znaleźć w systemie pomocy środowiska Visual C++.
Przedstawiony przykład pokazuje sposób wykorzystania kilku z opisanych metod.

////////////////////////////////////////////////////////

// CFileSample.h - plik nagłówkowy klasy CFileSample



#include <afx.h>

...



class CFileSample

{

 private:

    ...



    // Tablica przechowująca dane do odczytu i zapisu

    char m_StrTab[3][20];

    ...



 public:

    // Deklaracja funkcji zapisującej dane

    BOOL SaveStrTab();

    // Deklaracja funkcji odczytującej dane

    void ReadStrTab();

};



////////////////////////////////////////////////////////

// CFileSample.cpp - ciało klasy CFileSample



#include "CFileSample.h"

...



// Zapis danych zawartych w tablicy StrTab do pliku; dane 

// zapisywane w 20 - bajtowych rekordach,

BOOL CFileSample::SaveStrTab()

{

    try

    {

        // Otwarcie pliku do zapisu

        CFile OutFile("str.dat", CFile::modeCreate | CFile::modeWrite);

    }

    catch(CFileException* e)

    {

        // Obsługa wyjątków zgłoszonych przez CFile::CFile,

        TRACE("Wystąpił błąd otwarcia pliku\n");

        e->Delete();

        return FALSE;

    }



    for(int indx=0; indx<3; indx++)

    {

        try

        {

            OutFile.Write(m_StrTab[indx], 20);

        }

        catch(CFileException* e)

        {

            // Obsługa wyjątków zgłoszonych przez Write,

            TRACE("Wystąpił błąd zapisu pliku\n");

            e->Delete();

            OutFile.Abort();

            return FALSE;

         }

    }



    OutFile.Close();

    return TRUE;

}





// odczyt danych z pliku do tablicy Strtab; w procedurze tej

// pominięto obsługę wyjątków,

void CFileSample::ReadStrTab()

{

    CFile InFile("str.dat", CFile::modeRead);



    for(int indx=0; indx<3; indx++)

    {

        InFile.Seek(20*indx, CFileBegin);

        InFile.Read(m_StrTab[indx], 20);

    }

    InFile.Close();

}

W aplikacjach, których szkielet jest generowany przez kreator AppWizard po wybraniu polecenie Otwórz... (Open...), Zapisz (Save), Zapisz jako... (Save as...) z menu Plik (File) jest wywoływana metoda Serialize(CArchive& ar) należąca do klasy dokumentu. Aby zastąpić metodę serializującą własnymi procedurami obsługi plików należy za pomocą kreatora ClassWizard powiązać odpowiednie elementy menu z funkcjami obsługi. Aby w takim przypadku móc korzystać ze standardowego okna dialogowego do wyboru lokalizacji i nazwy pliku należy w kodzie funkcji zapisującej i odczytującej dane utworzyć element klasy CFileDialog.
Poniżej przedstawiono przykład metody otwierającej odpowiednie okno dialogowe i zapisującej dane we własnym formacie.

////////////////////////////////////////////////////////

// CFilerDoc.cpp - ciało klasy dokumentu.

...

void CFilerDoc::OnFileSave()

{

    // Otwarcie okna dialogowego, kolejne parametry oznaczają: okno

    // do zapisu plików, standardowe rozszerzenie nazwy, defaultowa

    // nazwa pliku (odpowiada nazwie dokumentu), odpowiednie flagi,

    // string określający filtr wyboru plików,



    CFileDialog dialog(FALSE, ".dat", GetTitle(),

                       OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,

                       "Zapis danych (*.dat)|*.dat|Wszystkie pliki (*.*)|*.*||");



    // Wyświetlenie okna, jeśli zostanie wybrany przycisk Ok zostanie 

    // zwrócona wartość IDOK, w przeciwnym przypadku IDCANCEL,

    if(dialog.DoModal() == IDOK)

    {

        try

        {

            // Otwarcie pliku do zapisu, ścieżka i nazwa zostaje

            // pobrana z okna dialogowego

            CStdioFile file(dialog.GetPathName(),

                            CFile::modeCreate | CFile::modeWrite | CFile::typeText);

            ...



            // właściwy zapis danych do pliku

            ...

        }

        catch(CException *e)

        {

            // obsługa wyjątków: wysłanie odpowiedniego komunikatu

            // do okna dialogowego i skasowanie wyjątku,

            e->ReportError();

            e->Delete();

        }

    }

}

 


Strona główna