it-swarm.com.de

Sollte man wirklich Zeiger auf "NULL" setzen, nachdem man sie befreit hat?

Es gibt scheinbar zwei Argumente, warum man nach dem Freigeben einen Zeiger auf NULL setzen sollte.

Vermeiden Sie Abstürze, wenn Sie Zeiger doppelt freigeben.

Short: Ein erneutes Aufrufen von free() stürzt nicht durch Zufall ab, wenn NULL eingestellt ist.

  • Fast immer verdeckt dies einen logischen Fehler, da es keinen Grund gibt, free() ein zweites Mal aufzurufen. Es ist sicherer, die Anwendung abstürzen zu lassen und sie beheben zu können.

  • Ein Absturz kann nicht garantiert werden, da manchmal neuer Speicher an derselben Adresse zugewiesen wird.

  • Double Free tritt meistens auf, wenn zwei Zeiger auf dieselbe Adresse zeigen.

Logische Fehler können auch zu Datenverfälschungen führen.

Vermeiden Sie die Wiederverwendung von freigegebenen Zeigern

Short: Der Zugriff auf freigegebene Zeiger kann zu Datenverfälschungen führen, wenn malloc() an derselben Stelle Speicherplatz zuordnet, es sei denn, der freigegebene Zeiger ist auf NULL gesetzt.

  • Es kann nicht garantiert werden, dass das Programm beim Zugriff auf den NULL-Zeiger abstürzt, wenn der Versatz groß genug ist (someStruct->lastMember, theArray[someBigNumber]). Anstelle eines Absturzes kommt es zu Datenbeschädigungen.

  • Durch Setzen des Zeigers auf NULL kann das Problem nicht gelöst werden, dass ein anderer Zeiger mit demselben Zeigerwert verwendet wird.

Die Fragen

Hier ist ein Beitrag gegen blindes Setzen eines Zeigers auf NULL nach dem Freigeben von .

  • Welches ist schwieriger zu debuggen?
  • Gibt es eine Möglichkeit, beide zu fangen?
  • Wie wahrscheinlich ist es, dass solche Fehler dazu führen, dass Daten beschädigt werden und nicht abstürzen?

Fühlen Sie sich frei, um diese Frage zu erweitern.

48
Georg Schölly

Der zweite Punkt ist viel wichtiger: Das erneute Verwenden eines freigegebenen Zeigers kann ein subtiler Fehler sein. Ihr Code funktioniert weiterhin einwandfrei und stürzt dann aus keinem eindeutigen Grund ab, da sich scheinbar nicht zusammenhängender Code in den Speicher geschrieben hat, auf den der erneut verwendete Zeiger gerade zeigt.

Ich musste einmal an einem really fehlerhaften Programm arbeiten, das jemand anderes geschrieben hat. Meine Instinkte sagten mir, dass viele der Fehler mit nachlässigen Versuchen zusammenhängen, Zeiger nach dem Freigeben des Speichers weiterzuverwenden; Ich habe den Code geändert, um die Zeiger auf NULL zu setzen, nachdem der Speicher freigegeben wurde, und bam, die Ausnahmen für den Nullzeiger begannen. Nachdem ich alle Ausnahmen für den Nullzeiger behoben hatte, war der Code plötzlich viel stabiler.

In meinem eigenen Code rufe ich nur meine eigene Funktion auf, die ein Wrapper um free () ist. Es benötigt einen Zeiger auf einen Zeiger und setzt den Zeiger auf Null, nachdem der Speicher freigegeben wurde. Und bevor es free aufruft, ruft es Assert(p != NULL); auf, so dass es immer noch versucht, den gleichen Zeiger doppelt freizugeben.

Mein Code führt auch andere Dinge aus, z. B. (nur im DEBUG-Build) Speicher unmittelbar nach der Zuweisung mit einem offensichtlichen Wert auffüllen. Gleiches tun, bevor free() aufgerufen wird, falls eine Kopie des Zeigers vorhanden ist. Details hier .

BEARBEITEN: pro Anfrage ist hier Beispielcode.

void
FreeAnything(void **pp)
{
    void *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null pointer");
    if (!p)
        return;

    free(p);
    *pp = NULL;
}


// FOO is a typedef for a struct type
void
FreeInstanceOfFoo(FOO **pp)
{
    FOO *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null FOO pointer");
    if (!p)
        return;

    AssertWithMessage(p->signature == FOO_SIG, "bad signature... is this really a FOO instance?");

    // free resources held by FOO instance
    if (p->storage_buffer)
        FreeAnything(&p->storage_buffer);
    if (p->other_resource)
        FreeAnything(&p->other_resource);

    // free FOO instance itself
    free(p);
    *pp = NULL;
}

Bemerkungen:

Sie können in der zweiten Funktion sehen, dass ich die zwei Ressourcenzeiger überprüfen muss, um festzustellen, ob sie nicht null sind, und dann FreeAnything() aufrufen. Dies liegt an der assert(), die sich über einen Nullzeiger beschwert. Ich habe diese Behauptung, um einen Versuch zu entdecken, doppelt befreit zu werden, aber ich glaube nicht, dass es tatsächlich viele Fehler für mich gefunden hat; Wenn Sie die Asserts auslassen möchten, können Sie die Überprüfung auslassen und einfach immer FreeAnything() aufrufen. Abgesehen von der Assertion passiert nichts Schlimmes, wenn Sie versuchen, einen Nullzeiger mit FreeAnything() freizugeben, da er den Zeiger überprüft und nur zurückgibt, wenn er bereits Null war.

Meine eigentlichen Funktionsnamen sind eher knapp, aber ich habe versucht, selbstdokumentierende Namen für dieses Beispiel auszuwählen. Außerdem habe ich in meinem aktuellen Code Debug-Only-Code, der die Puffer mit dem Wert 0xDC füllt, bevor free() aufgerufen wird. Wenn ich also einen zusätzlichen Zeiger auf den gleichen Speicher (einen Speicherbereich, der nicht leer gemacht wird) habe, wird es offensichtlich Die Daten, auf die sie zeigen, sind falsche Daten. Ich habe ein Makro, DEBUG_ONLY(), das auf einem Nicht-Debug-Build zu nichts kompiliert wird. und ein Makro FILL(), das eine sizeof() für eine Struktur ausführt. Diese beiden funktionieren gleich gut: sizeof(FOO) oder sizeof(*pfoo). Hier ist also das FILL()-Makro:

#define FILL(p, b) \
    (memset((p), b, sizeof(*(p)))

Hier ein Beispiel für die Verwendung von FILL() zum Einfügen der 0xDC-Werte vor dem Aufruf:

if (p->storage_buffer)
{
    DEBUG_ONLY(FILL(pfoo->storage_buffer, 0xDC);)
    FreeAnything(&p->storage_buffer);
}

Ein Beispiel dafür:

PFOO pfoo = ConstructNewInstanceOfFoo(arg0, arg1, arg2);
DoSomethingWithFooInstance(pfoo);
FreeInstanceOfFoo(&pfoo);
assert(pfoo == NULL); // FreeInstanceOfFoo() nulled the pointer so this never fires
25
steveha

Ich mach das nicht Ich erinnere mich nicht besonders an Fehler, die einfacher zu behandeln gewesen wären. Aber es hängt wirklich davon ab, wie Sie Ihren Code schreiben. Es gibt ungefähr drei Situationen, in denen ich alles frei mache:

  • Wenn der Zeiger, der den Zeiger hält, den Gültigkeitsbereich verlässt oder Teil eines Objekts ist, das den Gültigkeitsbereich verlässt oder befreit wird.
  • Wenn ich das Objekt durch ein neues ersetze (z. B. bei einer Neuzuordnung).
  • Wenn ich ein Objekt freigebe, das optional vorhanden ist.

Im dritten Fall setzen Sie den Zeiger auf NULL. Das ist nicht spezifisch, weil Sie es befreien, sondern weil das, was auch immer es ist, optional ist. Daher ist NULL natürlich ein besonderer Wert, der "Ich habe keinen" hat.

In den ersten beiden Fällen scheint mir das Setzen des Zeigers auf NULL ohne besonderen Zweck eine fleißige Arbeit zu sein:

int doSomework() {
    char *working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // wtf? In case someone has a reference to my stack?
    return result;
}

int doSomework2() {
    char * const working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // doesn't even compile, bad luck
    return result;
}

void freeTree(node_type *node) {
    for (int i = 0; i < node->numchildren; ++i) {
        freeTree(node->children[i]);
        node->children[i] = NULL; // stop wasting my time with this rubbish
    }
    free(node->children);
    node->children = NULL; // who even still has a pointer to node?

    // Should we do node->numchildren = 0 too, to keep
    // our non-existent struct in a consistent state?
    // After all, numchildren could be big enough
    // to make NULL[numchildren-1] dereferencable,
    // in which case we won't get our vital crash.

    // But if we do set numchildren = 0, then we won't
    // catch people iterating over our children after we're freed,
    // because they won't ever dereference children.

    // Apparently we're doomed. Maybe we should just not use
    // objects after they're freed? Seems extreme!
    free(node);
}

int replace(type **thing, size_t size) {
    type *newthing = copyAndExpand(*thing, size);
    if (newthing == NULL) return -1;
    free(*thing);
    *thing = NULL; // seriously? Always NULL after freeing?
    *thing = newthing;
    return 0;
}

Es ist wahr, dass das Nullstellen des Zeigers es deutlicher machen kann, wenn Sie einen Fehler haben, bei dem Sie versuchen, ihn nach der Freigabe zu demeferenzieren. Dereferencing verursacht wahrscheinlich keinen unmittelbaren Schaden, wenn Sie den Zeiger nicht NULL machen, ist aber auf lange Sicht falsch.

Es ist auch wahr, dass NULL-Zeiger verdeckt Fehler, wo Sie doppelt frei sind. Das zweite free fügt keinen unmittelbaren Schaden zu, wenn Sie den Zeiger NULL machen, ist aber auf lange Sicht falsch (weil es die Tatsache verrät, dass Ihre Objektlebenszyklen defekt sind). Sie können behaupten, dass Dinge nicht null sind, wenn Sie sie freigeben. Dies führt jedoch dazu, dass der folgende Code eine Struktur freigibt, die einen optionalen Wert enthält:

if (thing->cached != NULL) {
    assert(thing->cached != NULL);
    free(thing->cached);
    thing->cached = NULL;
}
free(thing);

Der Code sagt Ihnen, dass Sie zu weit reingekommen sind. Es sollte sein:

free(thing->cached);
free(thing);

Ich sage, NULL der Zeiger, wenn er vermutlich benutzbar bleiben soll. Wenn es nicht mehr verwendbar ist, sollten Sie es am besten machen, indem Sie einen potenziell sinnvollen Wert wie NULL angeben. Wenn Sie einen Seitenfehler provozieren möchten, verwenden Sie einen plattformabhängigen Wert, der nicht dereferenzierbar ist, den Rest des Codes jedoch nicht als besonderen Wert "Alles ist in Ordnung" behandelt:

free(thing->cached);
thing->cached = (void*)(0xFEFEFEFE);

Wenn Sie keine solche Konstante auf Ihrem System finden können, können Sie möglicherweise eine nicht lesbare und/oder nicht beschreibbare Seite zuordnen und deren Adresse verwenden.

7
Steve Jessop

Wenn Sie den Zeiger nicht auf NULL setzen, besteht eine nicht allzu geringe Chance, dass Ihre Anwendung weiterhin in einem undefinierten Zustand ausgeführt wird und später an einem völlig nicht zusammenhängenden Punkt abstürzt. Sie werden dann viel Zeit mit dem Debuggen eines nicht vorhandenen Fehlers verbringen, bevor Sie feststellen, dass es sich um eine Speicherbeschädigung von früher handelt.

Ich würde den Zeiger auf NULL setzen, da die Chancen höher sind, dass Sie die Stelle des Fehlers früher treffen, als wenn Sie ihn nicht auf NULL gesetzt hätten. Der logische Fehler, ein zweites Mal Speicherplatz freizugeben, ist noch zu bedenken, und der Fehler, dass Ihre Anwendung NICHT beim Null-Zeiger-Zugriff mit einem ausreichend großen Offset abstürzt, ist meiner Meinung nach absolut akademisch, jedoch nicht unmöglich.

Fazit: Ich würde den Zeiger auf NULL setzen.

3
Kosi2801

Die Antwort hängt von (1) der Projektgröße, (2) der erwarteten Lebensdauer Ihres Codes, (3) der Teamgröße ab .. _ Bei einem kleinen Projekt mit kurzer Lebensdauer können Sie das Setzen von Zeigern auf NULL überspringen und nur das Debugging durchführen.

Bei einem großen, langlebigen Projekt gibt es gute Gründe, Zeiger auf NULL zu setzen: (1) Defensives Programmieren ist immer gut. Ihr Code mag in Ordnung sein, aber der Anfänger von nebenan hat möglicherweise noch Probleme mit Zeigern (2) Mein persönlicher Glaube ist, dass alle Variablen immer nur gültige Werte enthalten sollten. Nach einem Löschvorgang ist der Zeiger kein gültiger Wert mehr, daher muss er aus dieser Variablen entfernt werden. Das Ersetzen durch NULL (der einzige Zeigerwert, der immer gültig ist) ist ein guter Schritt. (3) Code stirbt nie. Es wird immer wiederverwendet und oft auf eine Art und Weise, die Sie sich zu dem Zeitpunkt, als Sie es geschrieben haben, nicht vorgestellt haben. Ihr Codesegment wird möglicherweise in einem C++ - Kontext kompiliert und möglicherweise zu einem Destruktor oder einer Methode verschoben, die von einem Destruktor aufgerufen wird. Die Interaktionen von virtuellen Methoden und Objekten, die gerade zerstört werden, sind selbst für sehr erfahrene Programmierer subtile Fallen. (4) Wenn Ihr Code in einem Multithread-Kontext verwendet wird, wird dies möglicherweise von einem anderen Thread gelesen Variable und versuchen Sie, darauf zuzugreifen. Solche Kontexte treten häufig auf, wenn alter Code in einen Webserver eingepackt und wiederverwendet wird. Eine noch bessere Möglichkeit, Speicherplatz (aus paranoider Sicht) freizugeben, ist (1) den Zeiger auf eine lokale Variable zu kopieren, (2) die ursprüngliche Variable auf NULL zu setzen, (3) die lokale Variable zu löschen/freizugeben. 

3
Carsten Kuckuk

Wenn der Zeiger wiederverwendet werden soll, sollte is nach der Verwendung wieder auf 0(NULL) gesetzt werden, auch wenn das Objekt, auf das er zeigte, nicht vom Heap freigegeben wird. Dies ermöglicht eine gültige Überprüfung gegen NULL wie if (p) {// etwas tun}. Nur weil Sie ein Objekt freigeben, auf dessen Adresse der Zeiger zeigt, bedeutet dies nicht, dass der Zeiger auf 0 gesetzt ist, nachdem er das Schlüsselwort delete oder die Funktion free aufgerufen hat. 

Wenn der Zeiger einmal verwendet wird und Teil eines Gültigkeitsbereichs ist, der ihn lokal macht, muss er nicht auf NULL gesetzt werden, da er nach der Funktionsrückgabe aus dem Stapel entfernt wird.

Wenn der Zeiger ein Member (Struktur oder Klasse) ist, sollten Sie ihn auf NULL setzen, nachdem Sie das Objekt oder die Objekte auf einem Doppelzeiger wieder freigegeben haben, um eine gültige Überprüfung gegen NULL zu ermöglichen. 

Auf diese Weise können Sie die Kopfschmerzen durch ungültige Zeiger wie '0xcdcd ...' usw. verringern. Wenn der Zeiger 0 ist, wissen Sie, dass er nicht auf eine Adresse zeigt, und Sie können sicherstellen, dass das Objekt vom Heap freigegeben wird.

2
bvrwoo_3376

Beides ist sehr wichtig, da es sich um undefiniertes Verhalten handelt. Sie sollten in Ihrem Programm keinen Weg für undefiniertes Verhalten lassen. Beides kann zu Abstürzen, Datenverfälschungen, subtilen Fehlern und anderen negativen Folgen führen.

Beide sind schwer zu debuggen. Beides lässt sich insbesondere bei komplexen Datenstrukturen nicht mit Sicherheit vermeiden. Jedenfalls geht es Ihnen viel besser, wenn Sie die folgenden Regeln beachten:

  • initialisieren Sie immer Zeiger - setzen Sie sie auf NULL oder eine gültige Adresse
  • nachdem Sie free () aufgerufen haben, setzen Sie den Zeiger auf NULL
  • Überprüfen Sie alle Zeiger, die möglicherweise NULL sein können, vor dem Dereferenzieren, dass sie NULL sind.
1
sharptooth

In C++ könnten Sie sowohl durch die Implementierung Ihres eigenen intelligenten Zeigers (oder das Ableiten aus vorhandenen Implementierungen) als auch durch die Implementierung von etwas wie:

void release() {
    assert(m_pt!=NULL);
    T* pt = m_pt;
    m_pt = NULL;
    free(pt);
}

T* operator->() {
    assert(m_pt!=NULL);
    return m_pt;
}

Alternativ können Sie in C mindestens zwei Makros mit demselben Effekt bereitstellen:

#define SAFE_FREE(pt) \
    assert(pt!=NULL); \
    free(pt); \
    pt = NULL;

#define SAFE_PTR(pt) assert(pt!=NULL); pt
1
Sebastian

Diese Probleme sind meistens nur Symptome für ein viel tieferes Problem. Dies kann für alle Ressourcen vorkommen, für die eine Erfassung und eine spätere Version erforderlich sind, z. Speicher, Dateien, Datenbanken, Netzwerkverbindungen usw. Das Kernproblem besteht darin, dass Sie die Ressourcenzuteilung durch eine fehlende Codestruktur verloren haben, zufällige Mallocs werfen und die gesamte Codebasis freigeben.

Organisieren Sie den Code um DRY - Wiederholen Sie sich nicht. Halten Sie verwandte Dinge zusammen. Tun Sie nur eine Sache und tun Sie es gut. Das "Modul", das eine Ressource zuweist, ist für die Freigabe verantwortlich und muss dafür eine Funktion bereitstellen, die auch die Zeiger beachtet. Für eine bestimmte Ressource haben Sie dann genau einen Ort, an dem sie zugewiesen wird, und einen Ort, an dem sie freigegeben wird, beide eng beieinander.

Angenommen, Sie möchten eine Zeichenfolge in Teilzeichenfolgen aufteilen. Direkt mit malloc () muss Ihre Funktion für alles sorgen: Analyse der Zeichenfolge, Zuweisung der richtigen Menge an Arbeitsspeicher, Kopieren der Teilzeichenfolgen und und und. Machen Sie die Funktion kompliziert genug, und es ist nicht die Frage, ob Sie den Überblick über die Ressourcen verlieren, sondern wann.

Ihr erstes Modul kümmert sich um die tatsächliche Speicherzuordnung:


    void *MemoryAlloc (size_t size)
    void  MemoryFree (void *ptr)

Es gibt Ihren einzigen Ort in Ihrer gesamten Codebasis, an dem malloc () und free () aufgerufen werden.

Dann müssen wir Strings zuordnen:


    StringAlloc (char **str, size_t len)
    StringFree (char **str)

Sie sorgen dafür, dass len + 1 benötigt wird und der Zeiger bei der Freigabe auf NULL gesetzt wird. Stellen Sie eine weitere Funktion zum Kopieren eines Teilstrings bereit:


    StringCopyPart (char **dst, const char *src, size_t index, size_t len)

Es wird darauf geachtet, dass sich index und len in der src-Zeichenfolge befinden und diese bei Bedarf ändern. Es ruft StringAlloc für dst auf, und es wird darauf geachtet, dass dst korrekt beendet wird.

Jetzt können Sie Ihre Split-Funktion schreiben. Sie müssen sich nicht mehr um die Details auf niedriger Ebene kümmern, sondern analysieren nur die Zeichenfolge und erhalten die Teilzeichenfolgen daraus. Die meiste Logik ist jetzt in dem Modul, wo sie hingehört, anstatt in einer großen Monstrosität gemischt zu werden.

Natürlich hat diese Lösung ihre eigenen Probleme. Es bietet Abstraktionsebenen, und während andere Probleme gelöst werden, verfügt jede Ebene über eine eigene Gruppe.

1
Secure

Es gibt nicht wirklich einen "wichtigeren" Teil, bei dem Sie eines der beiden Probleme vermeiden möchten. Beides muss wirklich vermieden werden, wenn Sie zuverlässige Software schreiben möchten. Es ist auch sehr wahrscheinlich, dass eine der oben genannten Daten zu Datenverfälschungen, einem Webserver und sonstigem Spaß führt.

Es ist auch ein weiterer wichtiger Schritt zu beachten: Wenn Sie den Zeiger auf NULL setzen, nachdem Sie ihn freigegeben haben, ist er nur die Hälfte der Arbeit. Wenn Sie dieses Idiom verwenden, sollten Sie den Zeigerzugriff idealerweise in etwa wie folgt einschließen:

if (ptr)
  memcpy(ptr->stuff, foo, 3);

Wenn Sie den Zeiger selbst auf NULL setzen, stürzt das Programm nur an unpassenden Orten ab, was wahrscheinlich besser ist, als Daten unbemerkt zu beschädigen.

0
Timo Geusch

Es kann nicht garantiert werden, dass das Programm beim Zugriff auf den Nullzeiger abstürzt.

Vielleicht nicht durch den Standard, aber es wäre schwierig, eine Implementierung zu finden, die sie nicht als illegale Operation definiert, die einen Absturz oder eine Ausnahme (entsprechend der Laufzeitumgebung) verursacht.

0
Anon.