Обработка ошибок с помощью goto

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

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

Одним из ответов на эту проблему может быть вынесение кода освобождения ресурсов и обработки ошибок в конец файла в отдельную секцию, а в основном коде выполнять переход к этой секции с помощью оператора goto при возникновении ошибки, наподобие следующего:

void someFunction()
{
    if(!allocateResource1())    // выделяем память для 1-го ресурса
    {
        goto cleanup1;
    }
    if(!allocateResource2())    // выделяем память для 2-ого ресурса
    {
        goto cleanup2;
    }
    // основной код

// секция освобождения ресурсов и обработки ошибок
cleanup2:               
    cleanupResource2(); // освобождаем 2-й ресурс
cleanup1:
    cleanupResource1(); // освобождаем 1-й ресурс
}

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

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

#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 может быть и больше. Теперь используем goto:

#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"))){    
        goto error_fileopen;
    }
    // если память НЕ выделена
    if(!(buffer=malloc(word_size))) {
        goto error_malloc;
    }
        
    result = find_word(file_pointer, word, word_size, buffer); // собственно поиск слова 
    
// секция освобождения ресурсов
    free(buffer);
error_malloc:
    fclose(file_pointer);
error_fileopen:
    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 проверяет выделение ресурсов и в случае ошибки переходит к определенной метке в секции в конце функции:

if(!(file_pointer=fopen(file_name, "r"))){    
    goto error_fileopen;
}
// если память НЕ выделена
if(!(buffer=malloc(word_size))) {
    goto error_malloc;
}

В конце функции в отдельной секции происходит освобождение ресурсов и возвращение результата:

// секция освобождения ресурсов
    free(buffer);
error_malloc:
    fclose(file_pointer);
error_fileopen:
    return result;

То есть, если файл успешно открыт, буфер выделен, поиск в файле произведен, то освобождается буфер, закрывается файл и возвращается результат.

Если при попытке выделить память для буфера произошла ошибка, то происходит переход к метке error_malloc и выполняется закрытие файла (так как память для буфера не выделена, соответственно ее освобождать не надо).

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

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