Sollte man nach jedem kleinen Fehler in C suchen?

60

Als guter Programmierer sollte man robuste Codes schreiben, die jedes einzelne Ergebnis seines Programms verarbeiten. Fast alle Funktionen aus der C-Bibliothek geben jedoch 0 oder -1 oder NULL zurück, wenn ein Fehler auftritt.

Manchmal ist es offensichtlich, dass eine Fehlerprüfung erforderlich ist, beispielsweise beim Versuch, eine Datei zu öffnen. Aber ich ignoriere oft die Fehlerprüfung in Funktionen wie printfoder sogar, mallocweil ich mich nicht notwendig fühle.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Ist es eine gute Praxis, bestimmte Fehler einfach zu ignorieren, oder gibt es eine bessere Möglichkeit, alle Fehler zu behandeln?

Derek 朕 朕 會
quelle
13
Hängt von der Robustheitsstufe ab, die für das Projekt erforderlich ist, an dem Sie arbeiten. Systeme, die möglicherweise Eingaben von nicht vertrauenswürdigen Parteien erhalten (z. B. öffentlich zugängliche Server) oder in nicht vollständig vertrauenswürdigen Umgebungen betrieben werden, müssen sehr vorsichtig codiert werden, um zu vermeiden, dass der Code zu einer tickenden Zeitbombe wird (oder das schwächste Glied gehackt wird) ). Offensichtlich brauchen Hobby- und Lernprojekte keine solche Robustheit.
Rwong
1
Einige Sprachen bieten Ausnahmen. Wenn Sie keine Ausnahmen abfangen, wird Ihr Thread beendet, was besser ist, als ihn mit einem schlechten Zustand fortfahren zu lassen. Wenn Sie sich dafür entscheiden, Ausnahmen abzufangen, können Sie viele Fehler in zahlreichen Codezeilen, einschließlich der aufgerufenen Funktionen und Methoden, mit einer tryAnweisung abdecken , sodass Sie nicht jeden einzelnen Aufruf oder Vorgang überprüfen müssen. (Beachten Sie auch, dass einige Sprachen einfache Fehler wie Null-Dereferenzierung oder Array-Index außerhalb der Grenzen besser erkennen als andere.)
Erik Eidt
1
Das Problem ist größtenteils eine methodologische: Sie schreiben normalerweise keine Fehlerprüfung, wenn Sie noch herausfinden, was Sie implementieren sollen, und Sie möchten jetzt keine Fehlerprüfung hinzufügen, weil Sie den "glücklichen Pfad" erhalten möchten. gleich zuerst. Aber du solltest immer noch nach Malloc und Co. suchen . Anstatt den Schritt der Fehlerprüfung zu überspringen, müssen Sie Ihre Codierungsaktivitäten priorisieren und die Fehlerprüfung zu einem impliziten permanenten Refactoring-Schritt in Ihrer TODO-Liste machen, der angewendet wird, wenn Sie mit Ihrem Code zufrieden sind. Das Hinzufügen von greppable "/ * CHECK * /" Kommentaren könnte helfen.
Coredump
7
Ich kann nicht glauben, dass niemand etwas erwähnt hat errno! NULLWenn Sie nicht vertraut sind, wird zwar "fast alle Funktionen aus der C-Bibliothek 0 oder -1 zurückgeben oder wenn ein Fehler auftritt", aber auch die globale errnoVariable festgelegt , auf die Sie zugreifen können, indem Sie sie verwenden #include <errno.h>und dann einfach lesen der Wert von errno. Wenn beispielsweise open(2) zurückgibt -1, möchten Sie möglicherweise überprüfen, ob errno == EACCESdies auf einen Berechtigungsfehler hinweist oder ENOENTob die angeforderte Datei nicht vorhanden ist.
wchargin
1
@ErikEidt C unterstützt try/ nicht catch, obwohl Sie es mit Sprüngen simulieren könnten.
Mast

Antworten:

29

Im Allgemeinen sollte Code mit außergewöhnlichen Bedingungen umgehen, wo immer dies angemessen ist. Ja, das ist eine vage Aussage.

In höheren Sprachen mit Software-Ausnahmebehandlung wird dies häufig als "Ausnahme in der Methode abfangen, in der Sie tatsächlich etwas dagegen tun können" angegeben. Wenn ein Dateifehler aufgetreten ist, können Sie den Stapel möglicherweise auf den UI-Code hochsprudeln lassen, der dem Benutzer tatsächlich mitteilen kann, dass das Speichern Ihrer Datei auf der Festplatte fehlgeschlagen ist. Der Ausnahmemechanismus verschluckt effektiv "jeden kleinen Fehler" und behandelt ihn implizit an der richtigen Stelle.

In C haben Sie diesen Luxus nicht. Es gibt einige Möglichkeiten, um Fehler zu behandeln. Einige davon sind Sprach- / Bibliotheksfunktionen, andere Codierungsverfahren.

Ist es eine gute Praxis, bestimmte Fehler einfach zu ignorieren, oder gibt es eine bessere Möglichkeit, alle Fehler zu behandeln?

Bestimmte Fehler ignorieren? Vielleicht. Beispielsweise ist anzunehmen, dass das Schreiben auf die Standardausgabe nicht fehlschlägt. Wenn es fehlschlägt, wie würden Sie es dem Benutzer trotzdem mitteilen? Ja, es ist eine gute Idee, bestimmte Fehler zu ignorieren oder defensiv zu codieren, um sie zu verhindern. Überprüfen Sie beispielsweise vor dem Teilen, ob Null vorliegt.

Es gibt Möglichkeiten, mit allen oder zumindest den meisten Fehlern umzugehen:

  1. Sie können Sprünge, ähnlich wie bei gotos, zur Fehlerbehandlung verwenden . Während es sich um ein strittiges Problem unter Softwareprofis handelt, gibt es für sie gültige Verwendungen, insbesondere in eingebettetem und leistungskritischem Code (z. B. Linux-Kernel).
  1. Kaskadierung ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
  2. Testen Sie die erste Bedingung, z. B. Öffnen oder Erstellen einer Datei, und gehen Sie dann davon aus, dass nachfolgende Vorgänge erfolgreich sind.

Robuster Code ist gut und man sollte nach Fehlern suchen und diese behandeln. Welche Methode für Ihren Code am besten geeignet ist, hängt davon ab, was der Code tut, wie kritisch ein Fehler ist usw., und nur Sie können das wirklich beantworten. Diese Methoden sind jedoch kampferprobt und werden in verschiedenen Open Source-Projekten verwendet, in denen Sie nachsehen können, wie realer Code auf Fehler überprüft.

Gemeinschaft
quelle
21
Entschuldigung, kleiner Fehler - "Das Schreiben auf die Standardausgabe schlägt nicht fehl. Wenn dies fehlschlägt, wie würden Sie es dem Benutzer trotzdem mitteilen?" - Durch Schreiben auf Standardfehler? Es gibt keine Garantie dafür, dass ein Fehler beim Schreiben auf die eine impliziert, dass es unmöglich ist, auf die andere zu schreiben.
Damien_The_Unbeliever
4
@Damien_The_Unbeliever - zumal stdoutje nach System in eine Datei umgeleitet werden kann oder gar nicht existiert. stdoutist kein "Write to Console" -Stream, das ist normalerweise so , muss es aber nicht sein.
Davor Ždralo
3
@ JAB Sie senden eine Nachricht an stdout...
evident
7
@JAB: Sie können mit EX_IOERR oder so beenden, wenn dies angemessen ist.
John Marshall
6
Was auch immer Sie tun, rennen Sie nicht einfach weiter, als wäre nichts passiert! Wenn der Befehl dump_database > backups/db_dump.txtzu einem bestimmten Zeitpunkt nicht in die Standardausgabe schreiben kann, möchte ich nicht, dass er fortgesetzt und erfolgreich beendet wird. (Nicht, dass Datenbanken auf diese Weise gesichert werden, aber der Punkt steht noch)
Ben S
15

Fast alle Funktionen aus der C-Bibliothek geben jedoch 0 oder -1 oder NULL zurück, wenn ein Fehler auftritt.

Ja, aber Sie wissen, welche Funktion Sie angerufen haben, nicht wahr?

Sie haben tatsächlich eine Menge Informationen, die Sie in eine Fehlermeldung einfügen könnten. Sie kennen die aufgerufene Funktion, den Namen der aufrufenden Funktion, die übergebenen Parameter und den Rückgabewert der Funktion. Das sind viele Informationen für eine sehr informative Fehlermeldung.

Sie müssen dies nicht für jeden Funktionsaufruf tun. Wenn Sie jedoch zum ersten Mal die Fehlermeldung "Beim Anzeigen des vorherigen Fehlers ist ein Fehler aufgetreten" sehen, obwohl Sie wirklich nützliche Informationen benötigt haben, wird diese Fehlermeldung zum letzten Mal angezeigt, da Sie sie sofort ändern werden Die Fehlermeldung weist auf etwas Informatives hin, das Ihnen bei der Behebung des Problems hilft.

Robert Harvey
quelle
5
Diese Antwort brachte mich zum Lächeln, weil es wahr ist, aber die Frage nicht beantwortet.
RubberDuck
Wie beantwortet es die Frage nicht?
Robert Harvey
2
Ein Grund, warum ich denke, dass C (und andere Sprachen) den Booleschen Wert 1 und 0 vertauscht haben, ist derselbe Grund, dass jede anständige Funktion, die einen Fehlercode zurückgibt, "0" zurückgibt, ohne dass ein Fehler vorliegt. aber dann musst du sagen if (!myfunc()) {/*proceed*/}. 0 war kein Fehler, und alles, was nicht Null war, war ein Irrtum, und jeder Irrtum hatte seinen eigenen Code, der nicht Null war. Ein Freund von mir hat es so ausgedrückt: "Es gibt nur eine Wahrheit, aber viele Lügen." "0" sollte im if()Test "wahr" sein und alles, was nicht Null ist, sollte "falsch" sein.
Robert Bristow-Johnson
1
Nein, es gibt nur einen möglichen negativen Fehlercode. und viele mögliche no_error Codes.
Robert Bristow-Johnson
1
@ Robertbristow-Johnson: Dann lesen Sie es als "Es gab keinen Fehler." if(function()) { // do something }lautet wie folgt: "Wenn die Funktion fehlerfrei ausgeführt wird, tun Sie etwas." oder noch besser: "Wenn die Funktion erfolgreich ausgeführt wird, tun Sie etwas." Im Ernst, diejenigen, die die Konvention verstehen, sind davon nicht verwirrt. Das Zurückgeben false(oder 0), wenn ein Fehler auftritt, würde die Verwendung von Fehlercodes nicht zulassen. Null bedeutet in C falsch, weil Mathe so funktioniert. Siehe programmers.stackexchange.com/q/198284
Robert Harvey,
11

TLDR; Sie sollten Fehler so gut wie nie ignorieren.

Der Sprache C fehlt eine gute Fehlerbehandlung, sodass jeder Bibliotheksentwickler seine eigenen Lösungen implementieren kann. In neueren Sprachen sind Ausnahmen eingebaut, die die Handhabung dieses speziellen Problems erheblich erleichtern.

Aber wenn Sie mit C stecken sind, haben Sie keine solchen Vorteile. Leider müssen Sie den Preis einfach jedes Mal bezahlen, wenn Sie eine Funktion aufrufen, bei der die Möglichkeit eines Remote-Ausfalls besteht. Andernfalls treten viel schlimmere Folgen auf, z. B. das unbeabsichtigte Überschreiben von Daten im Speicher. In der Regel muss also immer nach Fehlern gesucht werden.

Wenn Sie nicht auf die Rückkehr von überprüfen, werden fprintfSie sehr wahrscheinlich einen Fehler zurücklassen, der im besten Fall nicht das tut, was der Benutzer erwartet, und im schlimmsten Fall wird das Ganze während des Flugs explodieren. Es gibt keine Entschuldigung, sich auf diese Weise zu untergraben.

Als C-Entwickler ist es jedoch auch Ihre Aufgabe, den Code einfach zu warten. Manchmal können Sie Fehler aus Gründen der Übersichtlichkeit ignorieren, wenn (und nur dann) keine Bedrohung für das Gesamtverhalten der Anwendung vorliegt. .

Es ist das gleiche Problem wie dies zu tun:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Wenn Sie sehen, dass sich innerhalb des leeren catch-Blocks keine guten Kommentare befinden, ist dies mit Sicherheit ein Problem. Es gibt keinen Grund zu der Annahme, dass ein Aufruf zu malloc()100% der Zeit erfolgreich ausgeführt wird.

Alex
quelle
Ich stimme zu, dass Wartbarkeit ein wirklich wichtiger Aspekt ist und dieser Absatz meine Frage wirklich beantwortet hat. Danke für die ausführliche Antwort.
Derek 朕 朕 功夫
Ich denke, die Unmenge von Programmen, die sich nie die Mühe machen, ihre Malloc-Aufrufe zu überprüfen und trotzdem nie abstürzen, sind ein guter Grund, faul zu sein, wenn ein Desktop-Programm nur wenige MB Speicher benötigt.
Whatsisname
5
@whatsisname: Nur weil sie nicht abstürzen, heißt das nicht, dass sie nicht im Hintergrund Daten beschädigen. malloc()ist eine Funktion, auf die Sie unbedingt die Rückgabewerte überprüfen möchten!
TMN
1
@TMN: Wenn malloc fehlschlagen würde, würde das Programm sofort einen Fehler machen und sich beenden, es würde nicht weiterarbeiten. Es geht nur um Risiko und was angemessen ist, nicht "fast nie".
Whatsisname
2
Bei @Alex C mangelt es nicht an einer guten Fehlerbehandlung. Wenn Sie Ausnahmen wünschen, können Sie diese verwenden. Die Standardbibliothek, die keine Ausnahmen verwendet, ist eine bewusste Wahl (Ausnahmen erzwingen eine automatische Speicherverwaltung, und häufig möchten Sie dies nicht für Code auf niedriger Ebene).
Martinkunev
9

Die Frage ist eigentlich nicht sprachspezifisch, sondern benutzerspezifisch. Denken Sie über die Frage aus der Sicht des Benutzers nach. Der Benutzer kann beispielsweise den Namen des Programms in eine Befehlszeile eingeben und die Eingabetaste drücken. Was erwartet der Benutzer? Wie können sie feststellen, ob etwas schief gelaufen ist? Können sie es sich leisten, im Fehlerfall einzugreifen?

In vielen Codetypen sind solche Überprüfungen übertrieben. In sicherheitskritischen Codes mit hoher Zuverlässigkeit, wie z. B. für Kernreaktoren, gehören jedoch die Überprüfung pathologischer Fehler und geplante Wiederherstellungspfade zum Tagesgeschäft. Es lohnt sich, sich die Zeit zu nehmen, um zu fragen: "Was passiert, wenn X ausfällt? Wie komme ich in einen sicheren Zustand zurück?" In weniger zuverlässigem Code, wie z. B. für Videospiele, kommt man mit weitaus weniger Fehlerprüfungen davon.

Ein anderer ähnlicher Ansatz ist, um wie viel können Sie den Zustand tatsächlich verbessern, indem Sie den Fehler abfangen? Ich kann die Anzahl der C ++ - Programme nicht zählen, die stolz Ausnahmen abfangen, nur um sie erneut zu werfen, weil sie eigentlich nicht wussten, was sie damit machen sollten ... aber sie wussten, dass sie Ausnahmebehandlungen durchführen sollten. Solche Programme haben durch den Mehraufwand nichts gewonnen. Fügen Sie nur Fehlerprüfcodes hinzu, von denen Sie glauben, dass sie die Situation tatsächlich besser bewältigen können, als den Fehlercode einfach nicht zu überprüfen. Abgesehen davon hat C ++ spezifische Regeln für die Behandlung von Ausnahmen, die während der Ausnahmebehandlung auftreten, um sie auf eine aussagekräftigere Weise abzufangen (und damit meine ich, terminate () aufzurufen, um sicherzustellen, dass der von Ihnen erstellte Scheiterhaufen leuchtet in seiner eigentlichen Pracht)

Wie pathologisch kannst du werden? Ich arbeitete an einem Programm, das ein "SafetyBool" definierte, dessen wahre und falsche Werte sorgfältig ausgewählt wurden, um eine gleichmäßige Verteilung von Einsen und Nullen zu erhalten, und das so ausgewählt wurde, dass einer von mehreren Hardwarefehlern (Einzelbitflips, Datenbus) Spuren, die gebrochen wurden usw.) haben nicht dazu geführt, dass ein Boolescher Wert falsch interpretiert wurde. Es erübrigt sich zu erwähnen, dass dies keine allgemeine Programmierpraxis ist, die in einem alten Programm verwendet werden kann.

Cort Ammon
quelle
6
  • Unterschiedliche Sicherheitsanforderungen erfordern ein unterschiedliches Maß an Korrektheit. In der Luftfahrt- oder Automobilsteuerungssoftware werden alle Rückgabewerte überprüft, vgl. MISRA.FUNC.UNUSEDRET . In einem schnellen Proof-of-Concept, der Ihre Maschine niemals verlässt, wahrscheinlich auch nicht.
  • Codierung kostet Zeit. Zu viele irrelevante Überprüfungen von nicht sicherheitskritischer Software sind an anderer Stelle sinnvoll. Aber wo liegt der Sweet Spot zwischen Qualität und Kosten? Das hängt von den Debugging-Tools und der Komplexität der Software ab.
  • Fehlerbehandlung kann den Steuerungsfluss verdecken und neue Fehler verursachen. Ich mag die Wrapper-Funktionen von Richard "network" Stevens, die zumindest Fehler melden.
  • Fehlerbehandlung kann selten ein Leistungsproblem sein. Die meisten C-Bibliotheksaufrufe werden jedoch so lange dauern, dass die Kosten für die Überprüfung eines Rückgabewerts unermesslich gering sind.
Peter - Setzen Sie Monica wieder ein
quelle
3

Ein bisschen abstrakt nehmen die Frage auf. Und es ist nicht unbedingt für die C-Sprache.

Für größere Programme hätten Sie eine Abstraktionsschicht; Vielleicht ein Teil der Engine, einer Bibliothek oder eines Frameworks. Diese Schicht würde über das Wetter egal Sie ein gültigen Daten erhalten oder die Ausgabe würde einiger Standardwert sein: 0, -1, nullusw.

Dann gibt es eine Ebene, die Ihre Schnittstelle zur abstrakten Ebene ist, die eine Menge Fehlerbehandlung und möglicherweise andere Dinge wie Abhängigkeitsinjektionen, Ereignisüberwachung usw. erledigt.

Und später haben Sie Ihre konkrete Implementierungsebene, auf der Sie die Regeln festlegen und die Ausgabe bearbeiten.

Ich gehe davon aus, dass es manchmal besser ist, die Fehlerbehandlung vollständig aus einem Teil des Codes auszuschließen, da dieser Teil diesen Job einfach nicht ausführt. Und dann einen Prozessor, der die Ausgabe auswertet und auf einen Fehler hinweist.

Dies geschieht hauptsächlich, um Verantwortlichkeiten zu trennen, die zur Lesbarkeit des Codes und zu einer besseren Skalierbarkeit führen.

Kreative Magie
quelle
0

Sofern Sie keinen triftigen Grund haben , diesen Fehlerzustand nicht zu überprüfen, sollten Sie dies im Allgemeinen tun . Der einzige gute Grund, warum ich daran denken kann, nicht auf einen Fehlerzustand zu prüfen, ist, dass Sie unmöglich etwas Sinnvolles tun können, wenn es fehlschlägt. Dies ist jedoch eine sehr schwierige Angelegenheit, da es immer eine praktikable Option gibt: ordnungsgemäßes Verlassen.

Kluger Neologismus
quelle
Ich würde Ihnen im Allgemeinen nicht zustimmen, da Fehler Situationen sind, die Sie nicht berücksichtigt haben. Es ist schwer zu wissen, wie sich der Fehler manifestieren könnte, wenn Sie nicht wissen, unter welcher Bedingung der Fehler aufgetreten ist. Und damit Fehlerbehandlung vor der eigentlichen Datenverarbeitung.
Creative Magic
Wie gesagt, wenn etwas einen Fehler zurückgibt oder auslöst, können Sie, auch wenn Sie nicht wissen, wie Sie den Fehler beheben und fortfahren können, immer ordnungsgemäß fehlschlagen, den Fehler protokollieren, dem Benutzer mitteilen, dass er nicht wie erwartet funktioniert hat, usw Der Punkt ist, dass der Fehler nicht unbemerkt bleiben, nicht protokolliert werden, im Hintergrund fehlschlagen oder die Anwendung zum Absturz bringen sollte. Das ist, was "anmutig scheitern" oder "anmutig verlassen" bedeutet.
Clever Neologism