Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
Über die Autoren
Über dieses Buch
Linux vs. BSD
1 Der Kernel
2 Die Grundlagen aus Anwendersicht
3 Die Shell
4 Reguläre Ausdrücke
5 Tools zur Dateibearbeitung
6 Die Editoren
7 Shellskriptprogrammierung
8 Benutzerverwaltung
9 Grundlegende Verwaltungsaufgaben
10 Netzwerk-Grundlagen
11 Anwendersoftware für das Netzwerk
12 Netzwerkdienste
13 Mailserver unter Linux
14 LAMP
15 DNS-Server
16 Secure Shell
17 Die grafische Oberfläche
18 Window-Manager und Desktops
19 X11-Programme
20 Multimedia und Spiele
21 Softwareentwicklung
22 Crashkurs in C und Perl
23 Sicherheit
24 Prozesse und IPC
25 Bootstrap und Shutdown
26 Dateisysteme
27 Virtualisierung und Emulatoren
A Die Installation
B Lösungen zu den einzelnen Aufgaben
C Kommandoreferenz
D X11-InputDevices
E MBR
F Die Buch-DVDs
G Glossar
H Literatur

Download:
- ZIP, ca. 6,3 MB
Buch bestellen
Ihre Meinung?

Spacer
 <<   zurück
Linux von Johannes Plötner, Steffen Wendzel
Das distributionsunabhängige Handbuch
Buch: Linux

Linux
2., aktualisierte und erweiterte Auflage
1119 S., 39,90 Euro
Galileo Computing
ISBN 978-3-8362-1090-4
gp 22 Crashkurs in C und Perl
  gp 22.1 Die Programmiersprache C – Ein Crashkurs
    gp 22.1.1 Hello World in C
    gp 22.1.2 Kommentare
    gp 22.1.3 Datentypen und Variablen
    gp 22.1.4 Operatoren
    gp 22.1.5 Bedingte Anweisungen
    gp 22.1.6 Schleifen
    gp 22.1.7 Funktionen
    gp 22.1.8 Präprozessor-Direktiven
    gp 22.1.9 Grundlagen der Zeiger (Pointer)
    gp 22.1.10 Grundlagen der Arrays
    gp 22.1.11 Strukturen
    gp 22.1.12 Arbeiten mit Zeichenketten (Strings)
    gp 22.1.13 Einlesen von Daten
    gp 22.1.14 FILE und das Arbeiten mit Dateien
    gp 22.1.15 Das war noch nicht alles!
  gp 22.2 Die Skriptsprache Perl
    gp 22.2.1 Aufbau eines Perl-Skripts
    gp 22.2.2 Variablen in Perl
    gp 22.2.3 Kontrollstrukturen
    gp 22.2.4 Subroutinen in Perl
    gp 22.2.5 Reguläre Ausdrücke in Perl
    gp 22.2.6 Arbeiten mit dem Dateisystem
  gp 22.3 Zusammenfassung
  gp 22.4 Aufgaben

»Ein Optimist ist ein Mensch, der ein Dutzend Austern bestellt, in der Hoffnung, sie mit der Perle, die er darin findet, bezahlen zu können.« -- Theodor Fontane

22 Crashkurs in C und Perl

Beim Thema »Programmieren unter Linux« beziehungsweise »Programmieren von Linux« gibt es vor allem drei wichtige Programmiersprachen: <Dieses Kapitel war eigentlich als Bestandteil des vorherigen Kapitels »Softwareentwicklung unter Linux« geplant, wurde aber so groß, dass wir es nun doch ausgegliedert haben.>

  • Die Shell
  • Mit der Shell sollten Sie bereits vertraut sein und sie dank vorheriger Kapitel auch programmieren können.
  • C
  • Die meisten Systemprogramme sowie fast der gesamte Kernel sind in dieser Sprache codiert.
  • Perl
  • Perl ist eine universelle Programmiersprache, die für alles nützlich ist, außer für die Kernel-Programmierung.

Wir geben Ihnen in diesem Buch eine Einführung in alle diese Sprachen – und da Sie die Shell schon kennen, folgt an dieser Stelle ein Crashkurs in C und Perl. Diese Sprachen bieten Ihnen dann die Möglichkeit, immer weiter in die Tiefen von Linux hinabzusteigen und immer mehr zu verstehen. Niemand kann Sie aufhalten. ;-)


Galileo Computing

22.1 Die Programmiersprache C – Ein Crashkurs  downtop

In diesem Buch wurde bereits die Programmierung mit der Shell bash besprochen, warum also noch mehr Programmiersprachen? Diese Frage ist leicht zu beantworten, da wir uns zwei ganz besondere Sprachen hierfür herrausgesucht haben.

Zum einen ist das die Programmiersprache C, die in diesem Abschnitt behandelt wird, und zum anderen ist dies die Skriptsprache Perl, die im nächsten Abschnitt folgt. <Warum wir uns ausgerechnet noch für Perl entschieden haben, erfahren Sie im nächsten Abschnitt. Hier beschäftigen wir uns zunächst nur mit C.>

Fast der gesamte Linux-Kernel (und das gilt auch für die BSD-Kernel) ist in der Sprache C geschrieben.

Des Weiteren sind fast alle wichtigen Basisprogramme eines Linux- und BSD-Systems ebenfalls in C geschrieben. Aus diesem Grund ist die Bedeutung von C für diese Systeme offenbar äußerst groß. Und ganz davon abgesehen handelt es bei C um eine äußerst schöne und performante Programmiersprache.

Es stimmt, C ist keine objektorientierte Sprache, aber wenn Sie auf objektorientierte Entwicklung verzichten können, dann ist C eine gute Wahl für Sie. <Ausserdem lässt sich mit C-Kenntnissen wohl am einfachsten C++ lernen.> Hier noch ein paar Gründe für C:

  • C ist äußerst performant! Sie können nur mit äußerst ausgefeiltem Assembler die Performance von compiler-optimiertem C-Code erreichen oder gar übertreffen.
  • Sie können NUR mit dieser Programmiersprache (und in einigen Bereichen mit noch etwas Know-How in Assembler) die tieferen Interna von Linux und BSD verstehen.
  • (ANSI-)C ist wesentlich portabler als etwa Java.
  • Und C sieht gut aus. ;-)

Da C eine sehr komplexe Programmiersprache ist, können wir Ihnen hier nur eine sehr grobe und kurze Einführung in diese schöne Sprache geben. Auf galileocomputing.de finden Sie allerdings das OpenBook »C von A bis Z« von Jürgen Wolf, das Sie auch in die tieferen Details der Sprache einführt. Anschließend sollten Sie dann noch einen Blick in »Expert C Programming: Deep C Secrets« von Peter Van Der Linden (SunSoft Press) werfen, das es leider nur in englischer Sprache gibt.

Zum Thema Linux/Unix/BSD-Programmierung in C gibt es auch diverse gute Bücher, die aber auch etwas Vorwissen verlangen:

  • »Linux Unix Programmierung« von Jürgen Wolf, erschienen bei Galileo Computing und verfügbar als OpenBook. Dieses Buch geht auch auf diverse Libraries (etwa Gtk und MySQL) ein. Es ist für die typischen Ansprüche der heutigen Leser wahrscheinlich am besten geeignet, da man »alles« einmal kennenlernen möchte. Daher empfehlen wir dieses Buch auch generell erst inmal jedem. <Der Verlag hat uns übrigens nicht bestochen, es handelt sich tatsächlich um unsere Meinungen.>
  • »Linux Unix Systemprogrammierung« von Helmut Herold, erschienen bei Addison-Wesley. Dieses Buch beschäftigt sich sehr intensiv mit der reinen Systemprogrammierung und geht fast gar nicht auf Libraries ein. Es ist unsere Empfehlung für die zukünftigen Unix-Nerds unter unseren Lesern.
  • Es gibt noch einige andere deutschsprachive Grundlagenbücher, die wir aber nicht gelesen haben und daher auch nicht empfehlen können.

Natürlich gibt es noch weitere populäre Bücher, die erwähnenswert sind (wie »Advanced Programming in the Unix Environment« und »Programmieren von Unix-Netzwerken« von Richard Stevens). Viele weitere gute Bücher aus unserem Bücherschrank sind jedoch entweder zu alt oder zu speziell, um sie hier aufzulisten.


Galileo Computing

22.1.1 Hello World in C  downtop

Da wir Ihnen die Grundlagen dieser Sprache so schonend wie möglich beibringen möchten, haben wir uns dazu entschlossen, sie anhand von Beispielen zu erläutern. Das erste Programm, was zu diesem Zweck herhalten soll, ist natürlich wie in so ziemlich jedem C-Buch »Hello World«. Es dient zunächst als Einstieg, bevor wir etwas mehr ins Detail gehen.

#include <stdio.h> 
 
void main() 
{ 
    printf("Hello World!\n"); 
}

Listing 22.1    Ein schlechter »Hello World«-Code ohne Rückgabewert

Das sieht etwas kompliziert aus, ist aber ganz einfach. Wir beginnen mit dem Codeschnipsel int main(). Jedes Programm muss an einem bestimmten Punkt anfangen. Das heißt: Irgendeine Anweisung muss als Erste ausgeführt werden.

Aus der Shellskript-Programmierung kennen Sie bereits Funktionen. In C wird gleich zum Programmstart eine Funktion aufgerufen, und diese bestimmt dementsprechend, welche Anweisungen als Erstes ausgeführt werden. Diese Funktion heißt in C main.

Hinter einem Funktionsnamen stehen in C immer Klammern, die die Parameterliste der Funktion beinhalten (dazu später mehr). Hat eine Funktion keine Parameter, so können diese Klammern auch leer bleiben (wie im Falle der main-Funktion.

Zudem geben Funktionen immer entweder Daten eines bestimmten Datentyps zurück oder gar keinen. Der Rückgabetyp wird dabei vor die Implementierung der Funktion geschrieben. Ist der Rückgabetyp void, so wird nichts zurückgegeben.

Normalerweise ist dies bei der main-Funktion nicht so, doch der Einfachheit halber haben wir es im oberen Beispiel trotzdem so gelassen.

Die Implementierung einer Funktion wird, wie in der bash, durch geschweifte Klammern eingeschlossen. Ein Funktionsaufruf (wie der der Funktion printf) hingegen wird ohne diese Klammern erledigt und mit einem Semikolon beendet.

Im Falle der printf-Funktion wird nun auch erstmals ein Funktionsparameter übergeben: Diese Funktion gibt den Text, den man ihr übergibt, auf dem Monitor aus.

Zu guter Letzt, bevor wir uns mit den Details befassen, soll noch die erste Programmzeile erläutert werden. Es handelt sich dabei um eine Anweisung für den sogenannten »Präprozessor« des Compilers. Anweisungen an den Proprocessor beginnen generell mit einer Raute (#). Hinter der Raute steht ein spezieller Befehl, etwa include oder define, und dahinter stehen die Parameter für diesen Befehl.

Im Falle des Präprozessor-Befehls include wird der Inhalt einer anderen Datei durch den Präprozessor in den Quellcode eingebaut. Der Quellcode der anderen Datei wird genau dort in den eigentlichen Quellcode eingebaut, wo der Programmierer den include-Befehl eingetragen hat.

Die Datei stdio.h enthält dabei die Deklaration der printf-Funktion. In einer Funktionsdeklaration ist festgeschrieben ob eine Funktion einen Wert (und wenn ja: welchen Typ) zurückgibt und welche Parameter sie akzeptiert.

Außerdem ist festgelegt, von welchem Typ diese Parameter sind.

Den Code übersetzen

Wie Sie einige Seiten zuvor bereits gelernt haben, wird ein C-Programm mit dem GNU Compiler gcc folgendermaßen übersetzt (der Dateiname der Quellcode-Datei sei hello.c): <Ignorieren Sie die darauffolgende Warnmeldung zunächst einmal.>

$ gcc -o hello hello.c 
hello.c: In function 'main': 
hello.c:4: warning: return type of 'main' is not 'int'

Listing 22.2    hello.c übersetzen

Ausgeführt wird das Programm dann über die Shell als ganz normales Programm im Arbeitsverzeichnis:

$ ./hello 
Hello World

Listing 22.3    hello ausführen


Galileo Computing

22.1.2 Kommentare  downtop

Wie in jeder anderen vernünftigen Programmiersprache gibt es auch in C die Möglichkeit, Kommentare in den Quellcode zu schreiben. Diese Kommentare werden vom Compiler nicht in Maschinencode übersetzt. Ein Kommentar wird durch die zwei hintereinanderstehenden Zeichen /* eingeleitet und durch */ beendet. Ein Kommentar kann in C über mehrere Zeilen verteilt sein und endet nicht automatisch beim Zeilenende. <Einige Compiler unterstützen auch die C++-Kommentare. Diese werden durch // eingeleitet und durch das Zeilenende abgeschlossen, doch diese sind nicht im ANSI-C-Standard festgeschrieben!>

/* Die Datei stdio.h einbinden. Sie befindet sich im globalen 
 * include-Verzeichnis des Rechners (meist /usr/include) 
 */ 
#include <stdio.h> 
 
/* Die main-Funktion verwendet keine Parameter und gibt keinen 
 * Wert zurück (was schlecht ist). 
 */ 
void main() 
{ 
    /* Die printf-Funktion gibt Text aus */ 
    printf("Hello World!\n"); 
 
    /* Am Ende der main-Funktion wird das Programm beendet */ 
}

Listing 22.4    Das obige Programm mit Kommentaren

Übrigens: Kommentare werden nicht als Kommentare gewertet, wenn sie innerhalb von Zeichenketten stehen. Hier würde nur der Kommentar Teil der Textausgabe sein:

printf("/* Gib Text aus */  Hello World!\n");

Listing 22.5    Kein Kommentar


Galileo Computing

22.1.3 Datentypen und Variablen  downtop

In C gibt es etwas, was in nur wenigen anderen Sprachen so komplex ausgelegt ist: Datentypen. Anders als bei der Programmierung der Shell kann in C nicht einfach irgendeine Variable ohne Typ erzeugt werden, in der Sie dann fast alles speichern können. Variablen in C müssen immer von einem bestimmten Datentyp sein.

Ein Datentyp hat dabei immer einen Wertebereich. Dieser Wertebereich richtet sich nach der Zahl der Bits, die zum Speichern dieses Datentyps benutzt werden. In einem 8 Bit großen Datentyp können dementsprechend 28 = 256 verschiedene Werte gespeichert werden.

Variablen erzeugen

Bevor wir auf die eigentlichen Datentypen eingehen, möchten wir noch zeigen, wie Variablen allgemein angelegt werden. Zunächst wird der Typ der Variable angegeben, danach die Bezeichnung der Variable. Diese Erzeugungsanweisung wird mit einem Semikolon abgeschlossen.

Datentyp Variablenname;

Listing 22.6    Variable erzeugen

Deklaration

Man spricht hierbei von einer Deklaration einer Variablen.

Initialisierung

Wenn man möchte, dann kann man den Wert einer Variablen auch gleich bei der Deklaration setzen. Man spricht beim Setzen des ersten Wertes einer Variable von einer Initialisierung.

Datentyp Variablenname = Wert;

Listing 22.7    Variable deklarieren und initialisieren

Später im Programmcode kann man Variablen ebenfalls Werte zuweisen:

Variablenname = Wert;

Listing 22.8    Variable-Werte verändern

Bezeichner von Variablen

Die Namen, die man einer Variablen gibt, dürfen nicht völlig frei gewählt werden. Es gibt einige Regeln zu beachten:

  • C unterscheidet zwischen Groß- und Kleinschreibung. Die Variablen ABC, abc, aBc, AbC und ABc sind daher unterschiedlich.
  • Es dürfen keine C-Schlüsselwörter als Variablennamen verwendet werden.
  • Variablen können entweder mit einem Groß-/Kleinbuchstaben oder einem Underscore (_) beginnen.
  • Der restliche Teil des Variablennamens darf sich aus Groß-/Kleinbuchstaben, Underscores und Zahlen zusammensetzen.
  • Die Namenslänge für Variablen wird vom Compiler beschränkt. Es empfiehlt sich aber, keine zu langen Bezeichner zu verwenden. Der ANSI-C-Standard beschränkt die gültige Länge eines Bezeichners auf 31 Zeichen.
/* gültige Variablennamen */ 
i, abc, _def, __gh, i_j, k0l9m, MeineKatzeKannFliegen 
 
/* ungültige Variablennamen */ 
0MeineKatzeKannFliegen, =Sonderzeichen, mäh

Listing 22.9    (Un)gültige Variablennamen

Ausgabe von Variablen-Werten

Wir verwenden für die Ausgabe von Werten einer Variablen im Folgenden immer die Funktion printf. Dieser Funktion wird ein sogenannter Formatstring übergeben. Optional können – durch Kommata getrennt – nach dem Formatstring Variablen gelistet werden, die durch die Funktion ausgegeben werden sollen.

printf( Formatstring [, optionale Variablen] );

Listing 22.10    Schema eines printf-Aufrufs

Damit printf eine Variable verwendet, muss diese nicht nur übergeben, sondern auch im Formatstring angegeben werden. Wie dies funktioniert, erklären wir anschließend für jeden Datentyp einzeln.

Im Formatstring können printf zudem spezielle Escape-Zeichen übergeben werden, die von der Funktion umgewandelt werden. Der Einfachheit halber werden wir uns in diesem Crashkurs auf die Escape-Sequenz \n beschränken, das die aktuelle Zeile beendet und an den Anfang der nächsten Zeile geht.

Hier nur ein kleines Beispiel zur Ausgabe eines Zeichens:

printf("Das Zeichen ist %c\n", zeichenvar);

Listing 22.11    Schema eines printf-Aufrufs

In diesem Fall würde der Text »Das Zeichen ist « ausgegeben werden. An diese Ausgabe würde das Zeichen in der Variable »zeichenvar« angehängt werden. Die Ausgabe würde in einer neuen Zeile (\n) enden.

Die printf-Funktion unterstützt eine Vielzahl von Features, die wir hier nicht benannt haben (u.a. können die Links-/Rechtsbündigkeit einer Ausgabe und die Genauigkeit von Ausgaben festgelegt werden). Alles Weitere erfahren Sie in jedem Grundlagenbuch zur C-Programmierung.

Wichtiger Hinweis

Bevor wir nun auf die eigentlichen Datentypen zu sprechen kommen, sollen Sie noch erfahren, dass die Größe dieser Datentypen von System zu System stark variiert und auch der ANSI-C-Standard nicht exakt definiert, welche Größe die einzelnen Datentypen haben. Wir verwenden daher die für Linux-PCs »üblichen« Werte.

int

Der wohl am einfachsten zu verstehende Datentyp ist der Integer. In C wird dieser Datentyp durch int verwendet. Eine Integer-Variable speichert eine Ganzzahl, diese Zahl kann auch 0 oder negativ sein. Laut ANSI-C-Standard ist eine Integer-Variable immer mindestens zwei Byte (also 16 Bit) groß und kann daher 216 = 65.536 verschiedene Werte speichern. Auf den meisten heutigen Rechnern (etwa Linux-PCs) sind Integer allerdings 4 Byte groß.

Gemäß dem oben erläuterten Schema kann eine Integer-Variable durch folgende C-Anweisung erzeugt und initialisiert werden:

/* Deklaration */ 
int i; 
/* ... mit Initialisierung */ 
int i = 123; 
/* Wert von 'i' ändern */ 
i = –123;

Listing 22.12    Eine Integer-Variable erzeugen

unsigned und signed

Datentypen können in C entweder signed oder unsigned sein. Das bedeutet zunächst nur, dass diese Variablen entweder ein Vorzeichen besitzen können oder nicht. Dabei steht signed für eine Variable mit Vorzeichen und unsigned für eine Variable ohne Vorzeichen.

Dies hat allerdings Auswirkungen auf die Wertebereiche dieser Variablen, denn schließlich müssen im Fall einer vorzeichenlosen Variable nur positive Werte in ihren Bits Platz finden. Im Falle einer vorzeichenbehafteten Variable hingegen müssen auch negative Zahlen innerhalb ihrer Bits dargestellt werden können, womit sich der Wertebereich in einen positiven inklusive 0 und in einen negativen aufteilt.

Kehren wir nun zum Beispiel der Integer-Variable zurück. In einer 16-Bit-Integer-Variable können, wie Sie wissen 65.536 verschiedene Werte gespeichert werden.

Ist diese Integer-Variable nun unsigned, so kann der gesamte Platz für die positiven Zahlen und die 0 verwendet werden. Dementsprechend ist für 65.536 – 1 = 65.535 positive Zahlen und eine 0 Platz.

Teilt sich der Wertebereich allerdings in einen positiven und in einen negativen Teil auf, so wird ein Bit benötigt, um zu signalisieren, dass es sich um eine positive bzw. negative Zahl handelt. Demnach bleiben noch 16 – 1 = 15 Bits des Wertebereichs für die positiven und die negativen Zahlen übrig. Das sind jeweils 215 = 32.768 verschiedene Werte.

Da der positive Teil auch hier wiederum die 0 mit beinhaltet, können mit einem 16-Bit-signed-Integer 32.768 – 1 = 32.767 positive Zahlen gespeichert werden. Der negative Teil hingegen, der die 0 nicht beinhaltet, reicht von –1 bis –32.768.

Deklaration

Die Deklaration von signed und unsigned Variablen ist einfach. Sie müssen nur das entsprechende Schlüsselwort mit angeben.

unsigned int a = 65635; 
signed int b = 32767; 
 
/* Den Wert von b verändern */ 
b = –32768;

Listing 22.13    Deklaration von (un)signed Integern

/* signed int */ 
printf("%i", a); 
printf("%d", a); 
 
/* unsigned int */ 
printf("%u", a);

Listing 22.14    Ausgabe von Integer-Werten

Hex- und Oktalwerte

In C können Sie Variablen auch Hex- und Oktalwerte zuweisen. Das sollte man allerdings nur tun, wenn man wirklich weiß, was diese Werte genau bedeuten (im Besonderen bei negativen Werten, die im 2er-Komplement dargestellt werden). Es gibt tatsächlich Fälle, bei denen Hexwerte sinnvoll erscheinen. Dazu zählt beispielsweise das Setzen von bestimmten Flags/Bits.

Hexwerte werden mit dem Präfix »0x« und Oktalwerte mit einer führenden »0« angegeben. Im folgenden Beispiel wird der Variable dreimal der gleiche Wert (42) zugewiesen.

/* Dezimale Schreibweise, wie gewohnt. */ 
int a = 42; 
 
/* Hexadezimale Schreibweise: Die Buchstaben können 
 * in Klein- und Großbuchstaben angegeben werden. */ 
a = 0x2a; 
 
/* Oktale Schreibweise */ 
a = 052;

Listing 22.15    Setzen des Wertes 42

char

Eine char-Variable dient zur Speicherung eines Zeichens und ist 1 Byte groß. Mit Ausnahme einiger Sonderfälle besteht ein Byte immer aus 8 Bit. Dementsprechend können 256 verschiedene Werte in einer solchen Variable gespeichert werden.

Deklaration und Initialisierung

Einer char-Variable können Sie auf die gleiche Weise Werte zuweisen wie einer Integer-Variable, nur dass die Größe dieser Werte eben auf 8 Bit beschränkt ist.

char a = 99; 
 
printf("%c", a);

Listing 22.16    Verwendung einer char-Variablen

Was Sie in diesem Fall tun, ist, der Variablen a den Wert 99 zuweisen. Dies wird bei Ausgaben als Wert für ein ASCII-Zeichen interpretiert. <Mehr zum ASCII-Standard erfahren Sie unter: http://de.wikipedia.org/wiki/ASCII.> Die 99 steht dabei für ein kleingedrucktes »c«.

Da man wohl kaum gern auf diese Weise Zeichen zuweisen will, gibt es aber noch eine wesentlich komfortablere Schreibweise für ASCII-Zeichen.

Dabei wird das entsprechende Zeichen in Hochkommas eingebettet.

char a = 'c'; 
char b = '9'; 
char c = 'M';

Listing 22.17    char-Deklaration und Initialisierung mit Zeichen

short

Eine short-Variable hat immer eine Länge von mindestens 16 Bits. short kann wie eine Integer-Variable verwendet werden. Die Wertebereiche einer (unsigned) short-Variablen Ihres Linux-Systems bekommen Sie übrigens ganz einfach über die Datei limits.h heraus.

$ egrep 'SHRT_MAX|SHRT_MIN' /usr/include/limits.h 
#  define SHRT_MIN      (-32768) 
#  define SHRT_MAX      32767 
#  define USHRT_MAX     65535

Listing 22.18    Größe einer short-Variablen ermitteln

short a = 123; 
a = –123; 
 
/* signed short: 
 * Zwei Möglichkeiten für die Ausgabe: 
 */ 
printf("%hd", a); 
printf("%hi", a); 
 
/* unsigned short: */ 
printf("%hu", a);

Listing 22.19    Verwendung einer short-Variablen

long

Der Datentyp long hat unter Linux laut [Love05A] auf 32-Bit-Systemen immer eine Größe von 32 Bit und auf 64-Bit-Systemen immer eine Größe von 64 Bit. <Robert Love: Linux Kernel Handbuch, Addison-Wesley, 2005. S. 408 ff.>

long a = 123; 
a = –123; 
 
/* signed long: 
 * Zwei Möglichkeiten für die Ausgabe: 
 */ 
printf("%ld", a); 
printf("%li", a); 
 
/* unsigned long: */ 
printf("%lu", a);

Listing 22.20    Verwendung einer long-Variablen

Gleitkomma-Datentypen

Mit den bisherigen Datentypen war es nur möglich, ganze Zahlen zu benutzen. Nun werden wir uns mit float, double und long double die sogenannten Gleitkomma-Datentypen ansehen. In einer Gleitkomma-Variablen können Zahlen mit Nachkommastellen gespeichert werden.

Dabei gibt es einige Tatsachen zu beachten:

  • Für all diese Datentypen existieren keine unsigned-Varianten.
  • Nachkommastellen werden nicht durch ein richtiges Komma (,) sondern durch einen Punkt (.) von dem ganzzahligen Teil einer Zahl getrennt.
  • Die verschiedenen Datentypen haben nicht nur eine unterschiedliche Bitgröße, sondern auch eine unterschiedliche Genauigkeit in ihren Nachkommastellen.

Größe

Der Datentyp float ist in der Regel 4 Byte groß. Es kann allerdings vorkommen, dass eine float-Variable die Größe einer double-Variable annimmt.

Eine double-Variable hat in der Regel eine Größe von 8 Byte. Eine double-Variable hat immer mindestens die Größe einer float-Variable und maximal die Größe einer long double-Variablen.

Der Datentyp long double hat immer mindestens die Größe einer double-Variablen. In der Regel ist er 10, 12 oder 16 Byte groß.

Kurz gesagt: Größe von float <= Größe von double <= long double.

Genauigkeit

Die Genauigkeit einer Gleitkomma-Variable nimmt mit ihrer Größe zu. Dies hängt damit zusammen, dass ein bestimmter Bereich der Bits, die für die Darstellung der Nachkommastellen verwendet werden, ebenfalls anwächst. Dieser Bereich wird als Mantisse bezeichnet.

Üblicherweise haben float-Variablen eine Genauigkeit von 6 Stellen, double eine Genauigkeit von 15 Stellen, und long double hat ganze 18 Stellen Genauigkeit vorzuweisen.

float a = 0.123; 
double b = –17.498568; 
 
/* Werte können auch in Zehnerpotenzen angegeben werden. 
 * Dazu wird die Schreibweise [Zahl] [e] [Exponent] 
 * benutzt. 3e2 würde dementsprechend für 3 * 10 * 10 
 * stehen. 
 */ 
long double c = 384.599e10; 
 
/* Die Ausgabe erfolgt auch hier auf verschiedene Weisen: */ 
printf("float: %f", a); 
printf("double: %lf", b); 
printf("long double: %Lf", c);

Listing 22.21    Verwendung einer Gleitkomma-Variablen

Die Ausgabe dieser Zeilen würde folgendermaßen aussehen:

float: 0.123000 
double: –17.485870 
long double: 3845990000000.000000

Listing 22.22    Ausgabe der Gleitkommawerte


Galileo Computing

22.1.4 Operatoren  downtop

Nun, da Sie gelernt haben, Werte für Variablen zu setzen, ist der nächste Schritt, diese Werte zu verwenden und mit ihnen zu rechnen. Zu diesem Zweck werden wir uns die Operatoren der Programmiersprache C ansehen. <Mit der Ausnahme der Vergleichsoperatoren, die erst später behandelt werden.>

Rechenoperatoren

+,-,*,/

Die einfachsten Operatoren sind (besonders für Nicht-Informatiker) Addition, Subtraktion, Multiplikation und Division.

Zuweisung

Die Zuweisung eines Wertes erfolgt, wie Sie bereits wissen, mit dem Zeichen =. Dieses Zeichen funktioniert auch dann, wenn man einer Variablen das Ergebnis einer Rechenoperation zuweisen möchte. Hier ein paar Beispiele:

int a = 4; 
int b = 2; 
int c; 
int d; 
 
c = a + 1; 
 
b = 3 + 4 + 5; 
 
c = a + b – 1; 
 
d = 2 * 2; 
 
c = 4 / 2;

Listing 22.23    Anwendung von Rechenoperationen

Vorrang

In C hat jeder Operator eine bestimmte Wertigkeit. Diese Wertigkeit entscheidet, welche Operatoren eines C-Ausdrucks zuerst berechnet werden und wie die weitere Reihenfolge ist. Hierfür gibt es verschiedene Regeln, auf die wir hier der Einfachheit halber nicht eingehen werden, doch so viel sei gesagt: »Punktrechnung geht vor Strichrechnung«.

Der folgende Code würde dementsprechend den Wert 10 (= 9 + (3/3)) und nicht 4 (= (9 + 3) / 3) liefern.

printf("%i\n", 9 + 3 / 3);

Listing 22.24    Punkt vor Strich

Doch was passiert, wenn zwei Punktrechnungen gleichzeitig verwendet werden? In diesem Fall gilt »rechts vor links«, was bedeutet, dass der Ausruck von der rechten zur linken Seite hin ausgewertet wird.

printf("%i\n", 9 * 3 / 3);

Listing 22.25    Rechts vor links

In diesem Fall wird also zunächst 3 durch 3 geteilt (was 1 ergibt). Das Ergebnis 1 wird anschließend mit der 9 multipliziert. Damit ist das Ergebnis ebenfalls 9.

Klammerung

Wenn Sie die Rechenreihenfolge selbst bestimmen möchten, dann verwenden Sie wie in der Schule Klammern. Der obere Ausdruck könnte beispielsweise durch Einklammern der Rechenoperation 9*3 (= 27) den Wert 9 (= 27 / 3) liefern.

printf("%i\n", (9 * 3) / 3);

Listing 22.26    Klammerung

Nachkommastellen

Allerdings ist zu beachten, dass diese Rechenoperationen nicht immer zum beabsichtigten Ergebnis führen. Beispielsweise können Integer-Variablen nur ganze Werte speichern. Was aber liefert dann eine Zuweisung von 5/2 an einen Integer? Die Antwort ist: 2. Das liegt daran, dass die Nachkommastellen abgeschnitten werden. Möchten Sie Komma-Werte im Ergebnis, so müssen Sie eine Gleitkommavariable verwenden.

float a = 5, b = 2; 
float c; 
 
c = a/b; 
printf("%f\n", c);

Listing 22.27    Rechnen mit Kommastellen

Die Ausgabe dieses Codes liefert 2.500000.

Typen-Mix

Mischt man allerdings mehrere Datentypen, so wird es leicht problematisch. Hier kann es zu Speicherüberläufen, Problemen mit (nicht vorhandenen) Vorzeichen und zum Abschneiden von Kommastellen kommen. Auf diese Probleme können wir in diesem Rahmen leider nicht eingehen. Eine relativ sichere Vorgehensweise ist allerdings, keine Datentypen zu mixen.

Weitere Rechenoperatoren

Modulo

Eine in der Informatik sehr wichtige Rechenoperation ist das Modulo-Rechnen. Dahinter verbirgt sich nichts anderes, als dass das Ergebnis dieser Rechnung der Rest ist, der bei einer Division übrig bleibt. Teilen Sie beispielsweise 5 durch 2, dann bleibt ein Rest von 1 übrig.

Der Modulo-Operator ist ein Prozentzeichen (%).

int a, b; 
 
/* a wird 0, da kein Rest bleibt */ 
a = 10 % 2; 
 
/* a wird 4 */ 
a =  9 % 5;

Listing 22.28    Modulo

++/–-

Nun kommen wir zu zwei sehr beliebten Operatoren: den doppelten Plus- und Minuszeichen. Fast jede Programmiersprache kennt diese Operatoren und das, was sie tun, ist sehr einfach zu verstehen: Sie inkrementieren (erhöhen) bzw. dekrementieren (verringern) den Wert einer Variablen um 1.

int a = 10; 
 
a ++;  /* a wird 11 */ 
a --;  /* a wird 10 */ 
a --;  /* a wird  9 */

Listing 22.29    Inkrementieren und Dekrementieren

In C unterscheidet man zwischen Pre- und Post-Inkrementierung bzw. -Dekrementierung. Der Unterschied besteht darin, ob der Operator vor oder hinter eine Variable geschrieben wird. Dies kann sich auf eine Rechnung auswirken, da hierbei entschieden wird, ob eine Variable vor ihrer Verwendung in-/dekrementiert wird oder ob sie erst nach einer Rechnung in-/dekrementiert wird.

int a, b, c; 
 
a = 10; 
 
++a; /* a wird 11 */ 
--a; /* a wird 10 */ 
 
/* Beispiel für Pre-Inkrementierung */ 
a = 10; 
b = ++a; /* b = 1 + a = 11; a = 11; */ 
 
/* Beispiel für Post-Inkrementierung */ 
a = 10; 
c = a++; /* c = a = 10; a = 11; */

Listing 22.30    Vorher oder nachher?

Im Beispiel der Pre-Inkrementierung bekommt b den Wert 11, da zuerst a inkrementiert wird (a = 11) und dieser Wert dann c zugewiesen wird.

Im Beispiel der Post-Inkrementierung bekommt c den Wert 10. Erst danach wird a imkremeniert (womit a auch hier den Wert 11 bekommt). Wie Sie sehen, führen beide Rechenanweisungen durchaus zu unterschiedlichen Ergebnissen.

Verkürzte Schreibweisen

Ein weiteres sehr beliebtes Feature der Programmiersprache spart Schreibarbeit und ist mit fast allen Operatoren anwendbar. Es wird dabei eine Zuweisung der folgenden Form vereinfacht.

VarA = VarA [Operator] VarB

In C können Sie anstelle dieser Schreibweise nämlich auch diese verwenden:

VarA [Operator]= VarB

Klingt kompliziert? Ist es aber nicht. Nach diesem Beispiel werden Sie es ganz locker verstanden haben. Es soll die folgende Rechenoperation vereinfacht werden:

int a = 10, b = 2; 
 
b = b + a;

Listing 22.31    Vor der Vereinfachung

Nun wird das Additionszeichen vor das Gleichheitszeichen gezogen und die zweite Verwendung von Variable b entfernt:

int a = 10, b = 2; 
 
b += a;

Listing 22.32    Nach der Vereinfachung

Hier sehen Sie noch einige weitere Beispiele für andere Rechenoperationen, die auf diese Weise vereinfacht werden können. <Wir werden gleich noch weitere Operatoren kennenlernen, jedoch beschränken wir uns an dieser Stelle auf die uns bekannten Operatoren.>

/* Lange Schreibweise */ 
a = a + b; 
a = a * b; 
a = a – b; 
a = a / b; 
a = a % b; 
 
/* Kurze Schreibweise */ 
a += b; 
a *= b; 
a -= b; 
a /= b; 
a %= b;

Listing 22.33    Nach der Vereinfachung

Bitweise Operatoren

Die nächste große Klasse an Operatoren, die in C zur Verfügung stehen, sind die bitweise angewandten Operatoren. Um diese Operatoren anzuwenden, muss man über die Darstellung der Variablenwerte im dualen Zahlensystem (also binär mit Nullen und Einsen) Bescheid wissen. Dieses Thema würde locker den Rahmen dieses Abschnitts sprengen und kann daher leider auch nicht behandelt werden. In der Wikipedia und in C-Büchern finden Sie allerdings sehr vernünftige und verständliche Erklärungen. <Bei Problemen leihen Sie sich »Mathematik für Informatiker« von Brill oder ein anderes Buch zum Thema oder Informatiker-Bücher für das Grundstudium aus einer Bibliothek aus.>

Shiften

Die sogenannten Shift-Operatoren verschieben die Bits in einer Variable nach rechts beziehungsweise nach links. Nehmen wir an, in einer 8-Bit-Integer-Variable steht der Wert 4 (dezimal). Binär wird dieser Wert als 00000100 dargestellt. Wird dieser Wert nun um eine Stelle nach links verschoben, dann steht anschließend der Wert 00001000 (also 8) in der Variable. Wird er um 1 nach rechts verschoben, steht anschließend 00000010 (also 2) in der Variable. Die Operatoren hierfür sind doppelte Größer- bzw. Kleiner-als-Zeichen.

int a = 4; 
int b, c; 
 
/* Den Wert von a um eine Stelle nach rechts shiften */ 
b = a >> 1; 
 
/* Den Wert von a um zwei Stellen nach links shiften */ 
c = a << 2;

Listing 22.34    Shiften

Weitere Operatoren

Es gibt noch weitere (und genauso wichtige) Operatoren, die bitweise angewandt werden. Dazu zählen das »logische Und« (&, im Folgenden »UND«), das »logische Oder« (|, im Folgenden »ODER«) und das »exklusive ODER« (^, im Folgenden XOR). Des Weiteren gibt es noch das Einerkomplement (~).

UND

Bei einem UND zwischen zwei Variablen wird geprüft, welche Bits in beiden Variablen gesetzt sind. Das Ergebnis der Operation sind nur die Bits, die in jeder der beiden Variablen gesetzt sind. Würde beispielsweise der Wert 6 (110) mit dem Wert 5 (101) UND-verknüpft werden, so wäre das Ergebnis 4 (100), da nur das 4er-Bit in beiden Werten vorkommt. Geschrieben wird eine UND-Verknüpfung mit einem kaufmännischen &.

   110 = 6 
 & 101 = 5 
 ----- 
   100 = 4

Listing 22.35    Beispiel einer UND-Verknüpfung

int a; 
int x = 6, y = 4; 
 
a = x & y;

Listing 22.36    Beispiel einer UND-Verknüpfung

ODER

Die ODER-Verknüpfung ist der UND-Verknüpfung sehr ähnlich. Der Unterschied besteht darin, dass alle Bits im Ergebnis landen, die entweder in einem der Werte oder in beiden vorkommen. Auf das obige Beispiel mit den Werten 6 (110) und 5 (101) angewandt, würde das Ergebnis 7 (111) sein. Geschrieben wird ein »logisches ODER« durch ein Pipe-Zeichen (|).

   110 = 6 
 | 101 = 5 
 ----- 
   111 = 7

Listing 22.37    Beispiel einer ODER-Verknüpfung

int a; 
int x = 6, y = 4; 
 
a = x | y;

Listing 22.38    Beispiel einer ODER-Verknüpfung

XOR

Das »exklusive ODER« verhält sich wiederum ähnlich wie das »logische ODER«. Es landen alle Bits im Ergebnis, die entweder im ersten oder im zweiten Wert vorhanden sind. Allerdings landen nicht die Bits im Ergebnis, die in beiden Werten gesetzt sind. Würde 6 (110) mit 5 (101) XOR-verknüpft werden, dann wäre das Ergebnis 3 (011). Der XOR-Operator wird durch ein Dach-Zeichen (^) verwendet.

   110 = 6 
 ^ 101 = 5 
 ----- 
   011 = 3

Listing 22.39    Beispiel einer XOR-Verknüpfung

int a; 
int x = 6, y = 4; 
 
a = x ^ y;

Listing 22.40    Beispiel einer XOR-Verknüpfung

Einerkomplement

Es bleibt nun noch das »Einerkomplement«. Hierbei werden die Bits eines Wertes negiert, das heißt umgekehrt. Aus einem 1er-Bit wird ein 0er-Bit, und aus einem 0er-Bit wird ein 1er-Bit.

Das Einerkomplement wird auf einen einzigen Wert angewandt und durch ein Tilde-Zeichen (~) benutzt.

Würden wir den Operator auf den Wert 6 (110) anwenden, so wäre das Ergebnis 1 (001).

 ~ 110 = 6 
 ----- 
   001 = 1

Listing 22.41    Beispiel eines Einerkomplements

Allerdings gibt es in C etwas zu beachten, das wir bisher nicht erwähnt haben: Nehmen wir an, Sie verwenden eine 32 Bit große Integer-Variable in der der Wert 6 gespeichert ist. In diesem Fall wird das Einerkomplement nicht 1 ergeben. Das liegt daran, dass vor den ersten 3 Bits (110) noch 29 weitere 0-Bits stehen, die durch die Rechenoperation zu einer 1 werden. Das Ergebnis wäre dann eine sehr große Zahl (unsigned int) beziehungsweise eine sehr kleine negative Zahl ((signed) int):

 ~ 00000000000000000000000000000110 = 6 
 ---------------------------------- 
   11111111111111111111111111111001 = 4.294.967.289

Listing 22.42    Beispiel eines Einerkomplements

int a; 
 
a = ~ 6;

Listing 22.43    Beispiel eines Einerkomplements

Der sizeof-Operator

Zum Schluss folgt noch eine sehr praktische C-Funktionalität: der sizeof-Operator. Dieser Operator gibt die Zahl der Bytes zurück, die eine Variable, auf die er angewandt wird, für sich beansprucht. Die Anzahl der Bytes, die zurückgegeben werden, ist daher niemals negativ und auch keine Gleitkommazahl.

#include <stdio.h> 
 
int main() 
{ 
  char q; 
  short r; 
  int s; 
  long t; 
  float u; 
  double v; 
  long double w; 
 
  printf("Groesse von char: %i\n", sizeof(q)); 
  printf("Groesse von short: %i\n", sizeof(r)); 
  printf("Groesse von int: %i\n", sizeof(s)); 
  printf("Groesse von long: %i\n", sizeof(t)); 
  printf("Groesse von float: %i\n", sizeof(u)); 
  printf("Groesse von double: %i\n", sizeof(v)); 
  printf("Groesse von long double: %i\n", sizeof(w)); 
 
  return 0; 
}

Listing 22.44    Beispielanwendung des sizeof-Operators

Dieses Programm liefert auf einem üblichen 32-Bit-x86-Linux-PC die folgende Ausgabe:

Groesse von char: 1 
Groesse von short: 2 
Groesse von int: 4 
Groesse von long: 4 
Groesse von float: 4 
Groesse von double: 8 
Groesse von long double: 12

Listing 22.45    Ausgabe des Programms

Übrigens kann der sizeof-Operator auch direkt auf einen Datentyp angewandt werden. Eine Anweisung wie sizeof(long) ist also gültig.

Übersicht der Operatoren

Zur Zusammenfassung folgt hier noch einmal eine Übersicht über die arithmetischen Operatoren.


Tabelle 22.1    Übersicht der arithmetischen Operatoren
Operator Beispiel Beschreibung

+

x = x + 1

Addition

-

x = x1

Subtraktion

*

x = y * z

Multiplikation

/

z = x / y

Division

%

m = x % y

Modulo (Restwert bei Division)

++

x++

Inkrement

--

x-

Dekrement

&

x = y & z

UND

|

x = y | z

ODER

^

x = y ^ z

XOR

~

x = ~ z

Negierung

»

x = y >> 2

rechts shiften

»

x = y << 2

links shiften

sizeof

x = sizeof(int)

»Größe von«



Galileo Computing

22.1.5 Bedingte Anweisungen  downtop

Ein äußerst wichtiges Element der Programmierung sind bedingte Anweisungen. Sie kennen bedingte Anweisungen bereits aus der Shellskript Programmierung. In der Shell hießen die zugehörigen Befehle if und case. Diese Namen sind in den meisten Programmiersprachen sehr ähnlich – so ist es auch mit C.

Hier noch einmal zur Erinnerung: Bei bedingten Anweisungen wird zunächst geprüft, ob eine »Bedingung« erfüllt ist (etwa ob der Wert der Variablen »anzahl« größer 1000 ist). Ist die Bedingung (nicht) erfüllt, dann wird eine bestimmte Anweisung (nicht) ausgeführt.

Vergleichsoperatoren

Bevor wir uns die einzelnen Anweisungen ansehen, soll noch ein Blick auf die Vergleichsoperatoren geworfen werden, die C kennt.

Positive Werte werden in C als »erfüllte« Bedingungen angesehen (man spricht auch von »wahren« oder »true-Bedingungen«). Werte, die negativ oder 0 sind, werden hingegen als »nicht erfüllt« (man spricht auch von »falschen« oder »false-Bedingungen«) bezeichnet.

Würde die Variable a als Vergleichstest verwendet, so wäre die Bedingung dann erfüllt, wenn in a ein positiver Wert steht. In Spezialfällen, bei denen signed- mit unsigned-Werten verglichen werden, kann es allerdings zu Problemen kommen.

Mehr zu diesem Thema erfahren Sie in vernünftigen C-Büchern und in unserem Buch »Praxisbuch Netzwerksicherheit« im Kapitel »Sichere Software entwickeln«.

Nehmen wir an, wir betreiben ein Verkaufssystem. Sobald ein Kunde für mehr als 100 EUR bestellt, sollen ihm die Versandkosten erlassen werden. Würde der Gesamtwert des Einkaufs in der Variable »wert« stehen, dann könnte man prüfen, ob »wert« größer oder gleich 100 EUR wäre. Die Versandkosten werden erlassen (etwa durch Setzen der Variable »vkosten« auf 0), wenn diese Bedingung erfüllt ist.


Tabelle 22.2    Übersicht der Vergleichsoperatoren
Operator Beispiel Beschreibung

==

a == 1

Gleichheit: Die Bedingung ist erfüllt, wenn die Werte auf beiden Seiten gleich sind.

!=

a != 1

Ungleichheit: Die Bedingung ist erfüllt, wenn die Werte sich auf beiden Seiten unterscheiden.

>

a > 1

Größer: Die Bedingung ist erfüllt, wenn der linke Wert größer als der rechten ist.

>=

a >= 1

Größer-Gleich: Die Bedingung ist erfüllt, wenn der linke Wert größer oder gleich dem rechten ist.

<

a < 1

Kleiner: Die Bedingung ist erfüllt, wenn der linke Wert kleiner dem rechten ist.

<=

a <= 1

Kleiner-Gleich: Die Bedingung ist erfüllt, wenn der linke Wert kleiner oder gleich als der rechte ist.

&&

a && 1

Und: Die Bedingung ist erfüllt, wenn sowohl die linke als auch die rechte Bedingung erfüllt ist.

||

a || a

Oder: Die Bedingung ist erfüllt, wenn die rechte oder die linke oder beide Bedingungen erfüllt sind.

!

! a

Negation: Die Bedingung ist erfüllt, wenn die rechts von ihr stehende Bedingung nicht erfüllt ist.


Die if-Anweisung

Die verständlichste Möglichkeit, eine bedingte Anweisung zu formulieren, ist wohl die if-Anweisung. Ihr Aufbau ist dabei dem Aufbau der if-Anweisung der Shell sehr ähnlich:

if ( Bedingung) 
{ 
    Anweisung(en) 
} 
else if ( Nebenbedingung ) 
{ 
    Anweisung(en) 
} 
else if ( Weitere Nebenbedingung ) 
{ 
    Anweisung(en) 
} 
... 
[ weitere Nebenbedingungen ] 
... 
} 
else 
{ 
    Anweisung(en) 
}

Listing 22.46    Aufbau einer if-Anweisung

Ist die Bedingung des if-Blocks erfüllt, so werden die entsprechenden Anweisungen ausgeführt. Dabei können die geschweiften Klammern weggelassen werden, wenn nur eine einzige Anweisung ausgeführt werden soll. Diese beiden Anweisungen sind also gleich:

if ( 1 ) 
{ 
     printf("True!"); 
} 
 
if ( 1 ) 
     printf("True!");

Listing 22.47    Nur eine Anweisung in if

else if

Ist die eigentliche if-Bedingung nicht erfüllt, dann gibt es die Möglichkeit, weitere Bedingungen (else if) abzufragen. Diese Bedingungen werden nur überprüft, wenn die erste Bedingung nicht erfüllt ist. Sobald eine dieser weiteren Bedingungen erfüllt ist, werden die entsprechenden Anweisungen ausgeführt, und es wird keine weitere Bedingung, die noch folgt, überprüft.

Versuchen wir auf diese Weise einmal die Variable »anzahl« auf drei verschiedene Werte zu überprüfen.

if ( a < 1000 ) 
{ 
    printf("a ist kleiner als 1000"); 
} 
else if ( a == 2948 ) 
{ 
    printf("a ist 2948"); 
} 
else if ( a == 494859) 
{ 
    printf("a ist 494859"); 
}

Listing 22.48    Testen auf verschiedene Werte

else

Was passiert aber, nur dann, falls keine dieser Bedingungen erfüllt ist, eine bestimmte Aktion ausgeführt werden soll? Nehmen wir an, dass »a« den Wert 123 zugewiesen bekommen soll, wenn a weder kleiner als 1000 noch eine der anderen Zahlen ist.

Für diesen Fall gibt es die else-Anweisung. Die Anweisungen in einem else-Anweisungsblock werden nur dann ausgeführt, wenn alle anderen Bedingungen nicht erfüllt sind.

if ( a < 1000 ) 
{ 
    printf("a ist kleiner als 1000"); 
} 
else if ( a == 2948 ) 
{ 
    printf("a ist 2948"); 
} 
else if ( a == 494859) 
{ 
    printf("a ist 494859"); 
} 
else 
{ 
    printf("a hat einen anderen Wert"); 
}

Listing 22.49    Testen auf verschiedene Werte

Mehrere Bedingungen, eine Anweisung

Möchte man die gleiche Anweisung bei mehreren verschiedenen Bedingungen ausführen, dann ist auch das in C kein Problem. Nehmen wir an, es soll der Text »Aktien kaufen« ausgegeben werden, wenn »a« entweder kleiner 1000 ist oder größer als 2000 ist. Mit der Oder-Anweisung aus Tabelle 2.2.2 ist dies kein Problem:

if ( a < 1000 || a > 2000) 
{ 
    printf("Aktien kaufen"); 
}

Listing 22.50    Testen auf verschiedene Werte

Diese Bedingung wäre übrigens auch erfüllt, wenn »a« gleichzeitig kleiner als 1000 und größer als 2000 wäre, was aber nicht möglich ist. Würde man aber prüfen, ob »a« kleiner 1000 und »b« größer 2000 ist, dann könnten beide Bedingungen gleichzeitig erfüllt sein.

Klammerung

Mit Klammern kommt man allerdings noch einen Schritt weiter. Möchte man etwa Prüfen ob die obige Bedingung erfüllt ist, aber den Text nur ausgeben, wenn die Variable »t« kleiner als 10 ist, dann klammert man die Oder-Bedingung ein. Warum das so ist, zeigt das Listing.

/* Ohne Klammern: Es ist nicht klar, ob entweder a > 2000 
 * UND t < 10 sein soll ODER a < 1000 sein soll. Oder aber, 
 * ob (wie es eigentlich gedacht ist) a < 1000 ODER > 2000 
 * sein soll UND zudem t < 10 sein soll. 
 */ 
if ( a < 1000 || a > 2000 && t < 10) 
{ 
    printf("Aktien kaufen"); 
} 
 
/* Mit Klammern: Es ist klar: Sowohl die Bedingung in der 
 * Klammer als auch t < 10 müssen erfüllt sein. 
 */ 
if ( ( a < 1000 || a > 2000 ) && t < 10) 
{ 
    printf("Aktien kaufen"); 
}

Listing 22.51    Ein Beispiel zur Klammerung

Zur Erinnerung: Bei einer einzigen auszuführenden Anweisung hätte dieses Beispiel auch ohne die Klammern geschrieben werden können.

Klammerung

Mit Klammern kommt man allerdings noch einen Schritt weiter. Möchte man etwa prüfen, ob die obige Bedingung erfüllt ist, aber den Text nur ausgeben, wenn die Variable »t« kleiner als 10 ist, dann klammert man die Oder-Bedingung ein. Warum das so ist, zeigt das Listing.

if ( ( a < 1000 || a > 2000 ) && t < 10) 
    printf("Aktien kaufen");

Listing 22.52    Ein Beispiel zur Klammerung

Die switch-Anweisung

In der bash gab es neben der if-Anweisung noch die case-Anweisung. Diese gibt es (nur unter anderem Namen) auch in C. In C heißt diese Anweisung switch. Ihr übergibt man einen Wert (direkt oder durch eine Variable) und kann anschließend die Vergleichswerte und die zugehörigen Anweisungen aufführen.

switch ( Wert ) 
{ 
     case Testwert1: 
          Anweisung(en) 
          [break;] 
     case Testwert2: 
          Anweisung(en) 
          [break;] 
     ... 
     ... 
     default: 
          Anweisung(en) 
          [break;] 
}

Listing 22.53    Schema einer switch-Anweisung

Hierbei wird geprüft, ob der Wert dem Testwert 1 oder Testwert 2 (oder weiteren Testwerten) entspricht. Jeweils werden die entsprechenden Bedingungen ausgeführt. Ist keine dieser Testwerte gleich dem übergebenen Wert, dann werden die Anweisungen des default-Blocks ausgeführt. Ist kein default-Block vorhanden, so wird gar keine Anweisung ausgeführt.

Ein default-Fall muss übrigens nicht angegeben werden. Eine switch-Anweisung kann entweder einen oder mehrere casees, eine default-Anweisung, beides oder nichts enthalten. Im Falle einer leeren switch-Anweisung werden natürlich auch keine Werte überprüft und dementsprechend auch keine Anweisungen ausgeführt.

break

Mit der break-Anweisung wird erzwungen, dass keine weiteren Anweisungen mehr ausgeführt werden und dass das switch-Statement verlassen wird. Würden Sie eine break-Anweisung am Ende der Anweisungen eines case-Bereiches vergessen, so würden die folgenden Anweisungen (egal welcher Bedingung) ebenfalls ausgeführt werden, bis entweder eine break-Anweisung auftritt oder das Ende der switch-Anweisung erreicht ist.

int q = 99; 
 
switch ( q ) 
{ 
    case 10: 
         q = 12; 
         break; 
    case 99: 
         q = 102; 
         /* An dieser Stelle fehlt ein 'break' */ 
    case 100: 
         q = 112; 
         break; 
}

Listing 22.54    Beispiel für break

Der ?-Operator

C kennt noch eine weitere Möglichkeit, eine bedingte Anweisung zu formulieren: den Fragezeichen-Operator. Dieser gibt im Gegensatz zu den anderen Varianten einen Wert zurück.

( 
     Bedingung 
   ? Anweisung bei erfüllter Bedingung 
   : Anweisung bei nicht erfüllter Bedingung 
)

Listing 22.55    Aufbau einer ?-Anweisung

Nehmen wir an, die Variable »a« soll auf den Wert 77 gesetzt werden, wenn die Variable »q« den Wert 99 enthält. Ist dem nicht so, soll »a« den Wert 2 verpasst bekommen.

a = ( q == 99 ? 77 : 2 )

Listing 22.56    Beispiel zum ?-Operator

Sie können durch diese Anweisung natürlich auch Zeichen für char-Variablen, Gleitkomma-Werte und sogar ganze Zeichenketten

(sogenannte Strings) zurückgeben lassen. Hier ein Beispiel für eine Textausgabe: Im Falle einer Begrüßung (das bedeutet, dass die Variable »beg« den Wert 1 hat) soll printf() »Hallo«, andernfalls »Tschuess« ausgeben. <Der Formatstring-Parameter %s besagt übrigens, dass es sich bei der Ausgabe nicht um eine Zahl oder ein einzelnes Zeichen, sondern um eine Zeichenkette handelt. Dazu später mehr.>

printf("%s\n", ( beg == 1 ? "Hallo" : "Tschuess" ) );

Listing 22.57    Zeichenketten


Galileo Computing

22.1.6 Schleifen  downtop

Sie haben noch nicht aufgegeben? Das ist schön! Jetzt, wo Sie bedingte Anweisungen und Datentypen von Variablen kennen, können wir ein weiteres Thema behandeln: Schleifen. Schleifen sind nach all dem, was Sie bisher wissen, sehr einfach zu verstehen, und ihr Nutzen ist riesig.

Im Prinzip verhält es sich hierbei auch wieder ähnlich wie bei der Shellskriptprogrammierung mit der bash: Auch in C gibt es eine while- und eine for-Schleife.

Zur Erinnerung: Eine Schleife führt bedingte Anweisungen so lange aus, wie die Bedingung, die ihr übergeben wurde, erfüllt ist.

Die while-Schleife

Die einfachste Schleife ist die while-Schleife. Ihr Aufbau ist dem der if-Anweisung sehr ähnlich:

while ( Bedingung ) 
{ 
     Anweisung(en) 
}

Listing 22.58    Aufbau einer while-Schleife

Nehmen wir an, es soll zehnmal in Folge der Text »Hallo Welt« ausgegeben werden. Sie könnten nun zehnmal eine printf-Anweisung untereinanderschreiben oder eine lange Zeichenkette mit vielen Newlines und vielen »Hallo Welt« übergeben. Doch viel schöner (und platzsparender) ist es, eine Schleife hierfür zu verwenden.

int i = 10; 
 
while ( i > 0 ) 
{ 
   printf("Hallo Welt"); 
   i = i – 1; 
}

Listing 22.59    10x Hallo Welt!

Am Ende jedes Schleifendurchlaufs haben wir einfach die Variable »i« dekrementiert. Die Bedingung der Schleife ist somit genau zehnmal erfüllt. Sobald »i« gleich 0 ist, ist die Bedingung nicht mehr erfüllt, und die Schleife wäre beendet.

Die Überprüfung auf die Erfüllung der Bedingung findet immer nur statt, nachdem der gesamte Anweisungsblock ausgeführt wurde. Auf das obige Beispiel angewandt bedeutet dies etwa, dass Sie die Anweisung, »i« zu dekrementieren, auch vor die Ausgabe stellen können.

int i = 10; 
 
while ( i > 0 ) 
{ 
   i = i – 1; 
   printf("Hallo Welt"); 
}

Listing 22.60    Eine weitere Variante

Richtig praktisch werden Schleifen aber erst, wenn man mit verschiedenen Variablenwerten arbeitet. Möchte man etwa die Zahlen von Eins bis Zehntausend ausgeben, dann geht dies mit genauso wenig Code-Zeilen, als wenn man die Zahlen von Eins bis Zwei oder bis Hundert oder von Eins bis Zehn Millionen ausgeben würde.

int i = 1; 
 
while ( i <= 1000000 ) 
{ 
   printf("%i\n", i); 
   i++; 
}

Listing 22.61    Bis 1.000.000 zählen

Natürlich sind auch komplexere Bedingungen sowie Unterschleifen möglich. Möchte man etwa zehn Zahlen pro Zeile ausgeben, dann ist auch das kein Problem.

Hier ein Beispiel: Es werden zehn Zahlen pro Zeile ausgegeben, und jeweils steigt die Zahl, die ausgegeben wird, an.

Nach zehn Zahlen wird also ein Newline-Zeichen eingefügt. Nach zehn Zeilen (das bedeutet, dass i % 10 den Wert 0 ergibt) wird eine Trennlinie ausgegeben.

#include <stdio.h> 
 
int main() 
{ 
   int i = 0; 
   int k; 
   int wert; 
 
   while ( i <= 100 ) { 
      k = 0; 
      while ( k < 10 ) { 
         wert = (i * 10) + k; 
         printf("%i ", wert); 
         k++; 
      } 
      printf("\n"); 
      if ( (i % 10) == 0) { 
         printf("---------------------------n"); 
      } 
      i++; 
   } 
   return 0; 
}

Wie erwartet gibt das Programm 10x10er-Blöcke von Zahlen aus:

$ gcc -o zahlen zahlen.c 
$ ./zahlen 
10 11 12 13 14 15 16 17 18 19 
20 21 22 23 24 25 26 27 28 29 
30 31 32 33 34 35 36 37 38 39 
40 41 42 43 44 45 46 47 48 49 
50 51 52 53 54 55 56 57 58 59 
60 61 62 63 64 65 66 67 68 69 
70 71 72 73 74 75 76 77 78 79 
80 81 82 83 84 85 86 87 88 89 
90 91 92 93 94 95 96 97 98 99 
100 101 102 103 104 105 106 107 108 109 
---------------------------- 
110 111 112 113 114 115 116 117 118 119 
120 121 122 123 124 125 126 127 128 129 
130 131 132 133 134 135 136 137 138 139 
140 141 142 143 144 145 146 147 148 149 
150 151 152 153 154 155 156 157 158 159 
160 161 162 163 164 165 166 167 168 169 
170 171 172 173 174 175 176 177 178 179 
180 181 182 183 184 185 186 187 188 189 
190 191 192 193 194 195 196 197 198 199 
200 201 202 203 204 205 206 207 208 209 
---------------------------- 
210 211 212 213 214 215 216 217 218 219 
220 221 222 223 224 225 226 227 228 229 
230 231 232 233 234 235 236 237 238 239 
240 241 242 243 244 245 246 247 248 249 
250 251 252 253 254 255 256 257 258 259 
260 261 262 263 264 265 266 267 268 269 
... 
... 
880 881 882 883 884 885 886 887 888 889 
890 891 892 893 894 895 896 897 898 899 
900 901 902 903 904 905 906 907 908 909 
---------------------------- 
910 911 912 913 914 915 916 917 918 919 
920 921 922 923 924 925 926 927 928 929 
930 931 932 933 934 935 936 937 938 939 
940 941 942 943 944 945 946 947 948 949 
950 951 952 953 954 955 956 957 958 959 
960 961 962 963 964 965 966 967 968 969 
970 971 972 973 974 975 976 977 978 979 
980 981 982 983 984 985 986 987 988 989 
990 991 992 993 994 995 996 997 998 999 
1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 
----------------------------

Listing 22.62    Die Ausgabe des Programms (gekürzt)

Die do-while-Schleife

Eine Abwandlung der while-Schleife ist die do-while-Schleife. Bei dieser Schleife wird der Anweisungsblock immer ein erstes Mal ausgeführt. Erst danach wird er nur noch ausgeführt, wenn die Bedingung erfüllt ist.

do 
{ 
    Anweisung(en) 
} 
while ( Bedingung );

Listing 22.63    Aufbau der do-while-Schleife

Vor Kurzem hatten wir mit einer while-Schleife bis 1.000.000 gezählt. In einer do-while-Schleife müsste in diesem Fall keine große Veränderung stattfinden:

/* Die while-Schleife: */ 
int i = 1; 
 
while ( i <= 1000000 ) 
{ 
   printf("%i\n", i); 
   i++; 
} 
/* Die do-while-Schleife: */ 
int i = 1; 
 
do 
{ 
   printf("%i\n", i); 
   i++; 
} 
while ( i <= 1000000);

Listing 22.64    Bis 1.000.000 zählen

Der Unterschied ist allerdings der, dass die erste Ausgabe auch dann erfolgt, wenn »i« größer als 1.000.000 ist.

Die for-Schleife

Wie Sie bereits sahen, wird in fast jeder Schleife gezählt. In der for-Schleife ist genau das kein Problem. Sie ist dazu gedacht, Werte während der Schleifendurchläufe zu verändern, und ist daher sehr beliebt.

for ( Initialisierung ; Bedingung; Werte-Veränderung ) 
{ 
     Anweisung(en) 
}

Listing 22.65    Aufbau einer for-Schleife

Bei der Initialisierung werden die Werte von Variablen für den Schleifendurchlauf festgelegt. Der Bedingungsteil ist wie bei anderen Schleifen zu verwenden, und die Werte-Veränderung kann für jegliche Anpassung (etwa Inkrementierung) von Variablen verwendet werden.

Im Gegensatz zu C++ können in C keine Variablen im Initialisierungsbereich einer for-Schleife angelegt werden, auch wenn einige Compiler (jedoch nicht der gcc) das erlauben.

Unser Lieblingsbeispiel ließe sich folgendermaßen auf die for-Schleife übertragen:

int i; 
 
for ( i = 1 ; i <= 1000000 ; i++ ) 
{ 
   printf("%i\n", i); 
}

Listing 22.66    Bis 1.000.000 zählen

In einer for-Schleife können sowohl der Initialisierungsteil als auch die beiden anderen Teile fehlen. Eine Variable könnte beispielsweise vorher initialisiert werden (wodurch der Initialisierungsteil überflüssig wäre).

Endlosschleifen

Manchmal möchte man, dass eine Schleife unendlich lange läuft (etwa in Prozessen von Netzwerkservern, die 24 Stunden am Tag in einer Schleife Verbindungen annehmen und an Kindprozesse weitergeben). Mit jeder C-Schleife ist dies möglich. Dazu wird einfach eine Bedingung angegeben, die immer wahr ist.

while ( 1 ) 
{ 
     Anweisung(en) 
} 
 
do 
{ 
     Anweisung(en) 
} while ( 1 ); 
 
for ( ; 1 ; ) 
{ 
     Anweisung(en) 
}

Listing 22.67    Endlosschleifen

for(;;)

In der for-Schleife können Sie auch die Bedingung für diesen Fall weglassen.

for (;;) 
{ 
    Anweisung(en) 
}

Listing 22.68    Endlose for-Schleife

goto

Sprünge sind eine sehr unbeliebte Möglichkeit, Schleifen zu programmieren, die eigentlich nur mit »Jumps« (auch »Branches« genannt) in Assembler ihren Nutzen finden sollten. In höheren Programmiersprachen wie C gelten Sprünge als ein schlechter Programmierstil und sollten nicht verwendet werden. Nichtsdestotrotz gibt es sie.

Für einen Sprung wird zunächst ein »Label« definiert. In C geht dies, indem man den Namen des Labels, gefolgt von einem Doppelpunkt, schreibt. Zu diesem Label wird »gesprungen«, indem man es als Sprungziel für die Anweisung goto benutzt.

Sprungziel: 
   Anweisung(en) 
goto Sprungziel;

Listing 22.69    Aufbau einer goto-Anweisung

Auf diese Weise lassen sich natürlich auch ganz einfach Endlosschleifen programmieren. In einigen Quelltexten (etwa dem OpenBSD-Kernel) werden goto-Anweisungen benutzt, um frühzeitig an den Endteil einer Funktion zu springen. Dies kann unter Umständen die Performance verbessern, sollte aber trotzdem vermieden werden.

/* Eine Endlosschleife mit goto */ 
endlos: 
  printf("Unendlich oft wird diese Zeile ausgegeben.\n"); 
goto endlos; 
 
/* Bis 1000000 zählen mit goto */ 
int i = 1; 
 
nochmal: 
    printf("%i\n", i); 
    i++; 
    if ( i <= 1000000 ) 
        goto nochmal;

Listing 22.70    Endlosschleife und Zähler


Galileo Computing

22.1.7 Funktionen  downtop

Möchte man bestimmte Anweisungen mehrmals ausführen, dann verwendet man in der Regel eine Schleife. Doch was ist, wenn man die gleichen Anweisungen (eventuell mit unterschiedlichen Werten) an verschiedenen Stellen des Programms mehrmals ausführen möchte? Dafür gibt es Funktionen. Eine Funktion führt eine Reihe von Anweisungen aus, indem man sie anstelle der Anweisungen aufruft.

Datentyp Funktionsname ( [Parameterliste] ) 
{ 
    Anweisung(en) 
}

Listing 22.71    Aufbau einer Funktion

In C können Funktionen Werte übergeben bekommen und zurückgeben. Beginnen wir aber mit einer sehr einfachen Funktion.

void sage_hallo() 
{ 
    printf("Hallo"); 
}

Listing 22.72    Eine einfache C-Funktion

Diese Funktion gibt nichts zurück (void) und hat den Namen »sage_hallo«. Ihre Parameterliste ist leer, und sie führt auch nur eine Anweisung (die Ausgabe von »Hallo«) aus.

Wie man eine Funktion aufruft, wissen Sie bereits durch die vielfach verwendete Funktion »printf«.

sage_hallo();

Listing 22.73    Aufruf der Funktion

Funktionsparameter

Nun gehen wir einen Schritt weiter und lernen, wie man Funktionen Parameter übergibt (auch diese Prinzipien wurden bereits in den Kapiteln zur Shell besprochen). Dazu implementieren wir eine Funktion, die die Summe aus zwei übergebenen Parametern berechnet.

/* Berechne die Summe aus a und b */ 
void summe(short a, short b) 
{ 
     /* Eine int-Variable ist groß genug, um das 
      * Ergebnis zweier short-Variablen aufzunehmen. 
      */ 
     int ergebnis = a + b; 
     /* Ausgabe der Integer-Variable 'ergebnis' */ 
     printf("%i\n", ergebnis); 
}

Listing 22.74    Funktion zur Berechnung einer Summe

Diese Funktion wird aufgerufen, indem man ihr zwei entsprechende Werte übergibt:

summe(777, 333);

Listing 22.75    Aufruf von summe()

Rückgabewerte

Diese Funktion kann aber immer noch stark verbessert werden. Wie wäre es etwa, wenn wir das Ergebnis der Rechenoperation weiter verwenden wollen, um damit etwas anderes zu berechnen? Dies ist mit der obigen Funktion nicht möglich, lässt sich aber durch Rückgabewerte erledigen.

Wenn eine Funktion einen Wert zurückgibt, dann muss zunächst der Datentyp dieses Rückgabewertes angegeben werden. void bedeutet, wie Sie bereits wissen, dass kein Wert zurückgegeben wird.

Da das Ergebnis ein Integer-Wert ist, können wir in diesem Fall int zurückgeben. Die Rückgabe eines Wertes wird in einer Funktion mit dem Befehl return [Wert oder Variable] erledigt.

int summe(short a, short b) 
{ 
    int ergebnis = a + b; 
    return ergebnis; 
}

Listing 22.76    Rückgabe des Ergebnisses

Diese Funktion nun zu benutzen ist ebenfalls recht einfach. Sie kann direkt an eine Variable zugewiesen werden:

int x; 
 
/* Den Rückgabewert in 'x' speichern: */ 
x = summe(485, 3921);

Listing 22.77    Benutzung von Rückgabewerten

»Stattdessen kann ich aber auch einfach x = 485 + 3921 schreiben!«, werden Sie nun sagen.

Das ist richtig. Dieses einfache Beispiel sollte aber auch nur zeigen, wie leicht es ist, Funktionen in C zu benutzen. Hier ein etwas nützlicheres Beispiel zur Berechnung der Fakultät einer Zahl:

#include <stdio.h> 
 
long fac(short n) 
{ 
    long ergebnis; 
 
    ergebnis = n; 
 
    for (n = n – 1; n > 0; n--) { 
        ergebnis *= n; 
    } 
 
    return ergebnis; 
} 
 
int main() 
{ 
    short val = 7; 
    long ret; 
 
    ret = fac(7); 
 
    printf("Die Fakultaet von %hi ist %ld\n", val, ret); 
 
    return 0; 
}

Listing 22.78    Fakultät berechnen


Galileo Computing

22.1.8 Präprozessor-Direktiven  downtop

Bevor ein C-Compiler den eigentlichen ausführbaren Code eines Programmes erzeugt, wird der Quellcode auf syntaktische Fehler und auf Präprozessor-Direktiven (engl. preprocessor directives) hin untersucht. <Ein Compiler erledigt noch einige weitere Aufgaben, beispielsweise erstellt er Objectfiles, ruft den Linker auf, erstellt Assembler-Code aus C-Code, ...>

Diese Präprozessor-Direktiven werden nicht als C-Anweisungen interpretiert, sondern sind direkte Anweisungen an den Compiler. Präprozessor-Direktiven beginnen immer mit einer Raute, auf die ein Schlüsselwort und je nach Direktive auch Parameter folgen.

# Schlüsselwort [Parameter]

Listing 22.79    Aufbau von Präprozessor-Direktiven

#define

Mit der Anweisung #define lassen sich Makros erstellen. Makros werden dort, wo sie im Programmcode eingefügt werden, durch den Programmcode ersetzt, der für sie definiert wurde. <Mit der Ausnahme, dass Makros innerhalb von Zeichenketten wirkungslos sind.>

Üblicherweise schreibt man Makros in Großbuchstaben, um sie von Variablen und Funktionsnamen zu unterscheiden.

#define ANZAHL 10

Listing 22.80    Ein Makro erstellen

Die Verwendung erfolgt wie gesagt über den Namen des Makros.

for ( i = 1 ; i < ANZAHL ; i++ ) 
{ 
      printf("%i\n", i); 
}

Listing 22.81    Verwenden eines Makros

Komplexere Ausdrücke

Makros können auch komplexere Ausdrücke enthalten:

#define CHECK if ( a < 0 ) { printf("Fehler: a zu klein!"); }

Listing 22.82    Komplexere Makros sind auch kein Problem.

Verteilung auf mehrere Zeilen

Wird ein Makro zu lang, dann kann es auch auf mehrere Zeilen verteilt werden. Am Ende einer Zeile muss dazu ein Slash (\) stehen. Dies weist den Compiler an, die nächste Zeile auch noch dem Makro zuzuordnen.

#define CHECK				\
   if ( a < 0 ) 				\
   { 					\
        printf("Fehler: a zu klein!");	\
   }

Listing 22.83    Makros über mehrere Zeilen

Makros mit Parametern

Doch C-Makros können noch mehr: Sie können Parameter benutzen und somit dynamisch verwendet werden. Nehmen wir einmal an, ein Makro soll einen Wert überprüfen und zudem einen zu übergebenden Text ausgeben. Dazu wird eine Schreibweise benutzt, die der einer Funktion ähnelt. Allerdings muss hierfür kein Datentyp angegeben werden.

#define CHECK (str)	\ 
   if ( a < 0 ) 		\
   { 			\
        printf(str); 	\
   }

Listing 22.84    Ein Makro mit Parameter

Der Aufruf – Sie ahnen es sicher schon – entspricht fast dem der Funktion:

CHECK("Fehler: a ist zu klein");

Listing 22.85    Aufruf des check-Makros mit Parameter

#undef

Ein Makro ist nur innerhalb der Datei definiert, in der es implementiert wurde. <Man kann Dateien mit der #include-Anweisung in andere Dateien einbinden und Makros damit in mehreren Dateien verfügbar machen.> Außerdem ist ein Makro nur ab der Zeile, in der es implementiert wurde, bis zum Ende einer Quelldatei bekannt. Möchte man ein Makro vor dem Dateiende löschen, dann benutzt man den Befehl #undef Makroname.

Unser CHECK-Makro ließe sich etwa folgendermaßen löschen:

#undef CHECK

Listing 22.86    Das Makro »CHECK« löschen.

#if, #ifdef, #elif, #endif und #if defined

Auch bedingte Anweisungen gibt es für den Präprozessor. Mit diesen lassen sich die Werte und das Vorhandensein von Makros überprüfen.

Die Überprüfung auf Werte wird dabei mit den Anweisungen #if (einfacher Test, wie if) und #elif (Test auf alternative Werte wie else if) erledigt. Am Ende einer solchen Anweisung muss der Befehl #endif stehen, der mit dem bash-Befehl fi und mit der geschlossenen geschweiften Klammer der if-Anweisung vergleichbar ist. #endif signalisiert also nur das Ende einer bedingten Anweisung.

#if ANZAHL < 100 
     printf("Anzahl ist kleiner als 100"); 
#elif ANZAHL == 100 
     printf("Anzahl ist genau 100"); 
#elif ANZAHL == 101 
     printf("Anzahl ist genau 101"); 
#else 
     printf("Anzahl ist größer 101"); 
#endif

Listing 22.87    Überprüfen des Wertes des Makros ANZAHL

Definierte Makros

Es ist zudem möglich, darauf zu prüfen, ob Makros überhaupt definiert sind.

#ifdef ANZAHL 
     printf("ANZAHL ist definiert."); 
#else 
     printf("ANZAHL ist nicht definiert."); 
#endif

Listing 22.88    Ist ANZAHL definiert?

Sie können auch gleichzeitig auf das Vorhandensein mehrerer Makros prüfen. Zudem können einige logische Operatoren verwendet werden.

#if !defined (ANZAHL) && !defined(MAXIMAL) 
    ... 
#endif

Listing 22.89    Ist ANZAHL definiert?

#include

Neben der Präprozessor-Anweisung #define gibt es noch eine weitere besonders wichtige Anweisung namens #include. Sie wird dazu eingesetzt, andere Dateien an einer bestimmten Stelle in eine Datei einzubinden.

Es gibt dabei zwei Schreibweisen für eine #include-Anweisung:

  • Entweder man schreibt den Dateinamen in eckige Klammern. Dann werden die dem Compiler bekannten Include-Verzeichnisse des Systems durchsucht. <Typischerweise /usr/include bekannten Include-Verzeichnisse des Systems durchsucht. <Typischerweise /usr/include
  • Oder, man benutzt Anführungszeichen für den Dateinamen. In diesem Fall wird das aktuelle Arbeitsverzeichnis nach der Datei durchsucht. Ist die Datei dort nicht zu finden, werden die dem Compiler zusätzlich angegebenen Include-Pfade nach der Datei durchsucht. <Diese include-Pfade werden über -IPfad gesetzt.>
#include <Dateiname> 
#include "Dateiname"

Listing 22.90    So verwendet man #include.

Hier ein kleines Beispiel. Die Datei main.h, die ein paar Makros und die #include-Anweisung für die Datei stdio.h enthält, soll in die Quellcode-Datei main.c eingefügt werden. Beide Dateien befinden sich im gleichen Verzeichnis.

#include <stdio.h> 
 
#define MIN 1 
#define MAX 9

Listing 22.91    Die Datei main.h

#include "main.h" 
 
int main() 
{ 
    int i; 
 
    for ( i = MIN ; i < MAX ; i++) 
       printf("%i\n", i); 
 
    return 0; 
}

Listing 22.92    Die Datei main.c

Der Compiler wird in diesem Fall wie immer aufgerufen:

$ gcc -o main main.c

Listing 22.93    Compiler aufrufen

-I

Typischerweise befinden sich die Header-Dateien in einem Unterverzeichnis (etwa include oder inc). Würde sich die Datei main.h dort befinden, dann müsste der gcc das Verzeichnis nach Header-Dateien untersuchen. Das erreicht man (wie bereits erwähnt) mit dem Parameter -I. <Es können mehrere Include-Verzeichnisse angegeben werden. In diesem Beispiel werden sowohl include/ als auch inc/ nach der Datei main.h durchsucht.>

$ gcc -o main main.c -Iinclude -Iinc

Listing 22.94    Compiler mit -I aufrufen

Relative Pfadangabe

Eine relative Pfadangabe ist auch möglich. Hier sind ein paar Beispiele:

#include "../include/main.h" 
#include "include/main.h" 
#include "inc/main.h"

Listing 22.95    Relative Pfadangabe: Beispiele

#error

Trifft der Präprozessor auf die Anweisung #error, bricht der Compiler den Übersetzungsvorgang ab.

#error "Lieber User, ich habe keine Lust mehr!"

Listing 22.96    Relative Pfadangabe: Beispiele

Der gcc bricht dann mit folgender Fehlermeldung ab:

a.c:1:2: error: #error "Lieber User, ich habe keine Lust mehr!"

Listing 22.97    Relative Pfadangabe: Beispiele

Nutzen?

Wann ist diese Anweisung nützlich? Nun, dem Compiler können dynamisch Makros inklusive Werte übergeben werden. Außerdem bringt der Compiler standardmäßig bestimmte Makros (teilweise mit Werten) mit, die beim Übersetzungsvorgang abgefragt werden können.

Der folgende Code überprüft, ob die vordefinierten Makros __OpenBSD__ oder __linux__ nicht definiert sind. <Diese Makros sind nur definiert, wenn das System, auf dem der Quellcode kompiliert wird, dem Namen des Makros entspricht.>

#if !defined (__OpenBSD__) && !defined(__linux__) 
 #error "Programm Tool laeuft nur unter Linux/OpenBSD" 
#endif

Listing 22.98    Ist __OPENBSD__ oder __linux__ definiert?

#pragma

Die Direktive #pragma wird sehr unterschiedlich verwendet. Ihre Funktionsweise ist abhängig von der Plattform und dem Compiler sowie von dessen Version. <Für Parallelprogrammierung werden beispielsweise »#pragma omp parallel for ...« und ähnliche Befehle verwendet.>

Vordefinierte Makros

Es gibt einige vordefinierte Makros, die im gesamten Programmcode verwendet werden können. Dazu zählen Makros die compiler-spezifisch sind (und mit denen man etwa die Version der C-Library, die des Compilers oder den Namen des Betriebssystems abfragen kann), und einige, die jeder ANSI-C-Compiler kennen sollte.

Wir beschränken uns an dieser Stelle auf solche, die jeder Compiler kennen sollte. Diese Makros sind manchmal für Debugging-Zwecke nützlich.

Zeile

Möchte man im Quellcode erfahren, in welcher Zeile sich eine Anweisung befindet, so kann das Makro __LINE__ verwendet werden. Es beinhaltet eine Integer-Zahl die etwa mit printf ausgegeben werden kann.

Datei

Den Namen der Datei, in der man sich derzeit befindet, erfährt man über das Makro __FILE__, das eine Zeichenkette des Dateinamens enthält.

Datum und Uhrzeit

Das Datum, an dem ein Programm kompiliert wurde, sowie die genaue Uhrzeit erfährt man durch die Makros __DATE__ und __TIME__.

ISOC

Ist ein Compiler ANSI-C-kompatibel, definiert er das Makro __STDC__.

Neu in ISOC99

Seit dem ISOC99-Standard kamen noch weitere Makros hinzu, die jeder ANSI-C99-kompatible Compiler kennen muss. <Für weitere – im Übrigen sehr interessante Informationen – werfen Sie bitte einen Blick in gcc.gnu.org/onlinedocs/gcc/Standards.html.> Für Einsteiger ist davon eigentlich nur __func__ interessant, das den aktuellen Funktionsnamen enthält.

Hier noch ein Beispiel:

#include <stdio.h> 
 
#ifndef __STDC__ 
  #error "Kein ANSI-C-Compiler!" 
#endif 
 
#define ANZAHL 999 
 
int main() 
{ 
 if (ANZAHL < 1000) 
   printf("%s %s: Fehler in Datei %s, Zeile %i\n", 
          __DATE__, __TIME__, __FILE__, __LINE__); 
 
  return 0; 
}

Listing 22.99    Nutzen vordefinierter Makros


Galileo Computing

22.1.9 Grundlagen der Zeiger (Pointer)  downtop

Nun kommen wir zu einem der letzten Themen unseres C-Crashkurses: den Zeigern. Dieses Thema macht C für viele Programmierer zu einer furchtbaren, unlernbaren Sprache und lässt einige Informatikstudenten im Grundstudium an ihren Fähigkeiten zweifeln. Im Grunde genommen ist das Thema »Zeiger« (engl. Pointer) aber gar nicht so schwer, also nur Mut!

Im Übrigen lassen sich durch Zeiger aufwendige Kopieraktionen im Speicher verhindern und sich somit Programme beschleunigen. Überhaupt sind Zeiger so praktisch, dass wir sie niemals missen wollten. Man spricht in diesem Zusammenhang auch von Referenzierung, da ein Zeiger eine Referenz auf eine Speicheradresse ist.

Adressen von Variablen

Adressoperator

Der Wert einer Variablen steht an einer Position im Speicher. Die Variable kann vereinfacht gesagt als »Name« dieser Speicherposition angesehen werden. Mit diesem Namen wird (ohne, dass Sie etwas davon erfahren müssen) auf die zugehörige Speicheradresse zugegriffen und deren Wert entweder gelesen oder geschrieben. Auf die Speicheradresse einer Variable wird mit dem Adressoperator (&) zugegriffen.

Im Folgenden soll die Adresse der Variable »a« in der Variable »adresse« gespeichert werden.

int a = 10; 
long adresse; 
 
adresse = & a; 
 
printf("Adresse der Variable a: %li \n", adressse);

Listing 22.100    Für den Adressoperator

Zeiger auf Adressen

Ein Zeiger zeigt nun auf genau so eine Adresse. Man arbeitet also nicht mehr mit der eigentlichen Variable, sondern mit einer Zeigervariable, die die Adresse des Speichers kennt, auf die man zugreifen möchte.

Anders formuliert: Ein Zeiger ist nichts weiter als eine Variable. Diese Variable enthält die Adresse eines Speicherbereichs.

Einen Zeiger deklariert man mit dem Referenz-Operator (*). Dieser ist nicht mit dem Multiplikationsoperator zu verwechseln, der durch das gleiche Zeichen repräsentiert wird.

int *zeiger;

Listing 22.101    Deklaration eines Zeigers

Möchte man einen Zeiger verwenden, so benötigt man natürlich irgendeine Speicheradresse. Entweder wird dafür – wie wir in diesem Buch allerdings nicht zeigen können – dynamisch Speicher reserviert, oder man benutzt die Adresse einer Variable.

Wir lassen die Variable »zeiger«, die ein Zeiger ist, auf die Adresse der Variable »wert« zeigen. An dieser Speicheradresse steht der Wert 99.

int *zeiger; 
int wert = 99; 
 
/* Zeiger = Adresse von 'wert' */ 
zeiger = & wert;

Listing 22.102    Ein Zeiger auf eine Integer-Variable

Werte aus Zeigern lesen

Mit dem Referenz-Operator (*) können auch Werte aus Zeigern gelesen werden. Man spricht in diesem Fall von Dereferenzierung.

Das Ergebnis einer solchen Operation ist der Wert, der an dieser Speicherstelle steht.

int *zeiger; 
int wert1 = 99; 
int wert2 = 123; 
 
/* Zeiger = Adresse von 'wert1' */ 
zeiger = &wert1 
/* 'wert2' = Wert an Adresse von Zeiger 
 * (das ist der Wert an der Adresse von 
 * 'wert1', also 99.) 
 */ 
wert2 = *zeiger;

Listing 22.103    Dereferenzierung eines Zeigers

Werte lassen sich ändern

Zeigt ein Zeiger auf eine Variable und ändert man den Wert einer Variablen so steht an der Adresse der Variablen natürlich auch dieser Wert. Demnach zeigt ein Zeiger auch immer auf den aktuellen Wert einer Variablen.

int a = 99; 
int *zeiger_a; 
 
zeiger_a = &a; 
 
/* a = 100 */ 
a++; 
 
/* a = 101 */ 
*zeiger_a = *zeiger_a + 1; 
 
printf("Zeiger: %i, Wert an Zeiger-Adresse: %i\n", 
       zeiger_a, *zeiger_a); 
printf("Wert von a: %i\n", a);

Listing 22.104    Verändern von Werten

Eine mögliche Ausgabe des Programms wäre die folgende. Die Adresse des Zeigers wird auf Ihrem Rechner mit sehr hoher Wahrscheinlichkeit eine andere sein, doch die beiden Werte von je 101 müssen dieselben sein. <Tatsächlich können sich die Speicheradressen bei jedem Programmstart ändern.>

Zeiger: 925804404, Wert an Zeiger-Adresse: 101 
Wert von a: 101

Listing 22.105    Die Ausgabe des Codes

Call by Reference in C

Bevor wir den kleinen Ausflug in die Zeiger beenden (es gibt noch ein paar weitere Themen, die auf dem bisher Gesagten aufbauen), werden wir uns aber noch eine recht nützliche Funktion von Speicheradressen anschauen: Call by Reference.

Unter Call by Reference versteht man den Aufruf einer Funktion nicht mit den Werten von Variablen, sondern mit den Adressen der Variablenwerte. Verändern die Funktionen dann die Werte an der Adresse einer Variablen, so sind die Werte auch in der übergeordneten Funktion gesetzt.

Dies ist sehr nützlich, da Funktionen immer nur einen Wert zurückgeben können. Auf diese Weise jedoch ist es möglich, mehr als einen Wert zurückzugeben. Die Schreibweise für einen Funktionsparameter, der als Referenz übergeben wird, ist so wie bei der Deklaration einer Zeigervariable: Es wird der *-Operator verwendet.

#include <stdio.h> 
 
void func(int *z) 
{ 
   *z = *z + 1; 
} 
 
int main() 
{ 
  int a = 99; 
  int *z = &a; 
 
  func(&a); 
  printf("a = %i\n", a); 
 
  func(z); 
  printf("a = %i\n", a); 
 
  return 0; 
}

Listing 22.106    Beispiel für Call by Reference

Die Ausgabe wird Sie nicht überraschen: Der Wert von »a« wurde nach jedem Funktionsaufruf inkrementiert:

$ gcc -Wall -o cbr cbr.c 
$ ./cbr 
a = 100 
a = 101

Listing 22.107    Ausgabe des Programms


Galileo Computing

22.1.10 Grundlagen der Arrays  downtop

Hat man in C eine Variable mit mehreren Elementen, so spricht man von einem Array. Sie kennen Arrays schon aus dem Kapitel zur Shellskript-Programmierung, doch wir werden gleich noch einmal an Beispielen erklären, worum es sich hierbei handelt. <Viele deutsche C-Bücher nennen diesen Datentyp auch Feld.> Arrays können in C mehrere Dimensionen haben, doch wir werden uns hier nur auf eindimensionale Arrays beschränken.

Am besten lassen sich Arrays wohl an einem Beispiel erklären. Nehmen wir an, es soll das Gewicht von zehn Personen gespeichert werden. Nun können Sie zu diesem Zweck zehn einzelne Variablen anlegen. Das wäre allerdings recht umständlich.

Besser ist es da, man nimmt eine Variable und nennt sie etwa »gewicht«. Dieser Variable verpasst man zehn Elemente, von denen jedes einen Wert speichern kann.

/* Integer-Array mit 10 Elementen deklarieren */ 
int gewicht[10]; 
 
gewicht[0] = 77; 
gewicht[1] = 66; 
gewicht[2] = 55; 
gewicht[3] = 67; 
gewicht[4] = 65; 
gewicht[5] = 78; 
gewicht[6] = 80; 
gewicht[7] = 105; 
gewicht[8] = 110; 
gewicht[9] = 65;

Listing 22.108    Deklaration und Intialisierung eines Arrays

Übrigens: Alle Elemente eines C-Arrays sind vom gleichen Datentyp. Außerdem ist das erste Array-Element in C immer das Element 0. Bei einem Array mit zehn Elementen ist das letzte Element also Element 9.

Der Zugriff auf Array-Elemente erfolgt mit name[Index]. Dies gilt sowohl für die Zuweisung von Werten an Array-Elemente wie auch für das Auslesen aus Werten von Array-Elementen:

/* Integer-Array mit 10 Elementen deklarieren */ 
int dreier[3]; 
int a, b, c; 
 
a = 3; 
 
dreier[0] = a; 
dreier[1] = dreier[0] * 2; /* = 6 */ 
dreier[2] = 9; 
 
b = dreier[3]; /* = 9 */ 
c = dreier[2] – 3; /* = 3 */

Listing 22.109    Benutzen von Arrays

Variablen als Index

Der Array-Index kann auch durch eine ganzzahlige Variable angegeben werden. So lassen sich hervorragend Schleifen bauen. Dazu eignen sich also die Datentypen int, short und char.

int main() 
{ 
        char c; 
        short s; 
        int i; 
        int array[10]; 
 
        for (c = 0; c < 10; c++) 
                array[i] = 99; 
 
        for (s = 0; s < 10; s++) 
                array[s] = 88; 
 
        for (i = 0; i < 10; i++) 
                array[i] = 77; 
 
        return 0; 
}

Listing 22.110    Variablen als Array-Index


Galileo Computing

22.1.11 Strukturen  downtop

Eine Struktur (engl. struct) stellt einen Datentyp dar, der aus mindestens einem Datentyp, meist aber aus mehreren Datentypen, besteht. In diversen anderen Programmiersprachen heißen Strukturen Records.

Die einzelnen Variablen in einer Struktur müssen von einem Datentyp sein (wobei es sich bei diesem auch um Strukturen, Arrays und Zeiger auf Variablen, Zeiger auf Strukturen, Zeiger auf Arrays und Zeiger auf Funktionen handeln kann). Außerdem muss jede Variable einer Struktur einen anderen Namen erhalten.

struct Name 
{ 
    Datentyp Variablenname [:Anzahl der Bits]; 
    Datentyp Variablenname [:Anzahl der Bits]; 
    Datentyp Variablenname [:Anzahl der Bits]; 
    ... 
};

Listing 22.111    Aufbau einer C-Struktur

Optional kann hinter jeder Variable noch – durch einen Doppelpunkt getrennt – die Anzahl der Bits angegeben werden, die für diese Variable benötigt wird. Dies ist besonders in der Netzwerkprogrammierung sinnvoll, wenn es darum geht, bestimmte Protokollheader abzubilden. Wir werden uns auf Strukturen mit »ganzen« Variablen, also Variablen ohne Bit-Angabe, beschränken. <Wird die Bit-Anzahl der normalen Bit-Anzahl des Datentyps angepasst, ist die Variable natürlich auch »ganz«.>

Beim Zuschauen lernt man ja bekanntlich am besten. Nehmen wir also an, es sollen mehrere Daten einer Person in einer Struktur gespeichert werden. Dazu zählen Alter, Gewicht und Größe. Die zugehörige Struktur benötigt daher drei verschiedene Variablen.

struct person 
{ 
        short gewicht; 
        short alter; 
        short groesse; 
};

Listing 22.112    Die Struktur 'person'

Initialisierung

Zur Zuweisung von Werten benötigen wir zunächst eine »Variable« von unserem neuen Datentyp »person«. Dazu erzeugen wir mit struct person Name die Variable »name« vom Typ der Struktur »person«. Werte können dann in der Form Name.Variable = Wert zugewiesen werden.

Hier also der entsprechende Code in einem Beispielprogramm: <Dreimal dürfen Sie raten, wessen Bauchansatz das ist ;-)>

struct person 
{ 
        short gewicht; 
        short alter; 
        short groesse; 
}; 
 
int main() 
{ 
        struct person p; 
 
        p.gewicht = 72; 
        p.alter = 22; 
        p.groesse = 182; 
 
        return 0; 
}

Listing 22.113    Verwenden der Struktur 'person'

Arrays und Strukturen

Richtig spaßig werden Strukturen aber meist erst in Array-Form. Sollen etwa drei Personen auf diese Weise gespeichert werden, dann ist auch das kein Problem. Dazu erzeugen wir uns von unserer Variable einfach ein Array mit drei Elementen.

#include <stdio.h> 
 
struct person { 
        short gewicht; 
        short alter; 
        short groesse; 
}; 
 
int main() 
{ 
        struct person p[3]; 
 
        p[0].gewicht = 70; 
        p[0].alter = 22; 
        p[0].groesse = 182; 
 
        p[1].gewicht = 88; 
        p[1].alter = 77; 
        p[1].groesse = 166; 
 
        p[2].gewicht = 95; 
        p[2].alter = 50; 
        p[2].groesse = 190; 
 
        return 0; 
}

Listing 22.114    Drei Personen als Struktur-Array


Galileo Computing

22.1.12 Arbeiten mit Zeichenketten (Strings)  downtop

Zeichenketten bestehen aus einzelnen Zeichen. Einzelne Zeichen können, wie Sie bereits wissen, in einer char-Variablen gespeichert werden. Die Lösung, ein Array aus char-Variablen für eine Zeichenkette zu verwenden, sollte also nicht sehr überraschend sein.

char zeichenkette[3]; 
 
zeichenkette[0] = 'A'; 
zeichenkette[1] = 'B'; 
zeichenkette[2] = 'C';

Listing 22.115    Eine erste Zeichenkette

Dies geht allerdings auch wesentlich einfacher. Dazu muss man allerdings wissen, dass C normalerweise ein abschließendes \0-Zeichen hinter jeder Zeichenkette benutzt. Dieses abschließende Null-Zeichen signalisiert nur das Ende der Zeichenkette und verhindert in vielen Fällen, dass Ihr Programm einfach abstürzt, weil Funktionen, die mit einer Zeichenkette arbeiten, sonst immer mehr Zeichen lesen würden und irgendwann in Speicherbereiche kommen würden, auf die sie keinen Zugriff haben.

Ein einfaches Verfahren, Text in einem Array zu speichern, besteht darin, die Anzahl der Elemente wegzulassen und nur den Text für das Array zu speichern. C setzt in diesem Fall automatisch die Anzahl der Array-Elemente sowie das abschließende \0-Zeichen.

char zeichenkette[] = "ABC";

Listing 22.116    So geht es einfacher.

Möchte man eine »leere« Zeichenkette anlegen, so sollte man den Speicherbereich des Arrays immer mit \0-Zeichen überschreiben, um sicherzugehen, dass keine zufälligen Daten enthalten sind. Die Schreibweise hierfür ist die folgende: <Es gibt viele alternative Möglichkeiten, dies zu erreichen, etwa die Funktion bzero(), die Funktion memset() oder eine Schleife.>

char zeichenkette[100] = { '\0' };

Listing 22.117    Nullen-Füller

Ausgeben von Zeichenketten

Die Ausgabe ließe sich natürlich in einer Schleife abwickeln, doch das wäre sehr umständlich. Stattdessen gibt es für die printf-Funktion den Formatparameter %s. Dieser bedeutet, dass eine Zeichenkette ausgegeben werden soll. Auch hierfür wird ein \0-Zeichen am Ende einer Zeichenkette benötigt.

printf("Zeichenkette: %s\n", zeichenkette);

Listing 22.118    Ausgabe einer Zeichenkette

Kopieren von Zeichenketten

Nun, da Sie wissen, wie man eine Zeichenkette anlegt, können wir einen Schritt weiter gehen und Zeichenketten kopieren. Dazu verwendet man entweder eine umständliche Schleife, oder man lässt diese Arbeit von einer Funktion erledigen. Zum Kopieren von Daten und speziell zum Kopieren von Zeichenketten gibt es verschiedenste Funktionen in C. Vorstellen werden wir die zwei wichtigsten: strcpy und strncpy. Beide Funktionsprototypen befinden sich in der Datei string.h.

strcpy

strcpy werden zwei Argumente übergeben. Das erste Argument ist das Ziel des Kopiervorgangs, und das zweite Argument ist die Quelle. Möchten Sie also die Zeichenkette aus dem Array »z1« in das Array »z2« kopieren, dann würde das so ablaufen:

#include <stdio.h> 
#include <string.h> 
 
int main() 
{ 
        char z2[] = "Hallo"; 
        char z1[10] = { '\0' }; 
 
        strcpy(z1, z2); 
        printf("%s = %s\n", z2, z1); 
 
        return 0; 
}

Listing 22.119    Kopieren einer Zeichenkette

strncpy

Die Funktion strncpy benötigt noch ein drittes Argument: die Anzahl der zu kopierenden Zeichen. Soll vom obigen String etwa nur ein Zeichen kopiert werden, dann läuft das so:

#include <stdio.h> 
#include <string.h> 
 
int main() 
{ 
        char z2[] = "Hallo"; 
        char z1[10] = { '\0' }; 
 
        strncpy(z1, z2, 1); 
        printf("%s != %s\n", z2, z1); 
 
        return 0; 
}

Listing 22.120    Anwenden von strncpy


Galileo Computing

22.1.13 Einlesen von Daten  downtop

In C können Werte für Variablen und ganze Zeichenketten sowohl von der Tastatur als auch aus Dateien eingelesen werden.

Auch hierfür gibt es diverse Funktionen wie getc, gets, fgets, scanf, fscanf, sscanf, vscanf und viele weitere. Wir werden uns allerdings auf die Funktionen scanf und fscanf beschränken, mit denen die meisten Aufgaben erledigt werden können. Beide Funktionsprototypen befinden sich in der Datei stdio.h.

scanf

Die Funktion scanf liest Werte direkt von der Standardeingabe (wenn man es nicht im Code umprogrammiert, dann ist dies fast immer die Tastatur beziehungsweise Daten aus einer Pipe). Ähnlich wie die Funktion printf wird dabei ein Formatstring übergeben. Dieser Formatstring enthält diesmal jedoch nicht die Werte, die auszugeben sind, sondern die Werte, die einzulesen sind.

Möchten Sie etwa einen Integer einlesen, so verwenden Sie den Parameter %i im Formatstring. Das Ergebnis wird in der entsprechend folgenden Variable gespeichert. Damit scanf den Wert einer Variablen setzen kann, benötigt es allerdings die Speicheradresse der Variablen (die Funktion arbeitet mit Zeigern). Daher müssen Variablen entsprechend übergeben werden.

#include <stdio.h> 
 
int main() 
{ 
        int wert; 
 
        printf("Bitte geben Sie eine ganze Zahl ein: "); 
        scanf("%i", &wert); 
        printf("Sie haben %i eingegeben\n", wert); 
        return 0; 
}

Listing 22.121    Einlesen eines Integers

Zeichenketten einlesen

Übergibt man ein Array an eine Funktion, dann wird dieses Array in C durch seine Adresse übergeben. Sie müssen in diesem Fall also nicht den Adressoperator (&) verwenden.

char wort[100]; 
 
printf("Bitte geben Sie ein Wort ein: "); 
scanf("%s", &wort); 
printf("Sie haben %s eingegeben\n", wort);

Listing 22.122    Eine Zeichenkette einlesen

Würde man in diesem Fall ein Wort mit mehr als 99 Zeichen eingeben, so könnte es zu einem sogenannten Speicherüberlauf kommen. Dies führt zu unvorhersehbarem Verhalten, meistens jedoch zu einem Programmabsturz. Mehr zu diesem Thema erfahren Sie in unserem Buch »Praxisbuch Netzwerksicherheit«. Entgegen vieler Meinungen gibt es allerdings einige Techniken, um mit diesem Problem umzugehen. Mehr dazu folgt im nächsten Kapitel.

fscanf

Die Funktion fscanf unterscheidet sich von scanf dadurch, dass die Eingabequelle im ersten Parameter angegeben wird. Damit ist es auch möglich, aus einer Datei zu lesen. Der erste Parameter ist dabei ein Zeiger vom Typ FILE.


Galileo Computing

22.1.14 FILE und das Arbeiten mit Dateien  downtop

Ein sehr spannendes Thema ist das Arbeiten mit Dateien. Zum Ende unseres kleinen C-Crashkurses lernen Sie nun also, wie aus Dateien gelesen und wie in sie geschrieben wird. Auch hier gibt es verschiedenste Möglichkeiten, dies zu tun. Man könnte etwa die Funktionen open, read, write und close benutzen. Wir empfehlen Ihnen, diese Funktionen einmal anzuschauen, sie sind in vielerlei Hinsicht (etwa auch bei der Netzwerkprogrammierung) von Nutzen. Wir werden uns allerdings mit den ANSI-C-Funktionen fopen, fwrite, fread und fclose beschäftigen. <Es gibt noch so viele weitere Funktionen wie etwa fseek(). Werfen Sie einen Blick in eines der genannten guten Bücher zur Linux-Programmierung, um mehr zu erfahren. Es lohnt sich!>

Die Funktionsprototypen der Funktionen fopen, fwrite, fread und fclose sowie die Definition des Datentyps FILE befinden sich in der Header-Datei stdio.h.

Öffnen und Schließen von Dateien

Dateien öffnen

Das Öffnen einer Datei erfolgt mit der Funktion fopen. Der Funktion werden zwei Argumente, der Dateiname und die Zugriffsart, übergeben. Bei der Zugriffsart unterscheidet man unter Linux zwischen den folgenden Arten:

  • r Die Datei wird zum Lesen geöffnet. Es wird vom Anfang der Datei gelesen.
  • r+ Die Datei wird zum Lesen und Schreiben geöffnet. Es wird vom Anfang der Datei gelesen und am Anfang der Datei geschrieben.
  • w Die Datei wird auf die Länge 0 verkürzt (oder, falls sie nicht existiert, neu angelegt) und zum Schreiben geöffnet. Es wird vom Anfang der Datei geschrieben.
  • w+ Die Datei wird wie im Fall von w geöffnet. Zusätzlich kann in die Datei geschrieben werden.
  • a Die Datei wird zum Schreiben geöffnet bzw. erzeugt, wenn sie nicht existiert. Es wird an das Ende der Datei geschrieben.
  • a+ Die Datei wird wie im Fall von a geöffnet. Allerdings kann auch von der Datei gelesen werden.

FILE

Die Funktion fopen gibt die Adresse eines Dateideskriptors vom Datentyp FILE zurück. Über diesen Deskriptor kann eine geöffnete Datei identifiziert werden. Für Lese-, Schreib- und Schließoperationen auf Dateien ist ein FILE-Deskriptor übrigens nötig.

Fehler

Für den Fall, dass eine Datei (etwa weil sie nicht existiert oder weil das Programm nicht die nötigen Zugriffsrechte auf die Datei hat) nicht geöffnet werden konnte, gibt fopen den Wert NULL zurück. <Meistens ist NULL als Makro für ((void *)0), also einen Zeiger auf die Adresse 0, definiert. Weiter kann in diesem Crashkurs leider nicht auf diesen Wert eingegangen werden.>

fclose

Ein »geöffneter« FILE-Deskriptor wird mit der Funktion fclose wieder geschlossen, indem er ihr als Parameter übergeben wird. Nachdem ein Deskriptor geschlossen wurde, können weder Lese- noch Schreibzugriffe über den Deskriptor erfolgen. Daher sollten Deskriptoren erst geschlossen werden, wenn man sie nicht mehr benötigt. Vergisst ein Programm, einen Deskriptor zu schließen, wird er nach dem Ende des Programms automatisch geschlossen. Es zählt allerdings zum guten Programmierstil, Deskriptoren selbst zu schließen und damit den Verwaltungsaufwand für offene Deskriptoren zu verringern und keine unnützen offenen Dateien im Programm zu haben.

#include <stdio.h> 
 
int main() 
{ 
   FILE * fp; 
 
   /* Oeffnen der Datei /etc/hosts im Nur-Lesen-Modus 
    * am Dateianfang. 
    */ 
   fp = fopen("/etc/hosts", "r"); 
 
   /* Konnte die Datei geoeffnet werden? */ 
   if (fp == NULL) { 
      printf("Konnte die Datei nicht oeffnen!\n"); 
      /* Das Programm mit einem Fehler-Rueckgabewert 
       * verlassen 
      */ 
      return 1; 
   } 
 
   /* Die Datei schliessen */ 
   fclose(fp); 
 
   return 0; 
}

Listing 22.123    Eine Datei öffnen und schließen

Lesen aus Dateien

fread

Aus einer zum Lesen geöffneten Datei kann mit der Funktion fread gelesen werden. Die gelesenen Daten werden dazu in einem char-Array gespeichert (beziehungsweise in einem dynamisch reservierten Speicherbereich aus char-Werten, den Sie in diesem Fall aber mit einem char-Array gleichsetzen können, und was wir im Rahmen dieses Crashkurses nicht behandeln können).

fread benötigt als Parameter die Speicheradresse des char-Arrays (den Adressoperator muss man in diesem Fall, wie bereits gesagt, nicht anwenden), die Größe der zu lesenden Datenblöcke, die Anzahl der zu lesenden Datenblöcke und einen Zeiger auf einen Deskriptor, von dem gelesen werden soll.

Wir erweitern das obige Beispiel nun um die Funktion, 1000 Bytes aus der geöffneten Datei zu lesen. Dazu legen wir ein 1000 Byte großes char-Array an (das letzte Byte wird für das abschließende \0-Zeichen benötigt) und lesen einmal einen Block von 999 Bytes aus der Datei. Anschließend geben wir den gelesenen Inhalt mit printf aus.

#include <stdio.h> 
 
int main() 
{ 
   FILE * fp; 
   char inhalt[1000] = { '\0' }; 
 
   fp = fopen("/etc/hosts", "r"); 
 
   if (fp == NULL) { 
      printf("Konnte die Datei nicht oeffnen!\n"); 
      return 1; 
   } 
 
   fread(inhalt, 999, 1, fp); 
   printf("Inhalt der Datei /etc/hosts:\n%s\n", 
          inhalt); 
   fclose(fp); 
 
   return 0; 
}

Listing 22.124    Eine Datei öffnen und schließen

$ gcc -o file file.c 
$ ./file 
Inhalt der Datei /etc/hosts: 
127.0.0.1       localhost 
127.0.1.1       hikoki.sun      hikoki 
 
192.168.0.1     eygo.sun        eygo 
192.168.0.2     milk.sun        milk 
192.168.0.5     yorick.sun      yorick 
192.168.0.6     hikoki.sun      hikoki 
192.168.0.11    amilo.sun       amilo

Listing 22.125    Aufruf des Progamms (gekürzt)

Schreiben in Dateien

fwrite

Die Parameter der Funktion fwrite sind denen der Funktion fread sehr ähnlich. Der Unterschied ist nur, dass man nicht den Buffer angibt, in den die gelesenen Daten geschrieben werden sollen, sondern den, von dessen Inhalt Daten geschrieben werden sollen. Der zweite Parameter gibt wieder die Größe der zu schreibenden Datenelemente an und der dritte Parameter gibt deren Anzahl an. Der vierte Parameter ist der Deskriptor der geöffneten Datei, in die geschrieben werden soll.

Als Beispiel soll die Zeichenkette »Hallo Welt!« in die Datei /tmp/irgendwas geschrieben werden. An dieser Stelle verwende ich den sizeof-Operator, um die Größe des Arrays zu erfahren. Eine andere Möglichkeit dafür wäre es, die Funktion strlen zu verwenden, die die Länge einer Zeichenkette zurückgibt, die wir in diesem Rahmen aber nicht behandeln können.

#include <stdio.h> 
 
int main() 
{ 
   FILE * fp; 
   char inhalt[] = "Hallo Welt!\n"; 
 
   fp = fopen("/tmp/irgendwas", "w"); 
 
   if (fp == NULL) { 
      printf("Konnte die Datei nicht oeffnen!\n"); 
      return 1; 
   } 
   fwrite(inhalt, sizeof(inhalt), 1, fp); 
 
   fclose(fp); 
 
   return 0; 
}

Listing 22.126    Schreiben in /tmp/irgendwas

Zum Beweis und auch zum Schluss unseres Crashkurses zeigen wir, hier noch einmal den Compileraufruf für das Schreibprogramm, den Aufruf des Programms und das Anschauen der geschrieben Datei.

$ gcc -o file2 file2.c 
$ ./file2 
$ cat /tmp/irgendwas 
Hallo Welt!

Listing 22.127    Compileraufruf und Test


Galileo Computing

22.1.15 Das war noch nicht alles!  toptop

C bietet Ihnen noch eine Menge weiterer Möglichkeiten als die, die wir Ihnen bisher gezeigt haben. So gibt es beispielsweise Aufzählungstypen (enums), Unions, mehrdimensionale Arrays, Zeiger auf Arrays, Pointer auf Arrays aus Pointern, Pointer auf ganze Funktionen (und natürlich auch Arrays aus Pointern auf Funktionen), Pointer auf Pointer auf Pointer auf Pointer (usw.), diverse weitere Schlüsselwörter für Datentypen (static, const, extern, ...), globale Variablen, unzählige weitere Funktionen des ANSI-C-Standards und Funktionen zur Systemprogrammierung aus Standards wie POSIX, weitere Operatoren, Casts, dynamische Speicherverwaltung (ein besonders tolles Feature von C!), Tonnen von Header-Dateien, ...



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






 <<   zurück
  
  Zum Katalog
Zum Katalog: Linux






 Linux
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


Zum Katalog: Einstieg in Linux






 Einstieg in Linux


Zum Katalog: Debian GNU/Linux






 Debian GNU/Linux


Zum Katalog: Ubuntu GNU/Linux






 Ubuntu GNU/Linux


Zum Katalog: Shell-Programmierung






 Shell-Programmierung


Zum Katalog: Linux-UNIX-Programmierung






 Linux-UNIX-
 Programmierung


Zum Katalog: Praxisbuch Netzwerk-Sicherheit






 Praxisbuch
 Netzwerk-Sicherheit


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Galileo Press 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de