Ra$cal
17.10.2009, 23:16
Пожалуй на форуме не хватает топика про преимущества ООП. Нада бы исправить сей недостаток. А то многовато мнений, что с++ мощный язык, но при этом используют его ровно так же, как и си и паскаль. Я опишу интересный прием программирования с использованием ООП, который пригодился мне. Если еще что нить интересное вспомню и будет интерес - продолжу тему.
Итак, начнем. Наверняка вы сталкивались с ситуациями, когда необходимо освобождать ресурсы. Например файлы, память, сокеты, етц етц етц. Все просто, когда в начале функции открыли файл, в конце закрыли. Но как только линейность использования ресурсов нарушается или появляются зависимости - начинаются проблемы. Вторая проблема - не забыть закрыть файл в конце большой функции. Типичный пример первой проблемы - использование маппингов файлов. Обычно код выглядит примерно так:
bool ReadData (char* File, char* buffer)
{
bool is_readed = false;
MappingAddr = 0;
hFile = CreateFile (File, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
goto ERROR_CREATE_FILE;
hMapping = CreateFileMapping (hFile, 0, PAGE_READWRITE, 0, 0, 0);
if (hMapping == 0)
goto ERROR_CREATE_MAPPING;
MappingAddr = (char*)MapViewOfFile(hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);
if (MappingAddr == 0)
goto ERROR_VIEW_MAPPING;
strcpy(buffer, (char*)MappingAddr);
is_readed = true;
ERROR_VIEW_MAPPING:
CloseHandle (hMapping);
ERROR_CREATE_MAPPING:
CloseHandle (hFile);
ERROR_CREATE_FILE:
return is_readed;
}
Вариант рабочий, но не очень красивый. Кутерьма из-за необходимости освободить хэндлы, которые удалось открыть, но произошла ошибка в дальнейшем доступе к хэндлам. Давайте поиграем в игру - найдите решение. Оно должно свестись к убиранию CloseHandle'ов из кода, т.е. чтобы ресурсы освобождались сами. Хинт - тема этого поста - деструкторы. Попробуйте придумать свое решение, потом глядите сюда. Если впадлу - просто читаем дальше.
Ну а теперь перейдем к решению. Итак, первым делом создаем 3 класса - KFile для файла, KMapping для маппинга и KViewOfFile для проекции файла в память.
#include <iostream>
#include <Windows.h>
using namespace std;
class KFile{
private:
HANDLE hFile;
public:
KFile(){hFile = INVALID_HANDLE_VALUE;}
~KFile(){
if(hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
HANDLE handle() const{
return hFile;
}
bool isOpened(){
return !(hFile == INVALID_HANDLE_VALUE);
}
bool open(char* path){
hFile = CreateFile (path, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
return isOpened();
}
};
class KMapping{
private:
HANDLE hMapping;
public:
KMapping(){hMapping = NULL;}
~KMapping(){
if(hMapping != NULL)
CloseHandle(hMapping);
hMapping = NULL;
}
HANDLE handle() const{
return hMapping;
}
bool isOpened(){
return !(hMapping == NULL);
}
bool open(const KFile& file){
hMapping = CreateFileMapping (file.handle(), 0, PAGE_READWRITE, 0, 0, 0);
return isOpened();
}
};
class KViewOfFile{
private:
BYTE* MappingAddr;
public:
KViewOfFile(){MappingAddr = NULL;}
~KViewOfFile(){
if(MappingAddr != NULL)
UnmapViewOfFile(MappingAddr);
MappingAddr = NULL;
}
BYTE* value() const{
return MappingAddr;
}
bool isOpened(){
return !(MappingAddr == NULL);
}
bool open(const KMapping& mapping){
MappingAddr = (BYTE*)MapViewOfFile(mapping.handle(), FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);;
return isOpened();
}
};
void test()
{
KFile file;
KMapping mapping;
KViewOfFile view;
if(!(file.open("test.txt") & mapping.open(file) & view.open(mapping))){
cout << "error opening file\n";
return;
}
char text[50];
strcpy(text, (char*)view.value());
cout << "file readed\n";
cout << text << "\n";
}
void main()
{
test();
system("pause");
}
Вся работа осуществляется в функции test(). Открываем маппинг одной строкой в ифе. Если хоть один из классов не отрабатывает - выходим из функции, причем не заморачиваясь об закрытии хэндлов. Так же не заморачиваемся этим вопросом и в случае удачного чтения. А все почему?
Суть приема. А потому, что мы объявили объекты классов локально. Это значит, что при выходе из функции они будут уничтожены. Уничтожение объекта приводит к вызову его деструктора. Всю логику контроля хэндлов мы как раз там и реализовали. В итоге все работает и без нашего вмешательства.
В данном примере есть только одно опасное место(не считая конструктора копирования, который лучше бы сделать приватным. Или решив проблему как то иначе. В чем суть проблемы - попробуйте понять сами, зная лишь что конструктор копирования просто копирует значение полей в новый объект) - в данном случае критичен порядок определения переменных наших классов, т.к. уничтожаются они в обратном порядке относительно создания. А для нас это вполне важно. Нужно сначала делать унмап, потом закрывать маппинг и только потом закрывать хэндл файла. Это на самом деле очень плохо, т.к. заставляет нас помнить об этой фиче. Но для удобства мы можем поступить просто - сделать еще один класс, который сокроет(инкапсулирует) все шаги открытия маппинга до одной операции - open. Например так:
class KFileMapping{
private:
KFile file;
KMapping mapping;
KViewOfFile view;
public:
KFileMapping(){
}
~KFileMapping(){
close();
}
void close(){
view.close();
mapping.close();
file.close();
}
bool open(char* path){
if(!(file.open(path) & mapping.open(file) & view.open(mapping))){
close();
return false;
}
return true;
}
BYTE* value() const{
return view.value();
}
};
Ну и использовать так:
void test2()
{
KFileMapping mapping;
if(!mapping.open("test.txt")){
cout << "error opening file\n";
return;
}
char text[50];
strcpy(text, (char*)mapping.value());
cout << "file readed\n";
cout << text << "\n";
}
Итого имеем:
1) Защита от ошибок
2) Упрощение кодирования
3) Повторное использование кода. Постепенно со временем вы соберете подборку своих классов, реализующих наиболее нужный вам функционал, с нужными вам интерфейсами(например не часто приходится указывать, как открывать файл - только для чтения, только для записи, итп. Вы делаете так, чтобы этим было максимально удобно пользоваться. Когда понадобится указывать доступ - просто добавите новый метод, минимально трогая старую реализацию). Ну вот собсно и все.
PS: то, что мы с вами сделали, это по сути паттерн RAII (велкам ту вики), только немного модифицированный(в идеале метод open нада заменить вызовом конструктора, но по мне итак вполне юзабельно)
Итак, начнем. Наверняка вы сталкивались с ситуациями, когда необходимо освобождать ресурсы. Например файлы, память, сокеты, етц етц етц. Все просто, когда в начале функции открыли файл, в конце закрыли. Но как только линейность использования ресурсов нарушается или появляются зависимости - начинаются проблемы. Вторая проблема - не забыть закрыть файл в конце большой функции. Типичный пример первой проблемы - использование маппингов файлов. Обычно код выглядит примерно так:
bool ReadData (char* File, char* buffer)
{
bool is_readed = false;
MappingAddr = 0;
hFile = CreateFile (File, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
goto ERROR_CREATE_FILE;
hMapping = CreateFileMapping (hFile, 0, PAGE_READWRITE, 0, 0, 0);
if (hMapping == 0)
goto ERROR_CREATE_MAPPING;
MappingAddr = (char*)MapViewOfFile(hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);
if (MappingAddr == 0)
goto ERROR_VIEW_MAPPING;
strcpy(buffer, (char*)MappingAddr);
is_readed = true;
ERROR_VIEW_MAPPING:
CloseHandle (hMapping);
ERROR_CREATE_MAPPING:
CloseHandle (hFile);
ERROR_CREATE_FILE:
return is_readed;
}
Вариант рабочий, но не очень красивый. Кутерьма из-за необходимости освободить хэндлы, которые удалось открыть, но произошла ошибка в дальнейшем доступе к хэндлам. Давайте поиграем в игру - найдите решение. Оно должно свестись к убиранию CloseHandle'ов из кода, т.е. чтобы ресурсы освобождались сами. Хинт - тема этого поста - деструкторы. Попробуйте придумать свое решение, потом глядите сюда. Если впадлу - просто читаем дальше.
Ну а теперь перейдем к решению. Итак, первым делом создаем 3 класса - KFile для файла, KMapping для маппинга и KViewOfFile для проекции файла в память.
#include <iostream>
#include <Windows.h>
using namespace std;
class KFile{
private:
HANDLE hFile;
public:
KFile(){hFile = INVALID_HANDLE_VALUE;}
~KFile(){
if(hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
HANDLE handle() const{
return hFile;
}
bool isOpened(){
return !(hFile == INVALID_HANDLE_VALUE);
}
bool open(char* path){
hFile = CreateFile (path, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
return isOpened();
}
};
class KMapping{
private:
HANDLE hMapping;
public:
KMapping(){hMapping = NULL;}
~KMapping(){
if(hMapping != NULL)
CloseHandle(hMapping);
hMapping = NULL;
}
HANDLE handle() const{
return hMapping;
}
bool isOpened(){
return !(hMapping == NULL);
}
bool open(const KFile& file){
hMapping = CreateFileMapping (file.handle(), 0, PAGE_READWRITE, 0, 0, 0);
return isOpened();
}
};
class KViewOfFile{
private:
BYTE* MappingAddr;
public:
KViewOfFile(){MappingAddr = NULL;}
~KViewOfFile(){
if(MappingAddr != NULL)
UnmapViewOfFile(MappingAddr);
MappingAddr = NULL;
}
BYTE* value() const{
return MappingAddr;
}
bool isOpened(){
return !(MappingAddr == NULL);
}
bool open(const KMapping& mapping){
MappingAddr = (BYTE*)MapViewOfFile(mapping.handle(), FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);;
return isOpened();
}
};
void test()
{
KFile file;
KMapping mapping;
KViewOfFile view;
if(!(file.open("test.txt") & mapping.open(file) & view.open(mapping))){
cout << "error opening file\n";
return;
}
char text[50];
strcpy(text, (char*)view.value());
cout << "file readed\n";
cout << text << "\n";
}
void main()
{
test();
system("pause");
}
Вся работа осуществляется в функции test(). Открываем маппинг одной строкой в ифе. Если хоть один из классов не отрабатывает - выходим из функции, причем не заморачиваясь об закрытии хэндлов. Так же не заморачиваемся этим вопросом и в случае удачного чтения. А все почему?
Суть приема. А потому, что мы объявили объекты классов локально. Это значит, что при выходе из функции они будут уничтожены. Уничтожение объекта приводит к вызову его деструктора. Всю логику контроля хэндлов мы как раз там и реализовали. В итоге все работает и без нашего вмешательства.
В данном примере есть только одно опасное место(не считая конструктора копирования, который лучше бы сделать приватным. Или решив проблему как то иначе. В чем суть проблемы - попробуйте понять сами, зная лишь что конструктор копирования просто копирует значение полей в новый объект) - в данном случае критичен порядок определения переменных наших классов, т.к. уничтожаются они в обратном порядке относительно создания. А для нас это вполне важно. Нужно сначала делать унмап, потом закрывать маппинг и только потом закрывать хэндл файла. Это на самом деле очень плохо, т.к. заставляет нас помнить об этой фиче. Но для удобства мы можем поступить просто - сделать еще один класс, который сокроет(инкапсулирует) все шаги открытия маппинга до одной операции - open. Например так:
class KFileMapping{
private:
KFile file;
KMapping mapping;
KViewOfFile view;
public:
KFileMapping(){
}
~KFileMapping(){
close();
}
void close(){
view.close();
mapping.close();
file.close();
}
bool open(char* path){
if(!(file.open(path) & mapping.open(file) & view.open(mapping))){
close();
return false;
}
return true;
}
BYTE* value() const{
return view.value();
}
};
Ну и использовать так:
void test2()
{
KFileMapping mapping;
if(!mapping.open("test.txt")){
cout << "error opening file\n";
return;
}
char text[50];
strcpy(text, (char*)mapping.value());
cout << "file readed\n";
cout << text << "\n";
}
Итого имеем:
1) Защита от ошибок
2) Упрощение кодирования
3) Повторное использование кода. Постепенно со временем вы соберете подборку своих классов, реализующих наиболее нужный вам функционал, с нужными вам интерфейсами(например не часто приходится указывать, как открывать файл - только для чтения, только для записи, итп. Вы делаете так, чтобы этим было максимально удобно пользоваться. Когда понадобится указывать доступ - просто добавите новый метод, минимально трогая старую реализацию). Ну вот собсно и все.
PS: то, что мы с вами сделали, это по сути паттерн RAII (велкам ту вики), только немного модифицированный(в идеале метод open нада заменить вызовом конструктора, но по мне итак вполне юзабельно)