

Программирование на языке MFC
Мой второй блог в серии программирования
Архив раздела «Приложения»
Что ж, в связи с важностью метода необходимо этот метод рассмотреть более подробно. Понятно, что в качестве аргумента методу передается указатель на записываемый в архив объект. Сразу после проведения необходимых проверок вызывается метод MapObject(). При этом (обратите, пожалуйста, на это внимание, читатель) методу MapObject() в качестве аргумента передается не указатель на объект, как следовало бы ожидать, a NULL. Исходный код метода MapObject() также находится в файле arcobj.cpp:
void CArchive::MapObject(const CObject* pOb) {
if (IsStoring()) {
if (m_pStoreMap == NULL) {
// initialize the storage map
// (use CMapPtrToPtr because it is used for
// HANDLE maps too)
m_pStoreMap = new CMapPtrToPtr(m_nGrowSize) ; m_pStoreMap->InitHashTable(m_nHashSize); m_pStoreMap->SetAt(NULL, (void*) (DWORD)wNullTag) ; m_nMapCount = 1;
}
if (pOb != NULL) {
CheckCount(); (*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;
}
}
else {
if (m_pLoadArray == NULL) {
// initialize the loaded object pointer array // and set special values m_pLoadArray = new CPtrArray; m_pLoadArray->SetSize(1, m_nGrowSize); ASSERT(wNullTag == 0); m_pLoadArray->SetAt(wNullTag, NULL); m_nMapCount = 1;
}
if (pOb != NULL) {
CheckCount() ;
m_pLoadArray->InsertAt(m_nMapCount++, (void*)pOb);
}
}
}
читать отзывы (0)
Для того чтобы записать объект в архив, необходимо воспользоваться методом WriteObject().Ero исходный код находится в файле arcobj.cpp:
void CArchive::WriteObject(const CObject* pOb) {
// object can be NULL
ASSERT(IsStoringO); // proper direction DWORD nOblndex;
ASSERT(sizeof(nOblndex) == 4); ASSERT(sizeof(wNullTag) == 2); ASSERT(sizeof(wBigObjectTag) == 2); ASSERT(sizeof(wNewClassTag) == 2);
// make sure m_pStoreMap is initialized MapObject(NULL);
if (pOb == NULL) {
// save out null tag to represent NULL pointer *this « wNullTag;
}
else if ((nOblndex = (DWORD)(*m_pStoreMap)[(void*)pOb])
!= 0)
// assumes initialized to 0 map
{
// save out index of already stored object if (nOblndex < wBigObjectTag)
*this « (WORD)nOblndex; else {
*this « wBigObjectTag; *this « nOblndex;
}
else {
// write class of object first
CRuntimeClass* pClassRef = pOb->GetRuntimeClass(); WriteClass(pClassRef);
// enter in stored object table, checking for overflow CheckCount();
(*m_pStoreMap) [ (void*) pOb] = (void**) m_nMapCount++;
// cause the object to serialize itself ((CObject*)pOb)->Serialize(*this); }
Как нетрудно догадаться, метод Write() действует так же, как и метод ReadQ. Как и в случае метода ReadQ, если число записываемых байтов равно нулю, то метод, не выполняя никаких действий, сразу же возвращает нулевое значение. Но если нам нужно на самом деле записать в архив определенное число байтов, то метод производит следующие действия. Сначала подготовленными для записи данными заполняется буфер архива. Если данные удалось нормально разместить в буфере архива, то работа метода на этом прекращается и метод возвращает число записанных в буфер архива байтов. Вероятно, что все данные, которые необходимо записать в архив, полностью размещены в буфере не будут. В этом случае метод сбрасывает буфер архива в файл, используя при этом метод FlushQ, а затем осуществляет запись в файл напрямую из буфера пользователя тех частей данных, которые потребовали бы перезаписи буфера архива. Как и в случае метода ReadQ, в буфере остается только последняя часть подготовленных данных. Все данные, которые могли бы полностью заполнить буфер, записываются в файл напрямую. Однако в этом методе есть минимум один «подводный камень». Обратите внимание на то, что последняя часть данных, оставшаяся в буфере, в файл НЕ СБРАСЫВАЕТСЯ! Если сейчас закрыть файл, то часть данных, которая находится в буфере архива, в файл сброшена не будет! Для того чтобы предотвратить потерю данных необходимо после записи всех данных в архив осуществлять вызов метода FlushQ, который произведет сброс в файл остатка данных, находящегося в буфере архива.
Подводя итог сказанному, можно сделать вывод, что метод предназначен для буферированной записи в архив подготовленных данных.
Что ж, уважаемый читатель, мы познакомились с некоторыми методами класса CArchive. Из того, что мы узнали про архив, можно пока сделать только один вывод – архив может использоваться для буферированного ввода и вывода данных в файл. Кроме этого, возможно, удобно бужет использовать перегруженные операторы и «»» ««» для работы сданными простых типов. Однако, как мне кажется, вы испытываете легкое разочарование. Подумаешь, стоило ли огород городить ради того, чтобы осуществлять буферированный ввод/вывод? Но не стоит торопить события. То, о чем мы говорили, является всего лишь прелюдией к тому, ради чего и создан класс CArchive. А создан он для того, чтобы можно было записывать в файл и читать из файла не только данные, но и объекты! А вот это уже интересно,верно?
Естественно, раз существует метод Read(), то, наверное, должен существовать и метод Write(). Действительно, такой метод существует, его исходный код находится в файле агссоге.срр:
void CArchive::Write(const void* lpBuf, UINT nMax) {
ASSERT_VALID(m_pFile);
if (nMax == 0) return;
ASSERT(lpBuf !=NULL);
ASSERT(AfxIsValidAddress(lpBuf, nMax, FALSE));
// read-only access needed
ASSERT(m_bDirectBuffer || m_lpBufStart !=NULL); ASSERT(mjDDirectBuffer || m_lpBufCur !=NULL); ASSERT(m_lpBufStart == NULL ||
AfxIsValidAddress(m_lpBufStart,
m_lpBufMax – m_lpBufStart) ) ; ASSERT(m_lpBufCur == NULL ||
AfxIsValidAddress(m_lpBufCur,
m_lpBufMax – m_lpBufCur) ) ;
ASSERT(IsStoring());
// copy to buffer if possible UINT nTemp = min(nMax, (UINT)(m_lpBufMax – m_lpBufCur)); memcpy(m_lpBufCur, lpBuf, nTemp); m_lpBufCur += nTemp; lpBuf = (BYTE*)lpBuf + nTemp; nMax -= nTemp;
if (nMax > 0) {
Flush(); // flush the full buffer
// write rest of buffer size chunks
nTemp = nMax – (nMax % m_nBufSize); m_pFile->Write(lpBuf, nTemp); lpBuf = (BYTE*)lpBuf + nTemp; nMax -= nTemp;
if (m_bDirectBuffer) {
// sync up direct mode buffer to new file position
VERIFY(m_pFile->GetBufferPtr(CFile::bufferWrite,
m_nBufSize, (void**) &m__lpBufStart, (void**)&m_lpBufMax) == (UINT)m_nBufSize); ASSERT((UINT)m_nBufSize == (UINT)(m_lpBufMax -
m_lpBufStart));
m_lpBufCur = m_lpBufStart;
}
// copy remaining to active buffer ASSERT(nMax < (UINT)m_nBufSize); ASSERT(m_lpBufGur == m_lpBufStart); memcpy(m_lpBufCur, lpBuf, nMax); m_lpBufCur += nMax;
}
}
Назначение аргументов, передаваемых методу, предельно ясно. Первый аргумент – это буфер с данными, подготовленными для записи. Второй аргумент – размер этих данных, т. е. число байтов в буфере, которые необходимо записать в архив.
В качестве первого аргумента передается указатель на буфер, в который будет производиться считывание данных. Второй аргумент – это число байтов, которые необходимо считать из файла, с которым архив ассоциирован. Посмотрим теперь, что происходит во время работы метода.
Само собой разумеется, что если число байтов, которые мы хотим считать, равно нулю, то никаких действий не производится и метод немедленно возвращает нуль. Однако, если мы все же хотим считать определенное количество байтов, то нам нужно вспомнить о том, что с нашим архивом ассоциирован буфер в оперативной памяти. Вполне вероятно, что до обращения к методу Read() мы уже осуществляли чтение в буфер, правильно? Значит, все данные, которые мы хотим считать, или хотя бы часть этих данных могут находиться в буфере, не так ли? Можно просто взять эту часть данных из буфера, а не обращаться к диску, верно? Метод копирует тот участок буфера архива, который находится между указателями mJpBufCur и mJpBufMax, в начало буфера, указатель на который передан методу в качестве первого аргумента. Корректировка указателей при этом пока не производится.
Значит, читать осталось только ту часть, которую мы не считывали ранее, верно? Соответственно этому, метод корректирует счетчик байтов, подлежащих считыванию. Кроме этого, мы уже, возможно, при копировании части буфера архива уже заполнили данными часть переданного методу буфера, верно? Значит, необходимо сместить и указатель на то место, куда мы будем записывать данные. Метод осуществляет и эту коррректировку.
Если те данные, которые мы собирались читать из архива, полностью находились в буфере, то на этом работа метода завершается. Метод возвращает число взятых из буфера байтов (в данном случае безразлично, откуда берутся данные, верно?).
Но если необходимо читать данные из файла… Если бы метод читал данные из файла в буфер архива, а затем бы копировал их в буфер пользователя, то при этом возникли бы непроизводительные затраты, так как при многократном считывании пришлось бы каждый раз затирать данные буфера архива. Разработчики MFC поступили в данном случае более интересно, в буфере архива остается только ПОСЛЕДНЯЯ часть данных, вся остальная информация копируется из-файла в буфер пользователя напрямую. Обратите внимание, как вычисляется размер этой последней части – просто берется остаток от деления числа байтов, подлежащих считыванию из файла за вычетом находившихся в буфере архива, на размер буфера архива. При этом производятся все действия по заполнению буфера, которые производилсь и в методе FillBufferQ. После того как чтение выполнено полностью, метод возвращает общее число считанных из буфера и из файла байтов.
После всего того, что мы узнали, можно сделать вывод о том, что метод осуществляет буферированное чтение из архива.
Но, несмотря на то, что текст метода дочтаточно велик, понять принцип его работы гораздо проще. Если мы осуществляем чтение из архива, устанавливаем указатель файла на конец действительно считанных, а не буферированных данных, после чего указатель на текущую позицию приравниваем указателю на считанные данные, т. е. определяем, что буфер становится пустым. Если мы осуществляем запись в архив, то все, что находится в промежутке от начала до текущей позиции буфера, записывается в файл. После этого указатель на текущую позицию буфера устанавливается на начало буфера. Все! Буфер пуст! © Другими словами, метод осуществляет запись в файл содержимого буфера, после чего текущей позицией в буфере становится начало буфера (буфер «сбрасывается»).
Для того чтобы осуществить чтение из архива определенного числа байтов, можно вопользоваться методом Read(), чей исходный код находится в файле агссоге.срр:
UINT CArchive::Read(void* lpBuf, UINT nMax) {
ASSERT_VALID(m_pFile); if (nMax == 0) return 0;
ASSERT(lpBuf !=NULL);
ASSERT(AfxIsValidAddress(lpBuf, nMax)); ASSERT(m_bDirectBuffer || m_lpBufStart !=NULL); ASSERT(m_bDirectBuffer || m_lpBufCur !=NULL); ASSERT(m_lpBufStart == NULL ||
AfxIsValidAddress(m_lpBufStart,
m_lpBufMax – m_lpBufStart, FALSE)); ASSERT(m_lpBufCur == NULL ||
AfxIsValidAddress(m_lpBufCur,
m_lpBufMax – m_lpBufCur, FALSE));
ASSERT(IsLoading());
// try to fill from buffer first UINT nMaxTemp = nMax; UINT nTemp = min(nMaxTemp,
(UINT)(m_lpBufMax – m_lpBufCur)); memcpy(lpBuf, m_lpBufCur, nTemp); m_lpBufCur += nTemp; lpBuf = (BYTE*)lpBuf + nTemp; nMaxTemp -= nTemp;
if (nMaxTemp != 0) {
ASSERT(m_lpBufCur == m_lpBufMax);
// read rest in buffer size chunks
nTemp = nMaxTemp – (nMaxTemp % m_nBufSize); UINT nRead = 0;
UINT nLeft = nTemp;
UINT nBytes;
do ;.
{
nBytes = m_pFile->Read(lpBuf, nLeft); lpBuf = (BYTE*)lpBuf + nBytes; nRead += nBytes; nLeft -= nBytes;
}
while ((nBytes > 0) && (nLeft > 0));
nMaxTemp -= nRead;
// read last chunk into buffer then copy if (nRead == nTemp) {
ASSERT(m_lpBufCur — m_lpBufMax); ASSERT(nMaxTemp < (UINT)m_nBufSize);
// fill buffer (similar to CArchive::FillBuffer,
// but no exception) «-
if (!m_bDirectBuffer)
{
UINT nLeft = max(nMaxTemp, (UINT)m_nBufSize) ; UINT nBytes;
BYTE* lpTemp = m_lpBufStart; nRead = 0;
do {
nBytes = m_pFile->Read(lpTemp, nLeft); lpTemp = lpTemp + nBytes; nRead += nBytes; nLeft -= nBytes;
}
while ((nBytes > 0) && (nLeft > 0) && nRead < nMaxTemp);
m_lpBufCur = m_lpBufStart; m_lpBufMax = m_lpBufStart + nRead;
}
else
{
nRead = m_pFile->GetBufferPtr(CFile::bufferRead,
m_nBufSize, (void**)&m_lpBufStart, (void**)&m_lpBufMax) ; ASSERT(nRead == (UINT) (m_lpBufMax – m_lpBufStart) ) ; m_lpBufCur = m_lpBufStart;
// use first part for rest of read nTemp = min(nMaxTemp,
(UINT)(m_lpBufMax – m_lpBufCur)); memcpy(lpBuf, m_lpBufCur, nTemp); m_lpBufCur += nTemp; nMaxTemp -= nTemp;
}
}
return nMax – nMaxTemp;
}
Теперь мы знаем, как осуществляется чтение из архива простых типов данных. Надеюсь, для того чтобы разобрать перегруженный оператор ««», нам потребуется значительно меньше времени. Код оператора также существует в двух вариантах, опять мы остановимся на более простом, который находится в файле afx.h:
Схема взаимоотношения буфера и его полей
_AFX_INLINE CArchive& CArchive::operator«(WORD w) {
if (m_lpBufCur + sizeof(WORD) > m_lpBufMax) Flush();
*(UNALIGNED WORD*)m_lpBufCur = w; m_lpBufCur += sizeof(WORD); return *this;
}
Так… В том случае, если для записи в буфер не хватает места, то вызывается метод Flush(), исходный код которого находится в файле агссоге.срр и тоже достаточно велик:
void CArchive::Flush() {
ASSERT_VALID(m_pFile);
ASSERT(m_bDirectBuffer || m_lpBufStart !=NULL); ASSERT(m_bDirectBuffer || m_lpBufCur !=NULL); ASSERT(m_lpBufStart == NULL ||
AfxIsValidAddress(m_lpBufStart,
m_lpBufMax – m_lpBufStart,
IsStoring ())); ASSERT(m_lpBufCur == NULL ||
AfxIsValidAddress(m_lpBufCur,
m_lpBufMax – m_lpBufCur,
IsStoring ()));
if (IsLoading()) {
// unget the characters in the buffer, // seek back unused amount
if (m_lpBufMax != m_lpBufCur)
m_pFile->Seek(-(m_lpBufMax – m_lpBufCur), CFile::current); m_lpBufCur = mjlpBufMax; // empty
}
else {
if (!m_bDirectBuffer) {
// write out the current buffer to file if (m_lpBufCur != m_lpBufStart) m_pFile->Write(m_lpBufStart, m_lpBufCur – m_lpBufStart);
}
else {
// commit current buffer
if (m_lpBufCur != m_lpBufStart)
m_pFile->GetBufferPtr(CFile::bufferCommit,
m_lpBufCur – m_lpBufStart) ;
// get next buffer «.
VERIFY(m_pFile->GetBufferPtr(CFile::bufferWrite,
m_nBufSize, (void**)&m_lpBufStart, (void**)&m_lpBufMax) == (UINT)m_nBufSize) ; ASSERT((UINT)m_nBufSize == (UINT)(m_lpBufMax -
m_lpBufStart)) ;
}
m_lpBufCur = m_lpBufStart;
}
}
Первым делом оператор проверяет, хватит ли распределенной под буфер памяти для того, чтобы записать еще одно слово. Если памяти не хватает, то вызывается метод FillBuffer(), которому в качестве аргумента передается количество байтов, недостаточных для нормального выполнения операции записи. Исходный код метода FillBuffer() можно найти в файле агссоге.срр. Я прошу читателя запастись терпением, потому что код метода достаточно объемен:
void CArchive::FillBuffer(UINT nBytesNeeded) {
ASSERT_VALID(m_pFile); ASSERT (IsLoadingO ) ;
ASSERT(m_bDirectBuffer || m_lpBufStart !=NULL); ASSERT(m_bDirectBuffer || m_lpBufCur !=NULL); ASSERT(nBytesNeeded > 0);
ASSERT(nBytesNeeded <= (UINT)m_nBufSize); ASSERT(m_lpBufStart == NULL ||
AfxIsValidAddress(m_lpBufStart,
m_lpBufMax – m_lpBufStart, FALSE)); ASSERT(m_lpBufCur == NULL ||
AfxIsValidAddress(m_lpBufCur,
m_lpBufMax – m_lpBufCur, FALSE));
UINT nUnused = m_lpBufMax – m_lpBufCur;
ULONG nTotalNeeded = ( (ULONG)nBytesNeeded) + nUnused;
// fill up the current buffer from file if (!m_bDirectBuffer) {
ASSERT(m_lpBufCur != NULL); ASSERT(m_lpBufStart != NULL); ASSERT(m_lpBufMax !=NULL);
if (m_lpBufCur > m_lpBufStart) {
// copy unused
if ( (int)nUnused > 0) {
memmove(m_lpBufStart, m_lpBufCur, nUnused); m_lpBufCur = m_lpBufStart; m_lpBufMax = m_lpBufStart + nUnused;
}
// read to satisfy nBytesNeeded or nLeft if possible UINT nRead = nUnused; UINT nLeft = m_nBufSize-nUnused; UINT nBytes;
BYTE* lpTemp = m_lpBufStart + nUnused;
do
{
nBytes = m_pFile->Read(lpTemp, nLeft); lpTemp = lpTemp + nBytes; nRead += nBytes; nLeft -= nBytes;
}
while (nBytes > 0 && nLeft > 0 && nRead < nBytesNeeded);
m_lpBufCur = m_lpBufStart; m_lpBufMax = m_lpBufStart + nRead;
}
}
else {
// seek to unused portion and get the buffer
I/ starting there if (nUnused != 0)
m_pFile->Seek(-(LONG)nUnused, CFile::current); UINT nActual = m_pFile->GetBufferPtr(CFile::bufferRead,
m_nBufSize, (void**)&m_lpBufStart, (void**)&m_lpBufMax) ; ASSERT(nActual == (UINT)(m_lpBufMax – m_lpBufStart)); m_lpBufCur = m_lpBufStart;
}
// not enough data to fill request?
if ( (ULONG) (m_lpBufMax – m_lpBufCur) < nTotalNeeded) AfxThrowArchiveException(CArchiveException::endOfFile);
}
После проверки и, при необходимости, выдачи всех предупреждений метод определяет прежде всего число неиспользуемых байтов в буфере. Обратите внимание на то, что это значение вычисляется как разность между значениями полей mJpBufMax и mJpBufCur, а не (mJpBufStart + m_nBufSize – mJpBufCur). Почему? Пока непонятно… Затем определяется, сколько всего байтов необходимо для того, чтобы операция чтения могла завершиться нормально. Естественно, число необходимых байтов равно сумме числа неиспользуемых байтов в буфере и числа недостающих байтов, переданных методу в качестве аргумента. Далее происходит следующее. Если у нас буфер занят частично, т. е. число неиспользуемых байтов не равно нулю, то мы копируем свободный участок памяти в начало буфера. При этом указатель на текущую позицию в буфере устанавливается на начало буфера. Это понятно. Но непонятно следующее – почему указатель m JpBufMax устанавливается на конец переписанного участка? Подождите, пожалуйста, осталось совсем недолго…
Идем дальше. И вот здесь… Посмотрите: значение переменной nRead делается равным числу неиспользованных байтов! Опять загадка. .. Определяем, сколько байтов еще необходимо считать для того, чтобы заполнить буфер до конца, читаем из файла в буфер… Полю mJpBufCur присваиваивается значение указателя на начало буфера… Вот оно! В поле mJpBufMax мы записываем число считанных байтов! Т. е. поле mJpBufMax указывает на конец тех данных, которые мы считали! Обычно поле mJpBufMax будет указывать на считывании достигли конца файла! В этом случае поле mJpBufMax будет указывать куда-то на середину буфера, на последний байт считанных данных! Т. е. поле mJpBufMax указывает на конец считанных данных!
Понятно теперь, почему неиспользованным участком считается участок от указателя на текущую позицию в буфере до указателя mJpBufMax. Конечно же! Ведь после предыдущих операций чтения мы могли выполнять операции записи, т. е. указатель на текущую позицию в файле мог сместиться в сторону уменьшения. Но ведь в этом участке находятся данные, которые мы уже считывали! Зачем нам осуществлять считывание одних и тех же данных повторно? Естественно, нам легче их скопировать из одного места буфера в другое, чем осуществлять лишние обращения к диску!
Схематично взаимоотношения буфера и его полей показаны на 9.
Подводя итог сказанному выше, можно сделать вывод о том, что метод FHIBufferQ осуществляет заполнение выделенного архиву буфера данными из файла, осуществляя буферированное чтение данных.
Обратите внимание, что указатель m JpBufCur после осуществления считывания данных указывает на начало буфера. А теперь, если мы вернемся в код перегруженного оператора «»», мы увидим, что сразу после вызова FillBuffer() оператор записывает по указанной ему ссылке считанное слово и увеличивает указатель mJpBufCur на величину, равную размеру считанного данного. Указатель mJpBufCur занимает то положение, которое и должен занимать!
В классе CArchive операторы ««» и «»» для каждого типа определены дважды. Применяются они в зависимости от того, осуществлено ли определение константы препроцессора _AFXJ3YTESWAP. Если такое определение осуществлено, то перед записью в архив порядок следования байтов в словах будет изменяться. Но помимо директивы препроцессора для того, чтобы изменялся порядок следования байтов, мы должны указать конструктору, что режим работы архива должен предусматривать изменение порядка следования байтов. Для простоты мы будем рассматривать случай, когда порядок следования байтов изменяться не должен.
Исходный код перегруженной операции ««» для переменной типа WORD, как, впрочем, и для переменных других типов, находится в файле afx.ini (коды для случая необходимости изменения порядка следования байтов находятся в файле afxcore.cpp):
_AFX_INLINE CArchive& CArchive:: operator» (WORD& w) { if (m_lpBufCur + sizeof(WORD) > m_lpBufMax)
FillBuffer(sizeof(WORD) – (UINT)(m_lpBufMax -
m_lpBufCur));
w = * (UNALIGNED WORD*)m_lpBufCur; m_lpBufCur += sizeof(WORD); return *this;
}
В данном случае аргументами конструктора являются указатель на буфер в памяти, размер буфера и значение приращения. При открытии указатель файла устанавливается в начало файла. Обратите внимание на то, каким образом устанавливается размер файла. Если при вызове конструктора мы указали, что nGrowBytes равно нулю, то размер файла будет соответствовать размеру буфера в памяти. Стоит указать ненулевое приращение – нулевым будет размер файла. Кстати, по умолчанию размер приращения при использовании этого конструктора равен нулю. Возможно, это связано с механизмом выделения дополнительной памяти буферу? Что ж, немного позже мы поймем, соответствует ли наша догадка действительности.

Естественно, поле mJpBuffer делается равным указанному размеру буфера. Таким образом, мы практически полностью определили то, что нам нужно для управления данными в памяти – указатель на буфер, размер буфера и размер приращения.
У класса CMemFile есть два конструктора. Первый из них в некотором смысле можно назвать конструктором «по умолчанию». Исходный код его находится в файле filemem.cpp:
CMemFile::CMemFile(UINT nGrowBytes) {
ASSERT(nGrowBytes <= UINT_MAX); m_nGrowBytes = nGrowBytes; m_nPosition = 0; m_nBufferSize = 0; m_nFileSize = 0; m_lpBuffer = NULL; m_bAutoDeiete = TRUE;
}
Очевидно, что при вызове конструктора не происходит никаких действий, за исключением инициализации полей объекта. Итак, поле m_nGrowBytes содержит приращение (количество байтов), на которое будет увеличиваться буфер в памяти в тех случаях, если размер буфера окажется недостаточным для хранения записываемых в него данных. По умолчанию значение m_nGrowBytes равно 1024. Поле m_nPosition представляет собой указатель текущей позиции в файле. Поле mJpBuffer, указатель на ассоциированный с объектом буфер в памяти, делается равным NULL, что фактически означает, что у объекта нет ассоциированного с ним буфера. В таком случае естественным выглядит и то, что значение длины файла, т. е. поля m_nFileSize, тоже делается равным нулю. Этот конструктор может быть использован в тех случаях, когда в момент вызова конструкторабуфер в памяти еще не выделен или же конструктор не может получить адрес этого буфера.
Исходный код второго конструктора тоже находится в файле filemem.cpp:
CMemFile::CMemFile(BYTE* lpBuffer,
UINT nBufferSize, UINT nGrowBytes)
{
ASSERT(nGrowBytes <= UINT_MAX);
m_nGrowBytes = nGrowBytes;
m_nPosition = 0;
m_nBufferSize = nBufferSize;
m_nFileSize = nGrowBytes == 0 ? nBufferSize : 0; m_lpBuffer = lpBuffer; m_bAutoDelete = FALSE;
}
В библиотеке классов MFC реализован еще один тип файлов, так называемый файл в памяти. Как следует из названия файла, файл осуществляет хранение данных не на устройстве, а в оперативной памяти. Как станет впоследствии понятно из исходных текстов методов, файл в памяти представляет собой просто буфер в памяти, к которому можно обращаться точно таким же образом, как к файлу на диске.
Устройство файла в памяти упрощенно можно представить следующим образом. В памяти физически выделяется буфер. Внутри этого буфера определяется некоторая зона, назовем ее зоной файла, в которую можно производить запись данных и из которой эти данные можно читать. При необходимости зона, а соответственно, и буфер, динамически увеличивают свои размеры, но при этом всегда размер буфера больше или, по крайней мере, равен размеру зоны файла.
Схематично устройство файла в памяти представлено на рисунке выше. На 8 весь прямоугольник представляет собой буфер в памяти. Зона файла на рисунке затенена. Заштрихованная часть зоны файла представляет собой расстояние от начала файла или, что то же самое, от начала файла, до текущей позиции файла.
При работе с файлом в памяти программист не задумывается о буфере и зоне, для работы с файлом в памяти используются привычные методы Read(), Write(), Seek() и так далее. С точки зрения программиста единственным отличием файла в памяти от файла на диске является то, что файл в памяти не имеет имени (в данном случае «именем» файла является указатель на буфер ©). Естественно, что файл в памяти существует до тех пор, пока программа может управлять памятью. При потере указателя на блок памяти, разрушении по какой-то причине памяти или, тем более, при выходе из операционной системы или выключении компьютеpa файл в памяти прекращает свое существование. В файле afx.h этот класс описан следующим образом:
class CMemFile : public CFile {
DECLARE_DYNAMIC (CMemFile) public:
// Constructors
CMemFile(UINT nGrowBytes = 1024); CMemFile(BYTE* lpBuffer,
UINT nBufferSize,
UINT nGrowBytes = 0) ;
// Operations
void Attach(BYTE* lpBuffer,
UINT nBufferSize,
UINT nGrowBytes = 0); BYTE* Detach();
// Advanced Overridables protected:
virtual BYTE* Alloc(DWORD nBytes);
virtual BYTE* Realloc(BYTE* lpMem, DWORD nBytes);
virtual BYTE* Memcpy(BYTE* lpMemTarget,
const BYTE* lpMemSource, UINT nBytes); virtual void Free(BYTE* lpMem); virtual void GrowFile(DWORD dwNewLen);
// Implementation protected:
UINT m_nGrowBytes;
DWORD m_nPosition;
DWORD m_nBufferSize;
DWORD m_nFileSize;
BYTE* m_lpBuffer;
BOOL m_bAutoDelete;
public:
virtual -CMemFile() ; #ifdef _DEBUG
virtual void Dump(CDumpContext& dc) const;
virtual void AssertValid() const;
#endif
virtual DWORD GetPosition() const;
BOOL GetStatus(CFileStatus& rStatus) const;
virtual LONG Seek(LONG lOff, UINT nFrom);
virtual void SetLength(DWORD dwNewLen);
virtual UINT Read(void* lpBuf, UINT nCount);
virtual void Write(const void* lpBuf, UINT nCount);
virtual void Abort ();
virtual void Flush ();
virtual void Close ();
virtual UINT GetBufferPtr(UINT nCommand,
UINT nCount = 0,
void** ppBufStart = NULL,
void** ppBufMax = NULL);
// Unsupported APIs
virtual CFile* Duplicate() const;
virtual void LockRange(DWORD dwPos, DWORD dwCount); virtual void UnlockRange(DWORD dwPos, DWORD dwCount);
};
Исходный код деструктора класса находится в файле filetxt.cpp:
CStdioFile::-CStdioFile() {
ASSERT_VAL-ID (this) ;
if (m_pStream != NULL && m_bCloseOnDelete) Close ();
}
Как нетрудно заметить, если у объекта есть ассоциированный с ним поток и установлени признак закрытия файла при уничтожении объекта, то файл, а соответственно и поток, закрывается.
В отличие от первой версии, функции в качестве аргумента передаются указатель на строку (не на объект класса CString), в которую будут считаны данные, и максимально возможная длина строки. Метод ЕДИНСТВЕННЫЙ РАЗ вызывает функцию _fgetts(). В том случае, если строка была считана нормально, метод возвращает указатель на считанную строку. В противном случае метод формирует исключение. Нужно заметить, что максимальный размер строки, которая может быть считана, ограничивается вторым аргументом метода.
Произвести запись строки в поток можно при помощи метода WriteString(). Его исходный код находится в файле filetxt.cpp:

void CStdioFile::WriteString(LPCTSTR lpsz) {
ASSERT(lpsz != NULL); ASSERT(m_pStream != NULL);
if (_fputts(lpsz, m_pStream) == _TEOF)
AfxThrowFileException(CFileException::diskFull,
_doserrno, m_strFileName);
}
В качестве аргумента функции передается указатель на записываемую строку. Для записи строки в поток метод вызывает функцию _fgetts(). Если во время записи происходит ошибка, формируется исключение.
Отсюда видно, что размер буфера фактически на единицу превышает указанную максимальную длину буфера. Последний элемент при выделении буфера получает нулевое значение, т. е. фактически является терминатором строки.
Далее в методе осуществляется запуск цикла, который прерывается только в случае считывания полной строки или достижения конца файла. Внутри цикла осуществляется считывание строки из потока, ассоциированного с объектом, при помощи функции _fgetts(). При этом метод фактически использует динамический буфер для считывания строки. Первоначальный размер буфера, выделяемого для строки, равен 128 символам. В том случае, если функцией _fgetts() были считаны 128 символов, но при этом не был достигнут конец строки, размер выделенного буфера увеличивается еще на 128 байтов (точнее, свыделяется новый буфер большего размера, содержимое старого буфера копируется в новый буфер, после чего старый буфер освобождается), и так до тех пор, пока строка не будет считана полностью или не будет достигнут конец файла. Если считывание строки прошло нормально, то метод возвращает значение TRUE. В том случае, если при чтении информации из потока произошла ошибка, то формируется исключение. Подводя итог сказанному выше, можно сказать, что метод осуществляет считывание строки в переменную типа CString. При этом размер считываемой строки практически не ограничен.
Существует также более простая версия метода ReadString(). Ее исходный код также находится в файле filetxt.cpp:
LPTSTR CStdioFile::ReadString(LPTSTR lpsz, UINT nMax) {
ASSERT(lpsz != NULL);
ASSERT(AfxIsValidAddress(lpsz, nMax)); ASSERT(m_pStream != NULL);
LPTSTR IpszResult = _fgetts(lpsz, nMax, m_pStream);
if (IpszResult == NULL && !feof(m_pStream))
{
clearerr(m_pStream);
AfxThrowFileException(CFileException::generic,
_doserrno, m_strFileName);
return IpszResult;
}
Аргументом метода является ссылка на переменную типа CString. Что же происходит при чтении строки из потока? Можно заметить, что переменную, переданную методу в качестве аргумента, перед вызовом метода обнулять вовсе не обязательно, потому что метод сразу же присваивав этой строке значение afxChNil, которое описано в файле strcore.cpp следующим образом:
AFX_DATADEF TCHAR afxChNil = Л\0′;
Другими словами, метод производит обнуление переданной ему строки самостоятельно. Затем происходит выделение буфера для считывания строки. При этом параметр nMaxSize определяет не количество символов в буфере, а максимальную размерность символьного массива, в который фактически будет записана строка. При выделении буфера указатель на этот буфер формируется следующим образом: pData = (CStringData*) new BYTE[sizeof(CStringData) +
(nLen+1)*sizeof(TCHAR)];
Методы Read() и Write() с функциональной точки зрения не отличаются от соответствующих методов родительского класса. С точки зрения реализации от методов родительского класса они отличаются тем, что для записи информации используется функция _fputts(), а не ReadFile(), а для чтения данных используется функция _fgetts(), а не WriteFile(). Однако у класса CStdioFile есть два дополнительных метода, позволяющих осуществлять запись в файл не просто определенного количества символов, а строки, завершающейся нулем. Методом, осуществляющим чтение строки из файла, является ReadString(), текст которого находится в файле fileext.cpp:
BOOL CStdioFile::ReadString(CString& rString) {
ASSERT_VALID(this);
rString = SafxChNil; // empty string without
// deallocating const int nMaxSize = 128;
LPTSTR lpsz = rString.GetBuffer(nMaxSize);
LPTSTR IpszResult;
int nLen = 0;
for (;;)
{
IpszResult = _fgetts(lpsz, nMaxSize+1, m_pStream); rString.ReleaseBuffer() ;
// handle error/eof case
if (IpszResult == NULL && !feof(m_pStream)) {
clearerr(m_pStream);
AfxThrowFileException(CFileException::generic,
_doserrno, m_strFileName);
}
»
// if string is read completely or EOF if (IpszResult == NULL ||
(nLen = lstrlen(lpsz)) < nMaxSize ||
lpsz[nLen-l] == л\п‘)
break;
nLen = rString.GetLength() ;
lpsz = rString.GetBuffer(nMaxSize + nLen) + nLen;
}
// remove л\п‘ from end of string if present lpsz = rString.GetBuffer(0); nLen = rString.GetLength(); if (nLen != 0 && lpsz[nLen-l] == л\п‘) rString.GetBufferSetLength(nLen-1);
return IpszResult != NULL;
}
Методы, используемые для получения информации о текущем положении указателя файла (GetPosition()) и установки указателя файла (Seek(), SeetToBegin(), SeekToEnd()) в объектах класса CStdioFile, практически ничем не отличаются от соответствующих методов родительского класса. Разница состоит лишь в том, что в случае класса CStdioFile для установки указателей файла используются функции fseek() и ftell(), а не функция SetFilePointer(), которая применяется в родительском классе.
Метод закрывает ассоциированный с объектом поток, что автоматически приводит и к закрытию файла, «очищает» значения полей m_hFile и тjpStream. В случае возникновении ошибки при закрытии потока формируется исключение.
В том случае, если для программиста не очень важно, закрылся поток нормально или с ошибкой, программист может воспользоваться методом Abort(), текст которого приведен ниже:
void CStdioFile::Abort() {
ASSERT_VALID(this);
if (m_pStream != NULL && m_bCloseOnDelete)
fclose(m_pStream); // close but ignore errors m_hFile = (UINT) hFileNull; m_pStream = NULL; m_bCloseOnDelete = FALSE;
Как и метод Close(), метод Abort() закрывает поток, обнуляет поля m_hFile и m_pStream, но в случае возникновения ошибки при закрытии потока никаких исключений не формируется.
Текст метода можно разделить на несколько частей. В первой части производится открытие файла при помощи вызова метода Ореп() родительского класса. Во второй части метода из переданных методу аргументов формируются параметры для открытия потока. Третья часть производит открытие потока, при этом полю m_p_Stream присваивается указатель на открытый поток, т. е. объект ассоциируется с потоком. И наконец, в том случае, если поток по каким-то причинам открыть невозможно, формируется исключение. Резюмируя сказанное выше, делаем вывод: метод открывает файл, открывает поток и ассоциирует файл и поток с объектом.
Для закрытия файла и потока используется метод Close(), исходный текст которого находится в файле filetxt.cpp:
void CStdioFile::Close() {
ASSERT_VALID(this); ASSERT(m_pStream != NULL);
int nErr = 0;
if (m_pStream != NULL)
nErr = fclose(m_pStream);
m_hFile = (UINT) hFileNull; m_bCloseOnDelete = FALSE; m_pStream = NULL;
if (nErr != 0)
AfxThrowFileException(CFileException::diskFull,
_doserrno, m_strFileName);
}
Поток, который будет ассоциирован с объектом, может быть открыт при помощи вызова метода Ореп(), текст которого находится в файле filetxt.cpp:
BOOL CStdioFile::Open(LPCTSTR IpszFileName,
UINT nOpenFlags, CFileException* pException)
{
ASSERT(pException == NULL ||
AfxIsValidAddress(pException,
sizeof(CFileException))); ASSERT(IpszFileName !=NULL); ASSERT(AfxlsValidString(IpszFileNameП;
m_pStream = NULL;
if (! CFile :-.Open (IpszFileName, (nOpenFlags & -typeText) , pException))
return FALSE;
ASSERT(m_hFile != hFileNull); ASSERT(m_bCloseOnDelete);
char szMode[4]; // C-runtime open string int nMode = 0;
// determine read/write mode depending on CFile mode
if (nOpenFlags & modeCreate) {
if (nOpenFlags & modeNoTruncate)
szMode[nMode++] = ла‘; else
szMode[nMode++] = yw’;
}
else if (nOpenFlags & modeWrite)
szMode[nMode++] = ла‘; else
szMode[nMode++] = лг‘; // add л+’ if necessary (when read/write modes mismatched)
if (szMode[0] == yr’ && (nOpenFlags & modeReadWrite) I I szMode[0] != лг‘ && !(nOpenFlags & modeWrite))
{
// current szMode mismatched, need to add y+’ to fix szMode[nMode++] = л+’;
}
// will be inverted if not necessary int nFlags = _0_RDONLY|_0_TEXT; if (nOpenFlags & (modeWriteImodeReadWrite)) nFlags A= _0_RDONLY;
if (nOpenFlags & typeBinary)
szMode[nMode++] = ЛЬ‘, nFlags л= _0_TEXT; else
szMode[nMode++] = yt’; szMode[nMode++] = Л0′;
// open a C-runtime low-level file handle
int nHandle = _open_osfhandle(m_hFile, nFlags);
// open a C-runtime stream from that handle if (nHandle != -1)
m_pStream = _fdopen(nHandle, szMode);
if (m_pStream == NULL) {
// an error somewhere along the way…
if (pException != NULL)
{
pException->m_10sError = _doserrno; pException->m_cause =
CFileException::OsErrorToException(_doserrno);
}
CFile::Abort(); // close m_hFile return FALSE;
}
return TRUE;
}
И наконец, третий конструктор. Его аргументами, как и в случае родительского класса, являются указатель на строку, содержащую имя открываемого файла, и флаги открытия файла. Текст этого конструктора можно найти в файле filetxt.cpp:
CStdioFile::CStdioFile(LPCTSTR IpszFileName, UINT nOpenFlags) {
ASSERT(IpszFileName != NULL);
ASSERT(AfxIsValidString(IpszFileName));
CFileException e;
if (!Open(IpszFileName, nOpenFlags, &e)) AfxThrowFileException(e.m_cause,
e.m_10sError,
e.m_strFileName);
>
Вполне очевидно, что конструктор создает объект и открывает файл при помощи вызова метода Ореп(). В случае возникновения ошибки формируется исключение.
Ниже приведен текст второго конструктора, аргументом которого является указатель на ранее созданный поток:
CStdioFile::CStdioFile(FILE* pOpenStream) : CFile(hFileNull) {
m_pStream = pOpenStream;
m_hFile = (UINT)_get_osfhandle(_fileno(pOpenStream)); ASSERT(!m_bCloseOnDelete);
}
При ближайшем рассмотрении текста конструктора становится очевидно, что конструктор создает объект, причем указатель на поток присваивается полю т__pStream, а полю mJhFile унасле-дованнму из родительского класса, присваивается значение хэндла файла, соответствующего потоку. Другими словами, конструктор ассоциирует уже созданный поток и соответствующий потоку файл с объектом. Конструктор целесообразно использовать в тех случаях, когда к моменту создания объекта поток уже создан.
Как и у родительского класса, у класса CStdioFile также есть три конструктора. Первый конструктор – по умолчанию. Его исходный код, который находится в файле filetxt.cpp, приведен ниже:
CStdioFile::CStdioFile() {
m_pStream = NULL;
}
Из текста следует, что конструктор создает объект и присваивает полю т_pStream значение NULL, указывая тем самым, что у объекта нет ассоциированного с ним потока. Как и в случае родительского класса, этот конструктор целесообразно использовать в том случае, когда к моменту создания объекта поток либо не существует и его создание только предполагается, либо у объекта нет никаких данных о потоке и предполагается, что поток будет ассоциирован с объектом позже.
Итак, связь между сообщениями, указателями на функции-обработчики, а также аргументами функций и возвращаемыми значениями нам вполне ясна. Значит, мы можем вернуться к рассмотрению макроса DECLARE_MESSAGE_MAP.
Для доступа к элементам таблицы сообщений используется структура messageMap типа AFXJVISGMAP. Эта структура описана в файле afxwin.h:
struct AFX_MSGMAP {
#ifdef _AFXDLL
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
const AFX_MSGMAP* pBaseMap;
#endif
const AFX_MSGMAP_ENTRY* IpEntries;
};
