2016-01-07 25 views
55

Die Funktion eine Instanz von struct Foo unten gegeben wird für die Freigabe:Ist dies eine gute Möglichkeit, Speicher in C freizugeben?

void DestroyFoo(Foo* foo) 
{ 
    if (foo) free(foo); 
} 

Ein Kollege von mir vorgeschlagen, die folgenden statt:

void DestroyFoo(Foo** foo) 
{ 
    if (!(*foo)) return; 
    Foo *tmpFoo = *foo; 
    *foo = NULL; // prevents future concurrency problems 
    memset(tmpFoo, 0, sizeof(Foo)); // problems show up immediately if referred to free memory 
    free(tmpFoo); 
} 

Ich sehe, dass der Zeiger auf NULL Einstellung nach Freigabe ist besser, aber ich bin mir nicht sicher über die folgenden:

  1. Müssen wir wirklich den Zeiger auf a zuweisen temporäre? Hilft es in Bezug auf Nebenläufigkeit und geteilten Speicher?
  2. Ist es wirklich eine gute Idee, den ganzen Block auf 0 zu setzen, um das Programm zum Absturz zu bringen oder zumindest Ergebnisse mit signifikanter Diskrepanz auszugeben?

Vielen Dank im Voraus!

+2

Es hängt alles von Use-Cases und was Sie versuchen zu erreichen. Beachten Sie auch, dass Sie 'free' mit einem Null-Zeiger aufrufen können, es tut dann einfach nichts. –

+3

Dies ist nicht die richtige Frage für Stackoverflow: Die Antworten sind nicht eindeutig. Es gibt Vorteile, die Dinge im zweiten Block des Codes zu tun, aber es gibt auch Nachteile. Ob der Nutzen die Kosten überwiegt, ist nur eine Frage der Meinung. –

+0

Der erste Fall ist falsch - er muss 'if (foo) free (foo)' sein. Andernfalls wird der Speicher bei 'if (! Foo) 'niemals freigegeben. – i486

Antwort

66

Müssen wir den Zeiger wirklich einem temporären zuweisen? Ist es Hilfe in Bezug auf Nebenläufigkeit und gemeinsamen Speicher?

Es hat nichts zu tun Nebenläufigkeit oder Shared Memory. Es ist zwecklos.

Ist es wirklich eine gute Idee, den ganzen Block auf 0 zu zwingen, das Programm zum Absturz zu bringen oder zumindest Ausgabeergebnisse mit erheblicher Diskrepanz zu setzen?

Nein. Überhaupt nicht.

Die von Ihrem Kollegen vorgeschlagene Lösung ist schrecklich. Hier ist warum:

  • Einstellung ganzen Block auf 0 erreicht auch nichts. Da jemand versehentlich einen freien() Ed-Block verwendet, würde er das aufgrund der Werte im Block nicht wissen. Das ist die Art von Block calloc() kehrt zurück. So ist es unmöglich zu wissen, ob es frisch zugeordneter Speicher ist (calloc() oder malloc()+memset()) oder der, der durch Ihren Code früher frei war(). Wenn es etwas ist, ist es zusätzliche Arbeit für Ihr Programm, jeden Speicherblock, der frei() ist, auf Null zu setzen.

  • free(NULL); ist wohldefiniert und ist ein No-Op, so dass die if Zustand in if(ptr) {free(ptr);} nichts erreicht.

  • Da free(NULL); no-op, würde versteckt den Zeiger auf NULL Einstellung tatsächlich diesen Fehler, denn wenn eine Funktion free() auf einem bereits frei() 'ed Zeiger Aufruf tatsächlich, dann würden sie das nicht wissen.

  • meisten Benutzerfunktionen würde eine NULL-Check am Anfang haben und nicht berücksichtigen kann NULL, um es als Fehlerzustand vorbei:

void do_some_work(void *ptr) { 
    if (!ptr) { 
     return; 
    } 

    /*Do something with ptr here */ 
} 

So die alle diese zusätzlichen Kontrollen und zero'ing Es gibt ein falsches Gefühl von "Robustheit", während es nichts wirklich verbessert. Es ersetzt nur ein Problem durch ein anderes, die zusätzlichen Kosten für Leistung und Code-Blähung.

ist also free(ptr); ohne Wrapper-Funktion nur anrufen einfach und robust (die meisten malloc() Implementierungen abstürzen würde sofort auf Doppel frei, die eine gute Sache ist).

Es gibt keinen einfachen Weg um "versehentlich" free() zweimal oder mehr zu rufen. Es liegt in der Verantwortung des Programmierers, den gesamten zugewiesenen Speicher und free() entsprechend zu verfolgen. Wenn jemand das schwer zu handhaben findet, dann ist C wahrscheinlich nicht die richtige Sprache für sie.

+4

Ich stimme dieser Antwort zu, außer dem Teil, der den Zeiger nicht auf NULL setzt. Mit NULL assigment ist doppelt frei zumindest konsistent. Ohne es führt doppelte freie Ergebnisse in UB, und es ** kann stumm scheitern **. Mit NULL zugewiesenem Pointer-Freigeben kann zumindest später erkannt werden. Wenn Sie NULL nicht zuweisen, erhalten Sie nur ein falsches Sicherheitsgefühl (Sie erwarten einen Absturz, der möglicherweise nicht auftritt). – user694733

+2

Einige Ihrer Kommentare sind Gegenstücke. Da es eine gute Übung ist zu überprüfen, ob der Zeiger nicht NULL ist, bevor "etwas mit ptr gemacht wird", sollten Sie ptr auf NULL setzen, nachdem Sie ihn freigegeben haben. Dies spart viel Zeit beim Debuggen schrecklicher Fehler, wenn Sie Speicher freigeben, setzen Sie nicht auf NULL, und dann wird jemand anderes die gleiche Speicherregion neu zuweisen. Dies verhindert auch doppelt frei, da Sie ptr, das auf NULL gesetzt ist, nicht freigeben können. – codewarrior

+1

Ich halte es ('free (ptr); ptr = NULL;') nicht besser, als einen Zeiger auf NULL zu setzen. Wenn ein Code * ist, * ist * ein Zeiger, der frei ist() 'ed, das ist ein * Bug *. Ich stimme zu, dass es in manchen Situationen nützlich sein kann, einen Fehler leicht zu finden. Wenn ptr == NULL ist eine Fehlerbedingung dann ja, es ist nützlich. Ansonsten, nein. Das hängt also ganz davon ab, wie der Rest des Codes damit umgeht. Ich würde nicht sagen, dass es direkt besser (oder schlechter) ist. –

9

Was Ihr Kollege vorschlägt, wird den Code "sicherer" machen, falls die Funktion zweimal aufgerufen wird (siehe sleske Kommentar ... da "sicherer" vielleicht nicht für alle gleich ist ... ;-).

mit Ihrem Code, dies wird höchstwahrscheinlich Absturz:

Foo* foo = malloc(sizeof(Foo)); 
DestroyFoo(foo); 
DestroyFoo(foo); // will call free on memory already freed 

Mit Ihrem collegues-Version des Codes, dies wird nicht abstürze:

Foo* foo = malloc(sizeof(Foo)); 
DestroyFoo(&foo); 
DestroyFoo(&foo); // will have no effect 

nun für dieses spezielle Szenario tun tmpFoo = 0; (innerhalb DestroyFoo) ist genug. memset(tmpFoo, 0, sizeof(Foo)); verhindert einen Absturz, wenn Foo über zusätzliche Attribute verfügt, auf die nach der Freigabe des Speichers fälschlicherweise zugegriffen werden kann.

So würde ich sagen, ja, kann es eine gute Praxis, dies zu tun .... aber es ist nur eine Art Sicherheit gegen Entwickler, die schlechten Praktiken haben (weil es definitiv kein Grund DestroyFoo zweimal anrufen, ohne es zu Neuzuweisung) ... am Ende machen Sie DestroyFoo "sicherer" aber langsamer (es macht mehr Sachen, um eine schlechte Nutzung davon zu verhindern).

+19

Eigentlich würde ich sagen, dass es den Code weniger sicher macht, indem er einen Fehler versteckt. Wenn Sie versehentlich doppelt freigeben, _want_ Sie es abstürzen. Beachten Sie, dass die doppelte Freigabe nicht garantiert abstürzt - das ist der beste Fall. Es kann auch Ihren Speicherzuordner beschädigen und alle Ihre Sachen überschreiben (undefiniertes Verhalten). – sleske

+0

@sleske Das stimmt (außer, wenn Sie 'DestroyFoo' absichtlich zweimal aus zwei verschiedenen Code/Statements aufrufen). Aber es verbirgt auf jeden Fall einen möglichen Fehler aufgrund von schlechter Programmierpraxis in den oberen Schichten .... Ich stimme völlig zu. – jpo38

+3

Ja, wenn Sie entscheiden, dass Sie Double-Freeing explizit unterstützen wollen, dann ist es definitionsgemäß kein Fehler. Dann brauchen Sie einen Wrapper, um es zu erlauben. Aber das scheint mir ein problematischer Entwurf zu sein; Es sollte einen bestimmten Punkt geben, an dem ein Objekt entsorgt wird. Selbst in diesem Fall besteht die etablierte Methode darin, den Zeiger nach dem Freigeben zu löschen (weil "frei" nichts für einen Null-Zeiger tut). – sleske

4

Die zweite Lösung scheint überentwickelt zu sein. Natürlich könnte es in manchen Situationen sicherer sein, aber der Overhead und die Komplexität sind einfach zu groß.

Was Sie tun sollten, wenn Sie auf der sicheren Seite sein möchten, ist das Setzen des Zeigers auf NULL nach dem Freigeben von Speicher. Dies ist immer eine gute Übung.

Was ist mehr, ich weiß nicht, warum Menschen überprüfen, ob der Zeiger vor dem Aufruf von free() NULL ist. Dies wird nicht benötigt, da free() die Arbeit für Sie erledigt.

Einstellung von Speicher auf 0 (oder etwas anderes) ist nur in einigen Fällen eine gute Übung, da free() den Speicher nicht löscht. Es markiert nur eine Speicherregion, um frei zu sein, damit sie wiederverwendet werden kann. Wenn Sie den Speicher löschen möchten, damit niemand ihn lesen kann, müssen Sie ihn manuell löschen. Aber das ist eine ziemlich schwere Operation und deshalb sollte dies nicht verwendet werden, um den gesamten Speicher freizugeben. In den meisten Fällen reicht es nur aus, ohne zu löschen, und Sie müssen nicht auf Leistung verzichten, um unnötige Operationen auszuführen.

+0

"Wenn Sie den Speicher löschen möchten, so dass niemand wird in der Lage sein, es zu lesen, müssen Sie es manuell reinigen. "Dies ist ein bisschen irreführend. Auf allen aktuellen Betriebssystemen mit mehreren Benutzern stellt das Betriebssystem sicher, dass kein anderer Prozess den Inhalt des freigegebenen Arbeitsspeichers erhält (normalerweise, indem er es auf Null setzt, bevor es einem anderen Prozess übergeben wird). Sie müssen sich also keine Sorgen machen, dass Daten in einen anderen Prozess gelangen. – sleske

+0

Ich habe versucht zu sagen, dass free() Speicher nicht löschen wird, sondern als unbenutzt markieren. Also, wenn Sie wirklich Speicher löschen möchten, sollten Sie es selbst tun. Darüber hinaus gibt es Möglichkeiten, den Speicher des aktuell laufenden Prozesses zu löschen. Wenn Ihr Speicher also nicht gelöscht ist, kann er von jemandem gelesen werden. – codewarrior

+0

Das stimmt, aber auf modernen Betriebssystemen erfordert dies das Ausführen mit den gleichen (oder höheren) Rechten wie der laufende Prozess, und dann ist es ein Wash. Der Punkt ist, dass dies nur ein Problem unter bestimmten Bedrohungsmodellen ist, also kann es durchaus sinnlos sein, so ist die Nullsetzung von Speicher nicht immer eine gute Übung. – sleske

1
void destroyFoo(Foo** foo) 
{ 
    if (!(*foo)) return; 
    Foo *tmpFoo = *foo; 
    *foo = NULL; 
    memset(tmpFoo, 0, sizeof(Foo)); 
    free(tmpFoo); 
} 

Ihr Kollege Code ist schlecht, weil

  • es stürzt ab, wenn fooNULL ist
  • es keinen Sinn, zusätzliche Variable bei der Schaffung von
  • es bei der Festlegung der Werte auf Null keinen Sinn
  • das Freigeben einer Struktur direkt funktioniert nicht, wenn sie Dinge enthält, die freizugeben sind

Ich denke, was Ihr Kollege könnte im Sinne hat, ist dies Use-Case

Foo* a = NULL; 
Foo* b = createFoo(); 

destroyFoo(NULL); 
destroyFoo(&a); 
destroyFoo(&b); 

In diesem Fall sollte es so sein. try here

void destroyFoo(Foo** foo) 
{ 
    if (!foo || !(*foo)) return; 
    free(*foo); 
    *foo = NULL; 
} 

Zuerst müssen wir einen Blick auf Foo nehmen, nehmen wir an, dass es wie folgt nun, wie es zerstört werden sollte, definieren

struct Foo 
{ 
    // variables 
    int number; 
    char character; 

    // array of float 
    int arrSize; 
    float* arr; 

    // pointer to another instance 
    Foo* myTwin; 
}; 

sieht, lassen Sie uns definieren zunächst, wie es sollte erstellt werden

Foo* createFoo (int arrSize, Foo* twin) 
{ 
    Foo* t = (Foo*) malloc(sizeof(Foo)); 

    // initialize with default values 
    t->number = 1; 
    t->character = '1'; 

    // initialize the array 
    t->arrSize = (arrSize>0?arrSize:10); 
    t->arr = (float*) malloc(sizeof(float) * t->arrSize); 

    // a Foo is a twin with only one other Foo 
    t->myTwin = twin; 
    if(twin) twin->myTwin = t; 

    return t; 
} 

Jetzt können wir eine Zerstörungsfunktion gegen t schreiben er Funktion erstellen

Foo* destroyFoo (Foo* foo) 
{ 
    if (foo) 
    { 
     // we allocated the array, so we have to free it 
     free(t->arr); 

     // to avoid broken pointer, we need to nullify the twin pointer 
     if(t->myTwin) t->myTwin->myTwin = NULL; 
    } 

    free(foo); 

    return NULL; 
} 

-Test try here

int main() 
{ 
    Foo* a = createFoo (2, NULL); 
    Foo* b = createFoo (4, a); 

    a = destroyFoo(a); 
    b = destroyFoo(b); 

    printf("success"); 
    return 0; 
} 
Verwandte Themen