Lint – ein geschwätziger Pedant für C und C++

Es gibt ein Tool für C und C++ Programmierer, das unglaublich viele Fehler kennt, die Programmierern unterlaufen können: Lint. Keine Syntaxfehler, dafür haben Sie Ihren Compiler, sondern programmtechnische Fehler, die zu finden viel Zeit kosten kann. Wollen Sie effektiver arbeiten? Linten Sie ebenfalls.

Vor einiger Zeit verschaffte mir C++ ein weiteres graues Haar. Dabei hatte es am Anfang ganz einfach ausgesehen:

Ein Programm, das für Windows 3.1 geschrieben worden war, sollte auf 32 Bit portiert werden. Im Rahmen des betreffenden Projekts hatten geometrische Aspekte von Gebäuden eine zentrale Rolle gespielt. Hierfür hatten wir eine bereits vorhandene Vektorklasse eingesetzt, die die für Vektoren üblichen Operationen (Vektoraddition, Skalarmultiplikation) zur Verfügung stellte. Alles funktionierte.

Es handelte sich um eine Anwendung auf der Basis verschiedener Sprachen, und ich rechnete bei der Portierung mit Schwierigkeiten bei den Delphi- ebenso wie den FORTRAN-Anteilen - aber bei C++?

Mir stand ein böser Schock bevor. In Listing 1 sehen Sie eine für dieses Beispiel minimierte Version unserer Vektorklasse. Der Compiler meldete in Zeile 46 illegal structure operation - der neue Compiler, wohlgemerkt. Die C++ Anteile im Projekt waren teilweise noch mit Borland C++ 3.1 entwickelt und später – im Rahmen des Windows-Projekts - mit BC 4.5 übersetzt worden. Für die Portierung auf 32 Bit machte es natürlich Sinn, den aktuellen Compiler einzusetzen, zu der Zeit BC 5.0.

Illegal structure operation bei der Addition von Vektoren - wunderbar, das kam in unserem Programm natürlich beliebig häufig vor. Außerdem war diese Fehlermeldung überaus hilfreich - ich hatte nicht die geringste Vorstellung, was das Illegale sein sollte an einer Operation, die alle Compiler von 3.1 bis 4.5 nicht nur klaglos übersetzt hatten, sondern die auch funktionierte.

Welche Optionen gab es?

Lint rettete meinen Tag

Nach dem ersten Schock fiel mir auch ein Weg ein, vielleicht zu einer Lösung zu kommen: Das fragliche Programm durch Lint analysieren zu lassen. Tatsächlich: Lint lieferte die Meldung

                      _
     v0 = v1 + v2 * 2.0; // kompiliert nicht
     vector.cpp 46 Error 1058: Initializing a non-const reference typed
    'Vector &' with a non-lvalue 

wobei '_' die genaue Stelle in der Programmzeile kennzeichnet, auf die sich die Meldung bezieht.

Nein, verstanden habe ich sie zunächst nicht. Aber es gibt zu allen Meldungen von
Lint eine ausführliche Erklärung im Handbuch:

1058: Initializing a non-const reference typed 'Type' with a non-lvalue
A reference is normally initialized with an lvalue. If you attempt to initialize a reference with a non-lvalue, a temporary is created to serve as a surrogate lvalue. However, modifications made to the temporary will be lost. This was legal at one time and is now illegal. Make the reference a const if you can. ...

Bis ich das vollständig verstanden hatte, brauchte es eine Weile. Zunächst verstand ich aber einen Schlüsselsatz: This was legal at one time and is now illegal.

Das stellte meinen Glauben an die Qualität von Borland Compilern wieder her: Allem Anschein nach war es in Ordnung, daß die Version 4.5 den Code akzeptiert hatte, die Version 5.0 aber nicht. Die Standardisierung von C++ wurde erst Ende letzten Jahres abgeschlossen, und daher handelte es sich bis zu diesem Zeitpunkt gewissermaßen um einen „Standard in Bewegung", dem die Compiler-Hersteller notgedrungen folgen mußten.

Der nächste Schlüsselsatz war: Make the reference a const if you can. Eine const Referenz? Ich änderte Deklaration und Definition von Vector operator + (Vector &v) nach Vector operator + (const Vector &v) – Zeilen 12 und 33. Das war tatsächlich alles. Der Compiler war happy - und ich war es auch.

Danach dauerte es dann schon noch einen Moment, bis ich wirklich begriffen hatte, worin das eigentliche Problem lag:

Um den Ausdruck v1 + v2 * 2.0; zu bestimmen, muß der Compiler zunächst ein temporäres Objekt erzeugen, dem er das Skalarprodukt v2 * 2.0 zuweist. Dann addiert er v1 und diesen temporären Vektor und weist das Ergebnis v0 zu, woraufhin der temporäre Vektor wieder zerstört werden kann.

Diese temporäre, vom Compiler erzeugte Variable ist nun implizit const - das ist augenscheinlich die Änderung zu früheren Versionen des Standards. So wie die ursprüngliche Fassung von operator + formuliert war, erwartete er aber eine normale, modifizierbare Variable – auch wenn natürlich nie beabsichtigt war, daß der Operator einen seiner Operanden verändern sollte. Aber so war seine Definition, und das stellte den Compiler mit seinem temporären const Objekt vor ein Problem.

Fazit: Mit einer marginalen Änderung das Problem vollständig gelöst – sogar mein Verständnis von C++ war wieder ein kleines Stück gewachsen. Ohne Lint wäre es allerdings sehr, sehr mühsam geworden ...

Lint findet die Fusseln an Ihrem Programm

Lint-Programme sind alt - fast so alt, wie die Sprache C selbst. Der Name ist übrigens nett gewählt: „Lint" bedeutet „Fusseln" oder „Flusen". Bleibt man bei diesem Bild, dann sorgt Ihr Compiler lediglich dafür, daß Ihr Jacket (= Programm) keine Löcher (= Syntaxfehler) hat, bevor er Sie damit loslaufen läßt.

Lint wird hingegen erst eingesetzt, wenn der Compiler sein OK zu Ihrem Programm gegeben hat und untersucht Ihr Programm auf „Fusseln", die den guten Gesamteindruck trüben könnten. Mit „untersuchen" ist hier eine statische Analyse gemeint: Lint betrachtet nur den Source-Code. Es ist ein absoluter Experte, was Programmierfehler betrifft: es kennt – und findet – mehr Fehler, als Sie (hoffentlich) je machen werden und weist Sie hin auf

Vermutlich gibt es heute eine ganze Reihe unterschiedlicher Lint-Programme. Im Unix-Bereich mag ein Lint jeweils zum Lieferumfang gehören, und neulich erfuhr ich von einem Freeware-Lint 1. Genauer kenne ich nur ein Produkt (PC-Lint von Gimpel Software), und alle spezifischen Aussagen – z.B. Texte von Meldungen – beziehen sich auf dieses Programm.

Im folgenden möchte ich Ihnen anhand einiger Beispiele einen Eindruck geben, was Lint alles findet. Mehr als ein Eindruck ist dabei nicht möglich: Es gibt so viele mögliche Fehler, daß jede auch nur annähernd vollständige Darstellung den Rahmen eines Artikels sprengt. Ich habe daher zum einen den Schwerpunkt auf C++ Beispiele gelegt und mich zum anderen gewissermaßen auf interessante Fehler beschränkt – Fehler mit einem „Aha“-Effekt.

Das heißt keineswegs, daß andere Fehler weniger gravierend sind. Wenn Sie einen uninitialisierten Zeiger benutzen, kann das im Extremfall Ihren Rechner zum Absturz bringen – unter DOS war das auch häufig genug der Fall. Lint findet das sofort, während Sie Stunden oder gar Tage mit der Fehlersuche verbringen können. Auf einen solchen Fehler hingewiesen zu werden, ist zweifellos extrem wertvoll – nur gibt dieser Fall für einen Artikel nicht viel her.

Strings sind gleich – aber nur 3245 Mal

Das Beispiel in Listing 2 enthält eine Zeitbombe. Sie ist hier von harmloser Natur, aber in einem „richtigen" Programm würde sie vermutlich Schaden anrichten.

Dieser Vergleich liegt in einer while-Schleife, die dann verlassen wird, wenn der Vergleich fehlschlägt. Das hat etwas Absurdes, da die Strings konstant und augenscheinlich gleich sind: Allem Anschein nach wird das Programm in einer Endlosschleife festhängen.

Nun, das Leben hält für Programmierer viele Überraschungen bereit: das Verhalten dieses Programms mag eine weitere sein. Natürlich wird kaum ein Leser das Programm aus Listing 2 abtippen und dann erst weiter lesen - was ich etwas schade finde, da das Laufzeitverhalten dieses Programms manch einen verblüffen würde. (Übrigens finden Sie dieses und andere Beispiele auch zum Download auf meiner Web-Site 2).

Wenn Sie das Programm als DOS-Programm im „small memory“ Modell kompilieren und starten, dann endet Ihr Programm nach kurzer Zeit und gibt eine Meldung aus wie

3245 Mal bestand Gleichheit !!

wobei die angegebene Zahl bei Ihnen eine andere sein wird, auch wird sie sich bei jedem Programmlauf ändern 3.

Wie kann es angehen, daß die Strings Tausende von Malen gleich und auf einmal verschieden sind?

Was sagt Lint uns zu diesem Programm?

                  _
     return newStr;
     ret_str.c 13 Warning 604: Returning address of auto (newStr)

Natürlich, was für ein dummer Fehler. Wir können nicht die Adresse einer lokalen Variablen zurückgeben! Wem es noch nicht so klar ist, erfährt die Erklärung aus dem Text zu Meldung 604 aus dem Handbuch: Die Gültigkeit einer auto-Variablen ist nach dem return nicht garantiert.

... this is most likely an error.

- und ob.

Auto-Variablen - ein anderer Name für lokale Variablen - liegen auf dem Stack. In unserem Beispiel der String newStr. Kehrt getStr zurück, befindet sich newStr nun unterhalb des Stack-Pointers, er liegt im ungeschützten Bereich. Zwar verliert er nicht automatisch seinen Wert, aber er kann jederzeit überschrieben werden.

In dem obigen Programm geschieht dies offensichtlich nicht sofort - das Programm läuft eine gewisse Zeit. Warum es nicht endlos lange läuft? Unabhängig von Ihrem Programm geschieht in Ihrem Rechner auch im Hintergrund einiges, es gibt zumindest den zyklischen Timer-Interrupt, durch den die zugehörige Interrupt-Routine aktiviert wird. Auch sie benötigt Platz auf dem Stack - der Stack-Pointer wandert zeitweise wieder nach unten - und augenscheinlich überlebt newStr das nicht: Wenn das unterbrochene Programm wieder die Kontrolle erhält, schlägt der Vergleich der beiden Strings fehl, und das Programm wird beendet.

Selbstverständlich ist das ein Anfängerfehler und würde Ihnen nie passieren. Auch ich würde nie öffentlich zugeben, daß ich schon mal einen ähnlichen Fehler produziert habe:

Eine Pascal-Routine diente als Schnittstelle zwischen einer C++ Library und der in Delphi entwickelten Benutzerschnittstelle. Einen Augenblick nicht genügend nachgedacht – und schon gab die Pascal-Funktion die Adresse einer lokalen Variablen zurück. Nein, ich habe vergessen, wie lange es gedauert hat, diesen Fehler zu finden – leider gibt es für Pascal kein Lint.

Ist es da nicht sehr beruhigend, daß es ein Tool gibt, das Fehler dieser Art sofort findet? Fehler zu suchen, die nur manchmal auftreten, gehört zum Schlimmsten, was es bei der Software-Entwicklung gibt.

Warum bellt der Hund nicht?

Was sagen Sie zu dem Beispiel in Listing 3? Es gibt eine Basisklasse Animal mit einer virtuellen Funktion speak. In zwei abgeleiteten Klassen – Cat und Dog – wird eine spezielle Fassung dieser Funktion für die jeweilige Tierart implementiert. Dann wird einem Zeiger auf Animal ein Dog zugewiesen und die speak-Funktion aufgerufen.

Aber warum gibt das Programm „---“ statt „Wau-Wau“ aus? Müßte nicht die Funktion speak von Dog aktiviert werden? Ist das nicht gerade das Merkmal virtueller Funktionen und eine der zentralen Eigenschaften der Klassen in C++?

Im Prinzip ja, aber leider hat der Programmierer einen kleinen Fehler gemacht. Lint
meint dazu:

     { public: char *speak( short) { return "Wau-Wau"; } };
     dog.cpp 16 Warning 1411: Member with different signature hides
     virtual member Animal::speak(int) (line 9)

Die Erklärung zur Warnung 1411 macht dann deutlich, worin das Problem besteht: Die Funktion speak von Dog hat eine andere Signatur (Parameterliste) als speak in der Basisklasse Animal. Das führt dazu, daß die virtuelle Funktion speak eben nicht überschrieben wird – wie der Programmierer es beabsichtigt hatte – sondern daß sie lediglich verborgen wird.

Da in dem obenstehenden Programm mit einem Zeiger auf die Basisklasse gearbeitet wird und speak für die Klasse Dog nicht überschrieben wurde, kommt es hier zum Aufruf von speak der Basisklasse – mithin zur Ausgabe von „---“.

Noch ein Beispiel gefällig? Schauen Sie sich Listing 4 an: eine virtuelle Funktion f einer Basisklasse X gibt als Default „X“ aus, wenn sie ohne Parameter aufgerufen wird. Die Funktion f einer abgeleiteten Klasse Y gibt unter diesen Umständen „Y“ aus. Der Programmierer erzeugt ein Y, weist es einem Zeiger auf die Basisklasse zu und aktiviert f. Er erwartet, daß das Programm „Y“ ausgibt – schließlich hat er ein Y erzeugt und der Mechanismus der virtuellen Funktionen sorgt dafür, daß die Funktion f der Klasse Y aufgerufen wird. Na, würden Sie eine Wette auf „Y“ riskieren?

Im Kontext dieses Artikels vermutlich nicht - und Sie haben recht: Das Programm gibt „X“ aus.

Bevor der Programmierer nun an sich oder am Compiler oder an C++ (ver-) zweifelt, befragt er Lint und erhält die Antwort

     default.cpp 10 Info 1735: Virtual function 'X::f(char)' has default
     parameter

sowie die gleiche Meldung noch einmal für Y::f.

„Na und?“ ist er geneigt zu sagen. „Das weiß ich. Ich habe es schließlich programmiert.“ Aber es gibt auch noch die Erklärung im Handbuch, und dort erfährt er dann zu seiner Verblüffung folgendes:

Zwar ist es korrekt, daß beim Aufruf von f durch einen Zeiger auf die Basisklasse der aktuelle (dynamische) Typ des Objekts bestimmt, welche Funktion ausgewählt wird: wie erwartet ist das hier Y::f. Aber der Default-Wert wird durch den nominellen (statischen) Typ festgelegt, und da p ein Zeiger auf die Basisklasse ist, wird deren Default-Wert benutzt.

Konsequenz: Wenn eine virtuelle Funktion einen Default-Parameter hat, dann

Da ist es vielleicht einfacher, bei virtuellen Funktionen ganz auf Default-Parameter zu verzichten.

Hätten Sie’s gewußt?

Von dieser Art Beispiele ließen sich nahezu unbegrenzt viele konstruieren, da C++ unglaublich viele Facetten für den Programmierer bereithält, die es leider alle zu kennen und zu beachten gilt. Wenn Sie es wünschen, bietet Lint Ihnen hierzu eine erweiterte Diagnose an: Es kann Ihr Programm gezielt auf Verletzung der Regeln untersuchen, die mehrere Autoren für C++ aufgestellt haben 4.

Wenn Sie (noch) nicht zu den „Gurus“ gehören, ist das sicher keine schlechte Art des Umgangs mit C++:

So ähnlich habe ich übrigens seinerzeit C gelernt: Erst mit dem Compiler gekämpft, bis wir der gleichen Ansicht über meinen Code waren – und mir dann anschließend von Lint sagen lassen, welche Fallstricke dort noch immer lauerten. Ich bin noch heute überzeugt davon, daß ich durch diese Vorgehensweise sehr viel schneller mit den Feinheiten von C vertraut wurde, als wenn ich sie einen nach dem anderen im Rahmen einer Fehlersuche selbst hätte herausfinden müssen.

Ich wechselte dann bald zu C++ und mußte mich von Lint verabschieden, denn die damalige Version unterstützte C++ noch nicht. Aber seit einigen Jahren kennt Lint auch C++ - es ist ein richtiger Experte, wie Sie an den Beispielen gesehen haben - und nach der im Eingangsbeispiel geschilderten Erfahrung gehört es wieder zu meinen regelmäßig genutzten Werkzeugen.

Lint ist nicht perfekt - aber der Hersteller arbeitet daran

Findet Lint alle Fehler? Nein, leider nicht. Ich vermute, daß ihm mittlerweile nahezu alle Fehler bekannt sind, die man in C machen kann – aber es gibt schließlich noch C++.

Betrachten Sie das Beispiel in Listing 5: Eine Klasse Employee soll Daten von Angestellten verwalten. Der Programmierer hat sorgfältig gearbeitet: Den dynamischen Speicher, den er im Constructor anfordert (via strdup), gibt er im Destructor wieder frei.

Trotzdem geht irgend etwas gewaltig schief, wie Sie an der vom Programm erzeugten Ausgabe sehen können. Ist der Constructor verantwortlich? cout wird doch wohl keinen Seiteneffekt haben, der den übergebenen String zerstört? Wer weiß Rat?

An dieser Stelle muß Lint – noch – passen. Erst, wenn man es mit der sensibelsten Einstellung startete – was man normalerweise vermeidet, Lint produziert auch in der „normalen“ Einstellung mehr als genug Meldungen – gab es einen kleinen Hinweis, mit dem man vielleicht dem eigentlichen Problem auf die Spur kommen könnte. (Falls es Sie interessiert, worin das Problem besteht: Unter (2) finden Sie eine komplette Darstellung.)

Aber Lint wird kontinuierlich weiter entwickelt. Es ist im Laufe der Jahre zu einem wirklich bemerkenswerten Experten in C geworden, und ich habe keinen Zweifel daran, daß für C++ das gleiche geschehen wird: Wie sie an den obigen Beispielen gesehen haben, ist Lint bereits mit sehr vielen Feinheiten von C++ vertraut. Wenn die neueren Features von C++ stärker zum Einsatz kommen (denken Sie an STL), entstehen damit auch neue Fehlerquellen – und Lint wird um die Erkennung auch dieser Fehler erweitert werden.

Auf der Web-Site des Herstellers (s.o.) gibt es kostenlose Patch-Files, mit denen Sie Ihre Version auf den neuesten Stand bringen können. Außerdem ist er sehr kooperativ:

Der Einsatz von Lint zur Analyse von Programmen, die mit dem Builder von Borland entwickelt werden und die die STL (Standard Template Library) benutzen, war mühsam, da Builder und Lint unter bestimmten Umständen lange Dateinamen unterschiedlich behandelten (für Einzelheiten s. (2)).

Ich habe daraufhin die Einführung einer zusätzlichen Option angeregt, die dieses Problem lösen würde, und – tatsächlich – seit kurzem gibt es sie als Option ftr. Ich bin daher sicher, daß Lint auch den in Listing 5 enthaltenen Fehler in Kürze erkennen wird.

Warum sind nicht alle Besitzer von Lint glückliche Menschen?

Sind jetzt alle Programmierer, die Lint zur Verfügung haben, glückliche Menschen? Nun, wohl nicht ganz, wenn die Aussage eines Software-Abteilungsleiters repräsentativ ist. Er meinte: „Ja, wir haben Lint, auch in der aktuellen Version, aber wir setzen es nur im Notfall ein."

Wir haben das Thema nicht weiter vertieft, aber ich glaube verstanden zu haben, was er meinte. Lint ist ein wertvolles Tool, und die Beispiele haben Ihnen vielleicht gezeigt, daß es eigentlich unverzichtbar ist. Aber man muß gleichwohl zugeben, daß das Arbeiten mit Lint manchmal wenig erfreulich ist – wenn man nicht richtig damit umgeht.

Lint ist das, was man bei einem Menschen einen Pedanten nennen würde, einen Besserwisser, und einen geschwätzigen obendrein. Jemand, der prinzipiell an allem und jedem herummäkelt. So kann es einem vorkommen, wenn man durch Lint ein korrektes, funktionierendes Programm analysieren läßt.

Lint moniert alles, was theoretisch ein Problem bedeuten könnte - und in C können viele Konstruktionen, die in der Praxis angewandt werden und die auch funktionieren, theoretisch ein Problem sein:

Das ist tatsächlich das große Problem mit Lint: Man ertrinkt buchstäblich in Meldungen: 40 - 60 Meldungen für ein mittelgroßes Source-File sind nichts Besonderes. Neben wirklichen Fehlern und ernsthaften Hinweisen, denen man unter allen Umständen nachgehen sollte, gibt es eine Unmenge von Ballast. Nachrichtentechniker würden sagen: „Das Signal geht im Rauschen unter."

Wenn es nun nur darum ginge, diese Meldungen einmalig durchzugehen und sich zu vergewissern, daß sie auf keine wirklichen Probleme hinweisen, wäre es sicher erträglich. Aber Sie werden nicht umhin kommen, die ganze Prozedur mehrfach zu wiederholen – wenn Sie dem Modul weitere Funktionen hinzugefügt haben oder wenn ein weiteres Problem auftritt – und Sie wollen mit Sicherheit nicht jedesmal Hunderte von Meldungen von Neuem ansehen.

Hunderte? Leider ja, denn bisher war implizit nur die Rede davon, daß Lint eine Datei untersucht. Lint kann aber auch ein Projekt insgesamt analysieren, d.h. es betrachtet die Interaktionen und Abhängigkeiten über die Grenzen eines Source-Files hinweg. Diese Möglichkeit sollten Sie auch unbedingt nutzen, da bestimmte Fehler nur so gefunden werden können.

Extrem wichtige Hinweise sind z.B.

Außerdem findet Lint Dinge, die nicht unbedingt problematisch sind, die Ihnen aber helfen, Ihr Programm zu vereinfachen:

Leider nimmt der Umfang der Meldungen dadurch noch einmal erheblich zu. Neben wichtigen Informationen werden Sie vieles erfahren, was Sie weniger interessieren wird.

Doch, man kann mit Lint leben

Bevor jetzt Ihre Enttäuschung zu groß wird, daß es ein so eindeutig nützliches Tool wie Lint gibt, das aber keiner einsetzen mag, weil seine Benutzung zu lästig ist: Es gibt Möglichkeiten, mit Lint zu leben, und darum soll es im letzten Teil dieses Artikels gehen.

Im wesentlichen wird es immer darauf ankommen, die Meldungsflut von Lint einzudämmen, ohne daß wirklich wichtige Hinweise unterdrückt werden. Glücklicherweise ist Lint in vielfältiger Hinsicht konfigurierbar: Sie können es anpassen

Die globale Konfiguration von Lint geschieht über sog. Lint-Files (Endung LNT). Ich nenne sie global, weil die in diesen Files enthaltenen Anweisungen für alle Ihre Source-Files gelten. Demgegenüber geschieht die lokale Anpassung direkt in Ihren Source-Files durch sog. Lint-Kommentare.

Eine der wichtigsten Anpassungen passiert bereits bei der Installation von Lint: Die Anpassung an Ihren Compiler. Angenommen, Sie geben an, daß Sie mit Borland 5.0 arbeiten, 32 Bit Windows-Programme entwickeln und mit der MFC-Bibliothek arbeiten. Ferner wollen Sie, daß Lint Ihre Programme auf die Einhaltung der von Scott Meyers formulierten Regeln überprüft. Dann wird bei der Installation ein Lint-File erzeugt, wie es in Listing 6 dargestellt ist.

Sie sehen, daß dort im wesentlichen andere Lint-Files aufgeführt sind, und die sind erstaunlich wichtig: Sie enthalten nämlich Anweisungen, welche Meldungen beim Lesen der Library Header-Files (Include-Files, die Sie mit dem Compiler zusammen erhalten) unterdrückt werden sollen. Es hat den Anschein, daß selbst die Entwickler der Compiler nicht so sauber programmierten, als daß Lint zufrieden wäre. So würde allein ein #include <windows.h> bereits mehrere hundert Meldungen verursachen, wenn sie nicht durch Einbeziehen des entsprechenden Lint-Files gezielt verhindert würden.

Damit haben Sie Lint an den Compiler angepaßt.

Die nächste Ebene der Anpassung betrifft Ihren eigenen Programmierstil. Zu manchen Meldungen gibt das Handbuch explizit den Hinweis: Wenn dies Ihr Stil ist, unterdrücken Sie diese Meldung. Beispielsweise führt ein Ausdruck 1[a], wobei a ein Array ist, zur Meldung 409. Aber wenn Sie Array-Zugriffe gern in dieser Form schreiben (anstelle von a[1]), möchten Sie die Meldung 409 vielleicht definitiv ausschalten. Das Handbuch schlägt hierfür das File OPTIONS.LNT vor, das deshalb auch in das bei der Installation erzeugte Lint-File mit aufgenommen wird (vgl. Listing 6).

Ich benutze dieses File vorzugsweise zum Unterdrücken der Meldung

534: Ignoring return value of xxx

wobei xxx die betreffende Funktion bezeichnet. Es gibt viele Funktionen der Standard Library von C, deren Rückgabewert man beruhigt ignorieren kann: denken Sie an printf oder viele der String-Funktionen. Der Wert, den strupr liefert, wird mich nie interessieren – gleichgültig, mit welchem Compiler oder an welchem Projekt ich arbeite. Deshalb ist OPTIONS.LNT hierfür die richtige Stelle.

Die folgende Ebene ist die der projekt-abhängigen Anpassung: Wenn Lint für eines Ihrer Header-Files Meldungen produziert, dann erhalten sie diese für jedes Ihrer Source-Files, das den betreffenden Header einbindet. Hier habe ich gute Erfahrungen mit einem Lint-File PROJ.LNT gemacht, das spezifisch für das jeweilige Projekt ist.

Angenommen, ich hätte eine Klasse X ohne einen Default-Constructor definiert. Dann meldet Lint sich mit

     Info 1712: default constructor not defined for class 'X‘

Wie immer ist der Hinweis prinzipiell berechtigt, denn wenn ich jemals einen Array der Objekte X erzeugen wollte, wird dafür ein Default-Constructor gebraucht – und ob der durch den Compiler erzeugte dann dafür ausreicht, sollte ich zumindest überprüfen.

Diese Meldung erhalte ich für jedes Source-File, das X.H per #include einbindet – vermutlich also sehr viel öfter, als ich will.

Ist X nun aber eine Klasse, bei der Arrays von Objekten aus inhaltlichen Gründen ausgeschlossen sind, dann ist Meldung 1712 überflüssig – für diese Klasse X – und ich kann sie beruhigt unterdrücken. Deshalb nehme ich in mein File PROJ.LNT (s. Listing 7) die Anweisung auf

     -esym(1712,X)

Damit wird die Meldung 1712 unterdrückt, aber nur für das Symbol X – für alle anderen Klassen erhielte ich ggf. immer noch die entsprechende Meldung und könnte überprüfen, ob Handlungsbedarf besteht oder nicht.

Jedes Source-File im Projekt wird dann durch den Aufruf

    Lint ... bc5.lnt proj.lnt xyz.cpp

von Lint überprüft (es sind nicht alle Optionen angegeben). Dabei ist BC5.LNT das oben erwähnte LNT-File, das die compiler-spezifischen Anweisungen enthält.

Hiermit habe ich erreicht, daß für alle Source-Files im Projekt, die die Klasse X benutzen, keine Meldung 1712 für X mehr erscheint – wieder etwas vom Rauschen eliminiert.

Ein Großteil der Meldungen von Lint bezieht sich aber auf einzelne Anweisungen in Ihrem Source-Code, bei denen Sie nur aus dem aktuellen Zusammenhang heraus entscheiden können, ob Sie auf ein wirkliches Problem hingewiesen wurden, oder ob die betreffende Stelle unkritisch ist. Daher lassen sich diese Meldungen nicht auf einer übergeordneten Ebene ausschalten (zumindest rate ich davon ab), sondern Sie setzen hierfür die sog. Lint-Kommentare ein: Anweisungen an Lint, die Sie in Ihren Source-Code aufnehmen, und die die Form von Kommentaren haben, damit der Compiler nicht durcheinander kommt.

Es gibt mehrere Möglichkeiten, am praktischsten ist die Form, die gezielt eine bestimmte Meldung an einer bestimmten Stelle ausschaltet: die Anweisung !exxx. Sie bewirkt, daß die Meldung xxx einmalig für die betreffende Zeile unterdrückt wird.

Listing 8 enthält zwei Beispiele:

Der Aufruf von strcpy (Zeile 18) erhielt ursprünglich die Meldung

     Warning 668: Possibly passing a null pointer to function
     strcpy(char *, const char *), arg. no. 1

Ein durchaus wichtiger Hinweis, denn strchr kann auch 0 liefern (wenn nämlich das gesuchte Zeichen nicht im String vorhanden ist), und p wird vor seiner Benutzung im Aufruf von strcpy nicht überprüft. Hier kann nun allerdings nichts passieren, da das abschließende 0-Zeichen immer gefunden wird. Also unterdrücke ich 668 für diesen Aufruf von strcpy. Da ich mir kaum merken werde, welche Bedeutung die Meldung 668 hat, erkläre ich in einem kurzen Kommentar, worauf sie sich bezieht und warum es hier in Ordnung ist, sie auszuschalten 6.

Die Anweisung while ( TRUE ) (Zeile 20) lieferte die Meldung

     Warning 506: Constant value Boolean

Hier genügt es mir, die Meldung zu unterdrücken – die Situation ist so offensichtlich, daß ich an dieser Stelle keinen erklärenden Kommentar brauche.

Von allen Maßnahmen zur Steigerung der Produktivität ist keine effektiver als der regelmäßige Einsatz von Lint

Natürlich bringt das zunächst Arbeit mit sich.

Unglaublich viel Arbeit, wenn Sie erst gegen Projektende damit anfangen – oder wenn mitten im Projekt ein mysteriöses Problem auftaucht. Dann müssen Sie Lint auf einen Schlag auf alle Ihre Source-Files loslassen – und ich garantiere Ihnen, Sie werden unter Meldungen begraben.

Erinnern Sie sich an die Aussage „... wird nur im Notfall eingesetzt"? Vermutlich handelt man dort nach dieser Strategie. Dann muß allerdings wahrlich ein Notfall vorliegen - schiere Verzweiflung aller Beteiligten - damit der Programmierer die Zeit erhält, Lint auf das gesamte Projekt anzusetzen.

Aber Sie müssen es nicht soweit kommen lassen. Es ist wenig zusätzliche Arbeit erforderlich, wenn Sie Lint kontinuierlich auf Modulebene einsetzen – wahrscheinlich gewinnen Sie sogar Zeit.

Skeptisch? Denken Sie doch einmal an die Art und Weise, wie Sie Ihre Programme kommentieren:

Vermutlich waren Sie nie der Ansicht, daß das Schreiben dieser Kommentare vergeudete Zeit darstellt – weshalb auch? Ohne diese Erläuterungen hätten selbst Sie vermutlich schon nach kurzer Zeit Schwierigkeiten, Ihre eigenen Programme nachzuvollziehen.

Das Kommentieren ist aber nur ein einzelner – kleiner – Arbeitsschritt bei der Erstellung eines Moduls. Insgesamt gehören dazu mindestens

Wenn Sie es so betrachten, werden Sie mir sicher zustimmen, daß der zeitliche Aufwand zur Erstellung Ihrer Kommentare bezogen auf die insgesamt erforderliche Entwicklungszeit überhaupt nicht ins Gewicht fällt.

Die Integration von Lint in Ihre tägliche Arbeit könnte nun so aussehen 7:

Welcher zusätzliche Aufwand entsteht hierbei?

Insgesamt dürfte der zusätzliche Aufwand höchstens dem entsprechen, den Sie für die Kommentierung Ihrer Programme schon immer akzeptiert haben. Die Zeitersparnis im Fehlerfall bedeutet hingegen einen echten Gewinn.

In jedem Fall werden Sie als Endergebnis stabilere und sicherere C bzw. C++ Programme produzieren als früher. Reden Sie mit Ihrem Management. Ich bin überzeugt davon, daß keine andere Maßnahme zur Steigerung der Produktivität ähnlich viel bewirken kann wie der regelmäßige Einsatz von Lint.

 


Fußnoten:

1 www.sds.lcs.mit.edu/lcLint/index.html
2 www.ratiosoft.com: u.a. noch mehr Beispiele zu Lint, ein beispielhaftes makefile, etc.
3 Wenn sich mit Ihrem Compiler das Verhalten des Programms nicht reproduzieren läßt, ändern Sie die Größe von newStr: Er muß größer sein als die Länge von „Hello world“ – aber das Laufzeitverhalten Ihres Programms hängt entscheidend davon ab, um wieviel größer er ist.
4 Diese Bücher bzw. Artikel sind
Meyers, Scott, Effective C++, Addison-Wesley, Reading Mass., 1992
Meyers, Scott, More Effective C++, Addison-Wesley, Reading Mass., 1996
Dan Saks in seiner Serie "C++ Gotchas" im C/C++ User’s Journal
5 Seit neuestem gibt es beide Bücher auf einer CD-ROM: Scott Meyers, Effective C++ CD, Addison Wesley, 1999, ISBN 020 1310 155
6 Zugegeben, das Beispiel ist konstruiert: Im wirklichen Leben würde man sicher strcat einsetzen.
7 Es lohnt sich unbedingt, bei der Benutzung von Lint mit make und einem makefile zu arbeiten. Sie finden hier ein Beispiel, das Sie leicht anpassen können.
8 Falls Sie nur das für ein theoretisches Problem halten: Es scheint auch im "wirklichen Leben" vorzukommen. Falls Sie Zugriff auf das C/C++ User's Journal haben, schauen Sie in der Ausgabe vom Okt. 99 in den Artikel "Singleton Creation the Thread-safe Way". Sein Ausgangspunkt ist genau das Problem der unterschiedlichen Reihenfolge bei der Initialisierung. Der Autor erwähnt nicht, ob er diesen Fehler durch den Einsatz von Lint gefunden hat, oder wieviel Zeit er zum Finden benötigte. Schwerpunkt seines Artikels ist - nachdem er den Fehler lokalisiert hat - die Lösung für diese spezielle Situation: Einsatz eines Singleton in einer Multithreaded-Umgebung.

 

 

 

 


 

Listings

Listing 1: Der Compiler läßt Sie allein

  1  // VECTOR.CPP : Vereinfachte 2 dimensionale Vektoren
  2
  3  // Klassen-Definition
  4  class Vector
  5  {
  6    private:
  7      double coords[2];
  8    public:
  9      Vector( double, double);
 10
 11      Vector  operator *  ( double d);
 12      Vector  operator +  ( Vector &v);
 13
 14      // restliche Operatoren / Funktionen entfernt
 15  };
 16
 17
 18  // Konstruktor
 19  Vector::Vector( double x, double y)
 20  {
 21    coords[0]=x;
 22    coords[1]=y;
 23  }
 24
 25
 26  // Operatoren
 27  Vector Vector::operator * ( double d)
 28  {
 29    return Vector( coords[0] * d, coords[1] * d);
 30  }
 31
 32
 33  Vector Vector::operator + ( Vector &v)
 34  {
 35    return Vector(coords[0] + v.coords[0], coords[1] + v.coords[1]);
 36  }
 37
 38
 39  // Testprogramm
 40  int main ( void)
 41  {
 42    Vector v0( 0.0, 0.0), v1( 1.0, 1.0), v2( 2.0, 2.0);
 43
 44    v0 = v1 + v2;             // ok
 45    v0 = v2 * 2.0;            // ok
 46    v0 = v1 + v2 * 2.0;       // kompiliert nicht
 47
 48    return 0;
 49  }
 50

Lint meldet zu oben stehendem Programm:
                    _
     v0 = v1 + v2 * 2.0;       // kompiliert nicht
   vector.cpp  46  Error 1058: Initializing a non-const reference typed
      'Vector &' with a non-lvalue

Der Compiler meldet hier nur (wie hilfreich):
Error vector.cpp 46: Illegal structure operation in function main()

Zurück zum Artikel

 

Listing 2: In diesem Programm lauert eine Zeitbombe

  1  /*
  2   * File RET_STR.C: Zeiger auf einen String übergeben
  3   */
  4
  5  #include <string.h>
  6  #include <stdio.h>
  7
  8  char orgStr[] = "Hello World";
  9
 10  char *getStr( void)
 11  {
 12     char newStr[20] = "Hello World";
 13     return newStr;
 14  }
 15
 16  int main( void)
 17  {
 18     long l = 0;
 19     while ( strcmp( orgStr, getStr()) == 0 )
 20        l++;
 21     printf( "%ld Mal bestand Gleichheit !!\n", l);
 22     return 0;
 23  }
 24

Lint meldet zu oben stehendem Programm:
                _
   return newStr;
ret_str.c  13  Warning 604: Returning address of auto (newStr)

Zurück zum Artikel

 

Listing 3: Wissen Sie, warum der Hund nicht bellt?

  1  //
  2  // File DOG.CPP
  3  //
  4
  5  #include <iostream.h>
  6
  7  class Animal
  8  { public:
  9    virtual char *speak( int) { return "---"; }
 10  };
 11
 12  class Cat : public Animal
 13  { public: char *speak( int) { return "Miau"; } };
 14
 15  class Dog : public Animal
 16  { public: char *speak( short) { return "Wau-Wau"; } };
 17
 18  int main()
 19  { Animal *p = new Dog; cout << p->speak( 0); return 0; }

Lint meldet zu oben stehendem Programm:

{ public: char *speak( short) { return "Wau-Wau"; } };
dog.cpp  16  Warning 1411: Member with different signature hides
    virtual member Animal::speak(int) (line 9)

Zurück zum Artikel

 

Listing 4: Default-Parameter können ihre Tücken haben ...

  1  //
  2  // File DEFAULT.CPP
  3  //
  4
  5  #include <iostream.h>
  6
  7  class X
  8  {
  9     public:
 10     virtual void f( char c = 'X') { cout << c; }
 11  };
 12
 13  class Y : public X
 14  {
 15     public:
 16     virtual void f( char c = 'Y'; ) { cout << c; }
 17  };
 18
 19  void main( void)
 20  {
 21     X *p = "new" Y;
 22     p->f();
 23  }
 24

Lint meldet zu oben stehendem Programm:
                                 _
   virtual void f( char c = 'X') { cout << c; }
default.cpp 10 Info 1735: Virtual function 'X::f(char)' has
   default parameter

                                 _
   virtual void f( char c = 'Y'; ) { cout << c; }
default.cpp 16 Info 1735: Virtual function 'Y::f(char)' has
   default parameter

Zurück zum Artikel

 

Listing 5: Daten eines Employee werden durch Ausgabe zerstört ???

  1  //
  2  // File EMPLOY.CPP: Ausgabe via 'cout' zerstört String ?
  3  //
  4
  5  #include <iostream.h>
  6  #include <stdlib.h>
  7  #include <string.h>
  8
  9  // Definition der Klasse 'Employee', nur das Notwendigste
 10  class Employee {
 11  private:
 12     char *name;
 13     int id;
 14  public:
 15     Employee( const char *n, int no) : id( no)
 16                                { name = strdup( n); }
 17     ~Employee( void)           { free( name); }
 18     const char *GetName( void) { return name; }
 19     int GetId( void)           { return id; }
 20  };
 21
 22  //
 23  // Ein Mini-Programm zum Testen
 24  //
 25  void showEmpl( Employee empl)
 26  {
 27     cout << empl.GetName() << ": " << empl.GetId() << endl;
 28  }
 29
 30  int main( void)
 31  {
 32     // einen Angestellten definieren
 33     Employee empl1( "Hans Mayer", 12345);
 34
 35     // ... und seine Daten anzeigen
 36     cout << "Ein Angestellter:" << endl;
 37     showEmpl( empl1);
 38
 39     // einen zweiten definieren
 40     Employee empl2( "Horst Schulze", 23456);
 41
 42     // ... und die Daten von beiden anzeigen
 43     cout << endl << "Zwei Angestellte:" << endl;
 44     showEmpl( empl1); // Ausgabe ergibt "Schrott"
 45     showEmpl( empl2);
 46
 47     return 0;
 48  }

 Das Programm erzeugt folgende Ausgabe:
    Ein Angestellter:
    Hans Mayer: 12345
    Zwei Angestellte:
    Horst Schulze: 12345
    ¨@: 23456

Zurück zum Artikel

 

Listing 6: Ein LNT-File zur Anpassung an Compiler und Plattform

//  Borland C, C++ 5.xx, -d__FLAT__ -u__SMALL__ -si4 -sp4,
//  Microsoft's Foundation Class library,
//  Scott Meyers (Effective C++),
//  Standard lint options

au-sm.lnt
co-bc5.lnt
lib-mfc.lnt
options.lnt  -d__FLAT__ -u__SMALL__ -si4 -sp4

Zurück zum Artikel

 

Listing 7: Ein LNT-File für projektspezifische Angaben

//
// File PROJ.LNT: Spezielle Anweisungen an Lint
//

   ...

// 1712: default constructor not defined for class xx
-esym(1712,S1PropHdl,LinkHdl,TreeHdl,KWHdl,UEArtHdl,UEFacade)

// 1725: class member 'xx' is a reference: ok for 'storage'
-esym(1725,S1PropHdl::strg,LinkHdl::strg,UEBaseHdl::strg)

// 1732 (1733): new in constructor for class x which has no
//              assignment operator (copy constructor)
-esym(1732,LinkHdl,KBHdl,UEFacade)
-esym(1733,LinkHdl,KBHdl,UEFacade)

-e1904            // old style C comment

// 1932: Base class x is not abstract - so what
-esym(1932,UEBase,LinkHdl,UEBaseHdl)

Zurück zum Artikel

 

Listing 8: LINT-Kommentare: Punktgenaue Unterdrückung einzelner Meldungen

  1  //
  2  // File LINT_CMT.C: Beispiel für "Lint-Kommentare"
  3  //
  4
  5  #include <string.h>
  6
  7  #define FALSE  0
  8  #define TRUE   ~FALSE
  9
 10  int main( void)
 11  {
 12     char *p, s[20] = "Hello ";
 13
 14     p = strchr( s, '\0');
 15
 16     // 668: Possibly passing a null pointer to function
 17     // hier ok: '\0' wird gefunden ==> p != 0
 18     strcat( p, "world");    //lint !e668
 19
 20     while ( TRUE ) {        //lint !e506
 21     }
 22
 23     return 0;
 24  }

Zurück zum Artikel

Home   Artikel-Übersicht   Top  

Copyright © Helmut Giese, email: hgiese@ratiosoft.com
Parkstr. 41, 34119 Kassel, Tel.: 0561 - 766 59 50, Fax: 0561 - 766 59 51
Königstor 59, 34119 Kassel, Tel.: 0561 - 739 35 30, Fax: 0561 - 739 35 31

Web Design von Bianca Engler:   email    internet