it-swarm.com.de

Führt "Variablen nicht immer initialisieren" nicht dazu, dass wichtige Fehler ausgeblendet werden?

Die C++ - Kernrichtlinien haben die Regel ES.20: Immer ein Objekt initialisieren .

Vermeiden Sie Fehler, die vor dem Festlegen verwendet wurden, und das damit verbundene undefinierte Verhalten. Vermeiden Sie Probleme mit dem Verständnis der komplexen Initialisierung. Vereinfachen Sie das Refactoring.

Diese Regel hilft jedoch nicht, Fehler zu finden, sondern verbirgt sie nur.
Nehmen wir an, ein Programm hat einen Ausführungspfad, in dem es eine nicht initialisierte Variable verwendet. Es ist ein Fehler. Abgesehen von undefiniertem Verhalten bedeutet dies auch, dass ein Fehler aufgetreten ist und das Programm wahrscheinlich nicht den Produktanforderungen entspricht. Wenn es für die Produktion bereitgestellt wird, kann es zu einem Geldverlust oder sogar noch schlimmer kommen.

Wie überprüfen wir Fehler? Wir schreiben Tests. Tests decken jedoch nicht 100% der Ausführungspfade ab, und Tests decken niemals 100% der Programmeingaben ab. Darüber hinaus deckt sogar ein Test einen fehlerhaften Ausführungspfad ab - er kann immer noch bestehen. Es ist schließlich ein undefiniertes Verhalten, eine nicht initialisierte Variable kann einen etwas gültigen Wert haben.

Zusätzlich zu unseren Tests haben wir jedoch die Compiler, die so etwas wie 0xCDCDCDCD in nicht initialisierte Variablen schreiben können. Dies verbessert die Erkennungsrate der Tests geringfügig.
Noch besser - es gibt Tools wie Address Sanitizer, die alle Lesevorgänge nicht initialisierter Speicherbytes erfassen.

Und schließlich gibt es statische Analysatoren, die das Programm betrachten und feststellen können, dass auf diesem Ausführungspfad ein Read-Before-Set vorhanden ist.

Wir haben also viele mächtige Werkzeuge, aber wenn wir die Variable initialisieren - Desinfektionsmittel finden nichts .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Es gibt eine andere Regel: Wenn bei der Programmausführung ein Fehler auftritt, sollte das Programm so schnell wie möglich sterben. Sie müssen es nicht am Leben erhalten, sondern nur abstürzen, einen Absturzspeicherauszug schreiben und ihn den Ingenieuren zur Untersuchung geben.
Das Initialisieren von Variablen bewirkt unnötigerweise das Gegenteil - das Programm wird am Leben gehalten, wenn es sonst bereits einen Segmentierungsfehler erhalten würde.

35
Abyx

Ihre Argumentation geht aus mehreren Gründen schief:

  1. Segmentierungsfehler sind bei weitem nicht sicher. Die Verwendung einer nicht initialisierten Variablen führt zu ndefiniertes Verhalten. Segmentierungsfehler sind eine Möglichkeit, wie sich ein solches Verhalten manifestieren kann, aber es ist genauso wahrscheinlich, dass es normal zu laufen scheint.
  2. Compiler füllen den nicht initialisierten Speicher niemals mit einem definierten Muster (wie 0xCD). Dies ist etwas, das einige Debugger tun, um Sie bei der Suche nach Orten zu unterstützen, an denen nicht initialisierte Variablen verwendet werden. Wenn Sie ein solches Programm außerhalb eines Debuggers ausführen, enthält die Variable vollständig zufälligen Müll. Es ist ebenso wahrscheinlich, dass ein Zähler wie der bytes_read hat den Wert 10 als das hat es den Wert 0xcdcdcdcd.
  3. Selbst wenn Sie in einem Debugger arbeiten, der den nicht initialisierten Speicher auf ein festes Muster festlegt, geschieht dies nur beim Start. Dies bedeutet, dass dieser Mechanismus nur für statische (und möglicherweise Heap-zugewiesene) Variablen zuverlässig funktioniert. Bei automatischen Variablen, die auf dem Stapel zugewiesen werden oder nur in einem Register gespeichert sind, ist die Wahrscheinlichkeit hoch, dass die Variable an einem zuvor verwendeten Speicherort gespeichert wird, sodass das verräterische Speichermuster bereits überschrieben wurde.

Die Idee hinter der Anleitung, Variablen immer zu initialisieren, besteht darin, diese beiden Situationen zu aktivieren

  1. Die Variable enthält von Anfang an einen nützlichen Wert. Wenn Sie dies mit der Anleitung kombinieren, eine Variable nur dann zu deklarieren, wenn Sie sie benötigen, können Sie vermeiden, dass zukünftige Wartungsprogrammierer in die Falle geraten, eine Variable zwischen ihrer Deklaration und der ersten Zuweisung zu verwenden, wenn die Variable vorhanden wäre, aber nicht initialisiert wäre.

  2. Die Variable enthält einen definierten Wert, den Sie später testen können, um festzustellen, ob eine Funktion wie my_read hat den Wert aktualisiert. Ohne Initialisierung können Sie nicht feststellen, ob bytes_read hat tatsächlich einen gültigen Wert, da Sie nicht wissen können, mit welchem ​​Wert es begonnen hat.

Sie haben geschrieben "Diese Regel hilft nicht, Fehler zu finden, sie verbirgt sie nur" - nun, das Ziel der Regel ist nicht, Fehler zu finden, sondern zu vermeiden sie. Und wenn ein Fehler vermieden wird, ist nichts verborgen.

Lassen Sie uns das Problem anhand Ihres Beispiels diskutieren: Angenommen, die Funktion my_read Hat unter allen Umständen den schriftlichen Vertrag zur Initialisierung von bytes_read, Ist jedoch im Fehlerfall nicht fehlerhaft und daher fehlerhaft. Zumindest für diesen Fall. Sie möchten die Laufzeitumgebung verwenden, um diesen Fehler anzuzeigen, indem Sie den Parameter bytes_read Nicht zuerst initialisieren. Solange Sie sicher sind, dass ein Adressdesinfektionsmittel vorhanden ist, ist dies in der Tat eine Möglichkeit, einen solchen Fehler zu erkennen. Um den Fehler zu beheben, muss die Funktion my_read Intern geändert werden.

Es gibt jedoch eine andere Sichtweise, die mindestens gleichermaßen gültig ist: Das fehlerhafte Verhalten ergibt sich nur aus der Kombination, bytes_read Nicht vorher zu initialisieren, und rufen anschließend my_read auf (mit der Erwartung, dass bytes_read danach initialisiert wird). Dies ist eine Situation, die häufig in Komponenten der realen Welt auftritt, wenn die geschriebene Spezifikation für eine Funktion wie my_read Nicht 100% klar ist oder sogar das Verhalten im Fehlerfall falsch ist. Solange jedoch bytes_read Vor dem Aufruf auf Null initialisiert wird, verhält sich das Programm genauso, als ob die Initialisierung in my_read Durchgeführt worden wäre. Daher verhält es sich korrekt. In dieser Kombination gibt es keinen Fehler im Programm.

Daraus folgt meine Empfehlung: Verwenden Sie den nicht initialisierenden Ansatz nur, wenn

  • sie möchten zum Testen, wenn eine Funktion oder ein Codeblock einen bestimmten Parameter initialisiert
  • sie sind zu 100% sicher, dass die betreffende Funktion einen Vertrag hat, bei dem es definitiv falsch ist, diesem Parameter keinen Wert zuzuweisen
  • sie sind zu 100% sicher, dass die Umgebung dies erfassen kann

Dies sind Bedingungen, die Sie normalerweise im Testcode für eine bestimmte Werkzeugumgebung anordnen können.

Im Produktionscode ist es jedoch besser, eine solche Variable immer vorher zu initialisieren. Dies ist der defensivere Ansatz, der Fehler verhindert, wenn der Vertrag unvollständig oder falsch ist oder wenn der Adressbereiniger oder ähnliche Sicherheitsmaßnahmen nicht aktiviert sind. Und die "Absturz-Früh" -Regel gilt, wie Sie richtig geschrieben haben, wenn bei der Programmausführung ein Fehler auftritt. Wenn jedoch die vorherige Initialisierung einer Variablen bedeutet, dass nichts falsch ist, muss die weitere Ausführung nicht gestoppt werden.

25
Doc Brown

Initialisieren Sie immer Ihre Variablen

Der Unterschied zwischen den Situationen, die Sie in Betracht ziehen, besteht darin, dass der Fall ohne Initialisierung zu undefiniertem Verhalten führt, während der Fall, in dem Sie sich die Zeit für die Initialisierung genommen haben, einen Brunnen erzeugt definierter und deterministischer Fehler. Ich kann nicht betonen, wie extrem unterschiedlich diese beiden Fälle sind.

Stellen Sie sich ein hypothetisches Beispiel vor, das einem hypothetischen Mitarbeiter in einem hypothetischen Simulationsprogramm möglicherweise passiert ist. Dieses hypothetische Team versuchte hypothetisch, eine deterministische Simulation durchzuführen, um zu demonstrieren, dass das Produkt, das sie hypothetisch verkauften, den Anforderungen entsprach.

Okay, ich werde mit den Word-Injektionen aufhören. Ich denke du verstehst den Punkt; -)

In dieser Simulation gab es Hunderte von nicht initialisierten Variablen. Ein Entwickler hat valgrind für die Simulation ausgeführt und festgestellt, dass mehrere Fehler beim Verzweigen auf nicht initialisierten Wert aufgetreten sind. "Hmm, das sieht so aus, als könnte dies zu Nichtdeterminismus führen, was es schwierig macht, Testläufe zu wiederholen, wenn wir es am dringendsten brauchen." Der Entwickler ging zum Management, aber das Management hatte einen sehr engen Zeitplan und konnte keine Ressourcen sparen, um dieses Problem aufzuspüren. "Am Ende initialisieren wir alle unsere Variablen, bevor wir sie verwenden. Wir haben gute Codierungspraktiken."

Einige Monate vor der endgültigen Lieferung, wenn sich die Simulation im vollständigen Abwanderungsmodus befindet und das gesamte Team sprintet, um alle versprochenen Dinge mit einem Budget zu erledigen, das wie jedes jemals finanzierte Projekt zu klein war. Jemand bemerkte, dass sie ein wesentliches Feature nicht testen konnten, weil sich die deterministische Sim aus irgendeinem Grund beim Debuggen nicht deterministisch verhielt.

Das gesamte Team wurde möglicherweise angehalten und verbrachte den größten Teil von zwei Monaten damit, die gesamte Simulationscodebasis zu kämmen, um nicht initialisierte Wertefehler zu beheben, anstatt Funktionen zu implementieren und zu testen. Unnötig zu erwähnen, dass der Mitarbeiter das "Ich habe es Ihnen gesagt" übersprungen hat und anderen Entwicklern sofort geholfen hat, zu verstehen, was nicht initialisierte Werte sind. Seltsamerweise wurden die Codierungsstandards kurz nach diesem Vorfall geändert, um Entwickler zu ermutigen, ihre Variablen immer zu initialisieren.

nd dies ist der Warnschuss. Dies ist die Kugel, die über Ihre Nase streifte. Das eigentliche Problem ist weit weit weit weit heimtückischer als Sie sich vorstellen.

Die Verwendung eines nicht initialisierten Werts ist "undefiniertes Verhalten" (mit Ausnahme einiger Eckfälle wie char). Undefiniertes Verhalten (oder kurz UB) ist so wahnsinnig und völlig schlecht für Sie, dass Sie niemals glauben sollten, dass es besser ist als die Alternative. Manchmal können Sie feststellen, dass Ihr bestimmter Compiler die UB definiert und dann sicher zu verwenden ist. Andernfalls ist undefiniertes Verhalten "jedes Verhalten, nach dem sich der Compiler fühlt". Es kann etwas tun, das Sie als "gesund" bezeichnen würden, beispielsweise einen nicht angegebenen Wert. Möglicherweise werden ungültige Opcodes ausgegeben, wodurch Ihr Programm möglicherweise selbst beschädigt wird. Es kann eine Warnung zur Kompilierungszeit auslösen, oder der Compiler kann sogar dies als Fehler betrachten.

Oder es kann überhaupt nichts tun

Mein Kanarienvogel in der Kohlenmine für UB ist ein Fall einer SQL-Engine, über die ich gelesen habe. Verzeihen Sie mir, dass ich es nicht verlinkt habe. Ich habe den Artikel nicht wiedergefunden. Es gab ein Problem mit dem Pufferüberlauf in der SQL-Engine, als Sie eine größere Puffergröße an eine Funktion übergeben haben, jedoch nur für eine bestimmte Version von Debian. Der Fehler wurde pflichtbewusst angemeldet und erkundet. Der lustige Teil war: Der Pufferüberlauf wurde überprüft . Es gab Code zur Behandlung des Pufferüberlaufs. Es sah ungefähr so ​​aus:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

Ich habe meiner Wiedergabe weitere Kommentare hinzugefügt, aber die Idee ist dieselbe. Wenn put + dataLength Wraps herum, es wird kleiner sein als der put Zeiger (sie hatten Kompilierungszeitprüfungen, um sicherzustellen, dass unsigned int die Größe eines Zeigers hatte, für Neugierige). In diesem Fall wissen wir, dass die Standard-Ringpuffer-Algorithmen durch diesen Überlauf verwirrt werden können. Daher geben wir 0 zurück. Oder doch?

Wie sich herausstellt, ist der Überlauf von Zeigern in C++ nicht definiert. Da die meisten Compiler Zeiger als Ganzzahlen behandeln, kommt es zu typischen Überlaufverhalten von Ganzzahlen, die zufällig das gewünschte Verhalten sind. Dieses ist jedoch undefiniertes Verhalten, was bedeutet, dass der Compiler alles tun darf ) es will.

Im Fall dieses Fehlers entschied sich Debian für die Verwendung einer neuen Version von gcc, auf die keine der anderen wichtigen Linux-Varianten in ihrer Produktion aktualisiert wurde Veröffentlichungen. Diese neue Version von gcc hatte einen aggressiveren Dead-Code-Optimierer. Der Compiler erkannte das undefinierte Verhalten und entschied, dass das Ergebnis der Anweisung if "was auch immer die Codeoptimierung am besten macht" sein würde, was eine absolut legale Übersetzung von UB war. Dementsprechend wurde davon ausgegangen, dass seit ptr+dataLength kann ohne einen UB-Zeigerüberlauf niemals unter ptr liegen, die Anweisung if würde niemals auslösen und die Pufferüberlaufprüfung optimieren.

Die Verwendung von "sane" UB führte tatsächlich dazu, dass ein großes SQL-Produkt einen Pufferüberlauf-Exploit hatte, für den es Code geschrieben hatte, um dies zu vermeiden!

Verlassen Sie sich niemals auf undefiniertes Verhalten. Immer.

22
Cort Ammon

Ich arbeite hauptsächlich in einer funktionalen Programmiersprache, in der Sie keine Variablen neu zuweisen dürfen. Je. Das beseitigt diese Klasse von Fehlern vollständig. Dies schien zunächst eine enorme Einschränkung zu sein, zwingt Sie jedoch dazu, Ihren Code so zu strukturieren, dass er mit der Reihenfolge übereinstimmt, in der Sie neue Daten lernen. Dies vereinfacht Ihren Code und erleichtert die Wartung.

Diese Gewohnheiten können auch in imperative Sprachen übertragen werden. Es ist fast immer möglich, Ihren Code umzugestalten, um zu vermeiden, dass eine Variable mit einem Dummy-Wert initialisiert wird. Das ist es, was diese Richtlinien Ihnen sagen. Sie möchten, dass Sie etwas Sinnvolles hineinstecken, nicht etwas, das automatisierte Tools glücklich macht.

Ihr Beispiel mit einer API im C-Stil ist etwas kniffliger. In diesen Fällen, wenn ich benutze die Funktion, werde ich auf Null initialisieren, um den Compiler davon abzuhalten, sich zu beschweren, aber einmal im my_read Unit-Tests, ich werde mit etwas anderem initialisieren, um sicherzustellen, dass die Fehlerbedingung ordnungsgemäß funktioniert. Sie müssen nicht bei jeder Verwendung jede mögliche Fehlerbedingung testen.

5
Karl Bielefeldt

Nein, es versteckt keine Fehler. Stattdessen wird das Verhalten so deterministisch, dass ein Entwickler einen Fehler reproduzieren kann, wenn er auf einen Fehler stößt.

5
user204677

TL; DR: Es gibt zwei Möglichkeiten, dieses Programm zu korrigieren, Ihre Variablen zu initialisieren und zu beten. Nur einer liefert konsistent Ergebnisse.


Bevor ich Ihre Frage beantworten kann, muss ich zunächst erklären, was Undefiniertes Verhalten bedeutet. Eigentlich werde ich einen Compilerautor den Großteil der Arbeit machen lassen:

Wenn Sie diese Artikel nicht lesen möchten, lautet ein TL; DR:

Undefiniertes Verhalten ist ein Gesellschaftsvertrag zwischen dem Entwickler und dem Compiler; Der Compiler geht mit blindem Glauben davon aus, dass sich sein Benutzer niemals auf undefiniertes Verhalten verlassen wird.

Der Archetyp "Dämonen fliegen aus deiner Nase" hat die Implikationen dieser Tatsache leider überhaupt nicht vermittelt. Obwohl es beweisen sollte, dass alles passieren konnte, war es so unglaublich, dass es größtenteils mit den Schultern gezuckt wurde.

Die Wahrheit ist jedoch, dass Undefiniertes Verhalten die Kompilierung selbst beeinflusst, lange bevor Sie überhaupt versuchen, das Programm zu verwenden (instrumentiert oder nicht, innerhalb eines Debuggers oder nicht) und kann sein Verhalten grundlegend ändern.

Ich finde das Beispiel in Teil 2 oben auffällig:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

verwandelt sich in:

void contains_null_check(int *P) {
  *P = 4;
}

weil es offensichtlich ist, dass P nicht 0 sein kann, da es vor der Überprüfung dereferenziert wird.


Wie trifft dies auf Ihr Beispiel zu?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

Nun, Sie haben den häufigen Fehler gemacht, anzunehmen, dass Undefiniertes Verhalten Einen Laufzeitfehler verursachen würde. Es darf nicht.

Stellen wir uns vor, die Definition von my_read Lautet:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

und verfahren Sie wie erwartet von einem guten Compiler mit Inlining:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Dann optimieren wir, wie von einem guten Compiler erwartet, nutzlose Zweige:

  1. Keine Variable sollte nicht initialisiert verwendet werden
  2. bytes_read Würde nicht initialisiert verwendet, wenn result nicht 0 War.
  3. Der Entwickler verspricht, dass result niemals 0 Sein wird!

Also ist result niemals 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, result wird nie verwendet:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, wir können die Erklärung von bytes_read Verschieben:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Und hier sind wir, eine streng bestätigende Transformation des Originals, und kein Debugger wird eine nicht initialisierte Variable abfangen, weil es keine gibt.

Ich war auf diesem Weg und es macht wirklich keinen Spaß, das Problem zu verstehen, wenn das erwartete Verhalten und die Versammlung nicht übereinstimmen.

4
Matthieu M.

Schauen wir uns Ihren Beispielcode genauer an:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Dies ist ein gutes Beispiel. Wenn wir einen solchen Fehler erwarten, können wir die Zeile assert(bytes_read > 0); einfügen und diesen Fehler zur Laufzeit abfangen, was mit einer nicht initialisierten Variablen nicht möglich ist.

Angenommen, wir tun dies nicht und finden einen Fehler in der Funktion use(buffer). Wir laden das Programm in den Debugger, überprüfen die Rückverfolgung und stellen fest, dass es aus diesem Code aufgerufen wurde. Also setzen wir einen Haltepunkt oben in dieses Snippet, führen es erneut aus und reproduzieren den Fehler. Wir versuchen in einem Schritt, es zu fangen.

Wenn wir bytes_read Nicht initialisiert haben, enthält es Müll. Es muss nicht jedes Mal den gleichen Müll enthalten. Wir gehen an der Zeile my_read(buffer, &bytes_read); vorbei. Wenn es sich um einen anderen Wert als zuvor handelt, können wir unseren Fehler möglicherweise überhaupt nicht reproduzieren! Es könnte das nächste Mal aus Versehen mit demselben Eingang funktionieren. Wenn es konstant Null ist, erhalten wir ein konsistentes Verhalten.

Wir überprüfen den Wert, vielleicht sogar auf einem Backtrace im selben Lauf. Wenn es Null ist, können wir siehe, dass etwas nicht stimmt; bytes_read Sollte bei Erfolg nicht Null sein. (Oder wenn es sein kann, möchten wir es vielleicht auf -1 initialisieren.) Wir können den Fehler wahrscheinlich hier abfangen. Wenn bytes_read Ein plausibler Wert ist, der einfach falsch ist, würden wir ihn dann auf einen Blick erkennen?

Dies gilt insbesondere für Zeiger: Ein NULL-Zeiger ist in einem Debugger immer offensichtlich, kann sehr einfach getestet werden und sollte auf moderner Hardware fehlerhaft sein, wenn wir versuchen, ihn zu dereferenzieren. Ein Garbage Pointer kann später zu nicht reproduzierbaren Speicherbeschädigungsfehlern führen, die kaum zu debuggen sind.

1
Davislor

Das OP verlässt sich nicht oder zumindest nicht genau auf undefiniertes Verhalten. In der Tat ist es schlecht, sich auf undefiniertes Verhalten zu verlassen. Gleichzeitig ist das Verhalten eines Programms in einem unerwarteten Fall auch undefiniert, aber eine andere Art von undefiniert. Wenn Sie eine Variable auf Null setzen, aber keinen Ausführungspfad haben wollten, der diese anfängliche Null verwendet, verhält sich Ihr Programm dann vernünftig, wenn Sie einen Fehler haben und do einen solchen Pfad haben? Du bist jetzt im Unkraut; Sie hatten nicht vor, diesen Wert zu verwenden, aber Sie verwenden ihn trotzdem. Möglicherweise ist es harmlos, oder das Programm stürzt ab, oder das Programm beschädigt Daten stillschweigend. Du weißt es nicht.

Was das OP sagt, ist, dass es Tools gibt, die Ihnen helfen, diesen Fehler zu finden, wenn Sie sie zulassen. Wenn Sie den Wert nicht initialisieren, ihn aber trotzdem verwenden, gibt es statische und dynamische Analysatoren, die Ihnen mitteilen, dass Sie einen Fehler haben. Ein statischer Analysator informiert Sie, bevor Sie mit dem Testen des Programms beginnen. Wenn Sie andererseits den Wert blind initialisieren, können die Analysatoren nicht erkennen, dass Sie diesen Anfangswert nicht verwenden wollten, sodass Ihr Fehler unentdeckt bleibt. Wenn Sie Glück haben, ist es harmlos oder stürzt das Programm nur ab. Wenn Sie Pech haben, werden Daten stillschweigend beschädigt.

Der einzige Ort, an dem ich mit dem OP nicht einverstanden bin, ist ganz am Ende, wo er sagt, "wenn es sonst schon einen Segmentierungsfehler bekommen würde". In der Tat führt eine nicht initialisierte Variable nicht zuverlässig zu einem Segmentierungsfehler. Stattdessen würde ich sagen, dass Sie statische Analysewerkzeuge verwenden sollten, mit denen Sie nicht einmal versuchen können, das Programm auszuführen.

1
Jordan Brown

Eine Antwort auf Ihre Frage muss in die verschiedenen Arten von Variablen unterteilt werden, die in einem Programm angezeigt werden:


Lokale Variablen

Normalerweise sollte die Deklaration genau an der Stelle sein, an der die Variable zuerst ihren Wert erhält. Deklarieren Sie Variablen nicht wie im alten Stil C:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Dadurch entfällt 99% des Initialisierungsbedarfs. Die Variablen haben ihren endgültigen Wert von Anfang an. Die wenigen Ausnahmen sind, wo die Initialisierung von einer Bedingung abhängt:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Ich glaube, dass es eine gute Idee ist, diese Fälle so zu schreiben:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e. Stellen Sie ausdrücklich sicher, dass eine sinnvolle Initialisierung Ihrer Variablen durchgeführt wird.


Mitgliedsvariablen

Hier stimme ich dem zu, was die anderen Antwortenden gesagt haben: Diese sollten immer von den Konstruktoren/Initialisiererlisten initialisiert werden. Andernfalls ist es schwierig, die Konsistenz zwischen Ihren Mitgliedern sicherzustellen. Und wenn Sie eine Gruppe von Mitgliedern haben, die nicht in allen Fällen initialisiert werden müssen, überarbeiten Sie Ihre Klasse und fügen Sie diese Mitglieder einer abgeleiteten Klasse hinzu, in der sie immer benötigt werden.


Puffer

Hier bin ich mit den anderen Antworten nicht einverstanden. Wenn Menschen beim Initialisieren von Variablen religiös werden, initialisieren sie häufig Puffer wie folgt:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Ich glaube, das ist fast immer schädlich: Der einzige Effekt dieser Initialisierungen ist, dass sie Werkzeuge wie valgrind machtlos machen. Jeder Code, der mehr aus den initialisierten Puffern liest, als er sollte, ist sehr wahrscheinlich ein Fehler. Bei der Initialisierung kann dieser Fehler jedoch nicht durch valgrind aufgedeckt werden. Verwenden Sie sie also nur, wenn Sie sich wirklich darauf verlassen, dass der Speicher mit Nullen gefüllt ist (und schreiben Sie in diesem Fall einen Kommentar, in dem angegeben ist, wofür Sie die Nullen benötigen).

Ich würde außerdem dringend empfehlen, Ihrem Build-System ein Ziel hinzuzufügen, das die gesamte Testsuite unter valgrind oder einem ähnlichen Tool ausführt, um Fehler und Speicherverluste vor der Initialisierung aufzudecken. Dies ist wertvoller als alle Vorinitialisierungen von Variablen. Dieses valgrind Ziel sollte regelmäßig ausgeführt werden, vor allem bevor Code veröffentlicht wird.


Globale Variablen

Sie können keine globalen Variablen haben, die nicht initialisiert sind (zumindest in C/C++ usw.). Stellen Sie daher sicher, dass diese Initialisierung Ihren Wünschen entspricht.