Deklarationen in C
Die Programmiersprache C wurde in den frühen 1970er Jahren entwickelt und ist seitdem eine der bedeutendsten und langlebigsten Programmiersprachen. Trotz ihrer langen Geschichte bereitet insbesondere die Syntax häufig Verständnisprobleme – sowohl für Einsteiger als auch für erfahrene Entwickler. Ein wesentlicher Grund hierfür ist das Fehlen einer konsistenten und umfassenden Dokumentation, die sämtliche Sprachmerkmale behandelt – insbesondere Zeiger und andere komplexe Konzepte – sowie die durch ANSI (American National Standards Institute) eingeführten Erweiterungen. Solange diese Sprachmerkmale nicht korrekt verstanden werden, bleibt das Potenzial von C in vielen Fällen ungenutzt. Dies führt häufig zu Programmen, die unnötig kompliziert strukturiert sind oder funktionale Mängel aufweisen. Ziel dieses Blogbeitrags ist es, typische Merkmale von C-Deklarationen zu erläutern, die besonders häufig zu Missverständnissen führen. Dabei sollen die Konzepte klar, systematisch und nachvollziehbar dargestellt werden, um das Verständnis – insbesondere für Einsteiger – zu erleichtern.
Deklarationssyntax
Um eine Programmiersprache effektiv nutzen zu können, ist ein grundlegendes Verständnis ihrer Struktur und Syntax erforderlich. Beim Lesen einer C-Deklaration besteht die erste Aufgabe darin, ihre Organisation und Bestandteile korrekt zu interpretieren. Innerhalb der zulässigen Struktur einer Deklaration können verschiedene Attribute angegeben werden, durch die sich der Typ eines Bezeichners bestimmen lässt.
Die allgemeine Syntax einer expliziten Deklaration in C lautet:
Speicherklasse Typ Qualifizierer Deklarator = Initialisierung;
Speicherklassen
Die Speicherklasse definiert die Sichtbarkeit und Lebensdauer eines Bezeichners. In C stehen folgende Speicherklassen zur Verfügung:
- typedef
- extern
- static
- auto
- register
Typangaben
Die Typangabe bestimmt die Art der Daten, die ein Bezeichner speichern kann. Sie kann aus einem oder mehreren der folgenden Schlüsselwörter bestehen:
- void
- char
- short, int, long
- float, double
- signed, unsigned
- struct .....
- union .....
Deklaratoren
Ein Deklarator enthält den eigentlichen Bezeichner sowie ggf. zusätzliche Zeichen, die dessen Bedeutung modifizieren. Dazu gehören unter anderem:
- * // für Zeiger
- () // für Funktionen
Diese Elemente können einzeln oder in Kombination auftreten und müssen in vielen Fällen durch Klammerung gruppiert werden, um die korrekte Bindung auszudrücken. Die genaue Interpretation hängt stark von der Position und Verschachtelung der Symbole ab.
Theoretische Grundlagen
Viele Entwickler sind in der Lage, einfache C-Deklarationen wie die folgenden problemlos zu lesen:
int i;
char *p;
Dank Brian W. Kernighan und Dennis M. Ritchie (K&R) und ihrem Buch "The C Prgramming Language" können wir sogar noch folgende verstehen.
int *ia[3]; // ia ist ein Array von 3 Zeigern auf int
int (*ia)[3]; // ia ist ein Zeiger auf ein Array von 3 int
Solche Beispiele lassen sich oft durch Auswendiglernen erfassen. Glücklicherweise deckt dieses Basiswissen rund 85 % der in der Praxis vorkommenden Deklarationen ab. Die verbleibenden 15 % sind jedoch deutlich schwieriger zu verstehen, da sie komplexere Strukturen und Bindungsregeln beinhalten. Ein tieferes Verständnis der zugrunde liegenden Theorie ist notwendig, um auch diese sicher analysieren und korrekt anwenden zu können.
Regeln zum Lesen und Schreiben von K&R Deklarationen:
- Klammern Sie Deklarationen so, als ob es Ausdrücke wären.
- Suchen sie die innerste Klammer.
- Sagen Sie >> Bezeichner ist <<, wobei Bezeicher der Name der Variablen ist. Sagen Sie >> ein Array von X << wenn Sie [X] sehen. Sagen Sie >> Zeiger auf << wenn Sie * sehen.
- Gehen Sie zur nächsten Klammerebene.
- Wenn es weiter geht, machen Sie bei Punkt 3 weiter.
- Sonst sagen Sie >> Typ << für den verbleibenden Typ auf der Linken Seite (wie z.B. short int)
Verständnis komplexer C-Deklarationen
Viele Programmierer haben Schwierigkeiten, komplexe Deklarationen in C korrekt zu interpretieren und die damit deklarierten Bezeichner sinnvoll zu verwenden. Aufgrund unzureichender oder uneinheitlicher Dokumentation bleibt häufig nur das „Raten“ als Ausweg. Auf Dauer führt dieses Vorgehen jedoch zu Missverständnissen und Verallgemeinerungen, die nicht zwangsläufig korrekt sind. Selbst wenn die Interpretation inhaltlich nahe an der beabsichtigten Bedeutung liegt, kann der tatsächlich vom Compiler erzeugte Code erheblich vom gewünschten Verhalten abweichen.
Rückblickend hätte ich mir beim Erlernen der Sprache C gewünscht, auf das mühsame Rätselraten verzichten zu können. Umso bedauerlicher ist es, dass die theoretischen Grundlagen von C-Deklarationen eigentlich sehr einfach sind.
Der zentrale Aspekt: Deklarationen folgen denselben Prioritätsregeln wie Ausdrücke, also der Hierarchie der C-Operatoren. Wer mit der Auswertung von Ausdrücken vertraut ist, kann diese Kenntnisse direkt auf Deklarationen übertragen.
Auswertungsregeln bei Deklarationen
Für Deklarationen gelten folgende Vorrangregeln (von höchster zu niedrigster Priorität):
- () (Funktionsoperator) und [] (Arrayoperator)
→ höchste Priorität, Auswertung erfolgt von links nach rechts - * (Zeigeroperator)
→ niedrigere Priorität, bindet schwächer
Wichtig ist: Klammern können die Bindungsregeln explizit verändern, genau wie bei arithmetischen Ausdrücken.
Praktische Konsequenz
Sobald eine komplexe Deklaration korrekt geklammert ist, besteht die Aufgabe lediglich darin, jede geklammerte Teilausdruckseinheit zu analysieren und semantisch korrekt zu interpretieren. Dieser Prozess ähnelt dem Zerlegen arithmetischer Ausdrücke, bei denen ebenfalls durch Klammerung die Reihenfolge der Operatorauswertung gesteuert wird.
Ein wesentlicher Unterschied besteht jedoch darin, dass arithmetische Operatoren wie * und / binär sind (sie benötigen zwei Operanden), während es sich bei den Operatoren in Deklarationen – insbesondere beim Zeigeroperator * – um unäre Operatoren handelt, die nur einen Operanden betreffen.
Vorgehensweise
Basierend auf den in The C Programming Language von Kernighan und Ritchie (K&R) erläuterten Regeln empfiehlt es sich, komplexe Deklarationen wie folgt zu analysieren:
- Klammerung gemäß den Vorrangregeln der Sprache C vornehmen
- Einzelne geklammerte Ausdrücke semantisch interpretieren
- Gesamtbedeutung aus der Hierarchie ableiten
In den oben angeführten Beispielen (siehe Beispielkasten) wird diese Methode exemplarisch angewendet, um verschiedene komplexe C-Deklarationen systematisch zu entschlüsseln.
Praktische Anwendung
Obwohl die im vorherigen Abschnitt vorgestellten Regeln zum Lesen und Schreiben von C-Deklarationen grundsätzlich einfach sind, erfordert die explizite Klammerung der Ausdrücke zusätzlichen Aufwand. Diesen möchte man in der Praxis oft vermeiden.
Erweiterte Regeln für C-Deklarationen
Mit den folgenden Regeln lassen sich C-Deklarationen „on the fly“ (also direkt beim Lesen) interpretieren.
1. Grundannahmen
Nicht-terminierende Attribute sind:
- [] Arrays
- () Funktionen
- * Zeiger
2. Rechts-nach-links-Regel
- Beginnen Sie beim Bezeichner (Variablenname).
- Schauen Sie zuerst nach rechts (innerhalb von Klammern).
- Falls dort ein Attribut steht, nehmen Sie es auf.
- Danach schauen Sie nach links und nehmen dort ebenfalls vorhandene Attribute auf.
3. Übersetzung einer C-Deklaration ins Deutsche
a) Bezeichner bestimmen
Bestimmen Sie den Namen der Variablen und beginnen Sie mit:
→ „Bezeichner ist …“
b) Rechte Seite analysieren
Schauen Sie rechts vom Bezeichner nach () oder [].
- [] → „ein Array von …“
- [x] → „ein Array von x Elementen“
- [x][y] → „ein x-mal-y Array von …“
- [x][y][z] → entsprechend erweitern
- () → „eine Funktion mit Rückgabewert …“
(insbesondere, wenn zuvor kein Array erkannt wurde)
c) Linke Seite analysieren
Nun betrachten Sie die linke Seite (gemäß der Rechts-nach-links-Regel).
- Relevant sind hier nur * (Zeiger)
- Für jedes * sagen Sie:
Beachte dabei auch mögliche Klammern!
d) Erneut nach rechts schauen
- Prüfen Sie erneut die rechte Seite
- Falls Klammern auftreten, wiederholen Sie den Prozess ab Schritt b)
e) Terminierende Attribute
Am Ende bleibt ein Basistyp übrig, z. B.:
char, int, float, double, struct, union, unsigned, static, extern, etc.
Übersetzung:
- struct y → „Struktur vom Typ y“
- union y → „Union vom Typ y“
- Ansonsten: Typ einfach von links nach rechts lesen
4. Umsetzung: Vom Deutschen zur C-Deklaration
a) Bezeichner schreiben
Beginne mit dem Variablennamen.
b) Hilfsvariable (Flag)
Verwenden Sie ein Flag:
→ Aktivierer-* (zeigt an, ob zuletzt ein * verarbeitet wurde)
Initialwert: 0
c) Zeiger verarbeiten
- Für jedes „Zeiger auf“:
- Schreiben Sie * links vom bisherigen Ausdruck
- Setzen Sie Aktivierer-* = 1
d) Arrays und Funktionen
- Falls Aktivierer-* = 1:
- Setzen Sie Klammern um den bisherigen Ausdruck
Dann:
- „Array von x“ → [x] rechts anhängen
- „Array von“ → []
- „x-mal-y Array“ → [x][y]
- „Funktion mit Rückgabewert“ → ()
e) Wiederholen
- Falls weitere Attribute folgen → zurück zu Schritt b)
f) Basistyp hinzufügen
- Schreiben Sie den Datentyp links vom gesamten Ausdruck
5. Wichtige Hinweise
- ❌ Arrays von Funktionen sind nicht erlaubt
✔️ Erlaubt: Array von Funktionszeigern
→ int a[5](); ist ungültig - ❌ Funktionen können keine Arrays zurückgeben
✔️ Aber: Zeiger auf Arrays sind erlaubt
→ int a()[]; ist ungültig - ❌ Funktionen können keine Funktionen zurückgeben
✔️ Aber: Zeiger auf Funktionen sind erlaubt
→ int a()(); ist ungültig
Resume
Auf den ersten Blick wirken diese erweiterten Regeln komplizierter als die grundlegenden Regeln. Tatsächlich handelt es sich jedoch nur um eine praktische Erweiterung.
Mit dem Verständnis von Operatoren und deren Priorität kann man in vielen Fällen auf zusätzliche Klammern verzichten und Deklarationen direkt lesen.
Abschließend möchte ich noch kurz - sofern sie für die C Deklarationen relevant sind, auf ANSI Standardisierung eingehen.
1.) C89/C90 - Der Grundstein moderner C- Entwicklung
Ziel:
Ein stabiler und einheitlicher Sprachstandard
Mit ANSI C (1989) später international als C90 übernommen - wurde erstmals eine verbindliche Specifikation geschaffen
Wichtige Neuerungen:
- Einführung der Standardbibliothek (stdio.h, stdlib.h, string.h, etc.)
- Funktionsprototypen für bessere Typprüfung
- Einheitliche Definition von Datentypen und Operatorverhalten
- Klare Sprachsyntax und Semantic
Bedeutung:
C89 machte C portabel und verlässlich - ein entscheidender Schritt für industrielle Softwareentwicklung.
Beispiel: Type Qualifiers const, volatile
const - unveränderliche Variable
const int x = 10;
x = 20; // ❌ Compilerfehler
volatile - Der Compiler darf den Code nicht optimieren oder zwischenspeichern, da die Hardware z.B. I/O Port den Wert verändern kann.
int sensor_value;
while (sensor_value == 0) {
// ❌ kann zu Endlosschleife werden
}
volatile int sensor_value;
while (sensor_value == 0) {
// ✔️ o.k.
}
Beispiel: Funktionsprototyp:
// Vor C89 (K&R Stil)
int add(a, b)
int a, b;
{
return a + b;
}
// C89
int add(int a, int b) {
return a + b;
}
Einschränkungen: Variablen nur am Blockanfang, keine // Kommentare, kein bool.
2. C99 – Große Modernisierung
- Variablen überall deklarierbar
- Neue Typen:
long long,_Bool - Variable Length Arrays (VLA)
inlineFunktionen//Kommentare- Designated Initializers
Beispiel: Variable in Schleife
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
Beispiel: Variable Length Array
int n;
scanf("%d", &n);
int arr[n];
Beispiel: Designated Initializer
struct Point { int x, y; };
struct Point p = {.y = 5, .x = 3};
Beispiel: Bool
#include <stdbool.h>
bool flag = true;
3. C11 – Sicherheit und Parallelität
_Static_assert(Compile-Time Checks)_Generic(Typabhängige Makros)- Threads (
threads.h) - Atomare Operationen (
stdatomic.h) - Alignment-Kontrolle
Beispiel: Compile-Time Prüfung
_Static_assert(sizeof(int) == 4, "int muss 4 Bytes haben");
Beispiel: _Generic
#define type(x) _Generic((x), \
int: "int", \
float: "float", \
default: "other")
printf("%s\n", type(3.14f));
Beispiel: Thread
#include <threads.h>
int func(void *arg) {
return 0;
}
int main() {
thrd_t t;
thrd_create(&t, func, NULL);
thrd_join(t, NULL);
}
4. C17 / C18 – Stabilisierung
Keine neuen Sprachfeatures – nur Fehlerkorrekturen und Klarstellungen des C11-Standards.
Praxis: C17 = stabiles C11
5. C23 – Moderne Erweiterungen
auto(Typinferenz)nullptr_BitInt(bitgenaue Integer)- Binärliterale (
0b1010) typeof- Attribute (
[[nodiscard]]) - Leere Initialisierung (
{})
Beispiel: auto
auto x = 5;
auto y = 3.14;
Beispiel: nullptr
nullptr_t p = nullptr;
Beispiel: _BitInt
_BitInt(8) x = 100;
Beispiel: Binärliteral
int x = 0b1010;
Beispiel: typeof
int x = 10;
typeof(x) y = 20;
Zusammenfassung
| Standard | Schwerpunkt | Wichtige Features |
|---|---|---|
| C89 | Grundlage | Standardbibliothek, Prototypen |
| C99 | Modernisierung | bool, long long, VLA |
| C11 | Parallelität | Threads, Atomics, Static Assert |
| C17 | Stabilität | Bugfixes |
| C23 | Neue Features | auto, nullptr, typeof |
Fazit
Für die Praxis sind C99 und C11 am wichtigsten. C23 bringt moderne Features, ist aber noch nicht in allen Compilern vollständig verfügbar. Wer besonderen Wert auf maximale Portierbarkeit seiner Software auf möglichst viele Computerplattformen legt, sollte sich jedoch an die Standardisierungsphasen C89 und C99 halten.
Literaturverweise
The C Programming Language von Brian W. Kernighan & Dennis M. Ritchie galt lange Zeit als das Standartwerk der C Programmierung - manche bezeichnen es auch als die Bibel, die jeder gläubige C- Programmierer gelesen haben sollte.
C Programming A Modern Approach von K. N. King ist meiner Meinung eines der besten C Lehrbücher
Beide Bücher sind in leicht verständlichem Englisch verfasst und bieten somit einen zusätzlichen Vorteil: Wer sie liest und versteht, verbessert ganz nebenbei auch sein Computerenglisch. Abschreckend wirkt allerdings der vergleichsweise hohe Preis von etwa 65 bzw. 80 Euro. Allerdings besteht auch die Möglichkeit, die Inhalte online kostenlos zu lesen.
Das C Buch von H. Herold u. W. Unger
Richtig einsteigen C++, Microsoft Corp.
Grundkurs C++, Galileo Computing
C von A bis Z, Das umfassende Handbuch von Galileo Computing
Der C++ Programmierer, Hanser Verlag
MC-Tools der Keil C51-Compiler, Einführung und Praxis
Arduino Cookbook.
AVR-Mikrocontroller
Quick C Microsoft
GNU
