C programmieren: Datentypen, Deklarationen, Operatoren und Ausdrücke
Prof. Dr. Christian Siemers *
Anbieter zum Thema
Die Syntax einer Programmiersprache stellt so etwas wie ihre Grammatik dar: Erst, wenn die lexikalischen Elemente beim Programmieren in C syntaktisch korrekt verwendet werden, können sinnvolle Anweisungen entstehen. Dieser Artikel beschreibt, wie die Syntax von C aufgebaut ist: Von Datentypen, Deklarationen, Operatoren bis hin zu Ausdrücken und Anweisungen.
In einem früheren Beitrag zu den Grundlagen von C haben wir erläutert, welche lexikalischen Elemente die Programmiersprache besitzt: welcher Zeichensatz unterstützt wird, welche essentielle Schlüsselwörter für Programmier-Befehle in C verwendet werden, welche Konstanten und Identifier existieren etc.
Doch nur wenn all diese Elemente in einem sinnvollen Zusammenhang zueinander stehen, entsteht auch ein funktionaler Code. Wenn also die lexikalischen Elemente so etwas wie den "Wortschatz" von C darstellen, sind die syntaktischen Elemente essentiell die Grammatik der Programmiersprache. So wie es in der normalen Sprache Verben, Hauptwörter, Objekte oder Adjektive gibt, gibt es in Programmiersprachen syntaktische Kategorien wie Datentypen, Deklarationen, Operatoren etc.
Im folgenden Beitrag wird näher darauf eingegangen, welche syntaktischen Elemente der Programmiersprache C zu Grunde liegen und wie diese eingesetzt werden. Da die Syntax einer Programmiersprache entsprechend umfangreich ausfallen kann, ist dieser Artikel in zwei Teile gesplittet. Im ersten Teil erläutern wir Elemente wie Datentypen, Deklarationen und Definitionen, Speicherklassen, Operatoren, Ausdrücke sowie Anweisungen in C.
Datentypen in C
Der Begriff des Datentyps beinhaltet folgendes:
- die Größe und Ausrichtung des belegten Speicherplatzes (size, alignment)
- die interne Darstellung (Bitbelegung)
- die Interpretation und damit den Wertebereich
- die darauf anwendbaren bzw. erlaubten Operationen
Die in der Bildergalerie enthaltene Tabelle 1 zeigt eine Übersicht zu den instrinsischen Datentypen in C.
ISO-C verfügt über einen reichhaltigen Satz von Datentypen, die sich wie in vorangegangener Übersicht gezeigt organisieren lassen. ISO-C verlangt binäre Codierung der integralen Typen. Für die Wertebereiche aller arithmetischen Typen sind Mindestwerte und Größenverhältnisse festgelegt. Die implementierten Größen dieser Datentypen sind in limits.h und float.h definiert.
In obiger Tabelle bezeichnet T* einen Zeiger auf den Typ T, T[...] ein Array vom Typ T, T(...) eine Funktion mit Rückgabetyp T. void ist der leere Typ. Als Rück-gabetyp einer Funktion deklariert zeigt er an, dass die Funktion nichts zurückgibt, in der Parameterliste, dass sie nichts nimmt. Ein Zeiger auf void ist ein Zeiger auf irgendetwas unbestimmtes, ein generischer Zeiger, den man nie dereferenzieren kann. Variablen oder Arrays vom Typ void können daher nicht deklariert werden. Der Array-Typ T[] und der Funktionstyp T() können nicht Typ einer Funktion sein.
Die Gruppen, Klassen und Kategorien dienen zur Kenntlichmachung der auf diesen Typen und in Verbindung mit diesen Typen erlaubten Operationen. Datentypen können durch die sog. type qualifiers const und volatile weiter qualifiziert werden. Dabei bedeutet const, dass ein so bezeichneter Datentyp nur gelesen werden darf (read only), d.h. er könnte z.B. in einem solchen Speicherbereich oder im ROM abgelegt sein. volatile bedeutet, dass die so qualifizierte Größe durch außerhalb des Wirkungsbereichs des Compilers liegende Einflüsse verändert werden könnte, z.B. kann es sich hier um in den Speicherbereich eingeblendete Hardwareregister (sog. Ports) handeln. Dies soll den Compiler davon abhalten, gewisse sonst mögliche Optimierungen des Zugriffs auf die entsprechende Variable vorzunehmen. Beide Qualifizierer können auch zusammen auftreten. Hier einige Beispiele:
int i; /* i ist als Variable vom Typ int definiert */
const int ic = 4711; /* ic ist als Konstante vom Typ int definiert */
const int *pc; /* pc ist Zeiger auf konstanten int */
int *const cpi = &i; /* cpi ist konstanter Pointer auf int */
const int *const cpc = ⁣ /* konstanter Pointer auf konstanten int */
volatile int vi; /* vi kann durch äußeren Einfluss verändert werden */
const volatile int vci; /* vci ist z.B. ein Timerport */
Als const vereinbarte Variablen dürfen vom Programm nicht verändert werden. Falls man es versucht, gibt es Fehlermeldungen vom Compiler. Falls man dies jedoch durch in C legale Mittel wie Typumwandlung zu umgehen versucht, kann es je nach System auch zu Laufzeitfehlern führen.
Deklarationen und Definitionen
C ist eine eingeschränkt blockstrukturierte Sprache, d.h. Blöcke sind das strukturelle Gliederungsmittel. Blöcke werden durch die Blockanweisung { ... } erzeugt.
Die Einschränkung ist, dass Funktionsdefinitionen (siehe dort) nur außerhalb von Blöcken möglich sind. Blöcke können beliebig geschachtelt werden. Alles, was außerhalb von Blöcken deklariert oder definiert wird, ist global. Alles, was in ei-nem Block deklariert oder definiert wird, ist lokal zu diesem Block und gilt bis zum Verlassen dieses Blocks. Ein in einem Block deklarierter Name kann einen in einer höheren Ebene deklarierten Namen maskieren, d.h. der äußere Name wird verdeckt und das damit bezeichnete Objekt ist dort nicht mehr zugreifbar.
Der Compiler bearbeitet (man sagt auch liest) den Quelltext (genauer die vom Präprozessor vorverarbeitete Übersetzungseinheit) Zeile für Zeile, von links nach rechts und von oben nach unten. Bezeichner bzw. Identifier müssen grundsätzlich erst eingeführt sein, d.h. deklariert und/oder definiert sein, bevor sie benutzt werden können.
Typ Name; oder Typ Name1 , Name2, . . . ;
Definitionen weisen den Compiler an, Speicherplatz bereitzustellen und, wenn das angegeben wird, mit einem bestimmten Wert zu initialisieren. Eine Definition ist gleichzeitig auch eine Deklaration. Eine Definition macht den Typ vollständig bekannt und benutzbar, d.h. es wird Speicherplatz dafür reserviert (im Falle von Datentypen) oder auch Code erzeugt (im Falle von Funktionsdefinitionen, siehe dort).
Definitionen von Datenobjekten mit Initialisierung haben die Form:
Typ Name = Wert; oder Typ Name1 = Wert1 , Name2 = Wert2 , . . . ;
Deklarationen machen dem Compiler Bezeichner (Namen) und ihren Typ bekannt. Sie können auch unvollständig sein, d.h. nur den Namen und seine Zugehörigkeit zu einer bestimmten Klasse bekannt machen, ohne wissen zu müssen, wie der Typ nun genau aussieht. Das reicht dann nicht aus, um dafür Speicherplatz zu reser-vieren, aber man kann z.B. einen Zeiger auf diesen jetzt noch unvollständigen Typ erzeugen, um ihn dann später, wenn der Typ vollständig bekannt ist, auch zu benutzen. Deklarationen können, abhängig von ihrer Typklasse, auch Definitionen sein. Wenn sie global, d.h. außerhalb von Blöcken erfolgen, sind sie standardmäßig auf den Wert Null initialisiert. Innerhalb eines Blocks ist ihr Wert bei ausblei-bender Initialisierung undefiniert. Definitionen haben die Form:
Speicherklassen, Sichtbarkeit und Bindung in C
Außerhalb von Blöcken vereinbarte Objekte gehören zur Speicherklasse static. Sie sind vom Start des Programms an vorhanden, und sind global, d.h. im ganzen Programm gültig und sichtbar – sie haben global scope und externe Bindung (external linkage). Wenn sie nicht im Programm auf bestimmte Werte gesetzt sind, werden sie auf den Wert 0 initialisiert (im Gegensatz zur Speicherklasse auto).
Durch Angabe des Schlüsselworts static kann der Sichtbarkeitsbereich (scope) für so vereinbarte Objekte auf die Übersetzungseinheit (Datei) eingeengt werden, das Objekt hat dann interne Bindung (internal linkage) und file scope.
Deklarationen und Definitionen in Blöcken können nur vor allen Anweisungen (siehe dort) stehen, also zu Beginn eines Blocks. Sie sind lokal zu dem Block, in dem sie erscheinen (block scope). Die so vereinbarten Objekte haben die Speicherklasse auto, d.h. sie existieren nur, solange der Block aktiv ist und werden bei Eintritt in den Block jedes Mal wieder neu erzeugt, jedoch ohne definierten Anfangswert. Durch Angabe des Schlüsselworts static kann die Speicherklasse auf static geändert werden, ohne dass Sichtbarkeit und Bindung davon berührt würden. Sie sind von Beginn an vorhanden und behalten ihren Wert auch nach Verlassen des Gültigkeitsbereichs.
Vereinbarungen von Objekten mittels register sind nur in Blöcken oder in Parameterlisten von Funktionen erlaubt und dienen lediglich als Hinweis an den Compiler, er möge sie in (schnellen) Prozessorregistern ablegen. Ob das denn auch geschieht, bleibt dem Compiler überlassen. Auf so vereinbarte Objekte darf der Adressoperator & nicht angewandt werden.
Der Sichtbarkeitsbereich einer Marke (label) ist die Funktion, in der sie deklariert ist (function scope). Innerhalb einer Funktion weist man bei Vereinbarung eines Namens mit extern darauf hin, dass das Objekt anderweitig definiert ist. Außerhalb von Funktionen gelten alle vereinbarten Objekte defaultmäßig als extern.
(ID:45383363)