4 häufige C-Programmierfehler - und 5 Tipps, um sie zu vermeiden

Nur wenige Programmiersprachen können mit C hinsichtlich Geschwindigkeit und Leistung auf Maschinenebene mithalten. Diese Aussage war vor 50 Jahren wahr und gilt bis heute. Es gibt jedoch einen Grund, warum Programmierer den Begriff „Fußwaffe“ geprägt haben, um die Art der Kraft von C zu beschreiben. Wenn Sie nicht aufpassen, kann C Ihnen die Zehen abblasen - oder die eines anderen.

Hier sind vier der häufigsten Fehler, die Sie mit C machen können, und fünf Schritte, die Sie unternehmen können, um sie zu verhindern.

Häufiger C-Fehler: Nicht mallocfreigegebener Speicher (oder mehr als einmal freigeben)

Dies ist einer der großen Fehler in C, von denen viele die Speicherverwaltung betreffen. Der zugewiesene Speicher (mit der malloc Funktion erledigt ) wird in C nicht automatisch entsorgt. Es ist die Aufgabe des Programmierers, diesen Speicher zu entsorgen, wenn er nicht mehr verwendet wird. Wenn Sie wiederholte Speicheranforderungen nicht freigeben, tritt ein Speicherverlust auf. Versuchen Sie, einen Speicherbereich zu verwenden, der bereits freigegeben wurde, und Ihr Programm stürzt ab - oder, schlimmer noch, humpelt und wird mit diesem Mechanismus anfällig für Angriffe.

Beachten Sie, dass ein Speicherleck nur Situationen beschreiben sollte , wo Speicher soll befreit werden, ist aber nicht. Wenn ein Programm weiterhin Speicher zuweist, weil der Speicher tatsächlich benötigt und für die Arbeit verwendet wird, ist die Verwendung des Speichers möglicherweise  ineffizient , aber genau genommen handelt es sich nicht um einen Verlust.

Häufiger C-Fehler: Lesen eines Arrays außerhalb der Grenzen

Hier haben wir noch einen der häufigsten und gefährlichsten Fehler in C. Ein Lesen nach dem Ende eines Arrays kann Mülldaten zurückgeben. Ein Schreibvorgang über die Grenzen eines Arrays hinaus kann den Status des Programms beschädigen oder vollständig zum Absturz bringen oder, was am schlimmsten ist, zu einem Angriffsvektor für Malware werden.

Warum muss der Programmierer die Grenzen eines Arrays überprüfen? In der offiziellen C-Spezifikation ist das Lesen oder Schreiben eines Arrays über seine Grenzen hinaus „undefiniertes Verhalten“, was bedeutet, dass die Spezifikation kein Mitspracherecht bei dem hat, was passieren soll. Der Compiler muss sich nicht einmal darüber beschweren.

C hat es lange Zeit vorgezogen, dem Programmierer auch auf eigenes Risiko Macht zu geben. Ein Lese- oder Schreibvorgang außerhalb der Grenzen wird normalerweise vom Compiler nicht abgefangen, es sei denn, Sie aktivieren speziell die Compileroptionen, um sich dagegen zu schützen. Darüber hinaus kann es durchaus möglich sein, die Grenze eines Arrays zur Laufzeit so zu überschreiten, dass selbst eine Compilerprüfung dies nicht verhindern kann.

Häufiger C-Fehler: Die Ergebnisse von werden nicht überprüft malloc

malloc und calloc (für Speicher vor Null) sind die C-Bibliotheksfunktionen, die vom Heap zugewiesenen Speicher vom System erhalten. Wenn sie keinen Speicher zuordnen können, wird ein Fehler generiert. In den Tagen, als Computer relativ wenig Speicher hatten, bestand eine gute Chance, dass ein Anruf mallocnicht erfolgreich war.

Auch wenn Computer heutzutage über Gigabyte RAM verfügen, besteht immer die Möglichkeit, dass mallocsie fehlschlagen, insbesondere unter hohem Speicherdruck oder bei gleichzeitiger Zuweisung großer Speicherplatten. Dies gilt insbesondere für C-Programme, die zuerst einen großen Speicherblock aus dem Betriebssystem „zuordnen“ und ihn dann für den eigenen Gebrauch aufteilen. Wenn diese erste Zuordnung fehlschlägt, weil sie zu groß ist, können Sie diese Ablehnung möglicherweise abfangen, die Zuordnung verkleinern und die Heuristiken für die Speichernutzung des Programms entsprechend anpassen. Wenn die Speicherzuweisung jedoch nicht abgefangen wird, kann das gesamte Programm aus dem Ruder laufen.

Häufiger C-Fehler: Verwendung void*für generische Zeiger auf den Speicher

Das Zeigen  void* auf das Gedächtnis ist eine alte Gewohnheit - und eine schlechte. Zeiger auf Speicher sollte immer sein char*, unsigned char*oder  uintptr_t*. Moderne C-Compiler-Suiten sollten uintptr_tals Teil von stdint.h

Bei einer dieser Beschriftungen wird deutlich, dass sich der Zeiger auf einen Speicherort in der Zusammenfassung bezieht und nicht auf einen undefinierten Objekttyp. Dies ist doppelt wichtig, wenn Sie Zeigermathematik durchführen. Mit  uintptr_t*und dergleichen sind das Größenelement, auf das gezeigt wird, und wie es verwendet wird, eindeutig. Mit void*nicht so sehr.

Vermeiden häufiger C-Fehler - 5 Tipps

Wie vermeiden Sie diese allzu häufigen Fehler, wenn Sie mit Speicher, Arrays und Zeigern in C arbeiten? Beachten Sie diese fünf Tipps. 

Strukturieren Sie C-Programme so, dass der Besitz für den Speicher klar bleibt

Wenn Sie gerade eine C-App starten, sollten Sie sich überlegen, wie Speicher zugewiesen und als einer der organisatorischen Grundsätze für das Programm freigegeben wird. Wenn unklar ist, wo oder unter welchen Umständen eine bestimmte Speicherzuordnung freigegeben wird, bitten Sie um Probleme. Machen Sie zusätzliche Anstrengungen, um den Speicherbesitz so klar wie möglich zu gestalten. Sie tun sich selbst (und zukünftigen Entwicklern) einen Gefallen.

Dies ist die Philosophie hinter Sprachen wie Rust. Rust macht es unmöglich, ein Programm zu schreiben, das ordnungsgemäß kompiliert wird, es sei denn, Sie drücken klar aus, wie der Speicher gehört und übertragen wird. C hat keine derartigen Einschränkungen, aber es ist ratsam, diese Philosophie nach Möglichkeit als Richtschnur zu verwenden.

Verwenden Sie C-Compileroptionen, die vor Speicherproblemen schützen

Viele der in der ersten Hälfte dieses Artikels beschriebenen Probleme können mithilfe strenger Compileroptionen gekennzeichnet werden. Neuere Ausgaben von gccbieten beispielsweise Tools wie AddressSanitizer („ASAN“) als Kompilierungsoption, um häufige Fehler bei der Speicherverwaltung zu überprüfen.

Seien Sie gewarnt, diese Tools erfassen nicht absolut alles. Sie sind Leitplanken; Sie greifen nicht nach dem Lenkrad, wenn Sie im Gelände fahren. Einige dieser Tools, wie z. B. ASAN, verursachen außerdem Kompilierungs- und Laufzeitkosten und sollten daher in Release-Builds vermieden werden.

Verwenden Sie Cppcheck oder Valgrind, um C-Code auf Speicherlecks zu analysieren

Wenn die Compiler selbst zu kurz kommen, schließen andere Tools die Lücke, insbesondere wenn es um die Analyse des Programmverhaltens zur Laufzeit geht.

Cppcheck führt eine statische Analyse des C-Quellcodes durch, um (unter anderem) nach häufigen Fehlern bei der Speicherverwaltung und undefinierten Verhaltensweisen zu suchen.

Valgrind bietet einen Cache mit Tools zum Erkennen von Speicher- und Threadfehlern beim Ausführen von C-Programmen. Dies ist weitaus leistungsfähiger als die Verwendung der Analyse zur Kompilierungszeit, da Sie Informationen über das Verhalten des Programms ableiten können, wenn es tatsächlich aktiv ist. Der Nachteil ist, dass das Programm mit einem Bruchteil seiner normalen Geschwindigkeit ausgeführt wird. Dies ist jedoch im Allgemeinen zum Testen in Ordnung.

Diese Werkzeuge sind keine Silberkugeln und fangen nicht alles. Aber sie arbeiten als Teil einer allgemeinen Verteidigungsstrategie gegen Missmanagement des Gedächtnisses in C.

Automatisieren Sie die C-Speicherverwaltung mit einem Garbage Collector

Da Speicherfehler eine auffällige Ursache für C-Probleme sind, gibt es eine einfache Lösung: Verwalten Sie den Speicher in C nicht manuell. Verwenden Sie einen Müllsammler. 

Ja, dies ist in C möglich. Mit dem Garehm Collector Boehm-Demers-Weiser können Sie C-Programmen eine automatische Speicherverwaltung hinzufügen. Bei einigen Programmen kann die Verwendung des Boehm-Kollektors sogar die Dinge beschleunigen. Es kann sogar als Lecksuchmechanismus verwendet werden.

Der Hauptnachteil des Böhm-Garbage-Collectors besteht darin, dass er den Standardspeicher nicht scannen oder freigeben kann malloc. Es verwendet eine eigene Zuordnungsfunktion und funktioniert nur mit Speicher, den Sie speziell damit zuweisen.

Verwenden Sie C nicht, wenn eine andere Sprache ausreicht

Einige Leute schreiben in C, weil sie es wirklich genießen und es fruchtbar finden. Im Großen und Ganzen ist es jedoch am besten, C nur dann zu verwenden, wenn Sie es müssen, und dann nur sparsam für die wenigen Situationen, in denen es wirklich die ideale Wahl ist.

Wenn Sie ein Projekt haben, bei dem die Ausführungsleistung hauptsächlich durch E / A- oder Festplattenzugriff eingeschränkt wird, wird es durch das Schreiben in C wahrscheinlich nicht schneller, und es wird wahrscheinlich nur fehleranfälliger und schwieriger pflegen. Das gleiche Programm könnte auch in Go oder Python geschrieben werden.

Ein anderer Ansatz besteht darin, C nur für die wirklich leistungsintensiven Teile der App und eine zuverlässigere, wenn auch langsamere Sprache für andere Teile zu verwenden. Auch hier kann Python zum Umschließen von C-Bibliotheken oder benutzerdefiniertem C-Code verwendet werden, was es zu einer guten Wahl für mehr Boilerplate-Komponenten wie die Behandlung von Befehlszeilenoptionen macht.