Проверка указателя и проблема висячих указателей

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

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

Стандартное решение в даннном случае состоит в том, чтобы проверять валидность указателя (проверка на NULL) перед использованием. При определении указателя после освобождения памяти ему присваивается значение 0 или NULL:

void someFunction()
{
    char* pointer = NULL; // явным обарзом инициализируем указатель нулем
    pointer = malloc(1024); // выделяем память

    if (pointer)    // проверяем указатель на NULL
    {
        // некоторая работа с памятью по указателю
    }

    free(pointer);  // освобождаем память по указателю
    pointer = NULL; // сбрасываем указатель в ноль
}

Проверка указателя и присвоения ему нуля увеличивает код, однако упрощает идентификацию тех мест в программе, где с указателем может возникнуть ошибка. Также повторное освобождение памяти по указателю, которому ранее присвоили NULL, не окажет никакого влияния на программу:

free(pointer);  // освобождаем память по указателю
pointer = NULL; // сбрасываем указатель в ноль
free(pointer);  // ни на что не влияет

Рассмотрим следующую программу:

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

// получаем длину файла
int getFileLength(FILE* fp)
{
    fseek(fp, 0, SEEK_END);
    int file_length = ftell(fp);
    return file_length;
}
// считываем файл в buffer
void readFile(FILE* fp, char* buffer, int file_length)
{
    fseek(fp, 0, SEEK_SET);
    int read_elements = fread(buffer, 1, file_length, fp);
    buffer[read_elements] = '\0';   // устанавливем концевой нулевой байт для строки
}
// вывод файла на консоль
void printFile(char* filename)
{
    FILE* fp = 0; // инициализируем нулем
    char* text = 0;         // инициализируем нулем 

    assert(filename && "Invalid file name"); // если некорректное имя файла, прерываем программу

    fp = fopen(filename, "r");  // открываем файл
    assert(fp && "Unable to open file!\n"); // если не удалось открыть файл, прерываем программу

    int file_length = getFileLength(fp);  // получаем длину файла
    if(file_length > 0)
    {
        text = malloc(file_length);
        if(text)    // если удалось выделять память
        {
            readFile(fp, text, file_length);  // считываем файл
            printf("%s\n",text);

            free(text); // освобождаем память
            text = 0;   // сбрасываем значение указателя в ноль
        }
    }
    fclose(fp);     // закрываем файл
    fp = 0;         // сбрасываем значение указателя в ноль
}

int main(void)
{
    char* filename = "test.txt";
    printFile(filename);
}

Данная программа предназначена для вывода на консоль текста из файла. Для этого определена функция printFile(), которая принимает имя файла. Вначале она определяет используемые указатели для взаимодействия с файлом указатель FILE* и указатель на буфер для считывания файла buffer:

FILE* fp = 0; // инициализируем нулем
char* text = 0;         // инициализируем нулем 

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

Затем с помощью выражения assert() проверяется корректность имени файла - проверка предусловия:

assert(filename && "Invalid file name"); // если некорректное имя файла, прерываем программу

Далее открываем файл и проверяем что файл открыт:

fp = fopen(filename, "r");  // открываем файл
assert(fp && "Unable to open file!\n"); // если не удалось открыть файл, прерываем программу

Выражение assert() проверяет, что fp не равен NULL, иначе прерывает программу. В общем случае у нас есть два способа обработки корректности указателя: жесткий с прерыванеим программы и мягкий с некоторой обработкой и продолжением работы программы. Оба способа имеют свои плюсы и минусы. В данном случае прерываем программу, так как дальше нет смысла в работе функции и вообще программы, так как файл не удалось открыть. Но это не аксиома, можно использовать более мягкие способы, например, проверку в конструкции if, особенно если дальше идут некоторые выражения, которые должны выполняться и при неудачне при открытии файла.

Далее с помощью вспомогательной функции getFileLength получаем длину файла:

int file_length = getFileLength(fp);  // получаем длину файла

Если длина файла меньше 1 байта, то нет смысла считывать его содержимое. Поэтому проверяем полученную длину и выделяем буфер для считывания данных:

if(file_length > 0)
{
    text = malloc(file_length);
    if(text)    // если удалось выделять память
    {
        readFile(fp, text, file_length);  // считываем файл
        printf("%s\n",text);

        free(text); // освобождаем память
        text = 0;   // сбрасывем значение указателя
    }
}

Если удалось выделить память, то считываем данные с помощью функции readFile и выводим считанный текст на строку. В отличие от проверки указателя на файл fp здесь используется мягкий подход с проверкой в конструкции if. После завершения работы с буфером его память освобождается, а указатель сбрасывается в 0, что позволит избежать висячих указателей:

free(text); // освобождаем память
text = 0;   // сбрасывем значение указателя

В конце закрываем файл и сбрасываем указатель файла в 0:

fclose(fp);     // закрываем файл
fp = 0;         // сбрасываем значение указателя в ноль

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

void printFile(char* filename)
{
    FILE* fp = 0; // инициализируем нулем
    char* text = 0;         // инициализируем нулем 

    assert(filename && "Invalid file name"); // если некорректное имя файла, прерываем программу

    fp = fopen(filename, "r");  // открываем файл
    assert(fp && "Unable to open file!\n"); // если не удалось открыть файл, прерываем программу

    int file_length = getFileLength(fp);  // получаем длину файла
    if(file_length > 0)
    {
        text = malloc(file_length);
        if(text)    // если удалось выделять память
        {
            readFile(fp, text, file_length);  // считываем файл
            printf("%s\n",text);
            free(text); // освобождаем память
            // text = 0  // закомментируем сброс буфера в ноль
        }
    }

    // пробуем вывести текст после освобождения памяти
    if(text)    // если удалось выделять память
    {
        printf("%s\n",text);
        free(text); // освобождаем память
    }

    fclose(fp);     // закрываем файл
    fp = 0;         // инициализируем указатель на файл нулем
}

Здесь после освобождения памяти по указателю text повторно проверяем валидность указателя и пробуем вывести текст на консоль. Результат в этом случае будет неопределенным. Вполне возможно, что будет выведен ранее считанный из файла текст. Однако формально память уже освобождено, и мы получили висячий указатель (dangling pointer), использование которого в дальнейшем может служить источником программных ошибок. Соответственно сброс указателя в ноль сразу после освобождения с последующей проверкой указателя на валидность позволил бы избавиться от ряда проблем.

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

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