Die Funktion zum Freigeben einer Instanz von struct Foo
ist unten angegeben:
void DestroyFoo(Foo* foo)
{
if (foo) free(foo);
}
Ein Kollege von mir schlug stattdessen Folgendes vor:
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 es besser ist, den Zeiger auf NULL
zu setzen, aber ich bin mir nicht sicher über Folgendes:
Müssen wir den Zeiger wirklich einem temporären zuweisen? Hilft es in Bezug auf Parallelität und Shared Memory?
Ist es wirklich eine gute Idee, den gesamten Block auf 0 zu setzen, um das Programm zum Absturz zu bringen oder zumindest Ergebnisse mit erheblichen Diskrepanzen auszugeben?
Müssen wir den Zeiger wirklich einem temporären Zeiger zuweisen? Hilft es in Bezug auf Parallelität und Shared Memory?
Es hat nichts mit Parallelität oder gemeinsamem Speicher zu tun. Es hat keinen Sinn.
Ist es wirklich eine gute Idee, den gesamten Block auf 0 zu setzen, um das Programm zum Absturz zu bringen oder zumindest Ergebnisse mit erheblichen Abweichungen auszugeben?
Nein überhaupt nicht.
Die von Ihrem Kollegen vorgeschlagene Lösung ist schrecklich. Hier ist der Grund:
Das Setzen des gesamten Blocks auf 0 führt ebenfalls zu nichts. Da jemand versehentlich einen free () 'ed -Block verwendet, würde er das aufgrund der Werte am Block nicht wissen. Das ist die Art von Block, den calloc()
zurückgibt. Es ist also unmöglich zu wissen, ob es sich um frisch zugewiesenen Speicher (calloc()
oder malloc()+memset()
) handelt oder um den Speicher, der zuvor von Ihrem Code freigegeben () wurde. Wenn überhaupt, ist es zusätzliche Arbeit für Ihr Programm, jeden Speicherblock auf Null zu setzen, der frei ist () 'ed.
free(NULL);
ist genau definiert und keine Operation, sodass die if
-Bedingung in if(ptr) {free(ptr);}
nichts bewirkt.
Da free(NULL);
no-op ist, würde das Setzen des Zeigers auf NULL
diesen Fehler tatsächlich verbergen, da eine Funktion tatsächlich free()
auf einem bereits freien () 'aufruft. ed Zeiger, dann würden sie das nicht wissen.
die meisten Benutzerfunktionen würden zu Beginn eine NULL-Prüfung haben und möglicherweise nicht in Betracht ziehen, NULL
als Fehlerbedingung an sie zu übergeben:
void do_some_work(void *ptr) {
if (!ptr) {
return;
}
/*Do something with ptr here */
}
All diese zusätzlichen Prüfungen und Nullen geben also ein falsches Gefühl von "Robustheit", während es nichts wirklich verbessert hat. Es hat nur ein Problem durch ein anderes ersetzt, nämlich die zusätzlichen Kosten für Leistung und Code Bloat.
Der Aufruf von free(ptr);
ohne Wrapper-Funktion ist einfach und robust (die meisten malloc()
-Implementierungen würden bei double free sofort abstürzen, was eine gute Sache ist).
Es gibt keinen einfachen Weg, um "versehentlich" free()
zweimal oder öfter aufzurufen. Es liegt in der Verantwortung des Programmierers, den gesamten zugewiesenen Speicher zu verfolgen und free()
entsprechend zu konfigurieren. Wenn jemand Probleme damit hat, ist C wahrscheinlich nicht die richtige Sprache für ihn.
Was Ihr Kollege vorschlägt, macht den Code "sicherer", falls die Funktion zweimal aufgerufen wird (siehe Kommentar dazu ... als "sicherer" bedeutet dies möglicherweise nicht für alle gleich ... ;-).
Mit Ihrem Code wird dies höchstwahrscheinlich abstürzen:
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
DestroyFoo(foo); // will call free on memory already freed
Mit der Version des Codes Ihrer Kollegen stürzt dies nicht ab:
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(&foo);
DestroyFoo(&foo); // will have no effect
Für dieses spezielle Szenario reicht es aus, tmpFoo = 0;
(innerhalb von DestroyFoo
) auszuführen. memset(tmpFoo, 0, sizeof(Foo));
verhindert den Absturz, wenn Foo zusätzliche Attribute hat, auf die möglicherweise nach der Freigabe des Speichers falsch zugegriffen wird.
Also würde ich ja sagen, es ist eine gute Praxis, dies zu tun ... aber es ist nur eine Art Sicherheit gegen Entwickler, die schlechte Praktiken haben (da es definitiv keinen Grund gibt, DestroyFoo
zweimal aufzurufen, ohne sie neu zuzuordnen... am Ende machen Sie DestroyFoo
"sicherer", aber langsamer (es wird mehr getan, um eine schlechte Verwendung zu verhindern).
Die zweite Lösung scheint übertrieben zu sein. In manchen Situationen ist es natürlich sicherer, aber der Aufwand und die Komplexität sind einfach zu groß.
Wenn Sie auf der sicheren Seite sein möchten, sollten Sie den Zeiger auf NULL setzen, nachdem Sie Speicher freigegeben haben. Dies ist immer eine gute Praxis.
Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
foo = NULL;
Außerdem weiß ich nicht, warum die Leute vor dem Aufruf von free () überprüfen, ob der Zeiger NULL ist. Dies ist nicht erforderlich, da free () die Arbeit für Sie erledigen wird.
Das Festlegen des Speichers auf 0 (oder etwas anderes) ist nur in einigen Fällen eine bewährte Methode, da free () den Speicher nicht löscht. Es wird nur einen Speicherbereich als frei markiert, damit er wiederverwendet werden kann. Wenn Sie den Speicher löschen möchten, damit niemand ihn lesen kann, müssen Sie ihn manuell bereinigen. Dies ist jedoch eine recht schwere Operation, und deshalb sollte nicht der gesamte Speicher freigegeben werden. In den meisten Fällen genügt das Freigeben ohne Löschen nur, und Sie müssen nicht auf die Leistung verzichten, um unnötige Operationen auszuführen.
void destroyFoo(Foo** foo)
{
if (!(*foo)) return;
Foo *tmpFoo = *foo;
*foo = NULL;
memset(tmpFoo, 0, sizeof(Foo));
free(tmpFoo);
}
Ihr Kollegencode ist schlecht, weil
foo
NULL
istIch denke, was Ihr Kollege im Sinn haben könnte, ist dieser Anwendungsfall
Foo* a = NULL;
Foo* b = createFoo();
destroyFoo(NULL);
destroyFoo(&a);
destroyFoo(&b);
In diesem Fall sollte es so sein. Versuch es hier
void destroyFoo(Foo** foo)
{
if (!foo || !(*foo)) return;
free(*foo);
*foo = NULL;
}
Zuerst müssen wir uns Foo
ansehen, nehmen wir an, dass es so aussieht
struct Foo
{
// variables
int number;
char character;
// array of float
int arrSize;
float* arr;
// pointer to another instance
Foo* myTwin;
};
Um zu definieren, wie es zerstört werden soll, definieren wir zunächst, wie es erstellt werden soll
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 die Erstellungsfunktion schreiben
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 hier versuchen
int main ()
{
Foo* a = createFoo (2, NULL);
Foo* b = createFoo (4, a);
a = destroyFoo(a);
b = destroyFoo(b);
printf("success");
return 0;
}
Leider funktioniert diese Idee einfach nicht.
Wenn die Absicht bestand, doppelt gefangen zu werden, deckt dies nicht die folgenden Fälle ab.
Nehmen Sie diesen Code an:
Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
free (ptr_1);
free (ptr_2); /* This is a bug */
Der Vorschlag lautet stattdessen zu schreiben:
Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
DestroyFoo (&ptr_1);
DestroyFoo (&ptr_2); /* This is still a bug */
Das Problem ist, dass der zweite Aufruf von DestroyFoo()
immer noch abstürzt, da ptr_2
nicht auf NULL zurückgesetzt wird und immer noch auf den bereits freigegebenen Speicher verweist.