ERCC (http://eriedel.info)
Teil 1 (Einleitung, Übersetzung, Layout)
Teil 3 (Array, String, Zeiger)

Die Sprache C

Teil 2 (Daten, Funktionen, Operatoren)


Datentypen

Obwohl C nur über wenige elementare Datentypen verfügt, hat man aufgrund einiger Modifikatoren und der häufig anzutreffenden Definition von Synonymen anfangs den Eindruck einer verwirrenden Vielfalt. Die folgende Übersicht enthält aber alle einfachen (skalaren) Datentypen in C.

char character ein Byte, insb. ein einzelnes Zeichen
(genauer: dessen Zahlenwert, z.B. gem. ASCII)
int integer eine ganze Zahl (systemabhängig 16-, 32- oder 64-Bit)
short oder short int short integer »kurze« Ganzzahl (zumeist 16-Bit)
long oder long int long integer »lange« Ganzzahl (32- oder 64-Bit)
float floating point
numbers
einfach genaue Gleitkommazahl (Single)
double doppelt genaue Gleitkommazahl

Die interne Darstellung und damit der Wertebereich ist einer C-Implementation grundsätzlich freigestellt, so dass die »natürlichen« Größen des Zielsystems berücksichtigt werden können. Auch die Relation der Integertypen short, int und long ist nicht allgemein festgelegt. 1  Die obigen Größenangaben treffen also nur auf PC-Systeme zu! Hier werden vorzeichenbehaftete (»signed«) Ganzzahlen im Zweierkomplement- und Gleitkommazahlen im IEEE-Format (32- und 64-Bit) gespeichert und sind damit kompatibel zu den entsprechenden Datentypen von BASIC und anderen Sprachen. 2 

Die Auswertung des höchsten Bits (MSB) als Vorzeichen bei den Integertypen (also auch char) kann für jede Integer-Variable explizit durch die Modifizierer signed und unsigned festgelegt werden. Die Voreinstellung ist signed.
C-Implementationen für PC-Systeme stellen mit long double noch den Typ einer erweitert genauen Gleitkommazahl bereit, bei der das 80-Bit-IEEE-Format verwendet wird (kompatibel beispielsweise zu Ext in PowerBASIC).

Neuerer Sprachstandard definiert zusätzlich den Typ long long int bzw. long long als einer Ganzzahl, die (auch auf 32-Bit-Systemen) 64-Bit umfasst.

Wer C-Code betrachtet, wird häufig auf weitere Typbezeichnungen wie WORD, UINT u.v.a. stoßen. Dabei handelt es sich aber nicht um zusätzliche Datentypen, sondern um Synonyme, deren Definition Programmänderungen oder ‑portierungen erleichtern sollen. So würde etwa eine Definition wie

#define WORD unsigned short

und die konsequente Verwendung des Synonyms WORD in der Vergangenheit eine Portierung des Programms von Windows 3.1 nach Windows 95 ff. sicher vereinfacht haben, da sich dieser Typ – anders als int – dabei nicht ändert.
Und umgekehrt könnte durch

#define HANDLE unsigned int

sichergestellt werden, dass eine systembedingte Größenänderung dieses Typs (16- bzw. 32-Bit) auch wirklich überall erfolgt. 3  
Auch aus anderen Gründen notwendige Typänderungen (etwa float in double) werden natürlich vereinfacht, wenn sie nur an einer Stelle durchzuführen sind.

Wenn ich nun feststelle, dass an dieser Stelle alle Datentypen von C genannt wurden, wird der BASIC-Programmierer fragen, was denn nun mit Strings (Zeichenketten) ist. Dieses Thema (und den Datentyp Zeiger) behandle ich in einem folgenden Abschnitt.

Aus den elementaren Datentypen lassen sich in C folgende zusammengesetzte Typen erzeugen:

array Feld (Reihenstruktur) eines Datentypen, in der C-Terminologie meist Vektor genannt
struct Struktur (Datensatz), Zusammenfassung auch unterschiedlicher Datentypen, in BASIC als (User Defined) Type (UDT) bezeichnet
union Variante, auch in PowerBASIC vorhanden; Variant in Visual Basic stellt eine transparente, funktionell erweiterte Form dieses Typs dar

Zur Deklaration dieser Datentypen gibt es im Grunde nicht viel zu sagen, da nur gelinde Unterschiede zu den jeweiligen BASIC-Implementationen bestehen. Für Arrays gilt das nicht ganz, und ich komme darauf später noch zu sprechen.


Konstante

Hinsichtlich der Definition von Konstanten in C möchte ich auf eine erschöpfende Darstellung verzichten und nur auf einige Besonderheiten eingehen, die mir erwähnenswert scheinen.

C unterstützt (sogar besonders) Oktalzahlen. Jede Zahl, die mit einer Null beginnt, wird als solche interpretiert, anderenfalls als Dezimalzahl. Das ist aber unproblematisch, wenn man nicht versehentlich – und überflüssigerweise – einer Zahl die Null voranstellt. Zahlen, die mit 0x (oder 0X) beginnen, werden als Hexadezimalzahlen interpretiert.

n = 10;      // dezimal 10
n = 010;     // oktal 10 = dezimal 8
n = 0x10;    // hex 10 = dezimal 16

Die Interpretation bestimmt auch die erlaubten Ziffern (und Buchstaben bei Hex-Zahlen). Grundsätzlich können Integerkonstante den Wertebereich von long und Gleitkommakonstante den von double einnehmen.

C unterscheidet deutlich zwischen Zeichen- und Stringkonstanten. Während die Stringkonstante "Hallo!" vertraut erscheint, ist die Zeichenkonstante 'A' ungewohnt. Der Anfänger muss darauf achten, die unterschiedlichen Anführungszeichen nicht zu verwechseln.

Zeichen- und Stringkonstante können unmittelbar jedes beliebige Zeichen enthalten, denn ein Zeichen, das nicht literal darstellbar ist, kann durch eine Escape-Sequenz definiert werden. Die Escape-Sequenz wird durch einen Backslash (\) eingeleitet, auf den bestimmte Zeichen oder Zahlenwerte folgen. 4  Einige Steuerzeichen sind nützlicherweise als Buchstaben vordefiniert, zum Beispiel:

\n Zeilenvorschub (new line)
\r Wagenrücklauf (return)
\t Tabulator, horizontal

Auch Zeichen, die in diesem Kontext eine besondere Bedeutung haben, können einfach via Escape-Sequenz einbezogen werden:

\\ ein (einzelner!) Backslash
\" ein Anführungszeichen
\' ein einfaches Anführungszeichen (Apostroph)

Außerdem kann jedes Zeichen durch seinen Codewert als Escape-Sequenz definiert werden:

\nnn ein Zeichen mit dem Codewert nnn – oktal!
\xnnn ein Zeichen mit dem hexadezimalen Codewert nnn
\0 ein Null-Byte

Auch hier wird eine gewisse Affinität zu Oktalzahlen deutlich. Jedenfalls stellt C hier sehr praktische Möglichkeiten bereit. Zu beachten ist, dass Dateipfadbezeichnungen im Programmtext ggfs. doppelte Backslashes enthalten müssen!

Zur Klarstellung:

All dies bezieht sich auf Konstante im Quellcode. Die Interpretationen erfolgen durch den Compiler, der übrigens Stringkonstante schon selbst durch ein Null-Byte ergänzt. Daher konnten wir die Stringausgabe in den Beispielen weiter oben einfach durch

printf("n ist größer Null\n");

anweisen. Der Compiler legt die Stringkonstante – einschließlich Zeilenvorschub- und Null-Zeichen – im Datenspeicher ab. (Die Bedeutung dieses Null-Bytes wird klar, wenn wir uns den Strings in C widmen.)


Variablen und Funktionen

Die Namensregeln in C unterscheiden sich nicht sehr von denen in BASIC, allerdings kann die Auswertung (Signifikanz) in der Länge deutlich beschränkt sein. 5   Bedeutsamer ist, dass zwischen Groß- und Kleinbuchstaben unterschieden wird. 6   Somit sind »SUMME«, »summe« und »Summe« drei unterschiedliche Namen!

In C müssen alle Variablen vor ihrer Verwendung deklariert werden. In den obigen Beispielen fehlt also (neben anderem) die Deklaration von n, etwa durch:

int n;

Die Deklaration gleicher Typen kann zusammengefasst werden, was üblicherweise auch ausgenutzt wird:

int a, b, c;

deklariert drei Integer-Variablen.

Die (explizite) Deklaration von Funktionen ist zwar nicht zwingend notwendig, aber empfehlenswert, schon weil sie die Aufdeckung von Unstimmigkeiten unterstützt.

In C werden lokale Variablen (genauer: solche der Speicherklassen auto und register) und Funktionswerte nicht automatisch initialisiert (bzw. definiert)!
Während die folgende BASIC-Funktion durchaus korrekt und zuverlässig ist (als Kommentare: die impliziten Wertzuweisungen)

Function Nosense() As Integer
    Dim n As Integer    ' (meist optional)
    ' n = 0
    Call Unknown(n)
    If n Then Exit Function   ' Nosense = 0 (False)
    Nosense = True
End Function

würde der C-Compiler einen gleichartigen Code sehr wahrscheinlich abweisen. Zu erwarten wären (wenigstens) zwei Warnungen, die sich genau auf unsere falschen Voraussetzungen bezögen: die Verwendung der Variable n bevor ihr Wert definiert ist und das Verlassen der Funktion ohne Definition eines Rückgabewertes. In jedem Fall hätte die Übersetzung einen unzuverlässigen Programmcode zum Ergebnis. 7  

Zum Vergleich eine korrekte C-Version:

int Nosense(void)
    {
    int n = 0;
    Unknown(n);
    if (n)
        return FALSE;
    return TRUE;
}

Hierbei wurde übrigens die Gültigkeit der Bezeichner TRUE und FALSE vorausgesetzt.

Wie die C-Funktion zeigt, kann eine Variablen-Deklaration gleich mit einer Initialisierung verbunden werden.
Gemäß der obigen Empfehlung sollte das C-Programm vorab eine Deklaration der Funktionen (sog. Prototypen) enthalten:

int Nosense(void);
void Unknown(int);

Man sieht an diesem Beispiel die schon früher erwähnte Tatsache, dass es in C keine Unterscheidung zwischen Function und Sub gibt, lediglich Funktionen, die entweder einen bestimmten oder keinen (void) Rückgabewert haben.
Die Definition des Rückgabewertes erfolgt mit der Anweisung

return AUSDRUCK;

wodurch die Funktion auch unmittelbar verlassen wird.

Jede Funktion kann in C zudem wie eine Sub-Routine aufgerufen werden, indem man den Funktionswert einfach ignoriert:

Nosense();

Die Klammern müssen auch bei einer parameterlosen Funktion angegeben werden (vgl. Visual Basic). Eine Call-Anweisung existiert nicht.

Funktionsparameter werden in BASIC normalerweise als Referenz (Adresse) übergeben. Die Übergabe als Wert (by value) ist optional. In C ist es gerade umgekehrt, der Programmierer muss also die Referenz-Übergabe selbst veranlassen. Erhält eine Funktion keine Parameter, so wird – auch bei der Definition – anstelle der Parameterliste wiederum das Schlüsselwort void eingesetzt.

Der Gültigkeitsbereich einer Variablen wird im wesentlichen von ihrem Deklarationsort bestimmt. 8  (Weil dabei auch der Speicherort der Variablen festgelegt wird, spricht man in diesem Zusammenhang auch von unterschiedlichen Speicherklassen.)
Einfach gesagt, kann eine Variable global (extern), innerhalb eines Moduls (static) oder nur lokal (auto) gültig sein.

Das Schlüsselwort extern wird nur in Deklarationen verwendet, um die Verbindung zu einer Variable (oder einer Funktion) eines anderen Moduls herzustellen. 9  

Eine Variable, die außerhalb von Funktionen deklariert wird, ist statisch und im Rest des Quelltextes gültig (bekannt). Das Schlüsselwort static ist nicht notwendig, kann aber zusätzlich (auch bei Funktionen) angegeben werden, um eine externe Gültigkeit (und eventuelle Namenskonflikte) auszuschließen.

Eine Variable, die innerhalb einer Funktion deklariert wird, ist grundsätzlich nur von lokaler Gültigkeit. 9   Das Schlüsselwort auto ist nicht notwendig und wird auch nie verwendet. Auch eine lokale Variable kann als static deklariert werden. Diese (interne) statische Variable behält ihren Wert auch zwischen Funktionsaufrufen.

C erlaubt die Deklaration von Variablen auch innerhalb eines Blocks. Solche Variablen sind nur innerhalb des Blocks gültig, also gewissermaßen sub-lokal. Gleichnamige Variablen außerhalb des Blocks werden vorübergehend »unsichtbar« und kollidieren daher nicht mit den sub-lokalen Namen.

Globale und statische Variablen werden vom Übersetzer (ggfs.) mit Null initialisiert. Wie bereits gesagt, gilt das nicht für lokale (auto) Variablen, ihr Wert ist also zunächst unbestimmt.


Operatoren

In C gibt es Unterscheidungen zwischen Operatoren, die in BASIC verborgen sind. So wird in BASIC für eine Wertzuweisung wie für einen Vergleich formal derselbe Operator verwendet

n = 20
If n = 20 Then '...

während in C unterschiedliche Operatoren anzuwenden sind!

Entsprechendes gilt für eine Reihe weiterer Operatoren. Zudem gibt es einige, in beiden Sprachen gleich aussehende Operatoren, die ganz unterschiedliche Bedeutungen haben. Die Gefahr der Verwechslung ist also leider nicht gering.

Die folgende Übersicht fasst die wichtigsten Unterschiede zusammen.

Operation/Relation C BASIC
Zuweisung = =
Integerdivision / (wie normale Division) \
Modulo % Mod
Potenzierung   ^
Inkrementierung ++  
Dekrementierung −−  
Gleichheit == =
Ungleichheit != <>
NOT: logisch / bitweise ! ~ Not
AND: logisch / bitweise && & And
OR: logisch / bitweise || | Or
XOR: logisch / bitweise   ^ Xor

Auffällig ist vor allem die Unterscheidung zwischen logischen und bitweisen Operatoren in C (bzw. deren Fehlen in BASIC).

C kennt noch einige weitere Operatoren, insb. eine ganze Klasse von Zuweisungsoperatoren, die eine arithmetische oder Bitoperation mit der Zuweisung kombinieren. Die Anweisungen

n = n + 5; a = a * b;

lassen sich damit auch als

n += 5; a *= b;

formulieren, wobei die kürzeren Anweisungen nicht nur bevorzugter C-Stil sind, sondern in der Regel auch effektiver übersetzt werden.

C verfügt ferner über die oben gezeigten Operatoren zur In- bzw. Dekrementierung von Variablen, so dass wir in manchen Fällen gleich drei Formulierungsmöglichkeiten haben (wobei Letzterer der Vorzug zu geben ist):

n = n + 1;
n += 1;
++n;

Ungewöhnlicherweise können die In-/Dekrement-Operatoren sowohl vor (Präfix) als auch nach (Suffix) dem Operanden stehen. Ich erkläre dies anhand der späteren Beispiele.

Sprachunterschiede sind ferner noch bei der Operatoren-Rangfolge zu berücksichtigen. So hat z.B. die logische wie auch bitweise Negation – anders als in Visual Basic und PowerBASIC – Vorrang vor den arithmetischen Operatoren. Dass solche Unterschiede bestehen und bedeutsam sein können, gilt selbstverständlich beim Wechsel auf jede Sprache, die komplexe Ausdrücke erlaubt. (Bei allen nicht standardisierten Sprachen sogar schon für einen Wechsel auf einen anderen Übersetzer derselben Sprache.)



Anmerkungen:

1 Für 64-Bit-Systeme existieren zudem verschiedene Datenmodelle, die sich vor allem beim Datentyp long int unterscheiden (32- oder 64-Bit).

2 Diese Begriffe und Datenformate werden in meinem Artikel »Vom Bit zum Wort: Computer und Daten« behandelt.

3 Jedoch verwendet man bei solchen Typdefinitionen meist die Anweisung typedef anstelle von #define.

4 Da es keine Ende-Kennung gibt, ist die Länge der Escape-Sequenz insb. für Ziffernfolgen implementationsabhängig.

5 Nach Kernighan/Ritchie [Programmieren in C, Hanser 1983] etwa auf acht oder weniger Zeichen, was nicht gerade viel ist. Moderne C-Compiler werten in der Regel jedoch eine deutlich höhere Zeichenanzahl aus. In C++ besteht keine Begrenzung.

6 Die Berücksichtigung von Groß- und Kleinschreibung kann meist durch Compiler- bzw. Linker-Einstellung verhindert werden. Nach meinen Erfahrungen ist das aber nicht unproblematisch und deshalb nicht sehr empfehlenswert.

7 Das Hauptproblem ist hier nicht, dass der Code nicht funktioniert, sondern dass er Annahmen über seine Verwendungsweise enthält! Der Code ist okay, wenn die Integer n vollständig durch Unknown(n) definiert und der Funktionswert von Nosense() nicht ausgewertet wird.

8 Um die Dinge nicht unnötig komplizierter zu machen, ist an dieser Stelle nur von Deklaration die Rede, wobei der Unterschied zur Definition (Speicherreservierung) vernachlässigt wird.

9 Die Deklaration als extern ist auch innerhalb einer Funktion für eine funktionsexterne Variable erforderlich, falls deren Definition im Quelltext erst nach der Verwendung erfolgt, die Funktion also vor der Variable definiert wird.



http://eriedel.info/info/c-prog/cpl2.html


Textanfang
ERCC (http://eriedel.info)  11/2011   © Erhard Riedel Computer Consulting (ERCC)


Link zur ERCC-Hauptseite   Link zur Info-Übersicht