Форум АНТИЧАТ

Форум АНТИЧАТ (https://forum.antichat.xyz/index.php)
-   С/С++, C#, Delphi, .NET, Asm (https://forum.antichat.xyz/forumdisplay.php?f=24)
-   -   C++ и ООП фича для чуть более продвинутых новичков (https://forum.antichat.xyz/showthread.php?t=149119)

Ra$cal 17.10.2009 23:16

C++ и ООП фича для чуть более продвинутых новичков
 
Пожалуй на форуме не хватает топика про преимущества ООП. Нада бы исправить сей недостаток. А то многовато мнений, что с++ мощный язык, но при этом используют его ровно так же, как и си и паскаль. Я опишу интересный прием программирования с использованием ООП, который пригодился мне. Если еще что нить интересное вспомню и будет интерес - продолжу тему.

Итак, начнем. Наверняка вы сталкивались с ситуациями, когда необходимо освобождать ресурсы. Например файлы, память, сокеты, етц етц етц. Все просто, когда в начале функции открыли файл, в конце закрыли. Но как только линейность использования ресурсов нарушается или появляются зависимости - начинаются проблемы. Вторая проблема - не забыть закрыть файл в конце большой функции. Типичный пример первой проблемы - использование маппингов файлов. Обычно код выглядит примерно так:

Код:

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 нада заменить вызовом конструктора, но по мне итак вполне юзабельно)

razb 18.10.2009 00:32

Хороший обзоз, пример паттерна + многократное использование кода )
Вот только жаль что все чаще и чаще встречаешь с++ код подобный первому примеру, в натив си, еще такой подход приемлен, но не в плюсах )

scrat 18.10.2009 00:35

тут важно понять, что нужно всегда учитывать требования к задаче, вероятность доработки программы и прочее. Один разок записать/прочитать один файл - можно и процедурным программированием обойтись, также как не стоит перегружать оператор сложения для собственных классов, чтобы сложить два числа.

А вообще в продвинутых(читать высокоуровневых) платформах реализованы Garbage Collector'ы, которые автоматически подчищают все объекты и прочее.

Ra$cal 18.10.2009 00:49

Цитата:

А вообще в продвинутых(читать высокоуровневых) платформах реализованы Garbage Collector'ы, которые автоматически подчищают все объекты и прочее.
это типичная ошибка. GC адекватно подходит для очистки памяти. Но вот для других ресурсов(файлы, сокеты, все, что влияет на систему целиком, а не только на среду выполнения) его применимость под большим вопросом. Советую прочитать о специфике деструкторов в Java и C#. Выяснится не очень приятная их особенность. В дотнете это решено IDisposable, в Java хз как. так вот это я к тому, что GC без использования мозга и адекватного программирования не решает проблем, а лишь добавляет.

scrat 18.10.2009 01:00

Цитата:

Сообщение от Ra$cal
это типичная ошибка. GC адекватно подходит для очистки памяти. Но вот для других ресурсов(файлы, сокеты, все, что влияет на систему целиком, а не только на среду выполнения) его применимость под большим вопросом. Советую прочитать о специфике деструкторов в Java и C#. Выяснится не очень приятная их особенность. В дотнете это решено IDisposable, в Java хз как. так вот это я к тому, что GC без использования мозга и адекватного программирования не решает проблем, а лишь добавляет.

я думаю на закрытие потоков и прочих стандартных вещей они его заточили, в остальном - да, никаких тебе "правильных" закрытий


Время: 07:48