Kapitel 1. Richte dich für eine einfache Zusammenstellung ein

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Pass auf, Schatz, denn ich benutze Technologie.

Iggy Pop, "Search and Destroy"

Die C-Standardbibliothek reicht einfach nicht aus, um ernsthafte Arbeit zu erledigen.

Stattdessen hat sich das C-Ökosystem über den Standard hinaus ausgedehnt, was bedeutet, dass du wissen musst, wie du Funktionen aus gängigen, aber nicht zum ISO-Standard gehörenden Bibliotheken einfach aufrufen kannst, wenn du über die Lehrbuchübungen hinauskommen willst. Wenn du mit einer XML-Datei, einem JPEG-Bild oder einer TIFF-Datei arbeiten willst, brauchst du libxml, libjpeg oder libtiff, die alle frei erhältlich, aber nicht Teil des Standards sind. Leider ist das der Punkt, an dem die meisten Lehrbücher aufhören und dich allein lassen. Deshalb gibt es C-Verächter, die selbstgefällige Dinge sagen wie "C ist 40 Jahre alt, also musst du alles von Grund auf neu schreiben" - siehaben nie herausgefunden, wie man mit einer Bibliothek verknüpft.

Hier ist die Tagesordnung für das Kapitel:

Richte die erforderlichen Werkzeuge ein
Das ist viel einfacher als in den dunklen Tagen, als du jedes einzelne Bauteil zusammensuchen musstest. Du kannst ein komplettes System mit allem Drum und Dran in vielleicht 10 oder 15 Minuten einrichten (plus die Zeit für den Download, um so viel gutes Material zu laden).
Kompiliere ein C-Programm
Ja, du weißt, wie man das macht, aber wir brauchen ein Setup, das Haken für die Bibliotheken und ihre Standorte hat; einfach cc myfile.c reicht nicht mehr aus. Make ist so ziemlich das einfachste System, um das Kompilieren von Programmen zu erleichtern, daher bietet es ein gutes Modell für die Diskussion. Ich zeige dir das kleinstmögliche Makefile, das genug Platz zum Wachsen bietet.
Variablen einrichten und neue Bibliotheken hinzufügen
Welches System wir auch immer verwenden, es wird auf einer kleinen Anzahl von Umgebungsvariablen basieren, deshalb werde ich erklären, was sie tun und wie man sie setzt. Wenn wir diese Kompilierungsmechanismen erst einmal eingerichtet haben, können wir ganz einfach neue Bibliotheken hinzufügen, indem wir die Variablen anpassen, die wir bereits eingerichtet haben.
Ein Kompilierungssystem einrichten
Als Bonus können wir alles, was wir bis hierher gelernt haben, nutzen, um ein noch einfacheres System für die Kompilierung einzurichten, mit dem wir Code ausschneiden und in die Eingabeaufforderung einfügen können.

Ein besonderer Hinweis für IDE-Benutzer: Du bist vielleicht kein make Benutzer, aber dieser Abschnitt ist trotzdem für dich relevant, denn für jedes Rezept, das make beim Kompilieren von Code ausführt, hat deine IDE ein entsprechendes Rezept. Wenn du weißt, was make macht, wird es dir leicht fallen, deine IDE zu optimieren.

Einen Paketmanager verwenden

Wenn du keinen Paketmanager verwendest, verpasst du etwas.

Ich erwähne Paketmanager aus mehreren Gründen: Erstens haben einige von euch die Grundlagen vielleicht noch nicht installiert. Für dich habe ich diesen Abschnitt an den Anfang des Buches gestellt, denn du brauchst diese Tools, und zwar schnell. Mit einem guten Paketmanager hast du ziemlich schnell ein komplettes POSIX-Subsystem, Compiler für jede Sprache, von der du je gehört hast, eine halbwegs anständige Auswahl an Spielen, die üblichen Office-Tools, ein paar hundert C-Bibliotheken und so weiter.

Zweitens ist der Paketmanager für uns als C-Autoren ein wichtiges Mittel, um Bibliotheken in unsere Arbeit einzubinden.

Drittens: Wenn du den Sprung von jemandem, der Pakete herunterlädt, zu jemandem, der ein Paket erstellt, schaffst, wird dich dieses Buch auf halbem Weg begleiten. Es zeigt dir, wie du dein Paket für eine einfache automatische Installation vorbereitest, so dass der Administrator eines Paket-Repositorys, der deinen Code in das Repository aufnehmen möchte, keine Probleme hat, das endgültige Paket zu erstellen.

Wenn du ein Linux-Benutzer bist, hast du deinen Computer mit einem Paketmanager eingerichtet und bereits gesehen, wie einfach der Softwarebeschaffungsprozess sein kann. Für Windows-Nutzer werde ich weiter unten Cygwin vorstellen. Mac-Benutzer haben mehrere Möglichkeiten, z. B. Fink, Homebrew und Macports. Alle Mac-Optionen hängen von Apples Xcode-Paket ab, das (je nach Jahrgang deines Macs) kostenlos über die OS-Installations-CD, das Verzeichnis der installierbaren Programme, den Apple App Store oder durch die Registrierung als Entwickler bei Apple erhältlich ist.

Welche Pakete wirst du brauchen? Hier findest du eine kurze Übersicht über die Grundlagen der C-Entwicklung. Da jedes System anders organisiert ist, können einige dieser Pakete unterschiedlich gebündelt sein, standardmäßig in einem Basispaket installiert werden oder einen seltsamen Namen haben. Im Zweifelsfall solltest du ein Paket installieren, denn die Zeiten, in denen die Installation von zu vielen Dingen das System instabil machte oder verlangsamte, sind vorbei. Allerdings hast du wahrscheinlich nicht die Bandbreite (oder vielleicht sogar den Speicherplatz), um alle angebotenen Pakete zu installieren, also musst du abwägen. Wenn du feststellst, dass dir etwas fehlt, kannst du es später immer noch nachholen. Pakete, die du unbedingt haben solltest:

  • Ein Compiler. Installiere auf jeden Fall gcc; clang kann verfügbar sein.

  • GDB, ein Debugger.

  • Valgrind, um auf Fehler bei der Speichernutzung von C zu testen.

  • gprofeinen Profiler.

  • makeso dass du deinen Compiler nie direkt aufrufen musst.

  • pkg-configfür die Suche nach Bibliotheken.

  • Doxygen, für die Erstellung der Dokumentation.

  • Ein Texteditor. Es gibt buchstäblich Hunderte von Texteditoren, aus denen du wählen kannst. Hier sind ein paar subjektive Empfehlungen:

    • Emacs und vim sind die Favoriten der Hardcore-Geeks. Emacs ist sehr umfassend (das E steht für extensible); vim ist eher minimalistisch und sehr freundlich zu Touch-Tippern. Wenn du damit rechnest, Hunderte von Stunden vor einem Texteditor zu verbringen, lohnt es sich, einen der beiden zu lernen.

    • Kate ist freundlich und attraktiv und bietet einen guten Teil der Annehmlichkeiten, die wir als Programmierer erwarten, wie z.B. die Syntaxhervorhebung.

    • Als letzten Ausweg kannst du nano ausprobieren, das sehr einfach und textbasiert ist und daher auch dann funktioniert, wenn dein GUI nicht funktioniert.

  • Wenn du ein Fan von IDEs bist, besorg dir eine - oder mehrere. Auch hier gibt es eine große Auswahl; hier sind ein paar Empfehlungen:

    • Anjuta: gehört zur GNOME-Familie. Befreundet mit Glade, dem GNOME GUI-Builder.

    • KDevelop: gehört zur KDE-Familie.

    • XCode: Die IDE von Apple für OS X.

    • Code::blocks: relativ einfach, funktioniert unter Windows.

    • Eclipse: das Luxusauto mit vielen Getränkehaltern und zusätzlichen Knöpfen. Auch plattformübergreifend.

In späteren Kapiteln werde ich mich mit diesen schwereren Werkzeugen befassen:

  • Autotools: Autoconf, Automake, libtool

  • Git

  • Alternative Schalen, wie z.B. die Z-Schale.

Und natürlich gibt es die C-Bibliotheken, die es dir ersparen, das Rad neu zu erfinden (oder, um es metaphorisch auszudrücken, die Lokomotive neu zu erfinden). Vielleicht möchtest du noch mehr, aber hier sind die Bibliotheken, die im Laufe dieses Buches verwendet werden:

  • libcURL

  • libGLib

  • libGSL

  • libSQLite3

  • libXML2

Es gibt keinen Konsens über die Namensgebung von Bibliothekspaketen und du musst herausfinden, wie dein Paketmanager eine einzelne Bibliothek in Teilpakete zerlegen möchte. In der Regel gibt es ein Paket für die Benutzer und ein zweites für die Autoren, die die Bibliothek in ihrer eigenen Arbeit verwenden werden. Achte also darauf, sowohl das Basispaket als auch die Pakete -dev oder -devel auszuwählen. Bei manchen Systemen wird die Dokumentation in ein weiteres Paket ausgelagert. In diesem Fall sollte GDB dich durch die Schritte führen, wenn du es zum ersten Mal auf einem System ohne Debugsymbole ausführst.

Wenn du ein POSIX-System verwendest, hast du nach der Installation der vorangegangenen Elemente ein komplettes Entwicklungssystem und kannst mit dem Programmieren beginnen. Für Windows-Benutzer machen wir einen kurzen Abstecher, um zu verstehen, wie das Setup mit dem Windows-Hauptsystem zusammenspielt.

C mit Windows kompilieren

Auf den meisten Systemen ist C die zentrale VIP-Sprache, die alle anderen Tools unterstützt; auf einem Windows-System wird C seltsamerweise ignoriert.

Deshalb muss ich kurz darauf eingehen, wie man eine Windows-Box für das Schreiben von Code in C einrichtet. Wenn du jetzt nicht auf einer Windows-Box schreibst, kannst du diesen Abschnitt überspringen und zu "Welcher Weg zur Bibliothek?" springen .

POSIX für Windows

Da sich C und Unix gemeinsam entwickelt haben, ist es schwierig, über das eine und nicht über das andere zu sprechen. Ich denke, es ist einfacher, mit POSIX zu beginnen. Auch für diejenigen unter euch, die versuchen, Code auf einem Windows-Rechner zu kompilieren, den ihr anderswo geschrieben habt, ist dies der natürlichste Weg.

Soweit ich das beurteilen kann, teilt sich die Welt der Dinge mit Dateisystemen in zwei leicht überlappende Klassen:

  • POSIX-kompatible Systeme

  • Die Windows-Familie der Betriebssysteme

POSIX-Konformität bedeutet nicht, dass ein System aussehen und sich anfühlen muss wie eine Unix-Box. Der typische Mac-Benutzer hat zum Beispiel keine Ahnung, dass er oder sie ein Standard-BSD-System mit einem attraktiven Frontend benutzt, aber wer sich auskennt, kann in den Ordner Dienstprogramme (im Ordner Programme) gehen, dann das Terminal-Programm öffnen und ls, grep und make nach Herzenslust ausführen.

Außerdem bezweifle ich, dass viele Systeme 100 % der Anforderungen des Standards erfüllen (z. B. einen Fortran '77-Compiler). Für unsere Zwecke brauchen wir eine Shell, die sich wie eine einfache POSIX-Shell verhält, eine Handvoll Dienstprogramme (sed, grep, make usw.), einen C99-Compiler und Ergänzungen zur Standard-C-Bibliothek wie fork und iconv. Diese können dem Hauptsystem als Nebenbemerkung hinzugefügt werden. Die zugrundeliegenden Skripte des Paketmanagers, die Autotools und fast jeder andere Versuch, portabel zu programmieren, stützen sich bis zu einem gewissen Grad auf diese Werkzeuge. Auch wenn du nicht den ganzen Tag auf eine Eingabeaufforderung starren willst, sind diese Werkzeuge bei Installationen sehr nützlich.

Auf Betriebssystemen der Serverklasse und den Vollversionen von Windows 7 bietet Microsoft das Subsystem für Unix-basierte Anwendungen (SUA) an, das früher INTERIX hieß und heute die üblichen POSIX-Systemaufrufe, die Korn-Shell und gcc bereitstellt. Das Subsystem wird normalerweise nicht standardmäßig bereitgestellt, sondern kann als Zusatzkomponente installiert werden. Das SUA ist jedoch nicht für andere aktuelle Windows-Editionen verfügbar und wird auch nicht für Windows 8 verfügbar sein. Wir können uns also nicht darauf verlassen, dass Microsoft ein POSIX-Subsystem für seine Betriebssysteme bereitstellt.

Und damit Cygwin.

Wenn du Cygwin von Grund auf neu bauen würdest, wäre das dein Programm:

  1. Schreibe eine C-Bibliothek für Windows, die alle POSIX-Funktionen bereitstellt. Sie muss einige Unstimmigkeiten zwischen Windows und POSIX ausgleichen, z. B. dass Windows verschiedene Laufwerke wie C: hat, während POSIX ein einheitliches Dateisystem hat. In diesem Fall kannst du C: als /cygdrive/c, D: als /cygdrive/d und so weiter bezeichnen.

  2. Jetzt, wo du POSIX-Standard-Programme kompilieren kannst, indem du auf deine Bibliothek linkst, kannst du das tun: Erzeuge Windows-Versionen von ls, bash, grep, make, gcc, X, rxvt, libglib, perl, python, und so weiter.

  3. Wenn du Hunderte von Programmen und Bibliotheken erstellt hast, richte einen Paketmanager ein, mit dem die Benutzer die Elemente auswählen können, die sie installieren möchten.

Als Cygwin-Benutzer musst du nur den Paketmanager von dem Setup-Link auf der Cygwin-Website herunterladen und Pakete auswählen. Natürlich brauchst du die oben genannte Liste und ein vernünftiges Terminal (versuch es mit mintty oder installiere das X-Subsystem und benutze das xterm, denn beide sind viel freundlicher als das von Windows cmd.exe), aber du wirst sehen, dass praktisch alle Annehmlichkeiten, die du von einem Entwicklungssystem kennst, irgendwo vorhanden sind.

In "Pfade" bespreche ich verschiedene Umgebungsvariablen, die sich auf die Kompilierung auswirken, darunter auch Pfade für die Suche nach Dateien. Das gilt nicht nur für POSIX: Auch Windows hat Umgebungsvariablen, die du in den Systemeinstellungen in der Systemsteuerung findest. Cygwin ist viel benutzerfreundlicher, wenn du sein bin Verzeichnis (wahrscheinlich c:\cygwin\bin) zum Windows PATH hinzufügst.

Jetzt kannst du mit dem Kompilieren von C-Code beginnen.

C mit POSIX kompilieren

Microsoft bietet mit Visual Studio einen C++-Compiler an, der über einen C89-Kompatibilitätsmodus verfügt (gemeinhin als ANSI C bezeichnet, auch wenn C11 der aktuelle ANSI-Standard ist). Dies ist die einzige Möglichkeit, C-Code zu kompilieren, die Microsoft derzeit anbietet. Viele Vertreter des Unternehmens haben deutlich gemacht, dass über die Unterstützung einiger C99-Funktionen hinaus (geschweige denn die Unterstützung von C11) keine weiteren Entwicklungen zu erwarten sind. Visual Studio ist der einzige große Compiler, der noch an C89 festhält, so dass wir uns anderweitig nach Alternativen umsehen müssen.

Natürlich bietet Cygwin gcc an, und wenn du Cygwin installiert hast, hast du bereits eine vollständige Build-Umgebung.

Standardmäßig hängen Programme, die du unter Cygwin kompilierst, von der Bibliothek der POSIX-Funktionen, cygwin1.dll, ab (unabhängig davon, ob dein Code POSIX-Aufrufe enthält oder nicht). Wenn du dein Programm auf einem Rechner ausführst, auf dem Cygwin installiert ist, hast du kein Problem. Die Benutzer können auf die ausführbare Datei klicken und sie wie erwartet ausführen, da das System die Cygwin-DLL finden sollte. Ein unter Cygwin kompiliertes Programm kann auch auf Rechnern laufen, auf denen Cygwin nicht installiert ist, wenn du cygwin1.dll mit deinem Code verteilst. Auf meinem Rechner ist der Pfad zu Cygwin folgendermaßen: /bin/cygwin1.dll. Die Datei cygwin1.dll hat eine GPL-ähnliche Lizenz (siehe "Die rechtliche Seite"), d.h. wenn du die DLL getrennt von Cygwin als Ganzes weitergibst, musst du den Quellcode für dein Programm veröffentlichen.1

Wenn das ein Problem ist, musst du einen Weg finden, dein Programm neu zu kompilieren, ohne von cygwin1.dll abhängig zu sein. Das bedeutet, dass du alle POSIX-spezifischen Funktionen (wie fork oder popen) aus deinem Code streichen und MinGW verwenden musst, wie später beschrieben. Mit cygcheck kannst du herausfinden, von welchen DLLs dein Programm abhängt, und so überprüfen, ob deine ausführbare Datei mit cygwin1.dll verknüpft ist oder nicht.

Hinweis

Um zu sehen, von welchen anderen Bibliotheken ein Programm oder eine dynamisch gelinkte Bibliothek abhängt:

  • Cygwin: cygcheck libxx.dll

  • Linux: ldd libxx.so

  • Mac: otool -L libxx.dylib

C ohne POSIX kompilieren

Wenn dein Programm die POSIX-Funktionen nicht benötigt, kannst du MinGW (Minimalist GNU for Windows) verwenden, das einen Standard-C-Compiler und einige grundlegende Werkzeuge zur Verfügung stellt. MSYS ist eine Ergänzung zu MinGW, die eine Shell und andere nützliche Hilfsprogramme bietet.

MSYS bietet eine POSIX-Shell (und du kannst die Terminals mintty oder RXVT finden, in denen du deine Shell ausführen kannst), oder du lässt die Eingabeaufforderung ganz hinter dir und versuchst es mit Code::blocks, einer IDE, die MinGW zur Kompilierung unter Windows verwendet. Eclipse ist eine weitaus umfangreichere IDE, die ebenfalls für MinGW konfiguriert werden kann, allerdings ist dafür etwas mehr Einarbeitungszeit erforderlich.

Oder wenn du dich an einer POSIX-Eingabeaufforderung wohler fühlst, dann richte Cygwin trotzdem ein, besorge dir die Pakete, die die MinGW-Versionen von gcc bereitstellen, und benutze diese zum Kompilieren anstelle der POSIX-verbindenden Standardversion von Cygwin gcc.

Wenn du die Autotools noch nicht kennst, wirst du sie bald kennenlernen. Ein Paket, das mit den Autotools erstellt wurde, zeichnet sich dadurch aus, dass es mit drei Befehlen installiert wird: ./configure && make && make install. MSYS bietet genügend Möglichkeiten, damit solche Pakete eine gute Chance haben, zu funktionieren. Wenn du die Pakete heruntergeladen hast, um sie über die Eingabeaufforderung von Cygwin zu bauen, kannst du das Paket auch so einrichten, dass es den Mingw32-Compiler von Cygwin verwendet, um POSIX-freien Code zu erzeugen:

./configure --host=mingw32

Dann führe make && make install wie gewohnt aus.

Sobald du unter MinGW kompiliert hast, entweder über die Befehlszeile oder mit den Autotools, hast du eine native Windows-Binärdatei. Da MinGW nichts von cygwin1.dll weiß und dein Programm sowieso keine POSIX-Aufrufe macht, hast du jetzt ein ausführbares Programm, das ein echtes Windows-Programm ist und von dem niemand weiß, dass du es in einer POSIX-Umgebung kompiliert hast.

Allerdings gibt es in MinGW derzeit nur wenige vorkompilierte Bibliotheken.2 Wenn du frei von cygwin1.dll sein willst, kannst du nicht die Version von libglib.dll verwenden, die mit Cygwin geliefert wird. Du musst GLib aus dem Quellcode in eine native Windows-DLL umkompilieren - aber GLib hängt von GNUs gettext für die Internationalisierung ab, also musst du diese Bibliothek zuerst bauen. Moderner Code hängt von modernen Bibliotheken ab, daher kann es sein, dass du viel Zeit damit verbringst, Dinge einzurichten, die in anderen Systemen mit einem einzeiligen Aufruf des Paketmanagers erledigt sind. Damit sind wir wieder bei den Dingen, die die Leute dazu bringen, darüber zu reden, dass C 40 Jahre alt ist und man alles von Grund auf neu schreiben muss.

Das sind also die Vorbehalte. Microsoft hat sich aus der Diskussion zurückgezogen und überlässt es anderen, einen Post-Grunge-C-Compiler und eine entsprechende Umgebung zu entwickeln. Cygwin tut dies und bietet einen vollständigen Paketmanager mit genügend Bibliotheken, um einen Teil oder die gesamte Arbeit zu erledigen, aber es ist mit einem POSIX-Schreibstil und der DLL von Cygwin verbunden. Wenn das ein Problem ist, musst du mehr Arbeit investieren, um die Umgebung und die Bibliotheken zu erstellen, die du brauchst, um anständigen Code zu schreiben.

Welcher Weg zur Bibliothek?

OK, du hast also einen Compiler, eine POSIX-Toolchain und einen Paketmanager, der problemlos ein paar hundert Bibliotheken installieren kann. Jetzt können wir uns dem Problem zuwenden, diese beim Kompilieren unserer Programme zu verwenden.

Wir müssen mit der Compiler-Befehlszeile beginnen, die schnell zu einem Durcheinander wird, aber wir werden mit drei (manchmal dreieinhalb) relativ einfachen Schritten enden:

  1. Setzt eine Variable, die die Compiler-Flags auflistet.

  2. Setze eine Variable, die die Bibliotheken auflistet, mit denen gelinkt werden soll. Der halbe Schritt ist, dass du manchmal nur eine Variable für das Linken beim Kompilieren setzen musst und manchmal zwei für das Linken zur Kompilier- und zur Laufzeit.

  3. Richte ein System ein, das diese Variablen verwendet, um die Kompilierung zu steuern.

Um eine Bibliothek zu verwenden, musst du dem Compiler zweimal sagen, dass du Funktionen aus der Bibliothek importieren wirst: einmal für die Kompilierung und einmal für den Linker. Bei einer Bibliothek an einem Standardort erfolgen die beiden Deklarationen über ein #include im Text des Programms und ein -l Flag in der Compilerzeile.

Beispiel 1-1 zeigt ein schnelles Beispielprogramm, das ein paar amüsante mathematische Berechnungen durchführt (zumindest für mich; wenn der statistische Jargon für dich griechisch ist, ist das okay). Die C99-Standardfehlerfunktion erf(x) ist eng verwandt mit dem Integral von Null bis x der Normalverteilung mit Mittelwert Null und Standardabweichung √2. Hier verwenden wir erf, um einen unter Statistikern beliebten Bereich zu überprüfen (das 95%-Konfidenzintervall für einen standardmäßigen large-n-Hypothesentest ). Wir nennen diese Datei erf.c.

Beispiel 1-1. Ein Einzeiler aus der Standardbibliothek. (erf.c)
#include <math.h>  //erf, sqrt
#include <stdio.h> //printf

int main(){
    printf("The integral of a Normal(0, 1) distribution "
           "between -1.96 and 1.96 is: %g\n", erf(1.96*sqrt(1/2.)));
}

Die Zeilen #include sollten dir bekannt vorkommen. Der Compiler fügt hier math.h und stdio.h in die Codedatei ein und somit auch die Deklarationen für printf, erf und sqrt. Die Deklaration in math.h sagt nichts darüber aus, was erf tut, sondern nur, dass sie eine double aufnimmt und eine double zurückgibt. Das ist genug Information für den Compiler, um die Konsistenz unserer Verwendung zu prüfen und eine Objektdatei mit einem Hinweis zu erzeugen, der dem Computer sagt: "Wenn du zu diesem Hinweis kommst, suche die Funktion erf und ersetze diesen Hinweis durch den Rückgabewert von erf."

Es ist die Aufgabe des Linkers, diese Notiz auszugleichen, indem er erf findet, die sich in einer Bibliothek irgendwo auf deiner Festplatte befindet.

Die mathematischen Funktionen in math.h sind in eine eigene Bibliothek ausgelagert, die du dem Linker mit dem Flag -lm mitteilen musst. Hier ist -l das Flag, das anzeigt, dass eine Bibliothek eingebunden werden muss, und die Bibliothek hat in diesem Fall einen Namen mit nur einem Buchstaben, m. Du bekommst printf umsonst, denn es gibt eine implizite -lc, die den Linker auffordert, die standardmäßig angenommene libc am Ende des Link-Befehls zu linken. Später werden wir sehen, dass GLib 2.0 über -lglib-2.0 eingebunden wird, die GNU Scientific Library wird über -lgsl eingebunden und so weiter.

Wenn die Datei erf.c hieße, sähe die vollständige Befehlszeile mit dem Compiler gcc einschließlich einiger zusätzlicher Flags, die wir gleich besprechen werden, wie folgt aus:

gcc erf.c -o erf -lm -g -Wall -O3 -std=gnu11

Wir haben also dem Compiler gesagt, dass er die mathematischen Funktionen über #include in das Programm einbinden soll, und wir haben dem Linker gesagt, dass er mit der mathematischen Bibliothek über -lm auf der Kommandozeile linken soll.

Das Flag -o gibt den Namen der Ausgabe an; andernfalls würden wir den Standardnamen der ausführbaren Datei a.out erhalten.

Ein paar meiner Lieblingsflaggen

Du wirst sehen, dass ich jedes Mal ein paar Compiler-Flags verwende, und ich empfehle dir, das auch zu tun.

  • -g fügt Symbole für die Fehlersuche hinzu. Ohne sie ist dein Debugger nicht in der Lage, dir Variablen- oder Funktionsnamen zu geben. Sie verlangsamen das Programm nicht und es ist uns egal, ob das Programm ein Kilobyte größer ist, also gibt es wenig Grund, dies nicht zu verwenden. Es funktioniert für gcc, clang und icc (Intel C Compiler).

  • -std=gnu11 ist clang- und gcc-spezifisch und legt fest, dass der Compiler Code zulassen soll, der den C11- und POSIX-Standards (sowie einigen GNU-Erweiterungen) entspricht. Ab diesem Zeitpunkt verwendet clang standardmäßig den C11-Standard und gcc den C89-Standard. Wenn deine Kopie von gcc, clang oder icc älter als C11 ist, benutze -std=gnu99 um den C99-Standard zu verwenden. Der POSIX-Standard schreibt vor, dass c99 auf deinem System vorhanden sein muss. Die compiler-agnostische Version der obigen Zeile für die Kompilierung von C99-Code wäre also:

    c99 erf.c -o erf -lm -g -Wall -O3

    In den folgenden Makefiles erreiche ich diesen Effekt, indem ich die Variable CC=c99 setze.

    Warnung

    Je nach Jahrgang deines Macs kann c99 eine speziell gehackte Version von gcc sein, die du wahrscheinlich nicht willst. Wenn du eine Version von c99 hast, die bei der Flagge -Wall aufhört oder ganz fehlt, kannst du deine eigene Version erstellen. Lege ein Bash-Skript mit dem Namen c99 in das Verzeichnis am Anfang deines Pfades mit dem folgenden Text:

    gcc --std=gnu99 $*

    oder

    clang $*

    wie du willst. Mach sie über chmod +x c99 ausführbar.

  • -O3 steht für die Optimierungsstufe drei, die alle Tricks ausprobiert, um schnelleren Code zu erstellen. Wenn du beim Ausführen des Debuggers feststellst, dass zu viele Variablen optimiert wurden, so dass du nicht mehr nachvollziehen kannst, was vor sich geht, dann ändere dies in -O0. Dies wird später eine übliche Änderung der Variable CFLAGS sein. Das funktioniert auch für gcc, clang und icc.

  • -Wall fügt Compiler-Warnungen hinzu. Das funktioniert für gcc, clang und icc. Für icc solltest du -w1 bevorzugen, das die Warnungen des Compilers anzeigt, aber nicht seine Anmerkungen.

Warnung

Nutze immer die Warnungen deines Compilers. Du bist vielleicht anspruchsvoll und kennst den C-Standard in- und auswendig, aber du bist nicht anspruchsvoller oder kenntnisreicher als dein Compiler. In alten C-Lehrbüchern wirst du seitenweise ermahnt, auf den Unterschied zwischen = und == zu achten oder zu überprüfen, ob alle Variablen vor der Verwendung initialisiert wurden. Als moderner Lehrbuchautor habe ich es leicht, denn ich kann all diese Ermahnungen in einem einzigen Tipp zusammenfassen: Verwende immer die Warnungen deines Compilers.

Wenn dein Compiler zu einer Änderung rät, solltest du ihn nicht anzweifeln oder die Korrektur aufschieben. Tu alles, was nötig ist, um (1) zu verstehen, warum du eine Warnung erhalten hast, und (2) deinen Code so zu korrigieren, dass er ohne Warnungen und ohne Fehler kompiliert werden kann. Compiler-Meldungen sind bekanntermaßen schwer zu verstehen. Wenn du also Probleme mit Schritt (1) hast, gib die Warnmeldung in deine Internet-Suchmaschine ein, um herauszufinden, wie viele andere vor dir von dieser Warnung verwirrt wurden. Du solltest vielleicht noch -Werror zu deinen Compiler-Flags hinzufügen, damit dein Compiler Warnungen als Fehler behandelt.

Pfade

Ich habe über 700.000 Dateien auf meiner Festplatte, und eine davon enthält die Deklarationen für sqrt und erf, und eine andere ist die Objektdatei mit den kompilierten Funktionen.3 Der Compiler muss wissen, in welchen Verzeichnissen er suchen muss, um die richtige Header- und Objektdatei zu finden, und das Problem wird noch komplizierter, wenn wir Bibliotheken verwenden, die nicht Teil des C-Standards sind.

In einer typischen Einrichtung gibt es mindestens drei Orte, an denen Bibliotheken installiert werden können:

  • Der Hersteller des Betriebssystems kann ein oder zwei Standardverzeichnisse definieren, in denen die Bibliotheken vom Hersteller installiert werden.

  • Es kann ein Verzeichnis geben, in dem der lokale Systemadministrator Pakete installiert, die bei der nächsten Aktualisierung des Betriebssystems durch den Hersteller nicht überschrieben werden sollen. Zum Beispiel könnte der Sysadmin eine speziell gehackte Version einer Bibliothek haben, die die Standardversion überschreiben soll.

  • Benutzer haben in der Regel keine Schreibrechte für diese Speicherorte und sollten daher die Bibliotheken in ihren Heimatverzeichnissen verwenden können.

Die Standardverzeichnisse des Betriebssystems bereiten in der Regel keine Probleme und der Compiler sollte wissen, dass er in diesen Verzeichnissen die C-Standardbibliothek und alles, was daneben installiert ist, finden muss. Im POSIX-Standard werden diese Verzeichnisse als "die üblichen Orte" bezeichnet.

Aber für die anderen Sachen musst du dem Compiler sagen, wo er suchen soll. Das wird kompliziert: Es gibt keinen Standardweg, um Bibliotheken an nicht standardmäßigen Orten zu finden, und das steht ganz oben auf der Liste der Dinge, die die Leute an C frustrieren. Auf der einen Seite weiß dein Compiler, wie er an den üblichen Orten suchen muss, und die Bibliotheksanbieter neigen dazu, die Dinge an den üblichen Orten abzulegen, sodass du vielleicht nie einen Pfad manuell angeben musst. Ein weiterer Pluspunkt ist, dass es einige Tools gibt, die dir bei der Angabe von Pfaden helfen. Ein weiterer Pluspunkt ist, dass du die nicht standardmäßigen Pfade in einer Shell- oder Makefile-Variable festlegen kannst, sobald du sie auf deinem System gefunden hast, und nie wieder darüber nachdenken musst.

Angenommen, du hast eine Bibliothek namens Libuseful auf deinem Computer installiert und weißt, dass ihre verschiedenen Dateien im Verzeichnis /usr/local/ abgelegt wurden, das offiziell für die lokalen Bibliotheken deines Sysadmins vorgesehen ist. Du hast #include <useful.h> bereits in deinen Code eingefügt; jetzt musst du dies in die Befehlszeile eingeben:

gcc -I/usr/local/include use_useful.c -o use_useful -L/usr/local/lib -luseful
  • -I fügt den angegebenen Pfad zum Include-Suchpfad hinzu, den der Compiler nach Header-Dateien durchsucht, die du #included in deinem Code hast.

  • -L zum Suchpfad der Bibliothek hinzufügt.

  • Die Reihenfolge ist wichtig. Wenn du eine Datei namens specific.o hast, die von der Libbroad-Bibliothek abhängt, und Libbroad von Libgeneral, dann brauchst du:

    gcc specific.o -lbroad -lgeneral

    Jede andere Reihenfolge, wie z. B. gcc -lbroad -lgeneral specific.o, wird wahrscheinlich fehlschlagen. Du kannst dir vorstellen, dass der Linker sich das erste Element, specific.o, ansieht und eine Liste der ungelösten Funktions-, Struktur- und Variablennamen aufschreibt. Dann geht er zum nächsten Eintrag, -lbroad, und sucht nach den Einträgen auf der noch fehlenden Liste, wobei er möglicherweise neue ungelöste Einträge hinzufügt, und überprüft dann -lgeneral auf die Einträge, die noch auf der fehlenden Liste stehen. Wenn am Ende der Liste noch Namen unauffindbar sind (einschließlich der impliziten -lc am Ende), hält der Linker an und gibt den Rest der Liste der fehlenden Elemente an den Benutzer weiter.

OK, zurück zum Speicherortproblem: Wo befindet sich die Bibliothek, auf die du verlinken willst? Wenn sie über denselben Paketmanager installiert wurde, mit dem du auch den Rest deines Betriebssystems installiert hast, befindet sie sich höchstwahrscheinlich an den üblichen Orten und du musst dir keine Sorgen machen.

Du hast vielleicht ein Gefühl dafür, wo sich deine eigenen lokalen Bibliotheken befinden, z. B. /usr/local, /sw oder /opt. Sicherlich hast du eine Möglichkeit, die Festplatte zu durchsuchen, z. B. ein Suchprogramm auf deinem Desktop oder das POSIX:

find /usr -name 'libuseful*'

um /usr nach Dateien zu durchsuchen, deren Namen mit libuseful beginnen. Wenn du feststellst, dass die Shared Object-Datei von Libuseful in /some/path/lib liegt, befinden sich die Header mit ziemlicher Sicherheit in /some/path/include.

Alle anderen finden es auch lästig, auf der Festplatte nach Bibliotheken zu suchen. pkg-config kümmert sich darum, indem es ein Repository mit den Flags und Speicherorten verwaltet, die die Pakete selbst als notwendig für die Kompilierung melden. Gib pkg-config in die Befehlszeile ein. Wenn du eine Fehlermeldung über die Angabe von Paketnamen erhältst, hast du pkg-config und kannst damit die Suche für dich erledigen. Auf meinem PC gebe ich zum Beispiel diese beiden Befehle in die Befehlszeile ein:

pkg-config --libs gsl libxml-2.0
pkg-config --cflags gsl libxml-2.0

gibt mir diese beiden Zeilen aus:

-lgsl -lgslcblas -lm -lxml2
-I/usr/include/libxml2

Das sind genau die Flags, die ich brauche, um mit GSL und LibXML2 zu kompilieren. Die -l Flaggen zeigen, dass die GNU Scientific Library von einer BLAS-Bibliothek (Basic Linear Algebra Subprograms) abhängt und die BLAS-Bibliothek der GSL von der Standard-Mathe-Bibliothek. Es scheint, dass sich alle Bibliotheken an den üblichen Stellen befinden, denn es gibt keine -L Flaggen, aber die -I Flagge gibt den Ort für die Header-Dateien von LibXML2 an.

Zurück in der Kommandozeile: Wenn du einen Befehl mit Backticks umgibst, ersetzt die Shell den Befehl durch seine Ausgabe. Das heißt, wenn ich tippe:

gcc `pkg-config --cflags --libs gsl libxml-2.0` -o specific specific.c

der Compiler sieht:

gcc -I/usr/include/libxml2 -lgsl -lgslcblas -lm -lxml2 -o specific specific.c

pkg-config nimmt uns also viel Arbeit ab, aber es ist nicht so standardisiert, dass wir erwarten können, dass jeder es hat oder dass jede Bibliothek damit registriert ist. Wenn du pkg-config nicht hast, musst du das selbst herausfinden, indem du das Handbuch deiner Bibliothek liest oder deine Festplatte durchsuchst, wie wir bereits gesehen haben.

Warnung

Oft gibt es Umgebungsvariablen für Pfade, wie CPATH oder LIBRARY_PATH oder C_INCLUDE_PATH. Du würdest sie in deiner .bashrc oder einer anderen benutzerspezifischen Liste von Umgebungsvariablen setzen. Sie sind hoffnungslos uneinheitlich -gcc unter Linux und gcc auf dem Mac verwenden unterschiedliche Variablen, und jeder andere Compiler kann noch andere verwenden. Meiner Meinung nach ist es einfacher, diese Pfade projektspezifisch im Makefile oder einer entsprechenden Datei zu setzen, indem du die Flags -I und -L verwendest. Wenn du diese Pfadvariablen bevorzugst, findest du am Ende der Manpage deines Compilers eine Liste der Variablen, die für deine Situation relevant sind.

Selbst mit pkg-config wird der Bedarf an etwas, das all das für uns zusammensetzt, immer deutlicher. Jedes Element ist leicht zu verstehen, aber es ist eine lange, mechanische Liste von mühsamen Teilen.

Makefiles verwenden

Das Makefile bietet eine Lösung für all diese endlosen Anpassungen. Es ist im Grunde ein organisierter Satz von Variablen und Abfolgen von einzeiligen Shell-Skripten. Das Programm make, das dem POSIX-Standard entspricht, liest das Makefile nach Anweisungen und Variablen und setzt dann die langen und mühsamen Befehlszeilen für uns zusammen. Nach diesem Abschnitt gibt es kaum noch einen Grund, den Compiler direkt aufzurufen.

In "Makefiles vs. Shell-Skripte" werde ich ein paar mehr Details über das Makefile behandeln; hier zeige ich dir das kleinste praktikable Makefile, das ein einfaches Programm kompiliert, das von einer Bibliothek abhängt. Hier ist es, alle sechs Zeilen davon:

P=program_name
OBJECTS=
CFLAGS = -g -Wall -O3
LDLIBS=
CC=c99

$(P): $(OBJECTS)

Verwendung:

  • Einmalig: Speichere sie (unter dem Namen makefile) im gleichen Verzeichnis wie deine .c-Dateien. Wenn du GNU Make verwendest, hast du die Möglichkeit, den Namen Makefile groß zu schreiben, wenn du meinst, dass er sich dadurch von den anderen Dateien abhebt. Setze den Namen deines Programms in die erste Zeile (verwende progname, nicht progname.c).

  • Jedes Mal, wenn du neu kompilieren musst: Gib ein. make.

Variablen einstellen

Wir werden uns bald mit der eigentlichen Funktionsweise des Makefiles befassen, aber fünf von sechs Zeilen dieses Makefiles befassen sich mit dem Setzen von Variablen (von denen zwei derzeit leer sind), was darauf hindeutet, dass wir uns einen Moment Zeit nehmen sollten, um die Umgebungsvariablen ein wenig genauer zu betrachten.

Warnung

Historisch gesehen gab es zwei Hauptströmungen der Shell-Grammatik: eine, die hauptsächlich auf der Bourne-Shell basiert, und eine andere, die hauptsächlich auf der C-Shell basiert. Die C-Shell hat eine etwas andere Syntax für Variablen, z. B. set CFLAGS="-g -Wall -O3”, um den Wert von CFLAGS zu setzen. Der POSIX-Standard basiert jedoch auf der Bourne-Syntax für das Setzen von Variablen, weshalb ich mich im Rest dieses Buches auf diese Syntax konzentriere.

Die Shell und make verwenden die $, um den Wert einer Variablen anzugeben, aber die Shell verwendet $var, während make alle Variablennamen, die länger als ein Zeichen sind, in Klammern setzt: $(var). In der vorangegangenen makefile wird $(P): $(OBJECTS) also so ausgewertet, dass es bedeutet

program_name:

Es gibt mehrere Möglichkeiten, make über eine Variable zu informieren:

  • Setze die Variable von der Shell aus, bevor du make aufrufst, und exportiere die Variable, so dass die Shell, wenn sie einen Kindprozess startet, die Variable in ihrer Liste der Umgebungsvariablen hat. So setzen Sie CFLAGS von einer POSIX-Standard-Befehlszeile aus:

    export CFLAGS='-g -Wall -O3'

    Zu Hause lasse ich die erste Zeile in diesem Makefile weg, P=program_nameund setze sie stattdessen einmal pro Sitzung über export P=program_nameDas bedeutet, dass ich das Makefile selbst noch seltener bearbeiten muss.

  • Du kannst diese Exportbefehle in das Startskript deiner Shell einfügen, z. B. .bashrc oder .zshrc. So ist gewährleistet, dass die Variable jedes Mal, wenn du dich anmeldest oder eine neue Shell startest, gesetzt und exportiert wird. Wenn du sicher bist, dass deine CFLAGS jedes Mal die gleiche ist, kannst du sie hier setzen und musst nie wieder darüber nachdenken.

  • Du kannst eine Variable für einen einzelnen Befehl exportieren, indem du die Zuweisung direkt vor den Befehl setzt. Der Befehl env listet die ihm bekannten Umgebungsvariablen auf, wenn du also Folgendes ausführst

    PANTS=kakhi env | grep PANTS

    solltest du die entsprechende Variable und ihren Wert sehen. Das ist der Grund, warum die Shell keine Leerzeichen um das Gleichheitszeichen herum zulässt: Das Leerzeichen dient der Unterscheidung zwischen der Zuweisung und dem Befehl.

    Mit diesem Formular werden die angegebenen Variablen nur für eine Zeile gesetzt und exportiert. Nachdem du dies in der Befehlszeile ausprobiert hast, führe env | grep PANTS erneut aus, um zu überprüfen, dass PANTS keine exportierte Variable mehr ist.

    Du kannst so viele Variablen angeben, wie du möchtest:

    PANTS=kakhi PLANTS="ficus fern" env | grep 'P.*NTS'

    Diese Form ist Teil der einfachen Befehlsbeschreibung der Shell-Spezifikation, was bedeutet, dass die Zuweisung vor einem tatsächlichen Befehl stehen muss. Das ist wichtig, wenn wir uns mit Shell-Konstrukten beschäftigen, die keine Befehle sind. Schreiben:

    VAR=val if [ -e afile ] ; then ./program_using_VAR ; fi

    wird mit einem obskuren Syntaxfehler fehlschlagen. Die richtige Form ist:

    if [ -e afile ] ; then VAR=val ./program_using_VAR ; fi
  • Wie in dem früheren Makefile kannst du die Variable am Anfang des Makefiles setzen, mit Zeilen wie CFLAGS=. Im Makefile kannst du Leerzeichen um das Gleichheitszeichen herum verwenden, ohne dass etwas kaputt geht.

  • make ermöglicht es dir, Variablen auf der Kommandozeile zu setzen, unabhängig von der Shell. Diese beiden Zeilen sind also fast gleichwertig:

    make CFLAGS="-g -Wall"   Set a makefile variable.
    CFLAGS="-g -Wall" make   Set an environment variable visible to make and its children.

Alle diese Mittel sind gleichwertig, soweit es dein Makefile betrifft, mit der Ausnahme, dass Kindprogramme, die von make aufgerufen werden, neue Umgebungsvariablen kennen, aber keine Makefile-Variablen kennen.

make bietet auch einige eingebaute Variablen. Hier sind die (POSIX-Standard-)Variablen, die du zum Lesen der folgenden Regeln benötigst:

$@

Der vollständige Name der Zieldatei. Mit Ziel ist die Datei gemeint, die gebaut werden muss, z. B. eine .o-Datei, die aus einer .c-Datei kompiliert wird, oder ein Programm, das durch die Verknüpfung von .o-Dateien entsteht.

$*

Die Zieldatei mit abgeschnittenem Suffix. Wenn die Zieldatei also prog.o ist, ist $* prog und $*.c würde zu prog.c.

$<

Der Name der Datei, die dieses Ziel ausgelöst und erstellt hat. Wenn wir prog.o erstellen, liegt das wahrscheinlich daran, dass prog.c kürzlich geändert wurde, also ist $< prog.c.

Die Regeln

Konzentrieren wir uns jetzt auf die Prozeduren, die das Makefile ausführt, und gehen wir dann darauf ein, wie die Variablen dies beeinflussen.

Abgesehen von den Variablen haben die Abschnitte des Makefiles die Form:

target: dependencies
        script

Wenn das Ziel über den Befehl make aufgerufen wird. targetaufgerufen wird, werden die Abhängigkeiten überprüft. Wenn das Ziel eine Datei ist, die Abhängigkeiten alle Dateien sind und das Ziel neuer ist als die Abhängigkeiten, dann ist die Datei aktuell und es gibt nichts zu tun. Andernfalls wird die Verarbeitung des Ziels aufgeschoben, die Abhängigkeiten werden ausgeführt oder generiert, wahrscheinlich über ein anderes Ziel, und wenn die Abhängigkeitsskripte alle fertig sind, wird das Skript des Ziels ausgeführt.

Bevor dies ein Buch wurde, war es zum Beispiel eine Reihe von Tipps, die in einem Blog veröffentlicht wurden. Für jeden Blogbeitrag gab es eine HTML- und eine PDF-Version, die alle mit LaTeX erstellt wurden. Um ein einfaches Beispiel zu geben, lasse ich viele Details weg (z. B. die vielen Optionen für latex2html), aber hier ist die Art von Makefile, die man für den Prozess schreiben könnte.

Warnung

Wenn du einen dieser Makefile-Schnipsel von einer Version auf deinem Bildschirm oder auf Papier in eine Datei mit dem Namen makefile kopierst, vergiss nicht, dass das Leerzeichen am Anfang jeder Zeile ein Tabulator sein muss, keine Leerzeichen. Schuld daran ist POSIX.

all: html doc publish

doc:
    pdflatex $(f).tex

html:
    latex -interaction batchmode $(f)
    latex2html $(f).tex

publish:
    scp $(f).pdf $(Blogserver)

Ich setze f in der Befehlszeile mit einem Befehl wie export f=tip-make. Wenn ich dann make in die Befehlszeile eingebe, wird das erste Ziel, all, überprüft. Das bedeutet, dass der Befehl make selbst dem Befehl make entspricht. first_target. Das hängt von html, doc und publish ab, also werden diese Ziele nacheinander aufgerufen. Wenn ich weiß, dass es noch nicht bereit ist, in die Welt zu kopieren, dann kann ich make html doc aufrufen und nur diese Schritte ausführen.

In dem einfachen Makefile von vorhin hatten wir nur eine Ziel-/Abhängigkeits-/Skriptgruppe. Zum Beispiel:

P=domath
OBJECTS=addition.o subtraction.o

$(P): $(OBJECTS)

Es folgt eine Abfolge von Abhängigkeiten und Skripten, ähnlich wie bei meinem Blogging-Makefile, aber die Skripte sind implizit. Hier ist P=domath das Programm, das kompiliert werden soll, und es hängt von den Objektdateien addition.o und subtraction.o ab. Da addition.o nicht als Ziel aufgeführt ist, verwendet make eine implizite Regel, die unten aufgeführt ist, um von der .c- zur .o-Datei zu kompilieren. Dann wird das Gleiche für subtraction.o und domath.o gemacht (weil GNU make implizit davon ausgeht, dass domath von domath.o abhängt, wenn man das hier berücksichtigt). Wenn alle Objekte gebaut sind, haben wir kein Skript, um das Ziel $(P) zu bauen, also füllt GNU make sein Standardskript zum Linken von .o-Dateien in eine ausführbare Datei aus.

Der POSIX-Standard make hat dieses Rezept für die Kompilierung einer .o-Objektdatei aus einer .c-Quellcodedatei:

$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $*.c

Die Variable $(CC) steht für deinen C-Compiler; der POSIX-Standard gibt CC=c99 als Standard vor, aber aktuelle Ausgaben von GNU make setzen CC=cc, was normalerweise ein Link zu gcc ist. Im minimalen Makefile am Anfang dieses Abschnitts wird $(CC) explizit auf c99 gesetzt, $(CFLAGS) auf die Liste der Flags von vorhin, und $(LDFLAGS) ist nicht gesetzt und wird daher durch nichts ersetzt. Wenn make also feststellt, dass your_program.o erzeugt werden muss, wird dieser Befehl mit dem Makefile ausgeführt:

c99 -g -Wall -O3 -o your_program.o your_program.c

Wenn GNU make entscheidet, dass du ein ausführbares Programm aus den Objektdateien bauen willst, verwendet es dieses Rezept:

$(CC) $(LDFLAGS) first.o second.o $(LDLIBS)

Erinnere dich daran, dass die Reihenfolge im Linker wichtig ist, deshalb brauchen wir zwei Linker-Variablen. Im vorherigen Beispiel brauchten wir:

cc specific.o -lbroad -lgeneral

als den relevanten Teil des Linking-Befehls. Wenn wir den korrekten Kompilierbefehl mit dem Rezept vergleichen, sehen wir, dass wir LDLIBS=-lbroad -lgeneral setzen müssen.

Hinweis

Wenn du die vollständige Liste der Standardregeln und -variablen sehen möchtest, die in deiner Version von make enthalten sind, probiere es aus:

make -p > default_rules

Das ist also das Spiel: Finde die richtigen Variablen und setze sie im Makefile. Du musst zwar immer noch recherchieren, was die richtigen Flags sind, aber zumindest kannst du sie im Makefile aufschreiben und musst nie wieder darüber nachdenken.

Wenn du eine IDE, CMAKE oder eine der anderen Alternativen zum POSIX-Standard make verwendest, wirst du das gleiche Spiel mit dem Finden der Variablen spielen. Ich werde die Diskussion über das vorangegangene minimale Makefile fortsetzen, und du solltest kein Problem haben, die entsprechenden Variablen in deiner IDE zu finden.

  • Die Variable CFLAGS ist eine fest eingebaute Gewohnheit, aber die Variable, die du für den Linker setzen musst, ändert sich von System zu System. Auch LDLIBS ist nicht der POSIX-Standard, aber es ist das, was GNU make verwendet.

  • In die Variablen CFLAGS und LDLIBS werden wir alle Compiler-Flags zum Auffinden und Identifizieren von Bibliotheken einfügen. Wenn du pkg-config hast, füge die Backticked-Aufrufe hier ein. Das Makefile auf meinem System, in dem ich Apophenia und GLib für so ziemlich alles verwende, sieht zum Beispiel so aus:

    CFLAGS=`pkg-config --cflags apophenia glib-2.0` -g -Wall -std=gnu11 -O3
    LDLIBS=`pkg-config --libs apophenia glib-2.0`

    Oder du gibst die Flags -I, -L und -l manuell an, z.B:

    CFLAGS=-I/home/b/root/include -g -Wall -O3
    LDLIBS=-L/home/b/root/lib -lweirdlib
  • Nachdem du eine Bibliothek und ihren Speicherort in die Zeilen LDLIBS und CFLAGS eingefügt hast und weißt, dass sie auf deinem System funktioniert, gibt es kaum noch einen Grund, sie zu entfernen. Macht es dir wirklich etwas aus, dass die endgültige ausführbare Datei 10 Kilobyte größer sein könnte, als wenn du für jedes Programm ein neues Makefile anpasst? Das bedeutet, dass du ein Makefile schreiben kannst, das zusammenfasst, wo sich alle Bibliotheken auf deinem System befinden, und es von Projekt zu Projekt kopieren kannst, ohne es neu zu schreiben.

  • Wenn dein Programm eine zweite (oder mehrere) C-Datei(en) benötigt, füge second.o, third.o usw. in die Zeile OBJECTS (keine Kommas, nur Leerzeichen zwischen den Namen) im Makefile am Anfang dieses Abschnitts ein.

  • Wenn du ein Programm hast, das aus einer einzigen .c-Datei besteht, brauchst du vielleicht gar kein Makefile. In einem Verzeichnis ohne Makefile und erf.c von vorhin kannst du versuchen, deine Shell zu benutzen:

    export CFLAGS='-g -Wall -O3 -std=gnu11'
    export LDLIBS='-lm'
    make erf

    und beobachte, wie make sein Wissen über die C-Kompilierung nutzt, um den Rest zu erledigen.

Bibliotheken aus der Quelle verwenden

Bis jetzt ging es darum, deinen eigenen Code mit make zu kompilieren. Das Kompilieren von Code, der von anderen bereitgestellt wird, ist oft eine andere Geschichte.

Lass uns ein Beispielpaket ausprobieren. Die GNU Scientific Library enthält eine Vielzahl von Routinen für numerische Berechnungen.

Die GSL wird über Autotools verpackt, eine Reihe von Werkzeugen, die eine Bibliothek für die Verwendung auf jedem Rechner vorbereiten, indem sie jede bekannte Macke testen und die entsprechende Abhilfe implementieren. Wie du deine eigenen Programme und Bibliotheken mit Autotools verpacken kannst, erfährst du in "Packaging Your Code with Autotools". Aber jetzt können wir erst einmal als Nutzer des Systems anfangen und die Leichtigkeit genießen, nützliche Bibliotheken schnell zu installieren.

Die GSL wird oft in vorkompilierter Form über den Paketmanager bereitgestellt, aber um die Schritte der Kompilierung durchzugehen, erfährst du hier, wie du die GSL als Quellcode bekommst und einrichtest, vorausgesetzt, du hast Root-Rechte auf deinem Computer.

wget ftp://ftp.gnu.org/gnu/gsl/gsl-1.16.tar.gz 1
tar xvzf gsl-*gz                               2
cd gsl-1.16
./configure                                    3
make
sudo make install                              4
1

Lade das gezippte Archiv herunter. Bitte deinen Paketmanager, wget zu installieren, wenn du es nicht hast, oder gib diese URL in deinen Browser ein.

2

Entpacke das Archiv: x=extract, v=verbose, z=unzip via gzip, f=filename.

3

Bestimme die Eigenheiten deines Rechners. Wenn du im Schritt configure eine Fehlermeldung über ein fehlendes Element erhältst, nutze deinen Paketmanager, um es zu beschaffen und führe configure erneut aus.

4

Installiere am richtigen Ort - wenn du die entsprechenden Rechte hast.

Wenn du das zu Hause ausprobierst, hast du wahrscheinlich Root-Rechte und es wird gut funktionieren. Wenn du auf der Arbeit bist und einen gemeinsam genutzten Server verwendest, sind die Chancen gering, dass du Superuser-Rechte hast. Du wirst also nicht in der Lage sein, das Passwort anzugeben, das du brauchst, um den letzten Schritt des Skripts als Superuser auszuführen. In diesem Fall musst du dich bis zum nächsten Abschnitt gedulden.

Wurde es installiert? In Beispiel 1-3 findest du ein kurzes Programm, mit dem du versuchen kannst, das 95 %-Konfidenzintervall mit Hilfe der GSL-Funktionen zu ermitteln; probiere es aus und schau, ob du es verknüpfen und zum Laufen bringen kannst:

Beispiel 1-3. Wiederholung von Beispiel 1-1 mit der GSL (gsl_erf.c)
#include <gsl/gsl_cdf.h>
#include <stdio.h>

int main(){
    double bottom_tail = gsl_cdf_gaussian_P(-1.96, 1);
    printf("Area between [-1.96, 1.96]: %g\n", 1-2*bottom_tail);
}

Um die soeben installierte Bibliothek zu verwenden, musst du das Makefile deines bibliotheksverwendenden Programms ändern, um die Bibliotheken und ihre Speicherorte anzugeben.

Je nachdem, ob du pkg-config zur Hand hast, kannst du eines davon machen:

LDLIBS=`pkg-config --libs gsl`
#or
LDLIBS=-lgsl -lgslcblas -lm

Wenn es nicht an einem Standardort installiert wurde und pkg-config nicht verfügbar ist, musst du Pfade zu den Köpfen dieser Definitionen hinzufügen, z.B. CFLAGS=-I/usr/local/include und LDLIBS=-L/usr/local/lib -Wl,-R/usr/local/lib.

Bibliotheken aus dem Quellcode verwenden (auch wenn dein Sysadmin das nicht will)

Du hast vielleicht keinen Root-Zugang, wenn du einen gemeinsam genutzten Computer auf der Arbeit benutzt oder wenn du zu Hause einen besonders kontrollierenden Partner hast. Dann musst du in den Untergrund gehen und dein eigenes privates Root-Verzeichnis erstellen.

Der erste Schritt besteht darin, das Verzeichnis einfach zu erstellen:

mkdir ~/root

Ich habe bereits ein ~/tech-Verzeichnis, in dem ich alle meine technische Logistik, Handbücher und Codeschnipsel aufbewahre, also habe ich ein ~/tech/root-Verzeichnis erstellt. Der Name spielt keine Rolle, aber ich benutze ~/root als das Dummy-Verzeichnis hier.

Hinweis

Deine Shell ersetzt die Tilde durch den vollständigen Pfad zu deinem Heimatverzeichnis und erspart dir so eine Menge Tipparbeit. Der POSIX-Standard verlangt, dass die Shell dies nur am Anfang eines Wortes oder direkt nach einem Doppelpunkt tut (was du für eine Pfadvariable brauchst), aber die meisten Shells erweitern Tilden auch in der Mitte eines Wortes. Andere Programme, wie z. B. make, erkennen die Tilde möglicherweise nicht als dein Heimatverzeichnis. In diesen Fällen kannst du die von POSIX vorgeschriebene Umgebungsvariable HOME verwenden, wie in den folgenden Beispielen.

Der zweite Schritt besteht darin, den richtigen Teil deines neuen Root-Systems zu allen relevanten Pfaden hinzuzufügen. Bei Programmen ist das die PATH in deiner .bashrc (oder einer entsprechenden Datei):

PATH=~/root/bin:$PATH

Wenn du das Unterverzeichnis bin deines neuen Verzeichnisses vor das ursprüngliche PATH setzt, wird es zuerst durchsucht und deine Kopie aller Programme wird zuerst gefunden. So kannst du alle Programme, die sich bereits in den gemeinsamen Standardverzeichnissen des Systems befinden, durch deine bevorzugte Version ersetzen.

Für die Bibliotheken, die du in deine C-Programme einbindest, notiere die neuen Pfade, die du im vorhergehenden Makefile suchen musst:

LDLIBS=-L$(HOME)/root/lib (plus the other flags, like -lgsl -lm ...)
CFLAGS=-I$(HOME)/root/include (plus -g -Wall -O3 ...)

Jetzt, wo du ein lokales Root-System hast, kannst du es auch für andere Systeme verwenden, z. B. für Javas CLASSPATH.

Der letzte Schritt besteht darin, Programme in deinem neuen Stammverzeichnis zu installieren. Wenn du den Quellcode hast und er Autotools verwendet, musst du nur noch Folgendes hinzufügen --prefix=$HOME/root an der richtigen Stelle einfügen:

./configure --prefix=$HOME/root && make && make install

Du brauchst sudo nicht, um den Installationsschritt zu machen, denn jetzt liegt alles in deinem Einflussbereich.

Da die Programme und Bibliotheken in deinem Home-Verzeichnis liegen und nicht mehr Rechte haben als du, kann sich dein Sysadmin nicht beschweren, dass sie eine Zumutung für andere sind. Wenn sich dein Systemadministrator trotzdem beschwert, dann ist es, so traurig das auch sein mag, vielleicht an der Zeit, sich zu trennen.

C-Programme über Here Document kompilieren

An diesem Punkt hast du das Muster der Zusammenstellung schon ein paar Mal gesehen:

  1. Setzt eine Variable, die Compilerflags ausdrückt.

  2. Setze eine Variable, die Linker-Flags ausdrückt, einschließlich eines -l Flags für jede Bibliothek, die du verwendest.

  3. Verwende make oder die Rezepte deiner IDE, um die Variablen in vollständige Kompilier- und Linkbefehle umzuwandeln.

Im restlichen Teil dieses Kapitels werden wir all das ein letztes Mal tun, und zwar mit einer absolut minimalen Einrichtung: nur der Shell. Wenn du ein kinetischer Lerner bist, der Skriptsprachen durch Ausschneiden und Einfügen von Codeschnipseln in den Interpreter erlernt hat, kannst du dasselbe mit dem Einfügen von C-Code in deine Eingabeaufforderung tun.

Einbinden von Header-Dateien über die Kommandozeile

gcc und clang haben ein praktisches Flag zum Einfügen von Kopfzeilen. Zum Beispiel:

gcc -include stdio.h

ist gleichbedeutend mit

#include <stdio.h>

am Anfang deiner C-Datei; das Gleiche gilt für clang -include stdio.h.

Wenn wir das zu unserem Compileraufruf hinzufügen, können wir hello.c endlich als die eine Codezeile schreiben, die sie sein sollte:

int main(){ printf("Hello, world.\n"); }

die sich gut übersetzen lässt:

gcc -include stdio.h hello.c -o hi --std=gnu99 -Wall -g -O3

oder Shell-Befehle wie:

export CFLAGS='-g -Wall -include stdio.h'
export CC=c99 
make hello

Dieser Tipp über -include ist compilerspezifisch und beinhaltet das Verschieben von Informationen aus dem Code in die Kompilieranweisungen. Wenn du denkst, dass das eine schlechte Form ist, dann überspringe diesen Tipp.

Die einheitliche Überschrift

Erlaube mir, für ein paar Absätze auf das Thema Header-Dateien abzuschweifen. Um nützlich zu sein, muss eine Header-Datei die Typendefinitionen, Makrodefinitionen und Funktionsdeklarationen für Typen, Makros und Funktionen enthalten, die von der Codedatei, die den Header enthält, verwendet werden. Außerdem sollte sie keine Typendefinitionen, Makrodefinitionen und Funktionsdeklarationen enthalten, die von der Codedatei nicht verwendet werden.

Um diese beiden Bedingungen wirklich zu erfüllen, müsstest du für jede Codedatei einen eigenen Header schreiben, der genau die relevanten Teile für die aktuelle Codedatei enthält. Das macht eigentlich niemand.

Es gab einmal eine Zeit, in der Compiler mehrere Sekunden oder Minuten brauchten, um selbst relativ einfache Programme zu kompilieren. Meine aktuellen Kopien von stdio.h und stdlib.h sind jeweils etwa 1.000 Zeilen lang (probier mal wc -l /usr/include/stdlib.h) und time.h weitere 400, was bedeutet, dass dieses siebenzeilige Programm:

#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    srand(time(NULL));       // Initialize RNG seed.
    printf("%i\n", rand());  // Make one draw.
}

ist eigentlich ein ~2.400 Zeilen langes Programm.

Dein Compiler hält 2.400 Zeilen nicht mehr für eine große Sache und kompiliert den Text in weniger als einer Sekunde. Der Trend geht also dahin, den Benutzern Zeit bei der Auswahl von Kopfzeilen zu sparen, indem mehr Elemente in eine einzige Kopfzeile aufgenommen werden.

Du wirst später Beispiele sehen, die GLib verwenden, mit einer #include <glib.h> am Anfang. Diese Kopfzeile enthält 74 Unterüberschriften, die alle Unterabschnitte der GLib-Bibliothek abdecken. Das GLib-Team hat die Benutzeroberfläche gut gestaltet, denn wer keine Zeit damit verbringen will, die richtigen Unterabschnitte der Bibliothek herauszusuchen, kann sich in einer Zeile durch den Header-Papierkram wühlen, während diejenigen, die eine detaillierte Kontrolle wünschen, genau die Header auswählen können, die sie brauchen. Es wäre schön, wenn die C-Standardbibliothek einen schnellen und einfachen Header wie diesen hätte; in den 1980er Jahren war das noch nicht üblich, aber es ist leicht, einen zu erstellen.

Wenn du einen öffentlichen Header für andere Benutzer schreibst, sollte dein Header gemäß der Regel, dass ein Header keine unnötigen Elemente enthalten sollte, wahrscheinlich keine #include "allheads.h" haben, die alle Definitionen und Deklarationen der Standardbibliothek enthält. Das ist in der Regel richtig: Deine Bibliothek hat vielleicht ein Codesegment, das die verknüpften Listen von GLib benutzt, um zu arbeiten, aber das bedeutet, dass du #include <glib.h> in dieser Codedatei und nicht im Header der öffentlichen Bibliothek aufrufen musst.

Um auf die Idee zurückzukommen, eine schnelle Kompilierung auf der Kommandozeile einzurichten, macht der einheitliche Header das Schreiben schneller Programme noch schneller. Sobald du einen einheitlichen Header hast, ist sogar eine Zeile wie #include <allheads.h> überflüssig, wenn du gcc oder clang benutzt, weil du stattdessen -include allheads.h zu deinem CFLAGS hinzufügen kannst und nie wieder darüber nachdenken musst, welche projektfremden Header du einfügen musst.

Hier Dokumente

Hier sind Dokumente eine Funktion von POSIX-Standard-Shells, die du für C, Python, Perl oder was auch immer verwenden kannst, und sie machen dieses Buch viel nützlicher und lustiger. Wenn du ein mehrsprachiges Skript erstellen willst, sind diese Dokumente ein einfacher Weg, dies zu tun. Du kannst in Perl parsen, in C rechnen und dann mit Gnuplot die hübschen Bilder erstellen.

Hier ist ein Python-Beispiel. Normalerweise würdest du Python sagen, dass es ein Skript ausführen soll:

python your_script.py

In Python kannst du den Dateinamen - angeben, um stdin als Eingabedatei zu verwenden:

echo "print 'hi.'" | python -

Theoretisch könntest du einige lange Skripte über echo in die Befehlszeile eingeben, aber du wirst schnell feststellen, dass es viele kleine, unerwünschte Parsings gibt - du brauchst zum Beispiel \"hi\" statt "hi".

Daher das Dokument hier, das überhaupt kein Parsing durchführt. Versuch das mal:

python - <<"XXXX"
lines=2
print "\nThis script is %i lines long.\n" %(lines,)
XXXX
  • Diese Dokumente sind eine Standard-Shell-Funktion, d.h. sie sollten auf jedem POSIX-System funktionieren.

  • "XXXX" ist eine beliebige Zeichenkette; "EOF" ist ebenfalls beliebt, und "-----" sieht gut aus, solange die Anzahl der Bindestriche oben und unten übereinstimmt. Wenn die Shell die von dir gewählte Zeichenkette allein in einer Zeile sieht, hört sie auf, das Skript an die stdin des Programms zu senden. Das ist alles, was beim Parsen passiert.

  • Es gibt auch eine Variante, die mit <<- beginnt. Bei dieser Variante werden alle Tabulatoren am Anfang jeder Zeile entfernt, so dass du ein here-Dokument in einen eingerückten Abschnitt eines Shell-Skripts einfügen kannst, ohne den Fluss der Einrückung zu unterbrechen. Für ein here-Dokument in Python wäre das natürlich eine Katastrophe.

  • Als weitere Variante gibt es einen Unterschied zwischen <<"XXXX" und <<XXXX. In der zweiten Version parst die Shell bestimmte Elemente, d.h. du kannst die Shell den Wert von $shell_variables für dich einfügen. Die Shell verlässt sich bei ihren Variablen und anderen Erweiterungen stark auf $; $ ist eines der wenigen Zeichen auf einer Standardtastatur, das für C keine besondere Bedeutung hat. Es ist, als ob die Leute, die Unix geschrieben haben, es von Grund auf so konzipiert haben, dass es einfach ist, Shell-Skripte zu schreiben, die C-Code erzeugen....

Kompilieren von stdin

OK, zurück zu C: Wir können hier Dokumente verwenden, um C-Code zu kompilieren, der über gcc oder clang in die Kommandozeile eingefügt wurde.

Wir werden das Makefile nicht verwenden, also brauchen wir einen einzigen Kompilierbefehl. Um uns das Leben weniger schwer zu machen, geben wir ihm einen Alias. Füge diesen Befehl in deine Befehlszeile ein oder füge ihn in deine .bashrc, .zshrc oder wo auch immer ein:

go_libs="-lm"
go_flags="-g -Wall -include allheads.h -O3"
alias go_c="c99 -xc - $go_libs $go_flags"

wobei allheads.h der Aggregatkopf ist, den du zuvor zusammengestellt hast. Die Verwendung des -include Flag bedeutet, dass du beim Schreiben des C-Codes an eine Sache weniger denken musst, und ich habe festgestellt, dass die Geschichte der Bash durcheinander gerät, wenn #im C-Code vorkommt.

In der Kompilierzeile erkennst du an -, dass du stdin verwenden sollst, anstatt aus einer benannten Datei zu lesen. -xc weist dies als C-Code aus, denn gcc steht für GNU Compiler Collection und nicht für GNU C Compiler. Da der Name der Eingabedatei nicht auf .c endet, müssen wir uns darüber im Klaren sein, dass es sich hier nicht um Java, Fortran, Objective C, Ada oder C++ handelt (und das Gleiche gilt für clang, auch wenn der Name die Sprache C aufrufen soll).

Was auch immer du für die Anpassung von LDLIBS und CFLAGS in deinem Makefile getan hast, tue es hier.

Jetzt sind wir startklar und können C-Code auf der Kommandozeile kompilieren:

go_c << '---'
int main(){printf("Hello from the command line.\n");}
---
./a.out

Wir können hier ein Dokument verwenden, um kurze C-Programme auf der Kommandozeile einzufügen und kleine Testprogramme zu schreiben, ohne dass es zu Problemen kommt. Du brauchst nicht nur kein Makefile, sondern nicht einmal eine Eingabedatei.4

Erwarte nicht, dass diese Art von Arbeit deine Hauptaufgabe ist. Aber das Ausschneiden und Einfügen von Codeschnipseln in die Kommandozeile kann Spaß machen, und die Möglichkeit, einen einzelnen Schritt in C in einem längeren Shell-Skript zu haben, ist ziemlich fabelhaft.

1 Cygwin ist ein von Red Hat, Inc. betriebenes Projekt, das es den Nutzern auch ermöglicht, das Recht zu erwerben, ihren Quellcode nicht gemäß der GPL weiterzugeben.

2 Obwohl MinGW über einen Paketmanager verfügt, der die Systemgrundlagen installiert und eine Reihe von Bibliotheken bereitstellt (hauptsächlich die, die für MinGW selbst benötigt werden), verblasst diese Handvoll vorkompilierter Bibliotheken im Vergleich zu den Hunderten von Paketen, die der typische Paketmanager bereitstellt. Tatsächlich hat der Paketmanager für meine Linux-Box mehr MinGW-kompilierte Bibliotheken als der MinGW-Paketmanager. Dies ist der aktuelle Stand; wenn du dies liest, haben Nutzer wie du vielleicht schon mehr Pakete zum MinGW-Repository beigetragen.

3 Du kannst find / -type f | wc -l ausprobieren, um eine ungefähre Dateizählung auf jedem System mit POSIX-Standard zu erhalten.

4 Es gibt einen POSIX-Brauch, dass, wenn die erste Zeile von file ist #!aninterpreterist, dann wird beim Ausführen von file von der Shell aus ausführt, wird die Shell tatsächlich aninterpreter file. Das funktioniert gut bei interpretierten Sprachen wie Perl oder Python (vor allem, weil diese Sprachen # als Kommentarmarker verwenden und daher die erste Zeile ignorieren). Mit den Hinweisen in diesem Abschnitt könntest du ein Skript schreiben (nennen wir es c99sh), das mit einer C-Datei, die mit #!c99sh beginnt, das Richtige tut: die erste Zeile korrigieren, den Rest der Datei über eine Pipe an den Compiler schicken und dann das resultierende Programm ausführen. Rhys Ulerich hat jedoch bereits ein solches c99sh für dich geschrieben und das Skript auf GitHub veröffentlicht.

Get 21st Century C, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.