Jestem trochę zdezorientowany, jak sprawdzić, czy alokacja pamięci nie powiodła się, aby zapobiec niezdefiniowanym zachowaniom spowodowanym przez dewelowaną {X0}} wskaźnik. Wiem, że malloc (i podobne funkcje) może zawieść i zwrócić NULL, a z tego powodu adres zwrócony powinien być zawsze sprawdzany przed kontynuowaniem z resztą programu. To, czego nie dostaję, to najlepszy sposób na radzenie sobie z tymi przypadkami. Innymi słowy: Jaki jest program, który ma robić, gdy malloc Powraca NULL?

Pracowałem nad tym wdrożeniem podwójnie połączonej listy, gdy ta wątpliwość podniesiona.

struct ListNode {

    struct ListNode* previous;
    struct ListNode* next;
    void* object;
};

struct ListNode* newListNode(void* object) {

    struct ListNode* self = malloc(sizeof(*self));

    if(self != NULL) {

        self->next = NULL;
        self->previous = NULL;
        self->object = object;
    }

    return self;
}

Inicjalizacja węzła występuje tylko wtedy, gdy jego wskaźnik został prawidłowo przydzielony. Jeśli tak się nie stało, ta funkcja konstruktora zwraca NULL.

Napisałem również funkcję, która tworzy nowy węzeł (wywołanie funkcji newListNode) począwszy od już istniejącego węzła, a następnie powraca.

struct ListNode* createNextNode(struct ListNode* self, void* object) {

    struct ListNode* newNext = newListNode(object);

    if(newNext != NULL) {

        newNext->previous = self;

        struct ListNode* oldNext = self->next;

        self->next = newNext;

        if(oldNext != NULL) {

            newNext->next = oldNext;
            oldNext->previous = self->next;
        }
    }

    return newNext;
}

Jeśli newListNode zwraca NULL, createNextNode, createNextNode, a także zwroty NULL i węzeł przekazywany do funkcji nie są dotknięte.

Następnie struktura Listnode jest używana do wdrożenia rzeczywistej listy połączonej.

struct LinkedList {

    struct ListNode* first;
    struct ListNode* last;
    unsigned int length;
};

_Bool addToLinkedList(struct LinkedList* self, void* object) {

    struct ListNode* newNode;

    if(self->length == 0) {

        newNode = newListNode(object);
        self->first = newNode;
    }
    else {

        newNode = createNextNode(self->last, object);
    }

    if(newNode != NULL) {

        self->last = newNode;
        self->length++;
    }

    return newNode != NULL;
}

Jeśli utworzenie nowego węzła nie powiedzie się, funkcja addToLinkedList zwraca 0, a sama lista połączona jest nietknięta.

Wreszcie, rozważmy tę ostatnią funkcję, która dodaje wszystkie elementy połączonej listy na inną linkę.

void addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {

    struct ListNode* node = other->first;

    while(node != NULL) {

        addToLinkedList(self, node->object);
        node = node->next;
    }
}

Jak powinienem poradzić sobie z możliwością zwrotu addToLinkedList? Za to, co zebrałem, malloc nie powiedzie się, gdy nie jest już możliwe przydzielenie pamięci, więc zakładam, że kolejne połączenia po awarii alokacji również nie mam racji? Więc jeśli zostanie zwrócone 0, jeśli pętla natychmiast się zatrzyma, ponieważ w każdym razie nie będzie można dodać żadnych nowych elementów do listy? Również jest poprawny, aby stosować wszystkie te sprawdzanie jednego nad innym sposobem, w jaki to zrobiłem? Czy nie jest zbędny? Czy błędnie byłoby natychmiast zakończyć program, gdy tylko Malloc się nie powiedzie? Czytałem, że byłoby problematyczne dla programów wielokrotnych, a także, że w niektórych istnieje program może być w stanie kontynuować uruchomienie bez dalszej alokacji pamięci, więc błędnie byłoby traktować to jako błąd fatalny w jakimkolwiek możliwym przypadku . Czy to jest poprawne?

Przepraszamy za naprawdę długiego postu i dziękuję za pomoc!

4
Gian 18 marzec 2020, 18:00

2 odpowiedzi

Najlepsza odpowiedź

Jak radzić sobie z brakiem pracy Malloc i zwracając NULL?

Zastanów się, czy kod jest zestawem funkcji / biblioteki / biblioteki lub aplikacji pomocniczych.

Decyzja o zakończeniu jest najlepiej obsługiwana przez kod wyższego poziomu.

Przykład: oprócz exit(), abort() i znajomych, standardowa biblioteka C nie wychodzi.

Podobnie powracające kody / wartości błędów jest rozsądnym rozwiązaniem dla zestawów funkcji niskiego poziomu OP. Nawet dla addAllToLinkedList(), rozważę propagowanie błędu w kodzie powrotnym. (Nie zero jest błędem.)

// void addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {
int addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {
  ...
  if (addToLinkedList(self, node->object) == NULL) {
    // Do some house-keepeing (undo prior allocations)
    return -1;
  }

W przypadku aplikacji wyższego poziomu podążaj za projektem. Na razie może być wystarczająco proste, aby wyjść z komunikatu niepowodzenia.

if (addAllToLinkedList(self, ptrs)) {
  fprintf(stderr, "Linked List failure in %s %u\n", __func__, __LINE__);
  exit(EXIT_FAILURE);
}

Przykład nie wychodzenia:

Rozważ procedurę, która odczytuje plik do struktury danych z wieloma zastosowaniem LinkedList, a plik był w jakiś sposób uszkodzony prowadzący do nadmiernych alokacji pamięci. Kod może chcesz po prostu uwolnić wszystko dla tego pliku (ale tylko dla tego pliku), i wystarczy zgłosić się do użytkownika "Nieprawidłowy plik / out-of-pamięci" - i kontynuuj uruchomienie.

if (addAllToLinkedList(self, ptrs)) {
  free_file_to_struct_resouces(handle);
  return oops;
}
...
return success;

zabierz

Lodowe procedury wskazują błąd jakoś . Procedury wyższego poziomu mogą wyjść z kodu w razie potrzeby.

2
chux - Reinstate Monica 18 marzec 2020, 20:20

To zależy od szerszych okoliczności. W przypadku niektórych programów po prostu przerwanie jest właściwą rzecz do zrobienia.

W przypadku niektórych aplikacji, właściwą rzecz jest skurczenie pamięci podręcznej i spróbuj ponownie malloc. W przypadku niektórych programów wielowymiarowych, po prostu czekając (aby dać inne wątki szansę na darmową pamięć) i ponowienie będzie działać.

W przypadku aplikacji, które muszą być bardzo niezawodne, potrzebujesz rozwiązania poziomu aplikacji. Jedno rozwiązanie, które użyłem i testowane bitwa jest to:

  1. Mieć awaryjną pulę pamięci przydzielonej na starcie.
  2. Jeśli malloc nie powiedzie się, bezpłatnie pulę awaryjną.
  3. W przypadku połączeń, które nie mogą mieć pewności za pomocą odpowiedzi NULL, snu i ponów próbę.
  4. Mieć wątek serwisowy, który próbuje uzupełnić basen awaryjny.
  5. Mieć kod, który wykorzystuje buforowanie odpowiadać na niepełny pulę awaryjną, zmniejszając zużycie pamięci.
  6. Jeśli masz możliwość rzucenia obciążenia, na przykład, przesuwając ładunek do innych instancji, zrób to, jeśli pula awaryjna nie jest pełna.
  7. W przypadku działań uznaniowych, które wymagają przydzielania wielu pamięci, sprawdź poziom basenu awaryjnego i nie wykonaj akcji, jeśli nie jest to pełne ani blisko.
  8. Jeśli basen awaryjny staje się pusty, przerwij.
6
David Schwartz 18 marzec 2020, 15:09