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

Inhaltsverzeichnis
Vorwort
1 Einleitung
TEIL I: Einstieg in Linux
2 Die Installation
3 Erste Schritte
4 Linux als Workstation für Einsteiger
TEIL II: Grundlagen
5 Kernel
6 Grundlagen aus Anwendersicht
TEIL III: Die Shell
7 Die Shell
8 Reguläre Ausdrücke
9 Konsolentools
10 Die Editoren
11 Shellskriptprogrammierung mit der bash
12 Die C-Shell
TEIL IV: System- & Netzwerkadministration
13 Benutzerverwaltung
14 Grundlegende Verwaltungsaufgaben
15 Netzwerkgrundlagen
16 Anwendersoftware für das Netzwerk
17 Netzwerkdienste
18 Mailserver unter Linux
19 LAMP & Co.
20 DNS-Server
21 Secure Shell
TEIL V: Die grafische Oberfläche
22 Die grafische Oberfläche
23 Window-Manager und Desktops
24 X11-Programme
25 Multimedia und Spiele
TEIL VI: Systeminterna
26 Prozesse und IPC
27 Bootstrap und Shutdown
28 Dateisysteme
29 Virtualisierung und Emulatoren
TEIL VII: Programmierung und Sicherheit
30 Softwareentwicklung
31 Crashkurs in C und Perl
32 Einführung in Computersicherheit
33 Netzwerksicherheit überwachen
TEIL VIII: Anhang
A Lösungen zu den einzelnen Aufgaben
B Kommandoreferenz
C X11-InputDevices
D MBR
E Buch-DVDs
F Glossar
G Literatur
Stichwort
Ihre Meinung?

Spacer
Linux von Johannes Plötner, Steffen Wendzel
Das umfassende Handbuch
Buch: Linux

Linux
Rheinwerk Computing
1282 S., 5., aktualisierte Auflage 2012, geb., mit 2 DVDs
49,90 Euro, ISBN 978-3-8362-1822-1
Pfeil 31 Crashkurs in C und Perl
Pfeil 31.1 Die Programmiersprache C – ein Crashkurs
Pfeil 31.1.1 Hello, World in C
Pfeil 31.1.2 Kommentare
Pfeil 31.1.3 Datentypen und Variablen
Pfeil 31.1.4 Operatoren
Pfeil 31.1.5 Bedingte Anweisungen
Pfeil 31.1.6 Schleifen
Pfeil 31.1.7 Funktionen
Pfeil 31.1.8 Präprozessor-Direktiven
Pfeil 31.1.9 Zeiger-Grundlagen
Pfeil 31.1.10 Array-Grundlagen
Pfeil 31.1.11 Strukturen
Pfeil 31.1.12 Arbeiten mit Zeichenketten (Strings)
Pfeil 31.1.13 Einlesen von Daten
Pfeil 31.1.14 FILE und das Arbeiten mit Dateien
Pfeil 31.1.15 Das war noch nicht alles!
Pfeil 31.2 Die Skriptsprache Perl
Pfeil 31.2.1 Aufbau eines Skripts
Pfeil 31.2.2 Variablen
Pfeil 31.2.3 Kontrollstrukturen
Pfeil 31.2.4 Subroutinen
Pfeil 31.2.6 Arbeiten mit dem Dateisystem
Pfeil 31.3 Zusammenfassung
Pfeil 31.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

31 Crashkurs in C und PerlZur nächsten Überschrift

Beim Thema »Programmieren unter Linux« beziehungsweise »Programmieren von Linux« sind drei Programmiersprachen besonders wichtig: [Fn. Dieses Kapitel war ursprünglich als Bestandteil von Kapitel 30, »Softwareentwicklung«, geplant, wurde aber so umfangreich, 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 Sie für alles mit Ausnahme der Kernel-Programmierung gebrauchen können.

Wir geben Ihnen in diesem Buch eine Einführung in alle drei Sprachen – und da Sie die Shell schon kennen, folgt an dieser Stelle ein Crashkurs in C und Perl. Wenn Sie diese Sprachen beherrschen, können Sie immer weiter in die Tiefen von Linux hinabsteigen und werden immer mehr verstehen. Niemand kann Sie dann mehr aufhalten.


Rheinwerk Computing - Zum Seitenanfang

31.1 Die Programmiersprache C – ein CrashkursZur nächsten ÜberschriftZur vorigen Überschrift

In diesem Buch wurde bereits die Programmierung mit der Shell bash besprochen, wozu brauchen Sie also noch mehr Programmiersprachen? Diese Frage ist leicht zu beantworten, da wir uns zwei ganz besondere Sprachen ausgesucht haben.

Zum einen ist das die Programmiersprache C, die wir in diesem Abschnitt behandeln, und zum anderen die Skriptsprache Perl, die im nächsten Abschnitt erläutert wird. [Fn. Warum wir uns ausgerechnet noch für Perl entschieden haben, erfahren Sie im nächsten Abschnitt – in diesem beschäftigen wir uns zunächst nur mit C.]

Fast der gesamte Linux-Kernel (ebenso wie die BSD-Kernel) ist in der Sprache C geschrieben, wie auch fast alle wichtigen Basisprogramme eines Linux- und BSD-Systems. Die Bedeutung von C für diese Systeme ist also herausragend. Und ganz davon abgesehen handelt es sich bei C um eine äußerst performante Programmiersprache.

Es stimmt: C ist keine objektorientierte Sprache, aber wenn Sie auf objektorientierte Entwicklung verzichten können, treffen Sie mit C eine gute Wahl. [Fn. Außerdem lässt sich mit C-Kenntnissen wohl am einfachsten C++ lernen.] Hier noch ein paar Gründe für C:

  • C ist hochperformant! 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.
  • C hat eine schöne Syntax.

Da C eine sehr komplexe Programmiersprache ist, können wir Ihnen hier nur eine sehr allgemeine 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 ebenfalls diverse gute Bücher, die aber auch etwas Vorwissen voraussetzen:

  • Das Buch »Linux-UNIX-Programmierung« von Jürgen Wolf ist 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 kann. Daher empfehlen wir dieses Buch auch generell erst einmal jedem. [Fn. Der Verlag hat uns übrigens nicht bestochen, es handelt sich tatsächlich um unsere Meinung.]
  • Das Buch »Linux/Unix-Systemprogrammierung« von Helmut Herold, erschienen bei Addison-Wesley, 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 deutschsprachige Grundlagenbücher, die wir aber nicht gelesen haben – hier müssen Sie gegebenenfalls Ihre eigenen Entdeckungen machen.

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 veraltet oder zu speziell, um sie hier aufzulisten.


Rheinwerk Computing - Zum Seitenanfang

31.1.1 Hello, World in CZur nächsten ÜberschriftZur vorigen Überschrift

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

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

#include <stdio.h>

void main()
{
printf("Hello, World!\n");
}

Die Ausgabe im Listing sieht etwas kompliziert aus, ist aber ganz einfach. Wir beginnen mit dem Codeschnipsel void main(). Jedes Programm muss an einem bestimmten Punkt anfangen: 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, die dann jeweils bestimmt, 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 mehr in Abschnitt 31.1.7). Hat eine Funktion keine Parameter, so können diese Klammern auch leer bleiben (wie im Fall 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 er void, so wird nichts zurückgegeben.

Normalerweise ist es bei der main-Funktion nicht so, dass sie keinen Rückgabewert liefert, doch der Einfachheit halber haben wir es im oberen Beispiel trotzdem so belassen.

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

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

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

Im Fall des Präprozessor-Befehls include wird der Inhalt einer anderen Datei durch den Präprozessor in den Quellcode eingebaut. Der Code der anderen Datei landet genau dort im eigentlichen Quellcode, 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 von Wert) zurückgibt und welche Parameter sie akzeptiert. Außerdem ist der Parametertyp festgelegt.

Den Code übersetzen

Wie Sie soeben gelernt haben, wird ein C-Programm mit dem GNU C Compiler GCC folgendermaßen übersetzt (der Dateiname der Quellcode-Datei sei hello.c): [Fn. Ignorieren Sie die darauffolgende Warnmeldung zunächst einmal.]

Listing 31.2 hello.c übersetzen

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

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

Listing 31.3 hello ausführen

$ ./hello
Hello, World

Rheinwerk Computing - Zum Seitenanfang

31.1.2 KommentareZur nächsten ÜberschriftZur vorigen Überschrift

Wie in jeder anderen ordentlichen Programmiersprache gibt es auch in C die Möglichkeit, Kommentare in den Quellcode zu schreiben, die vom Compiler nicht in Maschinencode übersetzt werden. Ein Kommentar wird durch die Folge der beiden Zeichen / eingeleitet und durch */ beendet. Ein Kommentar kann in C über mehrere Zeilen verteilt sein und endet daher nicht automatisch beim Zeilenende. [Fn. Einige Compiler unterstützen auch C++-Kommentare. Diese werden durch // eingeleitet und durch das Zeilenende abgeschlossen, doch sind sie nicht im ANSI-C-Standard festgeschrieben!]

Listing 31.4 Das obige Programm mit Kommentaren

/* 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 */
}

Übrigens: Kommentare werden nicht als Kommentare gewertet, wenn sie innerhalb von Zeichenketten stehen. Im folgenden Fall wäre der Kommentar Teil der Textausgabe:

Listing 31.5 Kein Kommentar

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

Rheinwerk Computing - Zum Seitenanfang

31.1.3 Datentypen und VariablenZur nächsten ÜberschriftZur vorigen Überschrift

Datentypen sind in C so komplex wie in nur wenigen anderen Sprachen ausgelegt. 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.

Jeder Datentyp hat dabei einen Wertebereich, der sich nach der Zahl der Bits, die zum Speichern dieses Datentyps benutzt werden, richtet. Mit einem 8 Bit großen Datentypen 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 Variablen angegeben und anschließend deren Bezeichnung. Diese Erzeugungsanweisung wird mit einem Semikolon abgeschlossen.

Listing 31.6 Variable erzeugen

Datentyp Variablenname;

Deklaration

Man spricht hierbei von der Deklaration einer Variablen.

Initialisierung

Sie können den Wert einer Variablen auch gleich bei der Deklaration setzen. Das Setzen des ersten Wertes einer Variablen bezeichnet man als Initialisierung.

Listing 31.7 Variable deklarieren und initialisieren

Datentyp Variablenname = Wert;

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

Listing 31.8 Variablenwerte verändern

Variablenname = Wert;

Bezeichner von Variablen

Variablennamen dürfen nicht völlig frei gewählt werden. Es gilt einige Regeln zu beachten:

  • C unterscheidet zwischen Groß- und Kleinschreibung – ABC, abc, aBc, AbC und ABc bezeichnen somit unterschiedliche Variablen.
  • 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 durch den 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.

Listing 31.9 (Un)gültige Variablennamen

/* gültige Variablennamen */
i, abc, _def, __gh, i_j, k0l9m, MeineKatzeKannFliegen

/* ungültige Variablennamen */
0MeineKatzeKannFliegen, =Sonderzeichen, mäh

Ausgabe von Variablenwerten

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

Listing 31.10 Schema eines printf()-Aufrufs

printf( Formatstring [, optionale Variablen] );

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 gesondert.

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, die die aktuelle Zeile beendet und an den Anfang der nächsten Zeile geht.

Hier nur ein kleines Beispiel zur Ausgabe eines Zeichens:

Listing 31.11 Schema eines printf()-Aufrufs

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

In diesem Fall würde der Text »Das Zeichen ist « ausgegeben. An diese Ausgabe würde das Zeichen in der Variablen zeichenvar angehängt; sie 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, sei darauf hingewiesen, 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 bezeichnet. Eine Integer-Variable speichert eine Ganzzahl, die auch Null oder negativ sein kann. Laut ANSI-C-Standard ist eine Integer-Variable immer mindestens 2 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-Anweisungen erzeugt und initialisiert werden:

Listing 31.11 Eine Integer-Variable erzeugen

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

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.

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

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

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

Teilt sich der Wertebereich allerdings in einen positiven und 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 Null einschließt, können mit einem 16-Bit-signed-Integer 32.768 – 1 = 32.767 positive Zahlen gespeichert werden. Der negative Teil ohne die Null 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.

Listing 31.12 Deklaration von (un)signed-Integern

unsigned int a = 65635;
signed int b = 32767;

/* Den Wert von b verändern */
b = –32768;

Listing 31.13 Ausgabe von Integer-Werten

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

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

Hex- und Oktalwerte

In C können Sie Variablen auch Hex- und Oktalwerte zuweisen. Das sollten Sie allerdings nur tun, wenn Sie wirklich wissen, was diese Werte genau bedeuten (insbesondere bei negativen Werten, die im Zweierkomplement 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 Variablen dreimal der gleiche Wert (42) zugewiesen.

Listing 31.14 Setzen des Wertes »42«

/* 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;

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, folglich können 256 verschiedene Werte in einer solchen Variablen gespeichert werden.

Deklaration und Initialisierung

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

Listing 31.15 Verwendung einer char-Variablen

char a = 99;

printf("%c", a);

Im obigen Listing weisen wir der Variablen a den Wert »99« zu. Dies wird bei Ausgaben als Wert für ein ASCII-Zeichen interpretiert. [Fn. Mehr zum ASCII-Standard erfahren Sie unter: de.wikipedia.org/wiki/ASCII.] Die »99« steht dabei für ein kleingedrucktes »c«.

Da man wohl kaum auf diese Weise Zeichen zuweisen will, gibt es noch eine wesentlich komfortablere Schreibweise für ASCII-Zeichen. Bei dieser Schreibweise wird das entsprechende Zeichen in Hochkommata eingebettet.

Listing 31.16 char-Deklaration und Initialisierung mit Zeichen

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

short

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

Listing 31.17 Größe einer short-Variablen ermitteln

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

Listing 31.18 Verwendung einer short-Variablen

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);

long

Der Datentyp long hat unter Linux auf 32-Bit-Systemen immer die Größe 32 Bit und auf 64-Bit-Systemen immer die Größe 64 Bit. [Fn. Siehe Robert Love: »Linux Kernel Handbuch«, Addison-Wesley, 2005. S. 408 ff.]

Listing 31.19 Verwendung einer long-Variablen

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);

Gleitkomma-Datentypen

Mit den bisherigen Datentypen war es nur möglich, ganze Zahlen zu benutzen. Im Folgenden 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 Hinweise zu beachten:

  • Für keine dieser Datentypen existieren unsigned-Varianten.
  • Nachkommastellen werden nicht durch ein Komma (,) sondern durch einen Punkt (.) von dem ganzzahligen Teil einer Zahl getrennt.
  • Die verschiedenen Datentypen weisen nicht nur eine unterschiedliche Bitgröße auf, 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-Variablen annimmt.

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

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

Kurz gesagt gilt:

Größe von float <= Größe von double <= Größe von long double.

Genauigkeit

Die Genauigkeit einer Gleitkomma-Variablen 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 sechs Stellen, double-Variablen haben eine Genauigkeit von 15 Stellen, und Variablen des Typs long double haben ganze 18 Stellen Genauigkeit vorzuweisen.

Listing 31.20 Verwendung einer Gleitkomma-Variablen

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);

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

Listing 31.21 Ausgabe der Gleitkommawerte

float: 0.123000
double: –17.485870
long double: 3845990000000.000000

Rheinwerk Computing - Zum Seitenanfang

31.1.4 OperatorenZur nächsten ÜberschriftZur vorigen Überschrift

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.

Rechenoperatoren

+, -, *, /

Die einfachsten Operatoren (besonders für Nicht-Informatiker) sind 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:

Listing 31.22 Anwendung von Rechenoperatoren

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;

Vorrang

In C hat jeder Operator eine bestimmte Wertigkeit. Sie 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 – in jedem Fall aber gilt: Punktrechnung geht vor Strichrechnung.

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

Listing 31.23 Punkt vor Strich

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

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

Listing 31.24 Rechts vor links

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

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

Klammerung

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

Listing 31.25 Klammerung

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

Nachkommastellen

Allerdings ist zu beachten, dass diese Rechenoperationen nicht immer zum erwarteten 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 Kommawerte im Ergebnis haben, so müssen Sie eine Gleitkomma-Variable verwenden.

Listing 31.26 Rechnen mit Kommastellen

float a = 5, b = 2;
float c;

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

Die Ausgabe dieses Codes liefert den Wert »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 im Rahmen dieses Buches leider nicht eingehen. Eine relativ sichere Vorgehensweise ist es allerdings, keine Datentypen zu mixen.

Weitere Rechenoperatoren

Modulo

Eine in der Informatik sehr wichtige Rechenoperation ist das Modulo-Rechnen. Das Ergebnis einer Modulo-Rechnung ist der Rest der ganzzahligen Division zweier Zahlen. Teilen Sie beispielsweise 5 durch 2, dann bleibt ein Rest von 1 übrig. Mathematisch ausgedrückt: (5 mod 2) = 1.

Der Modulo-Operator ist in C das Prozentzeichen (%).

Listing 31.27 Modulo

int a, b;

/* a wird 0, da kein Rest bleibt */
a = 10 % 2;

/* a wird 4 */
a = 9 % 5;

++/–-

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

Listing 31.28 Inkrement und Dekrement

int a = 10;

a++; /* a wird 11 */
a--; /* a wird wieder 10 */
a--; /* a wird 9 */

In C unterscheidet man zwischen Prä- und Post-Inkrement bzw. -Dekrement. Der Unterschied besteht darin, ob der Operator vor (Prä-) oder hinter (Post-) eine Variable geschrieben wird. Dies kann sich auf eine Rechnung auswirken, da hierbei entschieden wird, ob eine Variable vor erst nach einer Verwendung in- bzw. dekrementiert wird.

Listing 31.29 Vorher oder nachher?

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; */

Im Falle des Prä-Inkrements bekommt b den Wert »11«, da zuerst a inkrementiert wird (a = 11) und dieser Wert dann c zugewiesen wird. Im Falle des Post-Inkrements bekommt c den Wert »10«. Erst danach wird a imkrementiert (womit a auch hier den Wert »11« bekommt). Wie Sie sehen, führen beide Rechenanweisungen zu unterschiedlichen Ergebnissen.

Verkürzte Schreibweisen

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

Listing 31.30 Langform für die Benutzung eines Operators

VarA = VarA [Operator] VarB

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

Listing 31.31 Kurzform für die Benutzung eines Operators

VarA [Operator]= VarB

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

Listing 31.32 Vor der Vereinfachung

int a = 10, b = 2;

b = b + a;

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

Listing 31.33 Nach der Vereinfachung

int a = 10, b = 2;

b += a;

Im folgenden Listing sehen Sie noch einige weitere Beispiele für andere Rechenoperationen, die auf dieselbe Weise vereinfacht werden können. [Fn. Wir werden gleich noch weitere Operatoren kennenlernen, jedoch beschränken wir uns an dieser Stelle auf die bereits bekannten Operatoren.]

Listing 31.34 Schreibweisen

/* 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;

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, müssen Sie die Darstellung der Variablenwerte im dualen Zahlensystem beherrschen (also binär mit Nullen und Einsen). Dieses Thema würde den Rahmen dieses Abschnitts sprengen und kann daher leider nicht näher behandelt werden. In der Wikipedia und in C-Büchern finden Sie allerdings gute und verständliche Erklärungen. [Fn. Bei Problemen leihen Sie sich das Buch »Mathematik für Informatiker« von Manfred Brill oder Informatik-Bücher für das Grundstudium aus der Bibliothek aus.]

Shiften

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

Listing 31.35 Shiften

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;

Weitere Operatoren

Es gibt noch weitere (und ebenso 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 bei beiden Variablen gesetzt sind. Beim Ergebnis der Operation sind nur die Bits gesetzt, die es in jeder der beiden Variablen waren. Würde beispielsweise der Wert »6« (binär 110) mit dem Wert »5« (binär 101) durch ein UND verknüpft, so wäre das Ergebnis »4« (binär 100), da nur das 4er-Bit in beiden Werten vorkommt. Geschrieben wird eine UND-Verknüpfung mit &.

Listing 31.36 Beispiel einer UND-Verknüpfung (Rechnung)

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

Listing 31.37 Beispiel einer UND-Verknüpfung (C-Code)

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

a = x & y;

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« (binär 110) und »5« (binär 101) angewandt, lautete das Ergebnis »7« (binär 111) lauten. Geschrieben wird ein logisches ODER mit dem Pipe-Zeichen (|).

Listing 31.38 Beispiel einer ODER-Verknüpfung (Rechnung)

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

Listing 31.39 Beispiel einer ODER-Verknüpfung (C-Code)

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

a = x | y;

XOR

Das exklusive ODER (XOR) 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, nur nicht jene, die in beiden Werten gesetzt sind. Würde »6« (110) mit »5« (101) XOR-verknüpft werden, so lautete das Ergebnis »3« (011). Der XOR-Operator wird durch ein Dach-Zeichen (^) angegeben.

Listing 31.40 Beispiel einer XOR-Verknüpfung (Rechnung)

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

Listing 31.41 Beispiel einer XOR-Verknüpfung (C-Code)

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

a = x ^ y;

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 (\~{}) repräsentiert.

Wendeten wir den Operator auf den Wert »6« (110) an, so wäre das Ergebnis »1« (001).

Listing 31.42 Beispiel eines Einerkomplements

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

Allerdings gibt es in C etwas zu beachten, das wir bisher nicht erwähnt haben: Nehmen wir an, Sie verwendeten 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 drei 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) oder eine sehr kleine negative Zahl ((signed) int):

Listing 31.43 Beispiel eines Einerkomplements (Rechnung)

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

Listing 31.44 Beispiel eines Einerkomplements (C-Code)

int = ~ 6;

Der sizeof-Operator

Zum Schluss zeigen wir noch eine sehr praktische C-Funktionalität: den Operator sizeof. Er 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.

Listing 31.45 Beispielanwendung des sizeof-Operators

#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;
}

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

Listing 31.46 Ausgabe des Programms

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

Übrigens kann der sizeof-Operator auch direkt auf einen Datentyp angewandt werden; eine

Anweisung wie sizeof(long) ist also gültig.

Übersicht der Operatoren

Die folgende Übersicht fast die arithmetischen Operatoren noch einmal zusammen.

Tabelle 31.1 Übersicht der arithmetischen Operatoren

Operator Beispiel Beschreibung

+

x = x + 1

Addition

-

x = x – 1

Subtraktion

*

x = y * z

Multiplikation

/

z = x / y

Division

%

m = x % y

Modulo (Restwert bei Ganzzahldivision)

++

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 ...


Rheinwerk Computing - Zum Seitenanfang

31.1.5 Bedingte AnweisungenZur nächsten ÜberschriftZur vorigen Überschrift

Ein äußerst wichtiges Element der Programmierung sind bedingte Anweisungen, wie Sie sie bereits aus der Shellskriptprogrammierung kennen. In der Shell hießen die zugehörigen Befehle if und case. Diese Namen sind in den meisten Programmiersprachen sehr ähnlich – dies gilt auch für C.

Zur Erinnerung: Bei bedingten Anweisungen wird zunächst geprüft, ob eine Bedingung erfüllt ist (zum Beispiel ob der Wert der Variablen anzahl größer 1000 ist). Ist die Bedingung (nicht) erfüllt, so wird eine bestimmte Anweisung (nicht) ausgeführt.

Vergleichsoperatoren

Bevor wir uns die einzelnen Anweisungen ansehen, betrachten wir die Vergleichsoperatoren, die C kennt.

Werte ungleich 0 werden in C als erfüllte Bedingungen angesehen (man spricht auch von wahren oder true-Bedingungen). Werte, die gleich 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-Werte mit unsigned-Werten verglichen werden, kann es allerdings zu Problemen kommen. Mehr zu diesem Thema erfahren Sie in guten C-Büchern und in unserem Buch »Praxisbuch Netzwerksicherheit« in Kapitel 23, »Sichere Software entwickeln«.

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

% sind analog in perl, daher verweise ich hier drauf

Tabelle 31.2 Übersicht der Vergleichsoperatoren

Operator Beispiel Beschreibung

==

a == 1

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

!=

a != 1

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

>

a > 1

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

>=

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 als der rechte ist.

<=

a <= 1

Kleiner-Gleich: Die Bedingung ist erfüllt, wenn der linke Wert kleiner oder gleich dem rechten 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, die linke oder beide Bedingungen erfüllt sind.

!

! a

Negation: Die Bedingung ist erfüllt, wenn die rechts vom Operator stehende Bedingung nicht erfüllt ist.

Die if-Anweisung

Am verständlichsten formulieren Sie eine bedingte Anweisung mit if. Der Aufbau in C ist dabei dem der if-Anweisung der Shell sehr ähnlich:

Listing 31.47 Aufbau einer if-Anweisung

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

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. Die beiden folgenden Anweisungen sind also identisch:

Listing 31.48 Nur eine Anweisung in if

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

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

else if

Ist die eigentliche if-Bedingung nicht erfüllt, so 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 überprüft.

[zB]Versuchen wir, auf diese Weise einmal die Variable anzahl auf drei verschiedene Werte zu überprüfen.

Listing 31.49 Testen auf verschiedene Werte

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

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

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

Listing 31.50 Testen auf verschiedene Werte

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");
}

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 oder größer 2000 ist. Mit dem ODER-Operator ist dies kein Problem:

Listing 31.51 Testen auf verschiedene Werte

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

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. Prüfte man aber, ob a kleiner 1000 und b größer 2000 ist, so könnten beide Bedingungen gleichzeitig erfüllt sein.

Klammerung

Mit Klammern kommt man allerdings noch einen Schritt weiter. Möchte man zum Beispiel 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 folgende Listing.

Listing 31.52 Ein Beispiel zur Klammerung

/* 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");
}

Die switch-Anweisung

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

Listing 31.53 Schema einer switch-Anweisung

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

Hierbei wird geprüft, ob der Wert dem Testwert 1 oder Testwert 2 (oder weiteren Testwerten) entspricht. Die entsprechenden Bedingungen werden jeweils ausgeführt. Ist keiner dieser Testwerte gleich dem übergebenen Wert, so 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 case-Blöcke, 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 und das switch-Statement verlassen wird. Wenn Sie eine break-Anweisung am Ende der Anweisungen eines case-Bereichs vergessen, so werden die folgenden Anweisungen (egal welcher Bedingung) ebenfalls ausgeführt, bis entweder eine break-Anweisung auftritt oder das Ende der switch-Anweisung erreicht ist.

[zB]Im folgenden Beispiel würde der Variablen q durch die fehlende break-Anweisung im case-Block zunächst der Wert »102« und unmittelbar danach der Wert »112« zugewiesen werden.

Listing 31.54 Beispiel für break

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;
}

Der ?-Operator

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

Listing 31.55 Aufbau einer ?-Anweisung

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

Nehmen wir an, die Variable a solle auf den Wert »77« gesetzt werden, falls die Variable q den Wert »99« enthält. Andernfalls solle a den Wert »2« erhalten.

Listing 31.56 Beispiel zum ?-Operator

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

Sie können durch diese Anweisung natürlich auch Zeichen für char-Variablen, Gleitkommawerte und sogar ganze Zeichenketten (so genannte 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« ausgeben, andernfalls die Zeichenkette »Tschüss«. [Fn. 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.]

Listing 31.57 Zeichenketten

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

Rheinwerk Computing - Zum Seitenanfang

31.1.6 SchleifenZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben noch nicht aufgegeben? Das ist schön! Jetzt, da Sie bedingte Anweisungen und Datentypen von Variablen kennen, können wir uns Schleifen zuwenden. Schleifen sind nach all dem, was Sie bisher wissen, sehr einfach zu verstehen, und ihr Nutzen ist enorm.

Im Prinzip verhält es sich in C 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. Sie ist ganz ähnlich wie eine if-Anweisung aufgebaut:

Listing 31.58 Aufbau einer while-Schleife

while ( Bedingung )
{
Anweisung(en)
}

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

Listing 31.59 10x Hallo Welt!

int i = 10;

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

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ürde beendet.

Die Überprüfung auf 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.

Listing 31.60 Eine weitere Variante

int i = 10;

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

Richtig praktisch werden Schleifen aber erst, wenn man mit verschiedenen Variablenwerten arbeitet. Möchte man etwa die Zahlen von 1 bis 10 000 ausgeben, dann geht dies mit ebenso wenig Codezeilen, als würde man die Zahlen von 1 bis 2 oder von 1 bis 10 000 000 ausgeben.

Listing 31.61 Bis 1.000.000 zählen

int i = 1;

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

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

[zB]Hier ein Beispiel: Es werden zehn Zahlen pro Zeile ausgegeben, und es steigt jeweils die Zahl, die ausgegeben wird, an. Nach zehn Zahlen wird also ein Newline-Zeichen ausgegeben. Nach zehn Zeilen (das bedeutet, dass i % 10 den Wert 0 ergibt) wird eine Trennlinie ausgegeben.

Listing 31.62 Zahlenblöcke mit Schleifen ausgeben

#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;
}

Erwartungsgemäß gibt das Programm 10x10er-Blöcke von Zahlen aus:

Listing 31.63 Die Ausgabe des Programms (gekürzt)

$ 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
...
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
----------------------------

Die do-while-Schleife

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

Listing 31.64 Aufbau der do-while-Schleife

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

Vor Kurzem haben 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:

Listing 31.65 Bis 1.000.000 zählen

/* 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);

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 sehen konnten, wird in fast jeder Schleife gezählt. In der for-Schleife ist genau dies kein Problem. Diese Schleife kann Werte während der Schleifendurchläufe verändern und ist daher sehr beliebt.

Listing 31.66 Aufbau einer for-Schleife

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

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

[»]Anders als in C++ können in C keine Variablen im Initialisierungsbereich einer for-Schleife angelegt werden, selbst wenn einige Compiler (jedoch nicht der GCC) dies erlauben.

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

Listing 31.67 Bis 1.000.000 zählen

int i;

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

[»]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.

Listing 31.68 Endlosschleifen

while ( 1 )
{
Anweisung(en)
}

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

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

for(;;)

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

Listing 31.69 Endlose for-Schleife

for (;;)
{
Anweisung(en)
}

goto

Sprünge sind eine sehr unbeliebte Möglichkeit zur Programmierung von Schleifen. goto-Statements, die eigentlich nur als Jumps (auch Branches genannt) in Assembler ihren Nutzen finden sollten, machen den Programmfluss unübersichtlich und fehlerträchtig. In höheren Programmiersprachen wie C gelten Sprünge als schlechter Programmierstil und sollten nicht verwendet werden.

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

Listing 31.70 Aufbau einer goto-Anweisung

Sprungziel:
Anweisung(en)
goto Sprungziel;

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

Listing 31.71 Endlosschleife und Zähler

/* 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;

Rheinwerk Computing - Zum Seitenanfang

31.1.7 FunktionenZur nächsten ÜberschriftZur vorigen Überschrift

Möchte man bestimmte Anweisungen mehrmals ausführen, so 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? Für diesen Zweck gibt es Funktionen. Eine Funktion führt eine Reihe von Anweisungen aus, indem man sie anstelle der Anweisungen aufruft.

Listing 31.72 Aufbau einer Funktion

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

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

Listing 31.73 Eine einfache C-Funktion

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

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().

Listing 31.74 Aufruf der Funktion

sage_hallo();

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.

Listing 31.75 Funktion zur Berechnung einer Summe

/* 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);
}

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

Listing 31.76 Aufruf von summe()

summe(777, 333);

Rückgabewerte

Diese Funktion kann aber immer noch stark verbessert werden. Wie wäre es zum Beispiel, 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ückgabewerts angegeben werden. void bedeutet, wie Sie bereits wissen, dass kein Wert zurückgegeben wird.

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

Listing 31.77 Rückgabe des Ergebnisses

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

Der Rückgabewert der Funktion kann direkt einer Variablen zugewiesen werden und ihr Aufruf erfolgt analog zu dem der printf()-Funktion.

Listing 31.78 Benutzung von Rückgabewerten

int x;

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

»Stattdessen kann ich aber doch auch einfach x = 485 + 3921 schreiben!«, werden Sie nun einwenden. Das ist richtig. Dieses einfache Beispiel sollte 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:

Listing 31.79 Fakultät berechnen

#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(val);

printf("Die Fakultaet von %hi ist %ld\n", val, ret);

return 0;
}

Rheinwerk Computing - Zum Seitenanfang

31.1.8 Präprozessor-DirektivenZur nächsten ÜberschriftZur vorigen Überschrift

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

Diese 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.

Listing 31.80 Aufbau von Präprozessor-Direktiven

# Schlüsselwort [Parameter]

#define

Mit der Anweisung #define lassen sich Makros erstellen. Diese werden dort, wo sie im Programmcode eingefügt werden, durch den Code ersetzt, der für sie definiert wurde. [Fn. 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.

Listing 31.81 Ein Makro erstellen

#define ANZAHL 10

Die Verwendung erfolgt über den Namen des Makros.

Listing 31.82 Verwenden eines Makros

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

Komplexere Ausdrücke

Makros können auch komplexere Ausdrücke enthalten:

Listing 31.83 Komplexere Makros sind auch kein Problem.

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

Verteilung auf mehrere Zeilen

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

Listing 31.84 Makros über mehrere Zeilen

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

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.

Listing 31.85 Ein Makro mit Parameter

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

Der Aufruf – Sie ahnen es sicher schon – entspricht fast dem der Funktion (lediglich das Semikolon wird hier nicht benötigt):

Listing 31.86 Aufruf des CHECK-Makros mit Parameter

CHECK("Fehler: a ist zu klein")

#undef

Ein Makro ist nur innerhalb der Datei definiert, in der es implementiert wurde, [Fn. Man kann Dateien mit der #include-Direktive in andere Dateien einbinden und Makros damit in mehreren Dateien verfügbar machen.] und es ist nur von der Zeile ab, in der es implementiert wurde, bis zum Ende einer Quelldatei bekannt. Möchte man ein Makro vor dem Dateiende löschen, so nutzt man den Befehl #undef Makroname.

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

Listing 31.87 Das Makro CHECK löschen

#undef CHECK

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

Auch bedingte Anweisungen gibt es für den Präprozessor. Mit ihnen 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.

Listing 31.88 Überprüfen des Werts des Makros ANZAHL

#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

Definierte Makros

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

Listing 31.89 Ist ANZAHL definiert?

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

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

Listing 31.90 Ist ANZAHL definiert?

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

#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 zwei Schreibweisen für eine #include-Anweisung:

  • Man schreibt den Dateinamen in eckige Klammern. Dann werden die dem Compiler bekannten Include-Verzeichnisse des Systems durchsucht. [Fn. typischerweise /usr/include oder /usr/local/include]
  • Man setzt den Dateinamen in Anführungszeichen. Dann wird das aktuelle Arbeitsverzeichnis nach der Datei durchsucht. Ist sie dort nicht zu finden, werden die dem Compiler zusätzlich angegebenen Include-Pfade durchsucht. [Fn. Diese Include-Pfade werden beim gcc über -I<Pfad> gesetzt.]

Listing 31.91 So verwendet man #include.

#include <Dateiname>
#include "Dateiname"

[zB]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.

Listing 31.92 Die Datei main.h

#include <stdio.h>

#define MIN 1
#define MAX 9

Listing 31.93 Die Datei main.c

#include "main.h"

int main()
{
int i;

for ( i = MIN ; i < MAX ; i++)
printf("%i\n", i);

return 0;
}

Der Compiler wird in diesem Fall wie immer aufgerufen:

gcc -o main main.c

-I

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

Listing 31.94 Compiler mit -I aufrufen

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

Relative Pfadangabe

Eine relative Pfadangabe ist auch möglich:

Listing 31.95 Relative Pfadangabe: drei Beispiele

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

#error

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

Listing 31.96 Verwendung von #error

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

Der gcc bricht dann mit folgender Fehlermeldung ab:

Listing 31.97 gcc-Meldung für eine #error-Anweisung

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

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. [Fn. Diese Makros sind nur definiert, wenn das System, auf dem der Quellcode kompiliert wird, dem Namen des Makros entspricht.]

Listing 31.98 Ist __OPENBSD__ oder __linux__ definiert?

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

#pragma

Die Direktive #pragma wird sehr unterschiedlich verwendet. Ihre Funktionsweise ist abhängig von der Plattform und dem Compiler sowie von dessen Version. [Fn. Für Parallelprogrammierung werden beispielsweise Makros wie #pragma omp parallel for ... 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 obligatorische Makros. Sie 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 evaluiert zu einer Integer-Zahl, die zum Beispiel mit printf() ausgegeben werden kann.

Datei

Den Namen der Datei, in der man sich gerade befindet, erführt man über das Makro __FILE__, das zu einer Zeichenkette des Dateinamens evaluiert.

Datum und Uhrzeit

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

STDC

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

Neu in ISO C99

Seit dem ISO-C99-Standard gibt es zusätzliche Makros, die ein entsprechend kompatibler Compiler kennen muss. [Fn. 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.

[zB]Hier noch ein Beispiel:

Listing 31.99 Nutzen vordefinierter Makros

#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;
}

Rheinwerk Computing - Zum Seitenanfang

31.1.9 Zeiger-GrundlagenZur nächsten ÜberschriftZur vorigen Überschrift

Nun kommen wir zu einem der letzten Themen unseres C-Crashkurses: den Zeigern. Dieses Thema macht C in den Augen vieler 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 Programme sich somit beschleunigen. überhaupt sind Zeiger so praktisch, dass wir sie niemals missen wollten. Man spricht im Zusammenhang mit Zeigern 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 ihr Wert entweder gelesen oder geschrieben. Auf die Speicheradresse einer Variablen wird mit dem Adressoperator (&) zugegriffen.

[zB]Im Folgenden soll die Adresse der Variablen a in der Variable adresse gespeichert werden.

Listing 31.100 Für den Adressoperator

int a = 10;
long adresse;

adresse = & a;

printf("Adresse der Variable a: %li\n", adresse);

Zeiger auf Adressen

Ein Zeiger zeigt auf eine Speicheradresse. Man arbeitet also nicht mehr mit der eigentlichen Variablen, sondern mit einer Zeigervariablen, die die Adresse des Speichers kennt, auf den man zugreifen möchte.

Anders formuliert: Ein Zeiger ist eine Variable, die die Adresse eines Speicherbereichs enthält.

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

Listing 31.101 Deklaration eines Zeigers

int *zeiger;

Möchte man einen Zeiger verwenden, so benötigt man zunächst eine Speicheradresse. Entweder wird dafür – wie wir in diesem Buch allerdings nicht zeigen können [Fn. Mehr hierzu erfahren Sie im auf der DVD enthaltenen Openbook »C von A bis Z« von Jürgen Wolf.] – dynamisch Speicher reserviert, oder man benutzt die Adresse einer Variablen.

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

Listing 31.102 Ein Zeiger auf eine Integer-Variable

int *zeiger;
int wert = 99;

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

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.

Listing 31.103 Dereferenzierung eines Zeigers

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;

Werte lassen sich ändern

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

Listing 31.104 Verändern von Werten

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);

Eine mögliche Ausgabe des Programms wäre die folgende. Die Adresse des Zeigers wird auf Ihrem Rechner mit sehr hoher Wahrscheinlichkeit anders lauten, doch die beiden Werte von je »101« müssen gleich sein. [Fn. Tatsächlich können sich die Speicheradressen bei jedem Programmstart ändern.]

Listing 31.105 Die Ausgabe des Codes

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

Call by Reference in C

Bevor wir den kleinen Ausflug in die Welt der Zeiger beenden, 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 diese 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 analog der Deklaration einer Zeigervariablen: Der *-Operator wird verwendet.

Listing 31.106 Beispiel für Call by Reference

#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;
}

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

Listing 31.107 Ausgabe des Programms

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

Rheinwerk Computing - Zum Seitenanfang

31.1.10 Array-GrundlagenZur nächsten ÜberschriftZur vorigen Überschrift

Hat man in C eine Variable mit mehreren Elementen, so spricht man von einem Array. Sie kennen Arrays schon aus dem Kapitel zur Shellskriptprogrammierung, Kapitel 11, doch wir werden gleich noch einmal an Beispielen erklären, worum es sich hierbei handelt. [Fn. Viele deutsche C-Bücher nennen diesen Datentyp auch Feld oder Vektor.] Arrays können in C mehrere Dimensionen haben – wir werden uns im Folgenden allerdings auf eindimensionale Arrays beschränken.

[zB]Am besten lassen sich Arrays an einem Beispiel erklären. Nehmen wir an, es solle 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, nur eine Variable gewicht anzulegen. Dieser verpasst man zehn Elemente, von denen jedes einen Wert speichern kann.

Listing 31.108 Deklaration und Initialisierung eines Arrays

/* 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;

[»]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 demnach Element 9.

Der Zugriff auf Array-Elemente erfolgt mit name[Index]. Dies gilt sowohl für die Zuweisung von Werten an Array-Elemente

als auch für das Auslesen aus Werten von Array-Elementen:

Listing 31.109 Benutzen von Arrays

/* Integer-Array mit 3 Elementen deklarieren */
int tripel[3];
int a, b, c;

a = 3;

tripel[0] = a;
tripel[1] = tripel[0] * 2; /* = 6 */
tripel[2] = 9;


b = tripel[3]; /* = 9 */
c = tripel[2] – 3; /* = 3 */

Variablen als Index

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

Listing 31.110 Variablen als Array-Index

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;
}

Rheinwerk Computing - Zum Seitenanfang

31.1.11 StrukturenZur nächsten ÜberschriftZur vorigen Überschrift

Eine Struktur (engl. structure) stellt einen zusammengesetzten Datentypen dar, der aus mindestens einem, in der Regel aber aus mehreren anderen Datentypen besteht. In diversen anderen Programmiersprachen heißen Strukturen Records.

Die einzelnen Variablen in einer Struktur können Variablen sein, oder auch Strukturen, Arrays und Zeiger auf Variablen, auf Strukturen, auf Arrays und auf Funktionen). Außerdem muss jede Teilvariable einer Struktur einen anderen Namen erhalten.

Listing 31.111 Aufbau einer C-Struktur

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

x:Bits

Optional kann hinter jeder Variablen 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 beschränken uns auf Strukturen mit »ganzen« Variablen, also Variablen ohne Bit-Angabe. [Fn. Wird die Bit-Anzahl der normalen Bit-Anzahl des Datentyps angepasst, ist die Variable natürlich auch »ganz«.]

[zB]Nehmen wir an, es sollen mehrere Daten einer Person in einer Struktur gespeichert werden, nämlich Alter, Gewicht und Größe. Die zugehörige Struktur benötigt drei verschiedene Variablen.

Listing 31.112 Die Struktur »person«

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

Initialisierung

Zur Zuweisung von Werten benötigen wir zunächst eine Variable (oder vielmehr eine Instanz) 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.

Der entsprechende Code könnte wie folgt aussehen:

Listing 31.113 Verwenden der Struktur »person«

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

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

Arrays und Strukturen

Richtig spaßig werden Strukturen aber meist erst in Array-Form. Sollen zum Beispiel drei Personen auf diese Weise gespeichert werden, dann ist auch das kein Problem. Wir erzeugen einfach von unserer Struktur ein Array mit drei Elementen.

Listing 31.114 Drei Personen als Struktur-Array

#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;
}

Rheinwerk Computing - Zum Seitenanfang

31.1.12 Arbeiten mit Zeichenketten (Strings)Zur nächsten ÜberschriftZur vorigen Überschrift

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, liegt also nahe.

Listing 31.115 Eine erste Zeichenkette

char zeichenkette[3];

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

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 läsen und irgendwann in Speicherbereiche gerieten, auf die sie keinen Zugriff haben.

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

Listing 31.116 So geht es einfacher.

char zeichenkette[] = "ABC";

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 entsprechende Schreibweise sieht wie folgt aus: [Fn. Es gibt viele alternative Möglichkeiten, dies zu erreichen, etwa die Funktionen bzero() oder memset(); oder durch eine Schleife.]

Listing 31.117 Nullen-Füller

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

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 besagt, dass eine Zeichenkette ausgegeben werden soll. Auch hierfür wird ein \0-Zeichen am Ende einer Zeichenkette benötigt.

Listing 31.118 Ausgabe einer Zeichenkette

printf("Zeichenkette: %s\n", 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 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()

Der Funktion strcpy() werden zwei Argumente übergeben. Das erste ist das Ziel des Kopiervorgangs, das zweite die Quelle. Möchten Sie also die Zeichenkette aus dem Array z1 in das Array z2 kopieren, dann liefe dies so ab:

Listing 31.119 Kopieren einer Zeichenkette

#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;
}

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, so läuft dies wie folgt:

Listing 31.120 Anwenden von strncpy()

#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;
}

Rheinwerk Computing - Zum Seitenanfang

31.1.13 Einlesen von DatenZur nächsten ÜberschriftZur vorigen Überschrift

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 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, ist dies fast immer die Tastatur beziehungsweise Daten aus einer Pipe). Ähnlich wie bei der Funktion printf() wird dabei ein Formatstring übergeben. Dieser 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 Variablen 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.

Listing 31.121 Einlesen eines Integers

#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;
}

Zeichenketten einlesen

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

Listing 31.122 Eine Zeichenkette einlesen

char wort[100];

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

[»]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 einer verbreiteten Meinung gibt es allerdings einige Techniken, um mit diesem Problem umzugehen. Mehr dazu erfahren Sie im nächsten Kapitel (in Abschnitt ssp).

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.


Rheinwerk Computing - Zum Seitenanfang

31.1.14 FILE und das Arbeiten mit DateienZur nächsten ÜberschriftZur vorigen Überschrift

Ein sehr spannendes Thema ist das Arbeiten mit Dateien. Zum Ende unseres kleinen C-Crashkurses lernen Sie nun also, wie Sie aus Dateien lesen und in Dateien schreiben. Auch hier gibt es verschiedenste Möglichkeiten. Man könnte etwa die Funktionen open(), read(), write() und close() benutzen. Wir empfehlen Ihnen, sie sich 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. [Fn. 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 Headerdatei stdio.h.

Öffnen und Schließen von Dateien

Dateien öffnen

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

  • 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 Typ FILE zurück. Über diesen kann eine geöffnete Datei identifiziert werden. Für Lese-, Schreib- und Schließoperationen auf Dateien ist ein FILE-Deskriptor zwingend erforderlich.

Fehler

Für den Fall, dass eine Datei nicht geöffnet werden konnte (etwa weil sie nicht existiert oder weil das Programm nicht die nötigen Zugriffsrechte auf die Datei hat), gibt fopen() den Wert NULL zurück. [Fn. Meistens ist NULL als Makro für ((void *)0), also einen Zeiger auf die Adresse 0, definiert. Genauer 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 ihn erfolgen. Daher sollten Deskriptoren erst geschlossen werden, wenn man sie nicht mehr benötigt. Vergisst ein Programm, einen Deskriptor zu schließen, so 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.

Listing 31.123 Eine Datei öffnen und schließen

#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;
}

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).

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

[zB]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.

Listing 31.124 Eine Datei lesen

#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 31.125 Aufruf des Progamms (gekürzt)

$ 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

Schreiben in Dateien

fwrite()

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

[zB]Beispielhaft 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. Alternativ könnte ich hierzu die Funktion strlen verwenden, die die Länge einer Zeichenkette zurückgibt. Leider können wir die Funktion in diesem Rahmen nicht behandeln.

Listing 31.126 Schreiben in die Datei /tmp/irgendwas

#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;
}

Zum Beweis und auch zum Abschluss unseres Crashkurses zeigen wir hier noch einmal den Compiler-Aufruf für das Schreibprogramm, den Aufruf des Programms und das Anschauen der geschriebenen Datei.

Listing 31.127 Compiler-Aufruf und Test

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

Rheinwerk Computing - Zum Seitenanfang

31.1.15 Das war noch nicht alles!Zur vorigen Überschrift

C bietet Ihnen noch eine Menge weiterer Möglichkeiten: So gibt es beispielsweise Aufzählungstypen (engl. enumerations), Unions, mehrdimensionale Arrays, Zeiger auf Arrays, Zeiger auf Arrays aus Zeigern, Zeiger auf ganze Funktionen (und natürlich auch Arrays aus Zeigern auf Funktionen), Zeiger auf Zeiger auf Zeiger auf Zeiger (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 Headerdateien – diese Aufzählung ließe sich fast beliebig fortsetzen.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


Zum Rheinwerk-Shop: Linux Server






 Linux Server


Zum Rheinwerk-Shop: Raspberry Pi






 Raspberry Pi


Zum Rheinwerk-Shop: Ubuntu 14.04 LTS






 Ubuntu 14.04 LTS


Zum Rheinwerk-Shop: Roboter bauen mit Arduino






 Roboter bauen
 mit Arduino


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2012
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.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern