Всем нам требуется получать доступ к файлам в наших программах. Иногда это происходит не часто преимущественно при старте или завершении. В этом случае вы используете очень маленький конфигурационный файл и скорость работы вам не важна. Но очень часто, при программировании игры вы сталкиваетесь с тем, что у вас есть большие базы данных, которые сидят в огромных файлах и вам надо просматривать их на протяжении всего процесса выполнения программы. Если вы будете пытаться работать с такими файлами при помощи обычных функций для работы с файлами, вы рискуете заработать хронические приступы немотивированной агрессии на нервной почве, ибо программа будет работать очень м-е-е-е-д-л-е-н-н-о. А если вы еще и копируете содержимое всех ваших файлов, проблем вообще будет не счесть, потому что вам придется самому заниматься всей рутиной менеджмента памяти.
Из-за всех этих проблем, ребята из Microsoft разработали более быстрый и даже
более простой способ работы с файлами. Он имеет название файл-маппинга (создание
карт файла - file mapping) и входит в те необходимые сведения, которые должна
была сообщить вам мамочка в детстве, но не сделала этого.
Этот учебник имеет своей целью покрыть упущение вашей мамочки. Однако, в этом
учебнике речь пойдет и об обычных способах работы с файлами, поэтому если вы
вообще не работали с файловым менеджментом под Win32 и все еще используете стандартные
библиотеки C/C++ этот документ для вас.
Прежде, чем мы начнем, я бы рекомендовал вам скачать файл FILE.CPP, чтобы вы могли увидеть работающие функции, которые используют Win32 API для работы с файлами. Я использовать VC++ для тестирования всех примеров.
И напоследок, я бы хотел извинится, за те ошибки, которые я понаделал в английских словах (а я в русских, - прим. перев.) ибо мой английский еще далек от совершенства. Я из Аргентины, а там люди обычно говорят по-испански).
Ежели вы обнаружите какие-либо ошибки в этом учебнике, пожалуйста дайте мне знать.
Первая вещь, которую вы делаете с файлом - это открываете его, прямо как в старом добром DOSе. Но теперь это делается с помощью API-функции, которая называется CreateFile. Смущены, да? Ну вообще-то есть еще и функция OpenFile, но первая мне больше нравится.
Вот прототип для этой API-функции:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
Что означают все эти параметры:
lpFileName: имя файла, который вы хотите открыть/создать
dwDesiredAccess: значение, которое может быть 0, GENERIC_READ (означает, что вы хотите прочитать файл), GENERIC_WRITE (вы хотите писать в файл) или комбинацией GENERIC_READ | GENERIC_WRITE (и читать и писать одновременно)
dwShareMode: означает что надо делать, если кто-то попытается подступиться к тому же файлу, который вы используете. Если значение равно 0, совместное использование не разрешено. Если значение равно FILE_SHARE_READ, Windows будет разрешать другим программам открывать файл только как если бы он был с аттрибутом "только для чтения". Если же значение будет FILE_SHARE_WRITE, совместный доступ будет разрешен только если другие программы подступаются к этому файлу под доступ для записи. Конечно, вы можете комбинировать значения, но я не вижу смысла использовать варианты, отличные от 0 или FILE_SHARE_READ
lpSecurityAttributes: это имеет какое-то значение для NT. Оставьте его NULL.
dwCreationDisposition: этот параметр должен быть одним из следующих значений:
dwFlagsAndAttributes: может быть любой комбинацией из следующих аттрибутов за исключением лишь FILE_ATTRIBUTE_NORMAL, который должен использоваться отдельно от других
hTemplateFile: этот параметр всегда должен равняться NULL, иначе ваша
программа не будет работать
В большинстве случаев, я опустил несколько возможных значений или деталей, которые на самом деле не важны, если вы используете CreateFile для открытия файлов. Если вы заинтересованы в большей информации об этой функции, свяжитесь со мной.
Вы должны сохранить хэндл (указатель), возвращаемый функцией CreateFile, потому что он понадобится для работы с файлом, который вы открыли. Если вдруг функция вернула значение INVALID_HANDLE_VALUE, то значить что-то не так и вам не надо закрывать хэндл. Если все в порядке, то вы работаете с хэндлом файла и когда подойдет время завершения, вы закрываете его следующим способом:
CloseHandle(hndFile);
гдк hndFile - есть ваш хэндл файла.
Посмотрите как работают Windows API. У вас есть функция, описание которой занимает пару страниц, потому что в ней куча всяких параметров, хотя из них вам потребуются только два или три. Это хорошая причина, по которой вам следует создать на основе этих API класс или что-нить в этом роде.
Эй, вы, кто уже испугался файловой системы Windows, после того, как всего-лишь посмотрел на функцию CreateFile, слушайте сюда! На самом деле все будет гораздо проще выглядеть, когда вы начнете это дело программировать. Посмотрите на CPP-файл, который идет с этим документом и вы обнаружите, что большинство параметров установлено в NULL или 0. Ни в коем случае не продолжайте использовать файловые функции C++ потому что следуя этим путем, вы лишитесь мощи Win32 API.
Ну что, решили продолжать? Тогда добро пожаловать в мир файловых API!
Для того, чтобы читать данные из файла, вы можете использовать функцию ReadFile. Ниже показан ее прототип:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
а так же прототип функции для записи в файл:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
У обеих функций одинаковые параметры:
hFile: должен являться хэндлом файла, который вы открыли чуть раньше.
lpBuffer: указатель буфера откуда вы хотите получить данные или наоборот, куда хотите их поместить
nNumberOfBytesToRead, nNumberOfBytesToWrite: указывает количество байт, которое будет использовано для данной операции. Если оно равно 0, то WriteFile не будет записывать никакие данные, но зато изменить дату последнего обращения к файлу
lpNumberOfBytesRead, lpNumberOfBytesWritten: указатель на DWORD, куда Windows запишет сколько байт на самом деле было прочитано или записано. Это бесполезно для операций с файлами, но может пригодиться в других ситуациях
lpOverlapped: нам это не нужно, установите этот параметр в NULL.
Если обе функции нарвутся на ошибку, возвращаемое значение будет 0.
Когда вы используете WriteFile или ReadFile, информация читается (или записывается) yfxbyfz c байта, на который показываетуказатель файла. Когда вы открываете файл, он указывает на первый байт, после каждой операции, он увеличивается. Если вы получили первые 10 байт, последующие операции будут работать с 11м байтом и дальше.
Существует функция, которая позволяет вам менять местоположение указателя файла. Ее прототип:
DWORD SetFilePointer(
HANDLE hFile,
LONG lDistanceToMove,
PLONG lpDistanceToMoveHigh,
DWORD dwMoveMethod
);
Описание каждого параметра следует дальше:
hFile: тот самый хэндл файла
lDistanceToMove: количество байт (расстояние), на которое надо переместить указатель. Положительные значения означают, что надо двигаться вперед, а отрицательные - назад.
lpDistanceToMoveHigh: ребята из Microsoft не хотели ограничивать размер файла возможностями переменной LONG, поэтому придумали систему, которая позволяет программисту использовать километровые файлы, используя 64-битное значение, как расстояние. Впрочем, вам понадобятся эти значения, если размер файла больше, чем 4,294,967,294 байт (2^32 - 2), что всего на чуть-чуть меньше, чем четыре гигабайта. Если ваш файл еще не настолько турбоулетный, то вам лучше установить этот параметр в NULL
dwMoveMethod: может принимать одно из следующих значений:
SetFilePointer() возвращает значение нового файлового указателя, если все хорошо,
а если не очень, то его значение будет равно 0xFFFFFFFF. Если вы маньяк и не
используете NULL в четвертом параметре, то значения будут другими.
Если тип вашего доступа к файлу GENERIC_WRITE, вам может потребоваться использование функции FlushFileBuffers. Ее единственный параметр является хэндлом файла, а если она возвращает 0, то функция ошиблась. Эта функция сбрасывает на диск содержимое внутреннего буфера Windows. В большинстве случаев, эта функция вам не понадобится, потому что кэширование в Windows работает хорошо, однако если вы занимаетесь чем-то таким странным, то тогда наверное и понадобится. Имейте лишь в виду, что изменения, которые вы вносите в файл не остаются там сразу и навсегда. Зато когда вы закроете хэндл, все будет в порядке.
BOOL FlushFileBuffers(
HANDLE hFile
);
Есть еще одна полезная функция - SetEndOfFile. У нее только один параметр - хэндл файла. Если произошла ошибка, то возвращаемое значение 0. Действие, которое выполняет функция сводится к помещению в текущей позиции указателя файла индикатора конца файла (EOF).
BOOL SetEndOfFile(
HANDLE hFile
);
Другая функция, которая вам может понадобится, особенно если вы собираетесь
использовать файл-маппинг называется GetFileSize. Ее прототип:
DWORD GetFileSize(
HANDLE hFile,
LPDWORD lpFileSizeHigh
);
hFile, это как всегда хэндл файла, а lpFileSizeHigh - указатель
на DWORD, где Windows будет сохранять старший байт возвращенного значения. Он
может быть и NULL. Вам нужен файл, открытый способом GENERIC_WRITE или GENERIC_READ
для использования этой функции.
Размер файла будет кроме того как записан в lpFileSizeHigh, еще и возвращаться самой функцией, если значение lpFileSizeHigh отличается от NULL. Если нет, то только в возвращаемом значении.
Если вам вернули 0xFFFFFFFF и lpFileSizeHigh равен NULL, то значит, что функция выполнила недопустимую операцию и будет... тьфу... вы можете узнать причину с помощью GetLastError
Если возвращенное значение 0xFFFFFFFF, но lpFileSizeHigh не NULL, вам следует вызвать GetLastError, чтобы выяснить действительно ли это ошибка или все-таки это такой размер файла. В последнем случае, GetLastError должен возвратить NO_ERROR.
DWORD GetLastError(
VOID
);
Будьте осторожны с SetEndOfFile. БУДЬТЕ ОЧЕНЬ ОСТОРОЖНЫ!!! Прочитайте нижеследующее внимательно:
Некоторое время назад (пару лет) я программировал нечто, что требовало базы данных в несколько мегабайт. База данных была в моем собственном формате, но так как она должна была сохранять пользовательские данные, после первых запусков программы база была практически пуста.
Для генерации этой практически пустой базы данных я использовал WriteFile для записи некоторых данных и SetEndOfFile для придания ей нужного размера. К счастью, меня отличала изрядная толика параноидальности, так что перед распространением этой базы, я открыл ее в шестнадцатиричном редакторе. Мое лицо надо было видеть!!! Большая часть моих файлов, исходного кода и мыльников, по которым я писал и читал перед созданием базы данных были там, в конфигурационных файлах, которые я собирался распространять множеству людей. Винда решила заполнить пустую часть базы данных всяким барахлом, которое валялось в памяти. Включая и довольно интимные подробности.
Поэтому, вам нужно быть предельно осторожными. Если только вы не разрабатываете проект с Открытым Кодом, вам вряд ли понравится, чтобы всякие Василии Пупкины читали ваше расписание или другие вещи, как почту, которую вы получали от ваших друзей. Так что имейте в виду тот способ, которым Windows заполняет пустые участки перед использованием этой функции для искусственной накачки файла.
Файл-маппинг - это быстрейший и простейший способ доступа к файлам, особенно после того, как вы узнаете как им пользоваться. Это одна из возможностей Windows, которая настолько хороша, насколько велико будет ваше удивление почему лишь немногие используют ее.
При работе с файл-маппингом, вам не надо получать хэндл для использования его с другими API, вы получаете указатель на блок данных прямо в памяти. А далее - заботой Windows будет являться какую часть файла копировать в память и так далее.
Единственный недостаток этой системы сводится к тому, что вы не можете менять размер файла, когда к нему применен маппинг. Обычными функциями работы с файлами, вы можете выполнять WriteFile и указатель EOF будет расти вместе с размером файла. Но такая штука с файл-маппингом не пройдет. Вам надо отменить маппинг файла и изменять размер каким-нибудь способом (например, SetEndOfFile).
Но простота в использовании и прирост в скорости настолько велики, что вам больше никогда не потребуется использовать стандартные функции. Создайте на основе маппинга класс и используйте файлы, как память!
Первое, что вам надо сделать, чтобы применить маппинг к файлу - это открыть его с помощью CreateFile. Затем вам надо использовать следующую функцию:
HANDLE CreateFileMapping(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
Параметры означают следующее:
hFile: А ну отгадайте. Правильно! Хэндл файла, который вы получили с помощью CreateFile.
lpFileMappingAttributes: Какая-такая безопасность? Ставьте ее как NULL
flProtect: Говорит о способе, которым вы хотите использовать карту файла. Значения могут быть следующие:
Когда я писал этот учебник, я дуржал в уме фразу "Кратко и Просто", поэтому, я нарочно "забыл" те параметры, которые используются редко. Поверьте мне, так будет лучше. Если вы хотите получить больше информации, скиньте мне мыло. Если наберется целая команда из людей, которые будут спрашивать об этом, я можут быть напишу еще одну статью, в которой расскажу о сложных или редко используемых параметрах/вещах, связанных с обработкой файлов.
Когда вы создаете объект карты файла с хэндлом файла, вы не должны использовать
этот хендл с помощью обычных средств обработки файлов до тех пор, пока не закроете
объект файл-маппинга.
CreateFileMapping возвращает другой хэндл, который вы должны сохранить или NULL, если произойдет ошибка.
Когда вы используете объект FileMapping вы должны закрывать его. Делайте это с помощью ClseHandle прежде чем закрывать хэндл файла на диске.
Наконец, последним этапом прежде чем файл-маппинг будет работать является создание образа файла. Чтобы сделать это, вам надо использовать следующую API:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);
параметры этой функции:
hFileMappingObject: хэндл, возвращенный CreateFileMapping
dwDesiredAccess: посмотрите как много раз вам надо говорить какой способ доступа к файлу вам нужен. Это есть принципиальная причина, по которой стоит создавать класс. Могуть быть следующие значения:
Я всегда создаю карту на весь файл, потому что это проще, и вам не надо беспокоиться
о памяти Windows, даже если ваш файл не очень большой. Windows будет работать
с вашим файлом с примененным маппингом так, как она работает со swap-файлом,
когда какая-то часть его всегда находится в памяти, а когда вы пытаетесь подступиться
к байту, которого в памяти нет, Win32 берет его с диска.
Насколько я знаю, Windows работает с маппингом файлов так же, как она работает с виртуальной памятью, когда некоторые участки памяти являются на самом деле областями на диске, хотя вы можете подступаться к ним через указатели. Поэтому, если ОС потребуется немножко свободной памяти, она выкинет часть размеченного файла на диск, а потом заберет обратно, когда понадобится.
MapViewOfFile возвращает указатель на область памяти. Превосходство заключается в том, что все, что вы в этой памяти наизменяете, отразится на диске. Запомните только, что вы не можете менять размер размеченного файла, поэтому не используйте области, которые находятся дальше, чем последний байт файла, или Windows пришибет вашу программу. Используйте GetFileSize перед тем, как разметить файл, чтобы наверняка знать где находится EOF.
Вы можете получить столько образов файла из объекта FileMapping, сколько захотите. Но в большинстве своем, вам не понадобится более одного.
Наконец, чтобы закончить работу с файлом, вы должны уничтодить все образы файлов функцией UnmapViewOfFile и только затем закрывать хэндлы объекта FileMapping и файла. Прототип UnmapViewOfFile вот:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);
где lpBaseAddress все тот же указатель MapViewOfFile, данный вам. Функция
возвращает ноль, если что-то пошло не так.
Windows не записывает немедленно изменения, которые вы сделали в образе файла. В любое время, когда вы захотите записать их, используйте:
BOOL FlushViewOfFile(
LPCVOID lpBaseAddress,
DWORD dwNumberOfBytesToFlush
);
где lpBaseAddress говорит с какого байта API будет начинать запись, а
dwNumberOfBytesToFlush - количество байт, которое надо записывать. Если
это число равно 0, все содержимое файла начиная с lpBaseAddress и до конца будет
записано.
При закрытии объекта FileMapping, все изменения будут сохранены на диске, поэтому вам даже не надо беспокоиться об этой функии большинство времени.
Используя именованные объекты FileMapping, вы можете использовать совместный доступ к памяти. Но не пытайтесь использовать одни и те же хэндлы в разных процессах с дескриптором безопасности по умолчанию. Потому что вы не можете. В большинстве случаев, хэндлы в Win32 действительны только для процессов, которые иих создают. Чтобы использовать одни хэндлы в разных процессах, вам надо делать всякие странные штуки типа дубликации хэндлов и тому подобное. Но все-таки они вам большую часть времени не понадобятся.
Ну вот, пришло время закругляться. Вы мне можете с уверенностью мылить и высказать все, что думаете об этой статье или если у вас есть мысля, которая как вы думаете может быть полезна мне.
Ну и веселого вам миллениума (конечно сейчас это звучит дико, но кому какая разница!)
Best regards