Inhaltsverzeichnis

Kapitel zwölf - Header Dateien und der Preprozessor

Aufteilen von Code in mehrere Dateien

Alle Beispiele, die wir bisher gesehen haben, haben den gesamten Code für ein Programm in einer einzigen C-Datei zusammengefasst. Sobald Programme jedoch groß werden, ist es sinnvoller, sie in separate Dateien aufzuteilen und ähnliche Funktionen zu gruppieren. Um zu verstehen, wie dies funktioniert, müssen wir uns genauer ansehen, was der Compiler tatsächlich tut.

In allen bisherigen Beispielen haben wir gcc für eine einzelne Quelldatei aufgerufen und ein einzelnes ausführbares Programm erstellt. Dies verbirgt die Tatsache, dass gcc tatsächlich zwei Dinge tut: Erstens kompiliert es Ihre C-Quelldatei in eine sogenannte Objektdatei und verknüpft dann die Objektdatei mit allen Libary Funktionen, um die ausführbare Datei zu erstellen. Dieser zweite Schritt wird von einem Programm ausgeführt, das als Linker bezeichnet wird. gcc erledigt eigentlich beide Jobs.

Wenn Sie ein Programm mit mehreren Quelldateien erstellen, müssen Sie nur die Namen aller Quelldateien in den Aufruf von gcc aufnehmen. Anschließend wird für jede Quelldatei eine Objektdatei erstellt und anschließend alle Objektdateien miteinander verknüpft, um die ausführbare Datei zu erstellen.

Es gibt jedoch einen Haken. Wenn Sie Ihren Code in separate Dateien aufgeteilt haben (normalerweise als Module bezeichnet), verfügen Sie über einige Dateien, die Funktionen in anderen Dateien aufrufen, um zu funktionieren. Diese Dateien erfahren nichts voneinander, bis der Linker sie bearbeitet. Die Dateien werden einzeln kompiliert, und der Compiler beschwert sich, wenn Sie Funktionen in einer Datei verwenden, von der er nichts weiß.

[NAMEN EINHEITLICH HALTEN]
Sie können zwar Header-Dateien aufrufen, wie Sie möchten - die Namen sind nicht magisch -, aber es empfiehlt sich, der Header-Datei für die Funktionen in einer bestimmten C-Datei den gleichen Namen wie der C-Datei selbst zu geben, und zwar mit der Erweiterung .h anstelle von .c. Dies erleichtert es demjenigen, der Ihren Code liest, die Dateien zu finden, in denen Funktionen definiert sind.

Wir beheben dies mit Hilfe von Header-Dateien. Hierbei handelt es sich um Dateien mit der Erweiterung .h, die die in einem Modul definierten Funktionsdeklarationen enthalten, damit der Compiler darüber informiert werden kann, wenn sie von einem anderen Modul verwendet werden. Das haben wir schon oft gesehen. Erinnern Sie sich an die Zeile #include <stdio.h> oben in den Beispielen? Das ist genau dieser Prozess; Es teilt dem Compiler mit, dass Funktionen, die in der Systemheaderdatei stdio.h deklariert sind, in diesem Modul verwendet werden.

Aufteilen von Code in mehrere Dateien

Schauen wir uns ein Beispiel an, wie dies funktioniert. Erstellen Sie drei Dateien, zwei mit der Erweiterung .c und eine mit der Erweiterung .h, wie folgt:

function.c
 
int add_vals (int a, int b, int c)
{
  return a + b + c;
}
function.h
 
extern int add_vals (int a, int b, int c);
main.c
 
#include <stdio.h>
#include "function.h"
 
void main (void)
{
  printf ("Die gesamtsumme ist %d\n", add_vals (1, 2, 3));
}

Legen Sie alle drei Dateien in das selbe Verzeichnis und führen Sie gcc aus, wobei Sie die Namen beider .c-Dateien angeben - gcc –o myprog main.c function.c. Das resultierende Programm führt die Hauptfunktion von main.c aus aus, wodurch die Funktion add_vals von function.c aufgerufen wird.

Ein paar Dinge zur Beachtung. Zunächst deklarieren wir in der Header-Datei die Funktion mit dem Wort extern zu Beginn der Deklaration. Dies teilt dem Compiler mit, dass diese Funktion außerhalb der Datei zu finden ist, d.h. in einer anderen C-Datei.

Zweitens, während wir stdio.h immer mit seinem Namen zwischen <> Zeichen eingefügt haben, schließen wir function.h in doppelte Anführungszeichen ein. Die Zeichen <> weisen den Compiler an, nach der Datei in dem Verzeichnis zu suchen, in dem die Include-Dateien des Systems gespeichert sind. Die "" Zeichen zeigen an, dass die Datei lokal ist und sich im selben Verzeichnis befindet wie die .c-Dateien, die Sie erstellen. Wenn Sie Ihre eigenen Header-Dateien erstellen, schliessen Sie ihn beim Einfügen immer in doppelte Anführungszeichen ein.

pi@raspberry:~ $ ./myprog
Die gesamtsumme ist 6
pi@raspberry:~ $

Oben: Die Funktion add_vals wird von der main Funktion aufgerufen - der Linker verbindet den Aufruf von main.c mit der Funktionsdefinition in function.c

[#INCLUDE DATEIEN MIT DER ENDUNG .C NICHT EINSCHLIESSEN]
#include funktioniert für jede Datei. Es ersetzt lediglich die Include-Zeile durch den Inhalt der Datei. Gelegentlich wird dies missbraucht. Einige Programmierer umgehen die Verwendung von Header-Dateien, indem sie nur die anderen C-Dateien selbst einschließen. Dies funktioniert zwar, ist aber eine schlechte Praxis. Versuchen Sie es nicht!

Der Präprozessor

Was macht #include eigentlich? Dies ist eine Anweisung an den Präprozessor. Dies ist die erste Phase des Kompilierens. Es ersetzt Text in Quelldateien, bevor es an den Compiler selbst übergeben wird. Der Präprozessor wird mit sogenannten Direktiven gesteuert. Diese sind leicht zu erkennen, da sie alle mit einem #-Zeichen beginnen.

Die Anweisung #include weist den Präprozessor an, die Zeile durch die darin enthaltene Datei zu ersetzen. In unserem obigen Beispiel wird die Zeile #include „function.h“ in der .c-Datei durch den Inhalt der Datei function.h ersetzt. Dies bedeutet, dass das, was an den Compiler übergeben wird, folgendermaßen aussieht:

#include <stdio.h>
extern int add_vals (int a, int b, int c);
 
void main (void)
{
  printf ("Die gesamtsumme ist %d\n", add_vals (1, 2, 3));
}
[MAKEFILES]
Wie Sie sich vorstellen können, wäre es bei einem Projekt mit zehn oder Hunderten von C-Dateien etwas mühsam, jedes Mal alle Namen in den Aufruf von gcc einzugeben! Große Projekte werden mit einem Tool namens „make“ erstellt, das Build-Anweisungen in einem „Makefile“ speichert. Makefiles fallen nicht in den Geltungsbereich dieses Buches, aber es gibt online viele Informationen dazu.

#define

Eine weitere nützliche Anweisung ist #define, mit der konstante Werte definiert werden können. Schauen Sie sich dieses Beispiel an:

#include <stdio.h>
#define PI 3.14159
 
void main (void)
{
  float rad = 3;
  float circ = rad * 2 * PI;
  float area = rad * rad * PI;
  printf ("Der Umfang eines Kreisradius %f ist %f\n",
  rad, circ);
  printf ("Die Fläche eines Kreisradius %f ist %f\n", rad, area);
}

Die Direktive #define wird verwendet, um den Wert von PI festzulegen. Wichtig ist, dass PI keine Variable ist. Dieser Text wird durch den Präprozessor ersetzt. Die Zeile #define weist den Präprozessor an, die Datei zu durchsuchen und jede Instanz des Symbols PI durch die Ziffern 3.14159 zu ersetzen, bevor sie an den Compiler übergeben wird. Eine Zeile, die so etwas wie PI = 5; macht wird einen Fehler verursachen. Der Compiler sieht die bedeutungslose Anweisung 3.14159 = 5;.

Warum ist das nützlich? Warum nicht einfach eine Float-Variable namens PI deklarieren und auf 3.14159 setzen? Für eine Gleitkommavariable muss Speicher zugewiesen werden, in dem sie gespeichert werden soll. Die Verwendung von #define speichert diesen Speicher, was nützlich ist, wenn der Speicher begrenzt ist.

Sie können mit #define auch Funktionen definieren:

#include <stdio.h>
#define ADD(a,b) (a+b)
 
void main (void)
{
  printf ("Die Summe von %d und %d ist %d\n", 5, 2, ADD(5,2));
  printf ("Die Summe von %d und %d ist %d\n", 3, 7, ADD(3,7));
}

Dies führt wiederum eine Textersetzung durch. Wenn ADD(a,b) im Code erscheint, wird es durch (a+b) ersetzt, wobei die Werte von a und b durch die Argumente für ADD ersetzt werden.

[#DEFINES FÜR TEXT]
Wenn Sie #define für Textzeichenfolgen verwenden, sollten diese in doppelte Anführungszeichen gesetzt werden, da sonst der ersetzte Text am ersten Leerzeichen endet. Verwenden Sie also #define MY_TEXT "Dies ist ein zu ersetzender Text." Die doppelten Anführungszeichen sind im Ersatz enthalten, sodass Sie dann einfach printf (MY_TEXT) aufrufen können.

Der Präprozessor kann Bedingungen auch mit der Direktive #if auswerten:

#include <stdio.h>
 
void main (void)
{
  #if 0
    printf ("Ein Code\n");
  #else
    printf ("Ein anderer Code\n");
  #endif
}

Oben: Die häufigste Verwendung von #if ist das vorübergehende Entfernen von Code - wickeln Sie ihn einfach zwischen #if 0 und #endif ein. Das #else ist optional, aber manchmal möchten Sie den Code, den Sie entfernt haben, durch einen anderen Code ersetzen.

Mit einer 0 nach dem #if wird der Code zwischen dem #if und dem #else nicht aufgerufen, der Code zwischen dem #else und dem #endif jedoch schon. Wenn Sie den Wert nach #if in 1 ändern, wird der Code zwischen #if und #else aufgerufen, der Code zwischen #else und #endif jedoch nicht. Dies ist ein wirklich nützlicher Trick, um einen Code beim Debuggen vorübergehend zu entfernen oder zu ersetzen.


Zurück zum Inhalt    Vorige Seite    Nächste Seite