Если программа имеет сложную структуру, где выделение динамической памяти производится в одном месте, ее использование в другом, а освобождение памяти в третем месте программы, то возрастает риск использования недействительных указателей и соответственно программных ошибок. Например, можно столкнуться с использованием неинициализированного указателя или указателя, память по которому уже была освобождена или повторно освободить память.
Стандартное решение в даннном случае состоит в том, чтобы проверять валидность указателя (проверка на 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 позволит избежать ненужных операций с закрытым файлом.