(0 Abstimmungen)

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:

  1. Klammern Sie Deklarationen so, als ob es Ausdrücke wären.
  2. Suchen sie die innerste Klammer.
  3. 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.
  4. Gehen Sie zur nächsten Klammerebene.
  5. Wenn es weiter geht, machen Sie bei Punkt 3 weiter.
  6. Sonst sagen Sie >> Typ << für den verbleibenden Typ auf der Linken Seite (wie z.B. short int)

Verständnis komplexer C-Deklarationen

Bessere Verständlichkeit von C-Deklatationen dur KlammerungViele 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:

  1. Klammerung gemäß den Vorrangregeln der Sprache C vornehmen
  2. Einzelne geklammerte Ausdrücke semantisch interpretieren
  3. 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

Bessere Verständlichkeit von C-Deklatationen dur KlammerungNicht-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)
  • inline Funktionen
  • // 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