Zusammenfassung :
Sollte eine Funktion in C immer prüfen, ob sie keinen NULL
Zeiger dereferenziert ? Wenn nein, wann ist es angebracht, diese Prüfungen zu überspringen?
Details :
Ich habe einige Bücher über Programmierinterviews gelesen und frage mich, wie hoch der Grad der Eingabevalidierung für Funktionsargumente in C ist. Offensichtlich muss jede Funktion, die Eingaben von einem Benutzer entgegennimmt, eine Validierung durchführen, einschließlich der Überprüfung auf einen NULL
Zeiger, bevor er dereferenziert wird. Aber was ist mit einer Funktion in derselben Datei, die Sie nicht über Ihre API verfügbar machen möchten?
Zum Beispiel erscheint folgendes im Quellcode von git:
static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
if (!want_color(graph->revs->diffopt.use_color))
return column_colors_max;
return graph->default_column_color;
}
Wenn dies der Fall *graph
ist, NULL
wird ein Nullzeiger dereferenziert, was möglicherweise zum Absturz des Programms führt, möglicherweise jedoch zu einem anderen unvorhersehbaren Verhalten. Andererseits ist die Funktion static
und so hat der Programmierer möglicherweise die Eingabe bereits validiert. Ich weiß nicht, ich habe es nur zufällig ausgewählt, weil es ein kurzes Beispiel in einem in C geschriebenen Anwendungsprogramm war. Ich habe viele andere Stellen gesehen, an denen Zeiger verwendet werden, ohne nach NULL zu suchen. Meine Frage ist allgemein nicht spezifisch für dieses Codesegment.
Ich sah eine ähnliche Frage im Zusammenhang mit der Ausnahmebehandlung . Für eine unsichere Sprache wie C oder C ++ gibt es jedoch keine automatische Fehlerweitergabe für nicht behandelte Ausnahmen.
Andererseits habe ich in Open-Source-Projekten (wie im obigen Beispiel) viel Code gesehen, der vor der Verwendung keine Überprüfung von Zeigern durchführt. Ich frage mich, ob jemand über Richtlinien nachdenkt, wann eine Funktion überprüft werden soll, anstatt davon auszugehen, dass die Funktion mit korrekten Argumenten aufgerufen wurde.
Ich interessiere mich allgemein für diese Frage zum Schreiben von Produktionscode. Mich interessiert aber auch der Kontext von Programmierinterviews. Beispielsweise tendieren viele Algorithmus-Lehrbücher (wie CLR) dazu, die Algorithmen ohne jegliche Fehlerprüfung im Pseudocode darzustellen. Dies ist zwar gut, um den Kern eines Algorithmus zu verstehen, aber offensichtlich keine gute Programmierpraxis. Daher möchte ich keinem Interviewer mitteilen, dass ich die Fehlerprüfung übersprungen habe, um meine Codebeispiele zu vereinfachen (wie es in einem Lehrbuch der Fall sein könnte). Aber ich möchte auch nicht erscheinen, um ineffizienten Code mit übermäßiger Fehlerprüfung zu erzeugen. Zum Beispiel graph_get_current_column_color
könnte das geändert worden sein, um *graph
auf null zu prüfen, aber es ist nicht klar, was es tun würde, wenn *graph
null wäre, außer dass es es nicht dereferenzieren sollte.
quelle
Antworten:
Ungültige Nullzeiger können entweder durch Programmiererfehler oder durch Laufzeitfehler verursacht werden. Laufzeitfehler sind etwas, das ein Programmierer nicht beheben kann, z. B. ein Fehler
malloc
aufgrund von wenig Arbeitsspeicher oder wenn das Netzwerk ein Paket fallen lässt oder der Benutzer etwas Dummes eingibt. Programmiererfehler werden durch einen Programmierer verursacht, der die Funktion falsch verwendet.Die allgemeine Faustregel, die ich gesehen habe, ist, dass Laufzeitfehler immer überprüft werden sollten, aber Programmiererfehler müssen nicht jedes Mal überprüft werden. Nehmen wir an, ein idiotischer Programmierer hat direkt angerufen
graph_get_current_column_color(0)
. Beim ersten Aufruf tritt ein Seg-Fehler auf. Sobald Sie den Fehler behoben haben, wird der Fix dauerhaft kompiliert. Es muss nicht jedes Mal überprüft werden, wenn es ausgeführt wird.In einigen Fällen, insbesondere in Bibliotheken von Drittanbietern, wird
assert
anstelle einerif
Anweisung ein angezeigt , um nach Programmierfehlern zu suchen . Auf diese Weise können Sie die Prüfungen während der Entwicklung kompilieren und im Produktionscode weglassen. Ich habe auch gelegentlich unentgeltliche Überprüfungen gesehen, bei denen die Quelle des möglichen Programmierfehlers weit vom Symptom entfernt ist.Natürlich kann man immer jemanden finden, der pedantischer ist, aber die meisten mir bekannten C-Programmierer bevorzugen weniger überfüllten Code gegenüber Code, der etwas sicherer ist. Und "sicherer" ist ein subjektiver Begriff. Ein offensichtlicher Fehler während der Entwicklung ist einem subtilen Korruptionsfehler im Feld vorzuziehen.
quelle
Kernighan & Plauger schrieben in "Software Tools", dass sie alles überprüfen würden, und unter Bedingungen, von denen sie glaubten, dass sie tatsächlich niemals eintreten könnten, brachen sie mit der Fehlermeldung "Can't happen" ab.
Sie berichten, dass sie schnell demütigt sind, wenn sie gesehen haben, dass "Can't happen" auf ihren Terminals erscheint.
Sie sollten den Zeiger IMMER auf NULL prüfen, bevor Sie ihn dereferenzieren (versuchen). IMMER . Die Menge an Code, die Sie duplizieren, um nach NULL-Werten zu suchen, die nicht vorkommen, und die Prozessorzyklen, die Sie "verschwenden", werden durch die Anzahl der Abstürze bezahlt, die Sie nicht mehr als durch einen Absturzspeicherauszug debuggen müssen. wenn du so viel Glück hast.
Wenn der Zeiger innerhalb einer Schleife unveränderlich ist, reicht es aus, ihn außerhalb der Schleife zu überprüfen. Anschließend sollten Sie ihn in eine lokale Variable mit eingeschränktem Gültigkeitsbereich "kopieren", die von der Schleife verwendet wird und die entsprechenden const-Dekorationen hinzufügt. In diesem Fall MÜSSEN Sie sicherstellen, dass jede vom Schleifenkörper aufgerufene Funktion die erforderlichen Konstantendekorationen auf den Prototypen enthält, GANZ ABWÄRTS. Wenn Sie dies nicht tun oder nicht können (z. B. aufgrund eines Lieferantenpakets oder eines hartnäckigen Mitarbeiters), müssen Sie dies jedes Mal auf NULL prüfen , da COL Murphy ein unheilbarer Optimist war und daher jemand mitmachen wird um es zu zappen, wenn Sie nicht suchen.
Wenn Sie sich in einer Funktion befinden und der Zeiger nicht NULL sein soll, sollten Sie ihn überprüfen.
Wenn Sie es von einer Funktion erhalten und es nicht NULL sein soll, sollten Sie es überprüfen. malloc () ist dafür besonders berüchtigt. (Nortel Networks, das inzwischen nicht mehr existiert, hatte einen hart und schnell geschriebenen Codierungsstandard. Ich musste an einem Punkt einen Absturz debuggen, der auf malloc () zurückgeführt wurde, wobei ein Nullzeiger zurückgegeben wurde und der idiotische Codierer sich nicht die Mühe machte, dies zu überprüfen es, bevor er es schrieb, weil er wusste, dass er viel Gedächtnis hatte ... Ich sagte einige sehr böse Dinge, als ich es endlich fand.)
quelle
assert
, sicher. Ich mag die Fehlercode-Idee nicht, wenn Sie über das Ändern des vorhandenen Codes sprechen, umNULL
Überprüfungen einzuschließen.Sie können die Prüfung überspringen, wenn Sie sich irgendwie davon überzeugen können, dass der Zeiger möglicherweise nicht null sein kann.
Normalerweise werden Nullzeigerprüfungen in Code implementiert, bei denen erwartet wird, dass Null als Indikator dafür erscheint, dass ein Objekt derzeit nicht verfügbar ist. Null wird als Sentinel-Wert verwendet, um beispielsweise verknüpfte Listen oder sogar Arrays von Zeigern zu beenden. Der
argv
Vektor der übergebenen Zeichenfolgenmain
muss durch einen Zeiger mit Null abgeschlossen werden, ähnlich wie eine Zeichenfolge durch ein Nullzeichen:argv[argc]
ein Nullzeiger. Darauf können Sie sich beim Parsen der Befehlszeile verlassen.Die Situationen für die Überprüfung auf Null sind also solche, in denen es sich um einen erwarteten Wert handelt. Die Nullprüfungen implementieren die Bedeutung des Nullzeigers, z. B. das Stoppen der Suche in einer verknüpften Liste. Sie verhindern, dass der Code den Zeiger dereferenziert.
In einer Situation, in der ein Nullzeigerwert nicht von Entwurf erwartet wird, ist es nicht sinnvoll, danach zu suchen. Wenn ein ungültiger Zeigerwert auftritt, wird er mit hoher Wahrscheinlichkeit als nicht null angezeigt, was auf tragbare Weise nicht von gültigen Werten unterschieden werden kann. Zum Beispiel ein Zeigerwert, der durch Lesen eines nicht initialisierten Speichers erhalten wird, der als Zeigertyp interpretiert wird, ein Zeiger, der durch eine schattige Konvertierung erhalten wird, oder ein Zeiger, der außerhalb der Grenzen inkrementiert wird.
Informationen zu einem Datentyp wie
graph *
: Dies könnte so gestaltet werden, dass ein Nullwert ein gültiger Graph ist: etwas ohne Kanten und ohne Knoten. In diesem Fall müssen sich alle Funktionen, die einengraph *
Zeiger benötigen, mit diesem Wert befassen, da es sich bei der Darstellung von Diagrammen um einen korrekten Domänenwert handelt. Andererseitsgraph *
könnte a ein Zeiger auf ein containerähnliches Objekt sein, das niemals null ist, wenn wir einen Graphen halten. Ein Nullzeiger könnte uns dann mitteilen, dass "das Diagrammobjekt nicht vorhanden ist, wir es noch nicht zugeordnet haben oder es freigegeben haben, oder dass dies derzeit kein zugeordnetes Diagramm ist". Die letztere Verwendung von Zeigern ist ein kombinierter Boolescher Wert / Satellit: Wenn der Zeiger nicht null ist, wird "Ich habe dieses Schwesterobjekt" angezeigt und dieses Objekt bereitgestellt.Wir können einen Zeiger auf null setzen, auch wenn wir ein Objekt nicht freigeben, um einfach ein Objekt von einem anderen zu trennen:
quelle
Lassen Sie mich der Fuge noch eine Stimme hinzufügen.
Wie viele der anderen Antworten sage ich - prüfen Sie an dieser Stelle nicht; Es liegt in der Verantwortung des Anrufers. Aber ich habe eine Grundlage, auf der ich aufbauen kann, anstatt auf einfacher Zweckmäßigkeit (und C-Programmier-Arroganz).
Ich versuche Donald Knuths Grundsatz zu folgen, Programme so fragil wie möglich zu gestalten. Wenn irgendetwas schief geht, lassen Sie es groß abstürzen , und das Referenzieren eines Nullzeigers ist normalerweise eine gute Möglichkeit, dies zu tun. Die allgemeine Idee ist ein Absturz oder eine Endlosschleife ist weitaus besser als die Erstellung falscher Daten. Und es erregt die Aufmerksamkeit der Programmierer!
Das Referenzieren von Nullzeigern (insbesondere für große Datenstrukturen) führt jedoch nicht immer zu einem Absturz. Seufzer. Das ist richtig. Und hier fallen Asserts ins Spiel. Sie sind einfach, können Ihr Programm sofort zum Absturz bringen (und die Frage beantworten: "Was soll die Methode tun, wenn sie auf eine Null stößt?") Und können für verschiedene Situationen ein- und ausgeschaltet werden (ich empfehle) NICHT ausschalten, da es für Kunden besser ist, einen Absturz zu erleiden und eine kryptische Nachricht zu sehen, als schlechte Daten zu haben.
Das sind meine zwei Cent.
quelle
Ich überprüfe im Allgemeinen nur, wenn ein Zeiger zugewiesen ist. Dies ist im Allgemeinen das einzige Mal, dass ich tatsächlich etwas dagegen tun und möglicherweise wiederherstellen kann, wenn er ungültig ist.
Wenn ich zum Beispiel ein Handle für ein Fenster bekomme, überprüfe ich, ob es gleich null ist, und tue etwas gegen die Nullbedingung, aber ich werde nicht jedes Mal prüfen, ob es null ist Ich benutze den Zeiger in jeder Funktion, an die der Zeiger übergeben wird, sonst hätte ich Berge von doppeltem Fehlerbehandlungscode.
Funktionen wie können
graph_get_current_column_color
wahrscheinlich nichts Nützliches für Ihre Situation tun, wenn sie auf einen schlechten Zeiger stößt. Daher überlasse ich die Überprüfung auf NULL ihren Aufrufern.quelle
Ich würde sagen, dass es auf Folgendes ankommt:
CPU-Auslastung / Quotenzeiger ist NULL Jedes Mal, wenn Sie nach NULL suchen, dauert es eine Weile. Aus diesem Grund versuche ich, meine Prüfungen darauf zu beschränken, wo der Zeiger seinen Wert hätte ändern können.
Präventives System Wenn Ihr Code ausgeführt wird und eine andere Task ihn unterbrechen und möglicherweise den Wert ändern könnte, ist eine Überprüfung sinnvoll.
Eng gekoppelte Module Wenn das System eng gekoppelt ist, ist es sinnvoll, dass Sie mehr Prüfungen durchführen. Ich meine damit, dass es Datenstrukturen gibt, die von mehreren Modulen gemeinsam genutzt werden. Ein Modul könnte etwas unter einem anderen Modul ändern. In diesen Situationen ist es sinnvoll, öfter zu überprüfen.
Automatische Überprüfungen / Hardware-Unterstützung Als letztes muss berücksichtigt werden, ob die Hardware, auf der Sie ausgeführt werden, über einen Mechanismus verfügt, der nach NULL sucht. Insbesondere beziehe ich mich auf die Seitenfehlererkennung. Wenn Ihr System eine Seitenfehlererkennung hat, kann die CPU selbst nach NULL-Zugriffen suchen. Persönlich halte ich dies für den besten Mechanismus, da er immer ausgeführt wird und nicht darauf angewiesen ist, dass der Programmierer explizite Überprüfungen vornimmt. Es hat auch den Vorteil, dass der Overhead praktisch Null ist. Wenn dies verfügbar ist, empfehle ich es, das Debuggen ist etwas schwieriger, aber nicht allzu schwierig.
Um zu testen, ob es verfügbar ist, erstellen Sie ein Programm mit einem Zeiger. Stellen Sie den Zeiger auf 0 und versuchen Sie, ihn zu lesen / schreiben.
quelle
Meiner Meinung nach ist es eine gute Sache, Eingaben (dh Vor- / Nachbedingungen) zu validieren, um Programmierfehler zu erkennen, aber nur, wenn dies zu lauten und widerwärtigen, aufsehenerregenden Fehlern führt, die nicht ignoriert werden können.
assert
hat in der Regel diesen Effekt.Alles, was darunter leidet, kann ohne sorgfältig aufeinander abgestimmte Teams zum Albtraum werden. Und natürlich sind alle Teams im Idealfall sehr sorgfältig koordiniert und unter strengen Maßstäben vereinheitlicht, aber die meisten Umgebungen, in denen ich gearbeitet habe, haben das bei weitem nicht geschafft.
Nur als Beispiel habe ich mit einigen Kollegen zusammengearbeitet, die der Meinung waren, dass man religiös auf das Vorhandensein von Nullzeigern prüfen sollte, und deshalb eine Menge Code wie diesen gestreut haben:
... und manchmal einfach so, ohne dass ein Fehlercode zurückgegeben oder gesetzt wird. Und das in einer jahrzehntelangen Codebasis mit vielen erworbenen Plugins von Drittanbietern. Es war auch eine Codebasis, die mit vielen Fehlern behaftet war, und oft waren Fehler, die nur sehr schwer auf Grundursachen zurückzuführen waren, da sie dazu neigten, an Orten, die weit von der unmittelbaren Ursache des Problems entfernt waren, abgestürzt zu sein.
Und diese Praxis war einer der Gründe dafür. Es ist eine Verletzung einer festgelegten Vorbedingung der obigen
move_vertex
Funktion, einen Nullpunkt an sie zu übergeben, aber eine solche Funktion hat es einfach stillschweigend akzeptiert und nichts als Antwort darauf getan. In der Regel hat ein Plugin also einen Programmierfehler, der dazu führt, dass es null an diese Funktion weitergibt, sie nur nicht erkennt, nur viele Dinge später ausführt und das System irgendwann abbricht oder abstürzt.Das eigentliche Problem hierbei war jedoch die Unfähigkeit, dieses Problem leicht zu erkennen. Also habe ich einmal versucht zu sehen, was passieren würde, wenn ich den obigen analogen Code in einen verwandle
assert
:... und zu meinem Entsetzen stellte ich fest, dass diese Behauptung auch beim Starten der Anwendung nach links und rechts fehlschlug. Nachdem ich die ersten Anrufseiten repariert hatte, machte ich noch einige Dinge und bekam dann eine Schiffsladung mit mehr Behauptungsfehlern. Ich machte weiter, bis ich so viel Code geändert hatte, dass ich meine Änderungen rückgängig machte, weil sie zu aufdringlich geworden waren und diese Nullzeigerprüfung widerwillig beibehalten hatten, anstatt zu dokumentieren, dass die Funktion das Akzeptieren eines Nullscheitelpunkts zulässt.
Aber das ist die Gefahr, wenn auch im schlimmsten Fall, Verstöße gegen die Vor- / Nachbedingungen nicht leicht erkennbar zu machen. Sie können dann im Laufe der Jahre eine Schiffsladung Code ansammeln, die gegen diese Vor- / Nachbedingungen verstößt, während Sie unter dem Radar der Tests fliegen. Meiner Meinung nach können solche Nullzeigerprüfungen außerhalb eines offensichtlichen und abscheulichen Behauptungsfehlers tatsächlich weit, weit mehr schaden als nützen.
In Bezug auf die wesentliche Frage, wann Sie auf Null-Zeiger prüfen sollten, möchte ich großzügig behaupten, dass ein Programmiererfehler erkannt werden soll, ohne dass dies stumm und schwer zu erkennen ist. Wenn es sich nicht um einen Programmierfehler handelt und sich der Kontrolle des Programmierers entzogen hat, wie z. B. ein Speichermangel, ist es sinnvoll, nach Null zu suchen und die Fehlerbehandlung zu verwenden. Darüber hinaus ist es eine Entwurfsfrage und basiert darauf, was Ihre Funktionen als gültige Vor- / Nachbedingungen betrachten.
quelle
Eine Übung besteht darin, die Nullprüfung immer durchzuführen, es sei denn, Sie haben sie bereits geprüft. Wenn also eine Eingabe von Funktion A () an B () übergeben wird und A () den Zeiger bereits validiert hat und Sie sicher sind, dass B () nirgendwo anders aufgerufen wird, kann B () A () vertrauen bereinigte die Daten.
quelle
NULL
Prüfungen viel bewirken werden. Denken Sie darüber nach: JetztB()
prüftNULL
und ... macht was? Rückkehr-1
? Wenn der Anrufer nicht überprüftNULL
, welches Vertrauen können Sie haben, dass er den-1
Rückgabewert-Fall überhaupt bearbeiten wird?