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.
- Idea działania serializacji.
- Sposób wykorzystania serializacji.
- Zalety i wady serializacji.
- Własne procedury I/O, podstawowe funkcje umożliwiające operacje na plikach.
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ć:
- wszystkie dane związane z obiektem poddawanym serializacji są zapisywane sekwencyjnie do jednego pliku dyskowego, przy odczycie nie jest możliwy swobodny dostęp do danych o dowolnym "adresie" - jak się wydaje jest to najpoważniejsza wada serializacji, dyskwalifikująca ją w przypadku implementacji baz danych oraz programów odczytujących zapisane dane fragmentami (konieczność "poruszania się" po pliku),
- mało efektywny sposób gospodarowania przestrzenią dyskową - jest do przyjęcia w dobie wysoko pojemnych dysków twardych oraz tanich pamięci, jak również przy zapisie małych struktur danych,
- serializacja jest techniką, którą można wykorzystać jedynie w aplikacjach systemu Windows opartych o bibliotekę MFC - znaczne ograniczenie przenośności klas zapisujących swoje dane w oparciu o tą metodę,
- w przypadku serializacji nowo tworzonych klas istnieje konieczność ich dziedziczenia z klasy CObject.
Niewątpliwymi zaletami serializacji są:
- brak konieczności specyfikowania formatu zapisu danych - odpowiada za to mechanizm serializacji,
- łatwy, a co za tym idzie, szybki sposób implementacji metody Serialize odpowiedzialnej za zapis i odczyt danych.
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(); } } }