Operatoren und Funktionen

In diesem Kapitel geht es um eines der Herzstücke von C: mit den Operatoren und den Funktionen wird die Umsetzung der verschiedenen Algorithmen in die Programmiersprache C erst möglich.

Operatoren

Im folgenden sollen kurz sämtliche Operatoren[7] von C vorgestellt werden. Einige davon werden naturgemäß erst zu einem späteren Zeitpunkt verständlich werden.

Arithmetische Operatoren

C besitzt für die vier Grundrechenarten die entsprechenden zweistelligen (binären) arithmetischen Operatoren + (Addition), - (Subtraktion), * (Multiplikation) und / (Division), die jeweils für alle numerischen Datentypen existieren.

Der Ergebnistyp[8] einer solchen Operation richtet sich dabei nach den Operanden; so ist 7/9 (als int) 0, 7.0/9 jedoch der erwartete Wert 0,777778!

Daneben gibt es den Modulo-Operator %, der den Rest bei der Ganzzahldivision in C darstellt (13%5=3) und dem mod aus Pascal entspricht. Der entsprechende div-Operator ist der /, denn 13/5=2.

Inkrementoperatoren

In C können die häufig gebrauchten Formulierungen, die in Pascal noch i:=i+1 lauteten, kürzer gefaßt werden mit den sogenannten Inkrementoperatoren[9]: i++ oder ++i. Anstelle der C-Anweisungen i=i-1 kann geschrieben werden i-- oder --i (Inkrementoperatoren). Die Operatoren ++ und -- (jeweils als Präfix- oder Suffixoperator) inkrementieren bzw. dekrementieren die betreffende Variable jeweils um 1.

Datentyp-Operatoren

Wie bereits erwähnt: sizeof ist ein Operator, mit dem die Speicherplatzanforderungen einer Variablen oder eines Datentyps abgefragt werden können. Daneben ist der cast-Operator noch zu erwähnen, der eine explizite Typumwandlung erzwingt.

Beispiel:

unsigned long i;
int memory;
memory=sizeof i; /* Speicherbedarf von i */
memory=sizeof(unsigned long);/* Speicherbedarf von uns.long */
i = (unsigned long) memory; /* i wird der gecastete Wert */
/* von memory zugewiesen */

Logische und Vergleichsoperatoren

In C gibt es keinen Datentyp boolean wie in Pascal. Für C ist jeder numerische Wert ungleich 0 gleichwertig mit TRUE (wahr), nur die 0 wird als FALSE (falsch) interpretiert. Dementsprechend gibt es auch logische Operatoren, die als Ergebnisse die Werte 0 oder "ungleich 0", meistens konkret den Wert 1, zurückliefern. So ist && der logische UND-Operator, || der logische ODER-Operator und ! kennzeichnet die (logische) Negation.

Achtung: Verwechseln Sie bitte die logischen nicht mit den bitweisen Operatoren, die im Abschnitt Bit-Manipulationen vorgestellt werden!

Die üblichen sechs Vergleichsoperatoren besitzt C ebenfalls: mit < wird auf "kleiner als" geprüft, mit > auf "größer als", mit <= bzw. >= auf "kleiner oder gleich" bzw. "größer oder gleich", mit == wird die Gleichheit geprüft und mit != die Ungleichheit.

Achtung: C ist immer noch sehr liberal! Wird versehentlich a=b statt a==b geschrieben, so stört das den C-Compiler nicht; statt der logischen Abfrage a==b "ist a gleich b?" wird jedoch bei "a=b" der Wert von b der Variablen a zugewiesen und dieser Wert dient als Rückgabewert und somit als Beurteilung, ob TRUE oder FALSE vorliegt; nur wenn b den Wert 0 hat, ist "a=b" FALSE, sonst stets TRUE! Dies ist eine sehr häufige Fehlerquelle!!!

Bit-Manipulationen

Speziell für die ordinalen Datentypen (char, short, int, long in beiden Varianten signed oder unsigned) existieren sechs Operatoren für sogenannte Bit-Manipulationen.

Operator   Wertigkeit                 Bezeichnung / Erläuterung                   
    &      binär                      bitweise Und-Verknüpfung                    
    |      binär                      bitweise Oder-Verknüpfung                   
    ^      binär                      exclusive Oder-Verknüpfung (XOR)            
   <<      binär                      Bit-Verschiebung nach links (shift left)    
   >>      binär                      Bit-Verschiebung nach rechts (shift right)  
    ~      unär                       bitweises Komplement                        

Ein kleines Beispiel hierzu:

unsigned char a,b,c; /* Bitnummer: 7654 3210 */
a=0x11; /* = 17 Bitmuster: 0001 0001 */
b=0x0F; /* = 15 0000 1111 */

c=a & b; /* c wird gesetzt auf: 0000 0001 */
c=a | b; /* c wird gesetzt auf: 0001 1111 */
c=a ^ b; /* c wird gesetzt auf: 0001 1110 */
c=a << 1; /* c wird gesetzt auf: 0010 0010 */
c=b >> 2; /* c wird gesetzt auf: 0000 0011 */
c=~a; /* c wird gesetzt auf: 1110 1110 */

Zuweisungsoperatoren

C kennt eine Reihe von Zuweisungsoperatoren. Während Pascal nur den Operator := für die direkte Zuweisung kennt, gibt es bei C die folgenden. Für die Beispiele seien die Deklarationen int a,b,c; zugrundegelegt.

a = b + c; /* gewöhnliche Zuweisung */

a += b; /* steht für a = a + b; */

a -= b; /* steht für a = a - b; */

a *= b; /* steht für a = a * b; */

a /= b; /* steht für a = a / b; */

a %= 5; /* a = a % 5; */

a &= b; /* a = a & b; */

a |= b; /* a = a | b; */

a ^= b; /* a = a ^ b; */

a <<= 2; /* a = a << 2; */

b >>= a; /* b = b >> a; */


Der Sequenzoperator

Mit dem Sequenzoperator , können mehrere Anweisungen zu einer einzigen zusammengefaßt werden. Dieser wird später z.B. innerhalb der for-Schleife gelegentlich verwendet.

Beispiel:

int i=0,j=1,k=2; /* Eine "horizontale" Sequenz von drei Anweisungen */
i=1, j*=i, k+=i;

Der Bedingungsoperator

In dem Kapitel Kontrollstrukturen werden die if- und anderen Verzweigungskonstrukte von C behandelt. Im Reigen der Operatoren findet sich ein einziger dreiwertiger (ternärer) Operator, der Fragezeichenoperator oder Bedingungsoperator.

An die Stelle eines beliebigen Ausdruckes kann auch ein Ausdruck der Form <bedingung> ? <ausdruck1> : <ausdruck2> treten. Trifft die <bedingung> zu, d.h. ist <bedingung> != 0, so wird <ausdruck1> genommen, andernfalls <ausdruck2>.

Beispiel:

a = (b > c ? b : c);

Hier wird a der Wert von b zugewiesen, falls b > c ist; andernfalls wird a der Wert von c zugewiesen.

Übersichtstabelle: Prioritäten und Reihenfolge von Bewertungen

Nachstehend werden die Prioritäten und die Bewertungsreihenfolgen, die sogenannten Assoziativitäten, der Operatoren in ANSI-C aufgelistet. Die Prioritäten sind von oben nach unten abnehmend aufgeführt; die Operatoren innerhalb einer Zeile werden gemäß ihrer Assoziativität verarbeitet. Der * unter Priorität 14 ist die Pointerreferenzierung, der unter 13 ist der Rechenoperator Multiplikation, die Zeichen + und - unter 14 sind die unären Vorzeichenoperatoren, das &-Zeichen unter Priorität 14 ist der Adreßoperator, das &-Zeichen unter 8 ist das bitweise Und.

Priorität  Operator                                   Assoziativität       
   15       ( )   [ ]   ->   .                        von links nach       
                                                      rechts               
   14      ! ~ ++ -- + - (TYP) * & sizeof             von rechts nach      
                                                      links                
   13      * / %  (Rechenoperationen)                 von links nach       
                                                      rechts               
   12      + - (binär)                                von links nach       
                                                      rechts               
   11      <<  >>                                     von links nach       
                                                      rechts               
   10      < <= > >=                                  von links nach       
                                                      rechts               
    9      == !=                                      von links nach       
                                                      rechts               
    8      &                                          von links nach       
                                                      rechts               
    7      ^                                          von links nach       
                                                      rechts               
    6      |                                          von links nach       
                                                      rechts               
    5      &&                                         von links nach       
                                                      rechts               
    4      ||                                         von links nach       
                                                      rechts               
    3       ?:                                        von rechts nach      
                                                      links                
    2      =  +=  -=  /=  *=  %=  >>=  <<=  &=  |=    von rechts nach      
           ^=                                         links                
    1      , (Sequenz-Operator)                       von links nach       
                                                      rechts               

Der Preprocessor

Wie bereits erwähnt, fällt die erste Arbeit bei der C-Programmentwicklung, die der Compiler[10] zu erledigen hat, an den C-Preprocessor. Er ist im wesentlichen für Textersetzungen und die Compilersteuerung[11] zuständig (sh. Seite 25).

Include-Direktive

Zum einen werden von diesem die #include-Zeilen ausgewertet, die angesprochenen Dateien (Include-Files) zur Compilationszeit eingebunden. Hierbei handelt es sich in der Regel um Headerfiles, d.h. um Quelltextdateien, in denen (nur) Deklarationen stehen. Solche Dateien tragen die Endung .h; es können aber prinzipiell auch andere Quelltextteile ausgelagert und included werden.

Hinweis: Wird die Datei in spitzen Klammern angegeben (#include <stdio.h>), so wird im festgelegten Pfad (bei UNIX ist das in der Regel /usr/include) nach der Datei (stdio.h) gesucht; wird die Datei in doppelten Hochkommata angegeben (#include "myprog.h"), so wird nur im aktuellen Verzeichnis (bzw. in dem eventuell angegebenen relativen oder absoluten Pfad) gesucht.

Define-Direktive und Makros

Weiterhin leistet der Preprocessor Textersatzfunktionen. Eine solche Definition hat die Form

#define <name> <ersatztext>

und sorgt dafür, daß überall, wo im ursprünglichen Quelltext <name> vorkam, in der erweiterten Quelltextfassung <ersatztext> steht. Dies gilt jedoch nicht innerhalb von Zeichenketten! <name> kann dabei einer der üblichen Namen sein, per Konvention schreibt man diesen in Großbuchstaben; der <ersatztext> darf irgendeine Zeichenkette sein, die sogar nötigenfalls über mehrere Zeilen gehen kann: in diesem Fall muß auf der vorherigen Zeile mit einem Backslash \ abgeschlossen und in der ersten Spalte der Folgezeile fortgesetzt werden.

Beispiel:

#include <stdio.h>
#include "myprog.h"

#define MAXIMUM 120
#define MINIMUM 100

#define ANZAHL (MAXIMUM-MINIMUM) /* funktioniert auch! */

Darüber hinaus können aber auch über den Preprocessor Makros mit Parametern definiert werden.


Beispiel:

#define SQUARE(x) ((x)*(x))

Hiermit wird vereinbart, daß SQUARE(x) ein Makro ist, bei dem x dynamisch ersetzt wird. Eine Anweisung der Form

y = SQUARE(3);

wird vom Preprocessor somit expandiert zu

y = ((3)*(3));

Die Klammerung im Textersatz in der Definition von SQUARE ist übrigens nicht akademisch! Wird die Anweisung

y = SQUARE(x1+x2);

vom Preprocessor gelesen, so wird daraus bei obiger Definition korrekt die Zeile

y = ((x1+x2)*(x1+x2));

Betrachten wir dagegen folgende Definition:

#define SQUARE(x) x*x

Die Anweisung

y = SQUARE(x1+x2)+x3;

wird damit (nur auf den ersten Blick überraschend) ersetzt zu

y = x1+x2*x1+x2+x3;

Im Gegensatz zu Funktionen ist es den Makros übrigens egal, welche Datentypen da auf sie niederprasseln: der Preprocessor macht schließlich nur eine einfache Textersetzung und keine semantische Typüberprüfung!

Trigraphen

Beim Programmieren arbeiten wir, bewußt oder unbewußt, stets mit mehreren Zeichensätzen gleichzeitig. Zum einen ist der ganze Code (bei uns in der Regel der 8-Bit-ASCII-Zeichensatz) des Betriebssystems und mehr oder weniger der Tastatur verfügbar, zum zweiten ist da der Zeichensatz, den die jeweilige Programmiersprache versteht.

Da nicht auf allen (vor allem älteren) Tastaturen jedes benötigte Zeichen für C zu finden ist, gibt es die sogenannten Dreizeichenfolgen (Trigraphen, trigraph sequences). Hierbei handelt es sich um Ersatzzeichenfolgen für ein bestimmtes Zeichen, wie sie in der nachstehenden Tabelle aufgeführt sind. So ist a??(1??) ein gültiger, wenn auch schwer lesbarer Ersatz für a[1].

Soll etwas, z.B. eine Sequenz von mehreren Zeichen, nicht interpretiert werden, so kann stets mit dem Fluchtzeichen (Quotierungszeichen) \ gearbeitet werden: die Anweisung

printf("Was ist das?\?!");

führt nach der Phase der Textersetzung durch den Preprocessor zur Anweisung

printf("Was ist das??!");

und damit zur Ausgabe

Was ist das??!

auf dem Bildschirm.

    Dreizeichenfolge      ...ersetzt das      
       (Trigraph)         Zeichen             
          ??=             #                   
          ??(             [                   
          ??)             ]                   
          ??/             \                   
          ??'             ^                   
          ??<             {                   
          ??>             }                   
          ??!             |                   
          ??-             ~                   

Bedingte Compilation

Schließlich sei noch auf eine weitere, in der Praxis sehr wichtige Anwendung von #define hingewiesen: die bedingte Compilation. Unter Bedingter Compilation versteht man die Möglichkeit, ein Quelltextstück nur unter einer gewissen Voraussetzung überhaupt compilieren zu lassen. Diese Voraussetzung ist das Definiertsein einer symbolischen Konstanten oder die Gleichheit mit einem bestimmten Wert. Folgendes Beispiel soll dies verdeutlichen; dabei werden gleichzeitig die Preprocessor-Direktiven #if, #ifdef, #ifndef, #else, #elif und #endif vorgestellt.

Beispiel:

#define TESTPHASE 1 /* während der Programmentwicklung */
/* wird TESTPHASE definiert als 1 */

#if TESTPHASE == 1
# define PROGRAMMVERSION "Kolibri 1.0 [Testversion]"
#elif TESTPHASE == 2
# define PROGRAMMVERSION "Kolibri 1.0 [Alpha-Release]"
#else
# define PROGRAMMVERSION "Kolibri 1.0 [Final-Release]"
#endif
/* ....... */

printf("%s\n",PROGRAMMVERSION);
#ifdef TESTPHASE /* ist TESTPHASE definiert worden? */
printf("Wir befinden uns in der Testphase des Programms.\n");
#endif

/* ....... */
#ifndef TESTPHASE /* wenn nicht definiert, dann... */
printf("Wir befinden uns in der Abschlußphase...\n");
#endif

/* ....... */

Funktionen

Eine Funktion kennen Sie bereits: main(), das Hautprogramm von C. In gleicher Weise können beliebig viele Funktionen[12] für ein C-Programm deklariert und definiert werden.

Wir haben aber gelegentlich auch schon eine weitere Funktion verwendet: printf(), eine Bibliotheksfunktion (vgl. den Abschnitt zu Bibliotheken und Headerfiles), die in <stdio.h> deklariert wird.

Formaler Aufbau einer Funktion

Der formale Aufbau einer Funktion ist recht einfach:

<ergebnistyp> <funktionsname> ( <parameterliste> )

{

<deklarationen>

<anweisungen>

}

Hierbei ist <ergebnistyp> irgendein vordefinierter oder selbstdefinierter Datentyp, die <parameterliste> eine   eventuell leere  komma-getrennte Aufzählung von Übergabeparametern.

Die Aufrufbarkeit und Gültigkeit wird auf Seite 43 im Kapitel zur Modularität eingehender besprochen. An dieser Stelle sei lediglich ausgeführt, daß eine Funktion eine jede andere (und sich selbst - Rekursion!) aufrufen kann, die dem Compiler zu diesem Zeitpunkt bereits bekanntgemacht worden ist. Durch das sogenannte Prototyping, i.e. das Voranstellen der Deklarationen der Funktionen vor die eigentlichen Implementationen (Definitionen) wird in der Praxis erreicht, daß jede Funktion jede andere aufrufen kann.

Ein ganz wichtiger, für Pascal-Programmierer ernüchternder Punkt: ANSI C kennt nur Wertübergaben, call by value! Eine Referenzübergabe (call by reference) in dem Sinne gibt es nicht! Wir werden weiter unten sehen, wie das Leben in C trotzdem weitergehen kann[13].

Sehen wir uns einige einfache Beispiele an.

Beispiel:

#include <stdio.h>
/* Prototypen: Bekanntmachen aller Funktionen, damit u.a. main() diese aufrufen kann. */
float KreisFlaeche(float);
void main(void)
{
   float radius;
   printf("\nBitte einen Radius eingeben: ");
   scanf("%f",&radius); /* Einlesen eines float-Wertes */
   printf("Radius: %f Kreisfläche: %f",
   radius,KreisFlaeche(radius));
} /* end main */
float KreisFlaeche(float r)
{
   #define PI (3.1415926) /* #define kann überall im Source stehen */
   return PI*r*r;
} /* end KreisFlaeche */

Bei diesem kleinen Beispielprogramm ist einiges neu.

Zunächst einmal sehen wir (entsprechend kommentiert) einen sogenannten Prototypen der Funktion KreisFlaeche(). Hier wird dem Compiler mitgeteilt, daß es eine solche Funktion geben und welchen Rückgabewert/typ sowie welche Parameterliste sie haben wird. Der Prototyp wird mit Semikolon abgeschlossen.

Im Hauptprogramm main() ist der Aufruf von scanf() neu; dies ist das Gegenstück zu read(ln) bei Pascal. Wie printf() ist auch scanf() in <stdio.h> vereinbart; auf einige der zahlreichen Möglichkeiten von scanf() soll an anderer Stelle (sh. Seite 35) eingegangen werden. Hier nur die "lokale" Erläuterung: scanf() erwartet als ersten Parameter einen sogenannten Formatstring. In unserem Beispiel wird mit "%f" nur gesagt, daß ein float-Wert eingelesen werden soll. Als zweiter Parameter wird mit "&radius" gesagt, daß scanf() die Adresse der Variablen radius verwenden soll, um dort hinein den von Tastatur eingelesenen Wert abzuspeichern. Dies ist der angekündigte "Trick", wie trotz des call by value die Variable radius doch in der Funktion verändert werden kann: es wird einfach die Speicheradresse von radius ("by value") übergeben, damit kann de facto dann doch der Speicherplatz radius des Hauptprogramms angesprochen und verändert werden.

Neu ist bei printf() eine entsprechende Erweiterung: auch hier ist nun der erste Parameter ("Radius: %f Kreisfläche: %f") ein sogenannter Formatstring, der zweite und dritte Parameter sind die float-Variable radius und der Funktionsaufruf KreisFlaeche(radius), der einen float-Wert zurückliefert. "%f" steht also auch hier wieder für die sachgemäße Interpretation der beiden Werte durch printf().

Nach main() folgt nun (erstmals) eine weitere Funktion, hier KreisFlaeche(). Der formale Aufbau ist mit dem von main() vollkommen vergleichbar. Als Parameter wird ein float namens r vereinbart, als Rückgabetyp wird ebenfalls float benannt. Im Block der Funktion, d.h. zwischen den geschweiften Klammern, wird zunächst über den Preprocessor PI auf den allseits bekannten Wert 3.1415926 gesetzt, dann wird mit dem Schlüsselwort return der Rückgabewert festgelegt. An dieser Stelle wird übrigens die Funktion auch schon wieder verlassen, selbst wenn anschließend noch weitere Anweisungen folgen sollten!

Parameter und Rückgabewerte

Die Parameter bei einer C-Funktion können von beliebigen Datentypen sein. Es gibt auch die Möglichkeit, auf die hier allerdings nicht näher eingegangen werden soll, Parameterlisten offen zu gestalten, d.h. nicht schon bei der Deklaration der Funktion festzulegen, wieviele Parameter die Funktion beim konkreten Aufruf haben soll.

Auf einen historischen Aspekt soll an dieser Stelle eingegangen werden: man unterscheidet heutzutage zwischen dem üblichen ANSI C, das auch hier in wesentlichen Teilen besprochen wird, und dem klassischen Kernighan-Ritchie-C (K&R-C), dem old fashioned style. Die unterschiedliche Definition von Funktionen ist eine der wesentlichen Änderungen von Kernighan-Ritchie-C zu ANSI C. Im K&R-C würde die obige Funktion KreisFlaeche() wie folgt deklariert und definiert werden.

float Kreisflaeche(radius)
float radius;
{
   #define PI (3.1415926)
   return PI*r*r;
} /* end of KreisFlaeche K&R-Style */


Bibliotheken und Headerfiles (Übersicht)

Wie erwähnt umfaßt der eigentliche Kern von C noch nicht einmal Routinen zur Terminal-Ein- und Ausgabe! (Hierzu speziell mehr im nachfolgenden Kapitel auf Seite 30.) Für fast alle weitergehenden Aufgabenstellungen muß C daher auf die Standardbibliothek (standard library) zugreifen.

Darunter versteht man eine Menge von Deklarationen und Funktionen, die als Bibliothek (bei UNIX: Dateien mit den Endungen[14] .a (archive) oder .sl (shared library)) in Form von Object code mit eingebunden wird; die zugehörigen Deklarationen und Prototypen finden sich in den bereits mehrfach erwähnten Headerfiles, die mit der #include-Direktive durch den Preprocessor eingebunden werden.

In der folgenden Übersicht sind die Standard-Headerfiles gemäß ANSI aufgeführt, die bei einem Standard-UNIX-System üblicherweise in dem Verzeichnis /usr/include zu finden sind.

assert.h Programmdiagnose
ctype.h Zeichenklassen
errno.h beinhaltet Fehlernummern
float.h enthält Fließkomma-Grenzwerte
limits.h enthält Ganzzahl-Grenzwerte
locale.h Lokalisierung (Anpassung an spezielles System)
math.h Mathematische Deklarationen und Routinen
setjmp.h Globale Sprünge
signal.h Signalverarbeitung
stdarg.h Arbeiten mit variablen Argumentlisten
stddef.h Deklaration allgemeiner Werte (z.B. von NULL)
stdio.h Standard Ein-/Ausgabe (standard i/o)
stdlib.h Hilfsfunktionen
string.h Zeichenkettenverarbeitung
time.h Datum und Uhrzeit