Конструкторы и деструкторы

Последнее обновление: 04.09.2023

Если функция использует множество ресурсов, выделяет и осовобождает для них память, а выделение одного ресурса зависит от успешности выделения другого, то на уровне кода программы это может вылить в каскад вложенных конструкциий if. Что, в свою очередь, может затруднять чтение и поддержку подобной функции, особенно когда ресурсов много. В прошлых темах в качестве возможного решения предалагалось применение оператора goto или ленивое выделение ресурсов. Тем не менее при подобных подходах по мере возрастания количество используемых ресурсов все равно повышается сложность функции, снижается ее читабельность. И в качестве еще одного решения можно применять концепцию конструкторов и деструкторов.

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

void someFunction()
{
    allocateResources();    // условный конструктор
    // некоторые основные действия
    freeResources();        // условный деструктор
}

Если ресурсы не представляют глобальные переменные, а определяются локально в функции, то они передаются в конструктор/деструктор через параметры. Чтобы не передавать все множество ресурсов по отдельности, можно определить один сборный объект. Если выделение какого-то ресурса прошло неудачно, то эта информация сохраняется в переменную (например, в виде значения NULL). А при использовании или очистке ресурсов эта переменная проверяется на валидность.

Рассмотрим пример. Пусть изначально у нас имеется следующая программа, которая ищет определенное слово в некотором файле:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

// результат поиска
typedef enum {ERROR, FOUND, NOTFOUND} status;

status find(char*, char*, size_t);
status find_word(FILE*, char*, size_t, char*);

status find(char* file_name, char* word, size_t word_size)
{
    FILE* file_pointer = 0;
    char* buffer = 0;
    status result = ERROR;

    // если не соблюдаются предусловия, завершаем функцию
    assert(file_name!=NULL && "File name is invalid!");
    assert(word!=NULL && "Word is invalid!");

    // если удалось открыть файл
    if((file_pointer=fopen(file_name, "r"))){    
        
        // если удалось выделить память
        if((buffer=malloc(word_size))){
        
            result = find_word(file_pointer, word, word_size, buffer); // собственно поиск слова 
            free(buffer);
        }
        fclose(file_pointer);
    }
    return result;
}
// поиск слова
status find_word(FILE* file_pointer, char* word, size_t word_size, char* buffer){

    while(fgets(buffer, word_size, file_pointer)){
        
        if(strcmp(word, buffer)==0){ 

            return FOUND;   // если нашли слово
        }
    }
    return NOTFOUND;        // если не нашли слово
}
int main(void){
    char* filename ="test.txt";     // имя файла 
    char word[] = "hello";          // искомое слово
    size_t size = sizeof(word);     // размер слова

    status result = find(filename, word, size);

    switch(result) {
        case ERROR:
            printf("Error!\n");
            break;
        case NOTFOUND:
            printf("Text not found...\n");
            break;
        case FOUND:
            printf("Success! Text found!\n");
            break;
        
    }
}

Здесь нас интересует прежде всего функция find(), которая получает извне имя файла для поиска, слово для поиска и размер слова. Чтобы найти слово, открываем файл и затем выделяем буфер в памяти для считывания данных. Затем вызывается функция find_word(), которая считывает символы из файла в буфер и проверяет на их соответствие искомому слову. В качестве результата возвращается одно из значений перечисления status, по которому мы можем узнать результат операции.

Причем функция find() сразу проверяет предусловия с помощью выражений assert, тем самым нивелируя две конструкции if. А управление ресурсами и основные действия функции отделены путем вынесения поиска слова в отдельную функцию - find_word. Тем не менее применение вложенных if несколько снижает читабельность функции find

if((file_pointer=fopen(file_name, "r"))){    
    if((buffer=malloc(word_size))){
    ....
    }
........
}

Причем подобным вложенных конструкций if может быть и больше по мере увеличения используемых ресурсов.

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

// результат поиска
typedef enum {ERROR, FOUND, NOTFOUND} status;

typedef struct
{
    FILE* file_pointer;
    char* buffer;
    char* word;
    size_t word_size;
} Resources;

status find(char*, char*, size_t);
status find_word(Resources*);
Resources* constructor(char*, char*, size_t);
void destructor(Resources*);

Resources* constructor(char* file_name, char* word, size_t word_size)
{
    // если не соблюдаются предусловия, завершаем функцию
    assert(file_name!=NULL && "File name is invalid!");
    assert(word!=NULL && "Word is invalid!");

    Resources* resources = malloc(sizeof(Resources));
    if(resources)
    {
        // открываем файл
        resources->file_pointer=fopen(file_name, "r");
        // выделяем память для буфера
        resources->buffer = malloc(word_size);
        // если при выделении какого-то ресурса произошла ошибка
        if(!resources->file_pointer || !resources->buffer)
        {
            destructor(resources);  // вызываем деструктор, который освобождает остальные ресурсы
            return NULL;
        }
        resources->word = word;
        resources->word_size = word_size;
    }
    return resources;
}

// деструктор - освобождаем ресурсы
void destructor(Resources* res)
{
    if(res)   // если ресурсы ранее были выделены
    {
        if(res->buffer)   // если буфер выделен
        {
            free(res->buffer); // освобождаем память буфера
        }
        if(res->file_pointer) // если файл был открыт
        {
            fclose(res->file_pointer);    // закрываем файл
        }
        free(res);
    }
}

status find(char* file_name, char* word, size_t word_size)
{
    status result;
    Resources* res = constructor(file_name, word, word_size);
    result = find_word(res);
    destructor(res);

    return result;
}
// поиск слова
status find_word(Resources* res)
{
    if(res == NULL)   // если выделение ресурсов прошло неудачно
    {
        return ERROR;       // возвращаем код ошибки
    }
    while(fgets(res->buffer, res->word_size, res->file_pointer)){
        
        if(strcmp(res->word, res->buffer)==0){ 

            return FOUND;   // если нашли слово
        }
    }
    return NOTFOUND;        // если не нашли слово
}

int main(void){
    char* filename ="test.txt";
    char word[] = "hello";
    size_t size = sizeof(word);

    status result = find(filename, word, size);

    switch(result) {
        case ERROR:
            printf("Error!\n");
            break;
        case NOTFOUND:
            printf("Text not found...\n");
            break;
        case FOUND:
            printf("Success! Text found!\n");
            break;
    }
}

Разберем основные моменты. Прежде всего для управления ресурсами определяется структура Resources:

typedef struct
{
    FILE* file_pointer;
    char* buffer;
    char* word;
    size_t word_size;
} Resources;

Она хранит указатель на файл, указатель на выделенный (или не веделенный) буфер, указатель на искомое слово и размер слова.

Объект данной структуры создается в функции конструктора:

Resources* constructor(char* file_name, char* word, size_t word_size)
{
    // если не соблюдаются предусловия, завершаем функцию
    assert(file_name!=NULL && "File name is invalid!");
    assert(word!=NULL && "Word is invalid!");

    Resources* resources = malloc(sizeof(Resources));
    if(resources)
    {
        // открываем файл
        resources->file_pointer=fopen(file_name, "r");
        // выделяем память для буфера
        resources->buffer = malloc(word_size);
        // если при выделении какого-то ресурса произошла ошибка
        if(!resources->file_pointer || !resources->buffer)
        {
            destructor(resources);  // вызываем деструктор, который освобождает остальные ресурсы
            return NULL;
        }
        resources->word = word;
        resources->word_size = word_size;
    }
    return resources;
}

Функция конструктора в качестве параметров принимает все те значения, которые ранее передавались в функцию find и опять же с помощью выражений assert вначале проверяет предусловия, несоблюдение которых приведет к завершению программы. Затем выделяется память под структуру Resources:

Resources* resources = malloc(sizeof(Resources));

Если выделение памяти прошло успешно, то открываем файл и выделяем память для буфера:

if(resources)
{
    // открываем файл
    resources->file_pointer=fopen(file_name, "r");
    // выделяем память для буфера
    resources->buffer = malloc(word_size);

Но если либо открытие файла, либо выделение памяти прошло неудачно, то очищаем ресурсы (которые были выделены) с помощью деструктора и возвращаем NULL:

if(!resources->file_pointer || !resources->buffer)
{
    destructor(resources);  // вызываем деструктор, который освобождает остальные ресурсы
    return NULL;
}

Это гарантирует, что если, например, память для буфера была выделена, но файл не был открыт, то файл для буфера будет освобождена. Если все прошло удачно, возвращаем созданную структуру.

Функция деструктора освобождает ранее выделенные ресурсы.

void destructor(Resources* res)
{
    if(res)   // если ресурсы ранее были выделены
    {
        if(res->buffer)   // если буфер выделен
        {
            free(res->buffer); // освобождаем память буфера
        }
        if(res->file_pointer) // если файл был открыт
        {
            fclose(res->file_pointer);    // закрываем файл
        }
        free(res);
    }
}

Тут все просто - освобождаем выделенные ресурсы с помощью соответствующих функций. Стоит только отметить, что в конструкторе выделение ресурсов идет только после создания структуры Resources. Соответственно в деструкторе проверяем выделение памяти для этой структуры и только после этого освобождаем ресурсы.

В функции find() сначала вызываем конструктор, получаем из него ресурсы в виде структуры Resources и передаем их в функцию find_word для поиска слова. И после этого вызываем деструктор:

status result;
Resources* res = constructor(file_name, word, word_size);
result = find_word(res);
destructor(res);

В самой функции find_word сначала проверяем структуру Resources на NULL, так как если она равна NULL, то значит выделение ресурсов прошло неудачно, и нет смысла искать слово:

status find_word(Resources* res)
{
    if(res == NULL)   // если выделение ресурсов прошло неудачно
    {
        return ERROR;       // возвращаем код ошибки
    }

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850