LINUX.ORG.RU

Как гарантированно заNULLить указатель на уже освобожденный объект

 , , ,


0

4

Всех приветствую!

Может кому-нибудь мой вопрос покажется надуманным, но все равно напишу.

Есть (в данном примере игрушечный) код

KIT*   kit=NULL;

// создаем kit
kit = kit_create("Abracadabra", 5);

// пользуемся kit
// ...
kit_print(stdout, kit);
// ...

// освобождаем kit
kit_free(kit);
	
// ...	

// забыли, что уже освободили kit и делаем распечатку
kit_print(stdout, kit);
//Segmentation fault

Segmentation fault

Т.к. после

#define SAFE_FREE(object) if(object!=NULL) {free(object); (object)=NULL;}
void  kit_free(KIT *kit)
{   
	SAFE_FREE(kit->name);
	SAFE_FREE(kit->data);
	SAFE_FREE(kit);
}

указатель на kit не будет NULL на вызывающей стороне. И даже если сделать проверку в kit_print

if (kit != NULL && kit->data != NULL)
{  
  // печатаем kit
}
else
  printf("Извините, но kit=NULL");

все равно в kit_print придет ненулевой указатель.

Варианты решения:

   kit_free(kit);
   kit=NULL;

Недостаток: лишняя строка которую можно забыть написать, и вообще хотелось бы чтобы все делалось в kit_free

Сделать

KIT*  kit_free(KIT *kit)
{   
	SAFE_FREE(kit->name);
	SAFE_FREE(kit->data);
	SAFE_FREE(kit);
	return kit; // уже ставший NULL
}

и вызывать

kit = kit_free(kit);

Выглядит неплохо.

А теперь вопрос:

Имеются ли у кого-нибудь более изящные решения или возможен иной принципиально подход к освобождению памяти в pure C ?


Имеются ли у кого-нибудь более изящные решения или возможен иной принципиально подход к освобождению памяти в pure C ?

Один из вариантов можно передавать указатель на указатель(если не нравятся макросы):

void  kit_free(KIT **kit)
{   
	SAFE_FREE(*kit->name);
	SAFE_FREE(*kit->data);
	SAFE_FREE(*kit);
	kit = NULL;
}
kit_free(&kit);
V1KT0P ★★
()

kit_free(KIT** kit)

Но вообще не надо пытаться из фундаментально небезопасного языка пытаться делать подобие безопасного, ничего не получится. В каких-то случаях компилятор твои =NULL просто выкинет. Все нормальные люди будут использовать С библиотеку через плюсовую/rust/… обёртку, где будет raii и не будет вообще никакой возможности использовать объект после освобождения. А дебилам пишущим клиентский код на C подкладывать соломки не стоит.

anonymous
()
Ответ на: комментарий от aiqu6Ait

Как насчет проверки, была ли выделена память по указателю?

Памяти много. Это в большинстве случаев параноидальные паразитные проверки. Главное чтобы повреждения кучи не было.

Gyros
() автор топика
Ответ на: комментарий от anonymous

Ты вообще в курсе что такое use after free? Ну проверил, была выделена, доволен? Вот только не kit_create и вообще не под KIT.

Можно подробнее?

Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от anonymous

Ты мог бы просто внести звёздочку и показать себя чётким пацанчиком

void  kit_free(KIT **kit)
{   
	SAFE_FREE(*kit->name);
	SAFE_FREE(*kit->data);
	SAFE_FREE(*kit);
-	kit = NULL;
+       *kit = NULL;
}

Но нет! :) Как девка высмеиваешь торчащий волосок в причёске подружки.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от Gyros

Либо макросом который будет сначала делать free(ptr) и сразу ниже ptr=NULL. Либо макросом для Как гарантированно заNULLить указатель на уже освобожденный объект (комментарий) который будет ставить & к указателю.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от Gyros

Неуниверсальный макрос. Это чтож под каждую структуру писать.. Хотя может быть..

Если под каждую структуру требуется отдельная функция free то иначе скорее всего никак. Если конечно нет магии макросов которые вычислят тип структуры и подставят в название функции free.

V1KT0P ★★
()
Ответ на: комментарий от LINUX-ORG-RU

Как девка высмеиваешь торчащий волосок в причёске подружки

Это если бы такая ошибка не была типична для C. Но нет, я высмеиваю всю касту вонючих лохматых нерях.

anonymous
()
Ответ на: комментарий от anonymous

Это если бы такая ошибка не была типична для C.

Не вижу смысла раздувать из-за того что я на скорую руку написал. Я еще и плюсовик который в основном на Qt пишет, с указателями на указатели уже и не помню когда последний раз имел дело.

V1KT0P ★★
()
Ответ на: комментарий от Gyros

free это просто деаллокатор из хипменеджера, он один и тот же для всех типов естессно. просто освобождает кусок памяти.

в сишечке нет деструкторов. как и классов.

alysnix ★★★
()

Интересный подход к управлению памятью видел здесь:
https://codeberg.org/grunfink/snac2/src/branch/master/xs.h
Все типы линейные (даже json и html контейнеры), а память освобождается автоматически
но там не совсем чистый c т.к используется __attribute__ ((__cleanup__ ())) для автоматического освобождения памяти, зато явный free не нужен - free вызывается или нет в зависимости от типа переменной

mittorn ★★★★★
()

пиши макру. но естессно помни, что если два и более указателя указывают на один обьект, то safe_free(..) то будешь делать на один. и потому занулится только один.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от LINUX-ORG-RU

А может быть какие-нибудь smartpointerы использовать?

И освобождать и заNULLять все гарантированно в конце.

Кто нибудь пробовал такой вариант?

Или эти smartpointer будут непростым трудночитаемым кодом, который придется всюду таскать?

Gyros
() автор топика
Ответ на: комментарий от alysnix

Разве что что-то вроде этого:
STC 5.0
Но смартпоинтеры - довольно тяжёлая штука и если Си используется для боольшей производительности/компактности - то вероятно лучше использовать какие-нибудь пулы памяти, чем счётчики ссылок, если это конечно применимо

mittorn ★★★★★
()
Ответ на: комментарий от alysnix

откуда в си смартпоинтеры? смартпоитеры в плсах реализованы на классах, и концепции конструктора/деструктора и raii. в сишечке этого нет

Наворотить можно, особенно если использовать расширения компиляторов, аля __attribute__((cleanup()))). Как, например, с недавних пор делают в ядре Linux guard(mutex).

AlexVR ★★★★★
()
Ответ на: комментарий от mittorn

Но смартпоинтеры - довольно тяжёлая штука и если Си используется для боольшей производительности/компактности - то вероятно лучше использовать какие-нибудь пулы памяти, чем счётчики ссылок, если это конечно применимо

Тут нужен unique_ptr, а там нет никаких счётчиков ссылок - он вообще бесплатен.

anonymous
()

Как гарантированно…

Гарантированно — никак, ибо указателей на выделенный объект может быть более одного.

#define SAFE_FREE(object) if(object!=NULL) {free(object); (object)=NULL;}

Зачем? free не падает если попытаться освободить нулевой указатель, и это специфицированное поведение функции. Вынос проверки имел бы смысл, если free(NULL) был очень часто исполняющимся случаем, но оптимизировать явно редкий случай путём раздувания кода — бессмысленно.

kit = kit_free(kit);

Выглядит плохо — ты заставляешь писать имя переменной два раза.

Во-первых, зачем два раза писать одно и то же (имя переменной)? В твоём примере имя переменной короткое. А если это будет что-то типа array[i].something.kit = kit_free(array[i].something.kit);? Уже не так хорошо, да?

В-вторых, это дверь для ошибок: array[i].something.kit = kit_free(array[j].something.kit); — освободил одно, а занулил другое.

В-третьих, язык C позволяет игнорировать результат функции: kit_free(array[j].something.kit); — и зануление похерили.

Поэтому лучшее из возможного (но таки без гарантий), это

void kit_free(KIT **kit) {
    ...
    *kit = NULL;
};
debugger ★★★★★
()
Ответ на: комментарий от anonymous

stl'овый вроде не совсем беслпатен, но в целом да. По идее attribute cleanup, который тут уже пару раз упомянули - это близжайший аналог unique ptr в си, но повторить все возможности unique в сишке не выйдет. Для shared+weak с cleanup можно наверно

mittorn ★★★★★
()
Последнее исправление: mittorn (всего исправлений: 1)
Ответ на: комментарий от Gyros

У тебя C, а не C++. Хорошие решения уже дали выше. Выделил, заполнил, осводобил, занулил. Всё, любой иной подход будет делать тоже самое, но либо по своим правилам, которым ты опять же будешь вынужден не забывать следовать или завязывайся на компиляторные особенности с атрибутами, а оно надо?

У тебя задача ровно одна - это просто присвоить 1 значение, чтобы отличать освобождённый указатель от не освобождённого, всё! Как по мне нечего тут мудрить. И всё уже сказали

  • Занулять внутри вызываемой функции == передай указатель на указатель и там проставь NULL
    • опционально обернуть в макрос чтобы не писать/не забыть &
  • Занулять по месту ручного освобождения == вызови обычный free() и сразу присвой NULL обернув это в макрос для автоматизации

Не хочешь со всем этим возится, бери язык с GC или прикручивай GC к Си =)

А так, дело хозяйское, ничего лучшего чем тут уже сказали я не скажу :)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от mittorn

Вообще если нет планов собираться под таргет, на котором может не быть порта GCC или Clang, то можно действительно просто использовать атрибут, который скажет компилятору вставить free при выходе из scope.

Не стандарт, но и ТС судя по перечисленному и предложенному в треде явно хочет нестандартное решение (no pun intended).

a1ba ★★
()

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

Собери свои программы с такими вот флагами, это поможет отследить различные ошибки, уверен компилятор сразу найдет много всего что нужно исправить:

-g3 -Wall -Wextra -fanalyzer -fno-omit-frame-pointer -fsanitize=address,undefined

Имеются ли у кого-нибудь более изящные решения или возможен иной принципиально подход к освобождению памяти в pure C ?

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

entity_t *entity_new(const char *name); 
Пусть это будут функции
void entity_initialize(entity_t *entity, const char *name);
где вызывающий сам передает объект, он может быть выделен на куче, но лучше пусть он будет полем структуры или на стеке, шаг за шагом можно убрать много аллокаций.

2. Используй компилятор что бы избегать ошибок, __attribute__((__nonnull__)), __attribute__((cleanup()))

3. Для разных случаев нужны разные подходы к выделению и освобождению памяти, самый простой на мой взгляд, использование статических массивов со счетчиком элементов, и сброс их при выполнении задания, после чего можно загружать новое. А вот за постоянным изменением размеров всего уследить сложно. Но если прибегать к сложному подходу, то прочитай про ref counting, это защитит от проблемы когда ресурс еще используется, но кто то его удаляет.

MOPKOBKA ★★★★★
()
Ответ на: комментарий от Bfgeshka

free() принимает NULL, это удобно, если ты выделяешь массив только при использовании, а изначально у тебя он NULL, не надо писать проверки в функции очистки, просто вызываешь free и все.

И void** не будет работать как надо.

char *s = malloc(123);
kit_free_null(&s);

note: expected ‘void **’ but argument is of type ‘char **’

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA

Спасибо

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

А как они выглядят?

И потом разве в стеке (раз статический массив, если я правильно понял) можно много памяти использовать? Мне кажется без malloc/free не обойтись.

Gyros
() автор топика
Ответ на: комментарий от Gyros

Я не предлагал все выделять на стеке, это было бы странно. Давай покажу на примере «преобразования».

Была сложная функция с несколькими массивами

int work(void) {
    // Для начала выделяется пару элементов для массива
    // потом их надо будет растить с помощью realloc
    char *str1 = malloc(...); 
    int *arr1 = malloc(...);
    int *arr2 = malloc(...);
    int *arr3 = malloc(...);
    // Много проверок что все выделили

    // Так как изначально элементов выделили мало
    // нужно перераспределять память и добавить еще проверки
    arr1 = realloc(...);
    arr2 = realloc(...);

    // Надо следить за всеми выходами
    if (x < 2) {
      free(...); free(...);
      return 10;
    }

    // И не забыть все правильно очистить
    free(...);
    return 0;
}
Эту же функцию можно преобразовать в такую:
struct data {
  char str[PATH_MAX];
  int arr1[9999], arr2[100], arr3[32];
  int arr1_len, arr2_len, arr3_len;
};

int work(struct data *data) {
    // Ничего не выделяется, работа идет с data
    // Размер уже выделен достаточный для работы
    // но если не хватит то придется выдать ошибку
    // так что лучше расчитать заранее
    if (x < 2) {
      // Не надо заботится что может что то утечь
      return 10;
    }
    // И очистка на стороне data
    return 0;
}

int main() {
  // Один раз выделили
  struct data *d = malloc(...);
  data_initialize(d); // Ставим len по нулям
  work(d);  // Работаем не отвлекаясь на выделение/освобождение
  work2(d);
  work3(d);
  free(d); 
}

Если все функции написаны по второму примеру, то где то наверху, возможно только в main ты выделяешь разово массивы, а потом просто с ними работаешь, не трогая malloc/realloc. Код упрощается, скорость возрастает.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)

// забыли, что уже освободили kit и делаем распечатку

Имеются ли у кого-нибудь более изящные решения или возможен иной принципиально подход к освобождению памяти в pure C ?

имеется конечно: не забывать что освободили kit и ограничивать время жизни и видимость kit

MKuznetsov ★★★★★
()
Ответ на: комментарий от zudwa

Массивы внутри могут быть динамические но выделяться лишь один раз, принимая свой лимит через argv например, объектов struct data может быть несколько. Я решил показать общий шаблон, надеюсь понятно что я имел виду. Статически тоже можно выделять.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 4)
Ответ на: комментарий от mittorn

но там не совсем чистый c т.к используется attribute ((cleanup ())) для автоматического освобождения памяти, зато явный free не нужен - free вызывается или нет в зависимости от типа переменной

Это работает только если указатель не покидает область видимости.

hateyoufeel ★★★★★
()

// забыли, что уже освободили kit и делаем распечатку

Я вот одно никак понять не могу: как можно забыть о том, что мы уже освободили память и с ней нельзя работать? Это какого размера должна быть функция, чтобы в ней такие несуразицы возникали?

К слову, если код вашей функции не умещается в 25-50 строчек, то лучше программистом совсем не работать, не надо нормальных людей своим говнокодом травмировать.

ttyr
()
Ответ на: комментарий от ttyr

// забыли, что уже освободили kit и делаем распечатку

Пример игрушечный. Я нарочно создал ситуацию, чтобы показать, что kit_free не зануляет kit. Освобождает на то, что указывает, но не зануляет сам указатель.

если код вашей функции не умещается в 25-50 строчек

А как же парсить файл? Там не один проход, много условий в циклах.

PS Поздравляю с первым комментарием

Gyros
() автор топика
Ответ на: комментарий от zudwa

А есть ограничение на размер памяти выделяемой в стеке? Я где-то читал, что есть. Т.е. получается теоретически огромный масссив в стеке хранить нельзя. Только в куче (malloc/free).

Верно? Или я неправильную информацию читал (сейчас уже не помню откуда).

Gyros
() автор топика
Ответ на: комментарий от Gyros

А есть ограничение на размер памяти выделяемой в стеке?

да, есть. Можно посмотреть ulimit -a и изменить ulimit -s

Либо getrlimit/setrlimit

теоретически огромный масссив в стеке хранить нельзя

теоретически можно. Практически проще вынести такое в статическую память

В любом случае:

int main(){
  int a[1024*1024*1024] = {0};
}

не имеет практического смысла.

Ну и создавать на стеке массивы - это при помощи рекурсии только как-то возможно, если без alloca

zudwa
()